Design a Universal, Rock‑Solid Risk‑Control System with Redis‑Lua and Kotlin Annotations

This article explains why a custom risk‑control component is needed for AI‑heavy services, outlines the requirement for real‑time adjustable limits, and walks through a Redis‑Lua based implementation of daily, hourly, and combined counters together with a Kotlin @Detect annotation for seamless integration.

Architect's Guide
Architect's Guide
Architect's Guide
Design a Universal, Rock‑Solid Risk‑Control System with Redis‑Lua and Kotlin Annotations

Background

Our product relies heavily on AI capabilities such as OCR and voice evaluation, which are costly in terms of money and resources. To prevent abuse, we need a risk‑control (rate‑limiting) component that restricts the number of times a user can invoke these capabilities.

Why Build Our Own

Existing open‑source rate‑limiting libraries address generic scenarios and cannot satisfy our specific business requirements, which we refer to as “business risk control”. They lack real‑time adjustable limits and the combined daily‑hourly logic we need.

Requirements

Support real‑time adjustment of limits.

Count per natural day, per natural hour, and a combined day‑hour rule that rolls back the other counter when one condition fails.

Design Approach

We choose Redis + Lua scripts because they provide simple, atomic operations without the overhead of persistent databases. The scripts implement the three counting rules and handle the rollback logic.

Rule Implementation

Daily and hourly counters share a Lua script that increments the key only if the current value is below the configured limit.

//lua脚本
local currentValue = redis.call('get', KEYS[1]);
if currentValue ~= false then 
    if tonumber(currentValue) < tonumber(ARGV[1]) then 
        return redis.call('INCR', KEYS[1]);
    else
        return tonumber(currentValue) + 1;
    end;
else
    redis.call('set', KEYS[1], 1, 'px', ARGV[2]);
    return 1;
end;

The combined day‑hour rule uses two keys and four arguments (day limit, hour limit, day TTL, hour TTL). It increments both counters, then rolls back the other when one limit is exceeded.

//lua脚本
local dayValue = 0;
local hourValue = 0;
local dayPass = true;
local hourPass = true;
local dayCurrentValue = redis.call('get', KEYS[1]);
if dayCurrentValue ~= false then 
    if tonumber(dayCurrentValue) < tonumber(ARGV[1]) then 
        dayValue = redis.call('INCR', KEYS[1]);
    else
        dayPass = false;
        dayValue = tonumber(dayCurrentValue) + 1;
    end;
else
   redis.call('set', KEYS[1], 1, 'px', ARGV[3]);
   dayValue = 1;
end;

local hourCurrentValue = redis.call('get', KEYS[2]);
if hourCurrentValue ~= false then 
    if tonumber(hourCurrentValue) < tonumber(ARGV[2]) then 
        hourValue = redis.call('INCR', KEYS[2]);
    else
        hourPass = false;
        hourValue = tonumber(hourCurrentValue) + 1;
    end;
else
   redis.call('set', KEYS[2], 1, 'px', ARGV[4]);
   hourValue = 1;
end;

if (not dayPass) and hourPass then
    hourValue = redis.call('DECR', KEYS[2]);
end;

if dayPass and (not hourPass) then
    dayValue = redis.call('DECR', KEYS[1]);
end;

local pair = {};
pair[1] = dayValue;
pair[2] = hourValue;
return pair;

Invocation Method

A simple Kotlin component DetectManager calls the rule service and throws an exception when the limit is exceeded.

//简化版代码

@Component
class DetectManager {
    fun matchExceptionally(eventId: String, content: String) {
        //调用规则匹配
        val rt = ruleService.match(eventId, content)
        if (!rt) {
            throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
        }
    }
}

Annotation‑Based Usage

Define a @Detect annotation to mark service methods. An Aspect intercepts the annotation, parses the SpEL expression, builds a context with method arguments (arg1, arg2, …), and invokes DetectManager.

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
    /** 事件id */
    val eventId: String = "",
    /** content的表达式 */
    val contentSpel: String = ""
)
@Aspect
@Component
class DetectHandler {
    private val logger = LoggerFactory.getLogger(javaClass)

    @Autowired
    private lateinit var detectManager: DetectManager

    @Resource(name = "detectSpelExpressionParser")
    private lateinit var spelExpressionParser: SpelExpressionParser

    @Bean(name = ["detectSpelExpressionParser"])
    fun detectSpelExpressionParser(): SpelExpressionParser {
        return SpelExpressionParser()
    }

    @Around(value = "@annotation(detect)")
    fun operatorAnnotation(joinPoint: ProceedingJoinPoint, detect: Detect): Any? {
        if (detect.eventId.isBlank() || detect.contentSpel.isBlank()) {
            throw illegalArgumentExp("@Detect config is not available!")
        }
        //转换表达式
        val expression = spelExpressionParser.parseExpression(detect.contentSpel)
        val argMap = joinPoint.args.mapIndexed { index, any -> "arg${index+1}" to any }.toMap()
        //构建上下文
        val context = StandardEvaluationContext().apply {
            if (argMap.isNotEmpty()) this.setVariables(argMap)
        }
        //拿到结果
        val content = expression.getValue(context)

        detectManager.matchExceptionally(detect.eventId, content)
        return joinPoint.proceed()
    }
}

Testing

Running the annotated OCR service shows that the annotation value is retrieved and the SpEL expression is evaluated correctly.

Annotation value obtained successfully.

Expression parsed successfully.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

BackendRedisKotlinAnnotationRate LimitingLuaRisk Control
Architect's Guide
Written by

Architect's Guide

Dedicated to sharing programmer-architect skills—Java backend, system, microservice, and distributed architectures—to help you become a senior architect.

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.