¡Así es como debe diseñarse el sistema de control de viento!

Fuente del artículo: https://juejin.cn/post/7182774381448282172

Tabla de contenido

  • 1. Antecedentes

  • dos ideas

  • 3. Realización

  • Cuatro, pruébalo


1. Antecedentes

1. ¿Por qué hacer control de riesgos?

Esto no es gracias al jefe de producto.

En la actualidad, nuestra empresa utiliza muchas capacidades de IA, como reconocimiento OCR, evaluación de voz, etc. Estas capacidades suelen ser costosas o requieren muchos recursos, por lo que a nivel de producto, también esperamos poder limitar la cantidad de veces que los usuarios puede usar las capacidades, ¡así que el control del viento es imprescindible!

2. ¿Por qué escribir su propio control de riesgos?

¿Por qué escribir tantos componentes de control de riesgos de código abierto?¿Quiere reinventar la rueda?

4515fd1165fd48252f592fdb48ff4130.jpeg

Para responder a esta pregunta, debemos explicar la diferencia entre el control de riesgos que necesita nuestro negocio (denominado control de riesgos comerciales) y el control de riesgos común de código abierto (denominado control de riesgos ordinario):

4f2ede44592be02dca6c5df020d97129.png


Por lo tanto, el uso directo del control de riesgo común de código abierto generalmente no puede satisfacer las necesidades

3. Otros requisitos

Admite ajuste de límites en tiempo real

Cuando se establecen muchos valores límite por primera vez, son básicamente un valor establecido, y la posibilidad de un ajuste posterior es relativamente alta, por lo que es necesario que sea ajustable y surta efecto en tiempo real.



dos ideas

¿Qué se requiere para implementar un componente simple de control de riesgos comerciales?

1. Implementación de reglas de control de riesgos

a. Reglas que deben implementarse:

  • conteo de días calendario

  • conteo de horas naturales

  • día natural + conteo de horas naturales

El conteo de día natural + hora natural aquí no puede simplemente conectar dos juicios, porque si el juicio del día natural pasa pero el juicio de la hora natural falla, debe revertirse, ¡y ni el día natural ni la hora natural se pueden contar en esta llamada!

b. Elección del método de conteo:

Los que se me ocurren hasta ahora son:

  • Persistencia de transacciones Mysql+db, rastreabilidad de registros, más problemático de implementar, un poco "pesado"

  • Redis+lua es simple de implementar, y las características de los scripts lua ejecutables de redis también pueden cumplir con los requisitos para "transacciones"

  • Las transacciones distribuidas mysql/redis+ deben bloquearse, la implementación es complicada y puede lograr un conteo más preciso, es decir, realmente espera hasta que el bloque de código se ejecute con éxito antes de operar el conteo.

En la actualidad, no hay requisitos técnicos muy precisos, el costo es demasiado alto y no hay necesidad de persistencia, así que elija redis+lua

2. Implementación del método de llamada

a. La práctica común es definir una entrada común primero

//简化版代码

@Component
class DetectManager {
    fun matchExceptionally(eventId: String, content: String){
        //调用规则匹配
        val rt = ruleService.match(eventId,content)
        if (!rt) {
            throw BaseException(ErrorCode.OPERATION_TOO_FREQUENT)
        }
    }
}

Llame a este método en servicio

//简化版代码

@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
    }
    
}

¿Hay una forma más elegante? Puede ser mejor usar anotaciones (también es controvertido, de hecho, admite la implementación primero)

Dado que el contenido entrante está relacionado con el negocio, es necesario usar Spel para formar los parámetros en el contenido correspondiente.



3. Realización

1. Implementación de reglas de conteo de control de riesgos

a. Día natural/hora natural

El día natural/hora natural puede compartir un conjunto de luaguiones, porque solo son keydiferentes, los guiones son los siguientes:

//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;

Entre ellos KEYS[1]está la clave asociada con el día/hora, ARGV[1]el valor límite superior ARGV[2]y el tiempo de caducidad, y el valor de retorno es el resultado después del valor de conteo actual + 1, (si se ha alcanzado el límite superior, en realidad no contar)

b. Día natural + hora natural Como se mencionó anteriormente, la combinación de los dos en realidad no es un mosaico simple, y la lógica alternativa debe procesarse

//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;

Entre ellos KEYS[1]está la clave generada por la asociación del día, KEYS[2]es la clave generada por la asociación de la hora, ARGV[1]es el valor límite superior del día, ARGV[2]es el valor límite superior de la hora, ARGV[3]es la hora de vencimiento del día, ARGV[4]es la hora de vencimiento de la hora y el el valor de retorno es el mismo que el anterior

Aquí hay una forma aproximada de escribir. Lo principal para expresar es que cuando se juzgan dos condiciones, una de ellas no se cumple y la otra debe revertirse.

2. Implementación de anotaciones

a. Definir una anotación @Detect

@Retention(AnnotationRetention.RUNTIME)
@Target(AnnotationTarget.FUNCTION, AnnotationTarget.CLASS)
annotation class Detect(

    /**
     * 事件id
     */
    val eventId: String = "",

    /**
     * content的表达式
     */
    val contentSpel: String = ""

)

Entre ellos content, necesita ser analizado por expresiones, por lo que lo que se acepta es unString

b. Definir la clase de procesamiento de la anotación @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()
    }
}

Los parámetros deben ponerse en contexto y nombrarse arg1, arg2....


Cuatro, pruébalo

1. Escritura

Escribir después de usar anotaciones:

//简化版代码

@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. Depurar para ver

e0845158cbce667d43ce4717b134dc96.jpeg

  • Valor de anotación obtenido con éxito

  • Expresión analizada con éxito

Supongo que te gusta

Origin blog.csdn.net/g6U8W7p06dCO99fQ3/article/details/131179305
Recomendado
Clasificación