The wind control system should be designed like this (universal and universal), a stable batch!

1. Background

1. Why should we do risk control?

c0d2f7f4574ed5b2a9a42a3a8e89a3f1.png

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?

20045f8abc49d9c1332b43b0b34dd505.png

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):

28343488b7e7e8ec4e1a881851ede8cb.png

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

810f98a57fb62c44af03977e620810a7.png

  • 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! 

cab880b876643f075e74232697f40b2b.png