Backend Development 9 min read

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.

Architecture Digest
Architecture Digest
Architecture Digest
Design and Implementation of a Business Risk‑Control Component Using Redis, Lua, and Kotlin

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.

BackendRedisSpringKotlinAnnotationLuarisk 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

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