Design and Implementation of a Business Risk‑Control Component Using Redis+Lua and Spring Annotations
This article explains why a custom business risk‑control (风控) component is needed, compares business‑specific and generic risk‑control, outlines real‑time adjustable limits, and provides a complete backend implementation using Kotlin, Spring AOP, Redis Lua scripts, and custom annotations.
The document starts by describing the background: the product heavily uses AI services (OCR, speech evaluation) that are costly, so a business‑level risk‑control is required to limit user usage.
It then asks why a custom risk‑control should be built instead of using existing open‑source components, noting that generic controls (e.g., sliding‑window) cannot satisfy the complex, multi‑dimensional rules of business risk‑control.
Key requirements include real‑time adjustable limits and the ability to handle natural‑day, natural‑hour, and combined day‑hour counters.
Rule implementation lists the three required counters and explains that the combined day‑hour rule must roll back counts when one condition fails.
For counting methods, three options are considered: mysql+db transaction, redis+lua, and mysql/redis+distributed transaction. The article chooses redis+lua for simplicity and sufficient transactional guarantees.
Calling method shows a simplified Kotlin component:
@Component
class DetectManager {
fun matchExceptionally(eventId: String, content: String) {
val rt = ruleService.match(eventId, content)
if (!rt) {
throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
}
}
}And its usage in a service:
@Service
class OcrServiceImpl : OcrService {
@Autowired
private lateinit var detectManager: DetectManager
override fun submitOcrTask(userId: String, imageUrl: String): String {
detectManager.matchExceptionally("ocr", userId)
// do ocr
}
}The article then introduces a Lua script for natural‑day or natural‑hour counting:
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
endAnd a more complex Lua script handling combined day‑hour logic with rollback:
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;To make the usage more declarative, a custom annotation @Detect is defined:
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
val eventId: String = "",
val contentSpel: String = ""
)And an AOP handler processes the annotation, evaluates the SpEL expression to obtain the content, and invokes the DetectManager:
@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()
}
}Finally, the article shows how to apply the annotation to the OCR service method, enabling automatic risk‑control without manual calls.
Testing confirms that the annotation values and SpEL expressions are correctly resolved.
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.
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.
