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.
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
endCombined 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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
