1. Background
1. Why should we 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, two judgments cannot be simply connected in series, 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, traceable records, more troublesome to implement, a little “heavy”
-
Redis + lua is easy to implement, and the executable lua script of redis can also meet the requirements for “transactions”
-
mysql/redis + distributed transactions need to be locked, the implementation is complicated, and it can achieve a more accurate count, that is, it really waits until the code block is executed successfully before operating the count
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. Common practice First define a common entry
//simplified version code @Component class DetectManager { fun matchExceptionally(eventId: String, content: String){ //Call rule matching val rt = ruleService. match(eventId, content) if (!rt) { throw BaseException(ErrorCode. OPERATION_TOO_FREQUENT) } } }
Call this method in service
//simplified version code @Service class OcrServiceImpl : OcrService { @Autowired private lateinit var detectManager: DetectManager /** * Submit ocr task * Need to limit the number of times according to the user 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. Specific implementation
1. Implementation of risk control counting rules
a. Natural day/natural hour
Natural day/natural hour can share a set of lua
scripts, because they are only different in key
, the scripts are as follows:
//lua script 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]
is the upper limit value, and ARGV[2]
is the expiration time , 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 script 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 day ARGV[2]
is the upper limit of hours, ARGV[3]
is the expiration time of days, ARGV[4]
is the expiration time in hours, 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. Realization of annotation
a. Define a @Detect annotation
@Retention(AnnotationRetention.RUNTIME) @Target(AnnotationTarget. FUNCTION, AnnotationTarget. CLASS) annotation class Detect( /** * event id */ val eventId: String = "", /** * expression of content */ val contentSpel: String = "" )
Among them, content
needs to be parsed out by expressions, so a String
is accepted
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!") } // conversion expression val expression = spelExpressionParser. parseExpression(detect. contentSpel) val argMap = joinPoint.args.mapIndexed { index, any -> "arg${index + 1}" to any }.toMap() // build context val context = StandardEvaluationContext(). apply { if (argMap. isNotEmpty()) this. setVariables(argMap) } //get the result 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
….
4. Test it
1. Writing
Writing after using annotations:
//simplified version code @Service class OcrServiceImpl : OcrService { @Autowired private lateinit var detectManager: DetectManager /** * Submit ocr task * Need to limit the number of times according to the user 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
Previous recommendations: Douyin Two Sides: "Talk about the principle of QR code scanning and login" SpringBoot + Druid, perfect monitoring of MySQL performance Wrote a code tool, CRUD development efficiency directly increased by 100 times! Design and implementation of data range authority in large SaaS system SELECT COUNT(*) will cause a full table scan? Go back and wait for the notification Nginx visualization artifact! One-click generation of complex configurations, one-stop monitoring and management!