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?
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):
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 lua
guiones, porque solo son key
diferentes, 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
Valor de anotación obtenido con éxito
Expresión analizada con éxito