This is how the wind control system should be designed (universal and universal), stable batch

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 luascripts, because they are only keydifferent, 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 the day association,  KEYS[2] the key generated by the hour association, ARGV[1]the upper limit value of the day, the ARGV[2]upper limit value of the hour, ARGV[3]the expiration time of the day, ARGV[4]and the expiration time of the 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

     

Guess you like

Origin blog.csdn.net/z_ssyy/article/details/131395077