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.

Architecture Digest
Architecture Digest
Architecture Digest
Design and Implementation of a Business Risk‑Control Component Using Redis+Lua and Spring 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
end

And 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.

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.

Kotlinrisk control
Architecture Digest
Written by

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.

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.