Article source: https://juejin.cn/post/7182774381448282172
Table of contents
1. Background
Two, ideas
3. Realization
Four, test it
1. Background
1. Why do risk control?
This is not thanks to the product boss
At present, our business uses a lot of AI capabilities, such as OCR recognition, voice evaluation, etc. These capabilities are often costly or resource-intensive, so at the product level, we also hope that we can limit the number of times users can use the capabilities, so Wind control is a must!
2. Why write your own risk control?
Why write so many open source risk control components? Do you want to reinvent the wheel?
To answer this question, we need to explain the difference between the risk control that our business needs (referred to as business risk control) and the common risk control of open source (referred to as ordinary risk control):
Therefore, direct use of open source common risk control is generally unable to meet the needs
3. Other requirements
Supports real-time adjustment of limits
When many limit values are set for the first time, they are basically a set value, and the possibility of subsequent adjustment is relatively high, so it is necessary to be adjustable and take effect in real time
Two, ideas
What is required to implement a simple business risk control component?
1. Implementation of risk control rules
a. Rules that need to be implemented:
calendar day count
natural hour count
natural day + natural hour count
Natural day + natural hour counting here cannot simply connect two judgments, because if the judgment of natural day passes but the judgment of natural hour fails, it needs to be rolled back, and neither natural day nor natural hour can be counted in this call !
b. Choice of counting method:
The ones I can think of so far are:
Mysql+db transaction persistence, record traceability, more troublesome to implement, a little "heavy"
Redis+lua is simple to implement, and the characteristics of redis executable lua scripts can also meet the requirements for "transactions"
mysql/redis+ distributed transactions need to be locked, the implementation is complicated, and it can achieve more accurate counting, that is, it really waits until the code block is executed successfully before operating the counting
At present, there are no very precise technical requirements, the cost is too high, and there is no need for persistence, so choose redis+lua
2. Implementation of calling method
a. The common practice is to define a common entry first
//简化版代码
@Component
class DetectManager {
fun matchExceptionally(eventId: String, content: String){
//调用规则匹配
val rt = ruleService.match(eventId,content)
if (!rt) {
throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
}
}
}
Call this method in service
//简化版代码
@Service
class OcrServiceImpl : OcrService {
@Autowired
private lateinit var detectManager: DetectManager
/**
* 提交ocr任务
* 需要根据用户id来做次数限制
*/
override fun submitOcrTask(userId: String, imageUrl: String): String {
detectManager.matchExceptionally("ocr", userId)
//do ocr
}
}
Is there a more elegant way? It may be better to use annotations (it is also controversial, in fact, it supports implementation first)
Since the incoming content is related to the business, it is necessary to use Spel to form the parameters into the corresponding content
3. Realization
1. Implementation of risk control counting rules
a. Natural day/natural hour
The natural day/natural hour can share a set of lua
scripts, because they are only key
different, the scripts are as follows:
//lua脚本
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;
Among them KEYS[1]
is the key associated with the day/hour, ARGV[1]
the upper limit value, ARGV[2]
and the expiration time, and the return value is the result after the current count value + 1, (if the upper limit has been reached, it will not actually count)
b. Natural day + natural hour As mentioned above, the combination of the two is actually not a simple patchwork, and the fallback logic needs to be processed
//lua脚本
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;
Among them KEYS[1]
is the key generated by day association, KEYS[2]
is the key generated by hour association, ARGV[1]
is the upper limit value of day, ARGV[2]
is the upper limit value of hour, ARGV[3]
is the expiration time of day, ARGV[4]
is the expiration time of hour, and the return value is the same as above
Here is a rough way of writing. The main thing to express is that when two conditions are judged, one of them is not satisfied, and the other needs to be rolled back.
2. Implementation of annotations
a. Define a @Detect annotation
@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(
/**
* 事件id
*/
val eventId: String = "",
/**
* content的表达式
*/
val contentSpel: String = ""
)
Among them content
, it needs to be parsed out by expressions, so what is accepted is aString
b. Define the processing class of @Detect annotation
@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()
}
}
The parameters need to be put into the context and named arg1
, arg2
....
Four, test it
1. Writing
Writing after using annotations:
//简化版代码
@Service
class OcrServiceImpl : OcrService {
@Autowired
private lateinit var detectManager: DetectManager
/**
* 提交ocr任务
* 需要根据用户id来做次数限制
*/
@Detect(eventId = "ocr", contentSpel = "#arg1")
override fun submitOcrTask(userId: String, imageUrl: String): String {
//do ocr
}
}
2.Debug to see
Annotation value obtained successfully
Expression parsed successfully