Design and Implementation of a Business Risk‑Control Component Using Redis, Lua, and Kotlin
This article explains why a custom business risk‑control module is needed, outlines the required features, and provides a complete Kotlin‑based implementation that uses Redis + Lua scripts for daily and hourly counting, as well as a Spring annotation for seamless integration.
Background
Our product heavily uses AI capabilities such as OCR and voice evaluation, which are costly, so we need to limit the number of times a user can invoke these services. Therefore, a business‑level risk‑control mechanism is required.
Why build our own risk‑control?
Existing open‑source risk‑control components focus on generic scenarios and cannot satisfy our specific needs, such as real‑time adjustable limits and integration with AI service calls.
Other requirements
Limits must be adjustable in real time because the initial values are often provisional and may need frequent changes.
Approach
We design a simple business risk‑control component that implements three counting rules: natural‑day count, natural‑hour count, and combined day‑hour count, with a fallback mechanism when one rule fails.
Implementation of counting rules
a. Natural day / natural hour – Both can share a single Lua script because they only differ by the key used.
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;b. Natural day + natural hour – This requires a rollback logic when either the day or hour limit is exceeded.
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;Annotation implementation
a. Define @Detect annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
val eventId: String = "",
val contentSpel: String = ""
)b. Define the aspect that processes @Detect
@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
Using the annotation in a service method:
@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
return "result"
}
}Debugging shows that the annotation value and SpEL expression are correctly resolved, and the risk‑control check is applied before the OCR task is executed.
Architecture Digest
Focusing on Java backend development, covering application architecture from top-tier internet companies (high availability, high performance, high stability), big data, machine learning, Java architecture, and other popular fields.
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.