Implementing a Simple Business Rate‑Limiting Component with Redis+Lua and Kotlin Annotations

This article explains why a custom business rate‑limiting (risk‑control) component is needed for AI‑driven services, compares open‑source solutions, and provides a complete implementation using Redis+Lua scripts, Kotlin code, and Spring AOP annotations, including testing and deployment tips.

Top Architect
Top Architect
Top Architect
Implementing a Simple Business Rate‑Limiting Component with Redis+Lua and Kotlin Annotations

Background

Our product heavily uses AI capabilities such as OCR and voice evaluation, which are costly in terms of money and resources. To control usage and cost, we need a business‑level rate‑limiting (risk‑control) mechanism.

Why Build Our Own?

Although many open‑source risk‑control components exist, they cannot satisfy our specific requirements, such as real‑time adjustable limits and combined day‑hour counting logic.

Requirements

Support real‑time adjustment of limits.

Handle natural‑day, natural‑hour, and combined day‑hour counting.

Design Idea

We decided to implement a simple business risk‑control component using Redis + Lua scripts to achieve atomic counting without the complexity of distributed transactions.

1. Rate‑limit Rules

Three rules are needed:

Natural‑day counting

Natural‑hour counting

Combined day‑hour counting (requires rollback logic when one condition fails)

2. Counting Method Choice

Options considered:

MySQL + DB transaction (heavy, persistent, hard to implement)

Redis + Lua (simple, supports atomic operations)

MySQL/Redis + distributed transaction (complex, requires locking)

Given no strict persistence requirement, we chose redis+lua.

3. Implementation of Call‑side

Define a generic entry point in a Spring component:

@Component
class DetectManager {
    fun matchExceptionally(eventId: String, content: String) {
        val rt = ruleService.match(eventId, content)
        if (!rt) {
            throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
        }
    }
}

Service calls this manager before executing the business logic:

@Service
class OcrServiceImpl : OcrService {
    @Autowired
    private lateinit var detectManager: DetectManager

    /** Submit OCR task with user‑based rate limit */
    override fun submitOcrTask(userId: String, imageUrl: String): String {
        detectManager.matchExceptionally("ocr", userId)
        // do OCR
    }
}

4. Redis Lua Scripts

Day/Hour counting script (shared logic):

//lua script
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

Combined day‑hour script with rollback handling:

//lua script
local dayValue = 0
local hourValue = 0
local dayPass = true
local hourPass = true
-- day logic
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
-- hour logic (similar)
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
-- rollback if needed
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
return {dayValue, hourValue}

5. Annotation‑Based Approach

Define a @Detect annotation:

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
    val eventId: String = "",
    val contentSpel: String = ""
)

Implement an AOP handler that parses the SpEL expression, obtains the content, and invokes the manager:

@Aspect
@Component
class DetectHandler {
    @Autowired private lateinit var detectManager: DetectManager
    @Resource(name = "detectSpelExpressionParser") private lateinit var spelExpressionParser: SpelExpressionParser

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

    @Around("@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()) setVariables(argMap) }
        val content = expression.getValue(context)
        detectManager.matchExceptionally(detect.eventId, content)
        return joinPoint.proceed()
    }
}

Usage in service:

@Service
class OcrServiceImpl : OcrService {
    @Autowired private lateinit var detectManager: DetectManager

    @Detect(eventId = "ocr", contentSpel = "#arg1")
    override fun submitOcrTask(userId: String, imageUrl: String): String {
        // do OCR
    }
}

Testing

After adding the annotation, the framework successfully retrieves the annotation values, parses the SpEL expression, and enforces the rate limit.

In summary, a simple business risk‑control demo is completed; real‑world systems are far more complex, so do not underestimate them.

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.

BackendredisspringKotlinannotationrate limitingLua
Top Architect
Written by

Top Architect

Top Architect focuses on sharing practical architecture knowledge, covering enterprise, system, website, large‑scale distributed, and high‑availability architectures, plus architecture adjustments using internet technologies. We welcome idea‑driven, sharing‑oriented architects to exchange and learn together.

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.