之前给很多公司和部门做过风控系统的技术分享,听的时候大家都很过瘾,不过不久以后好像都忘记了,没一个人记得。
去年公司风控部门采购的风控系统不好用了,要换,我帮他们写了一个新的。后来部门调整,我不负责了,就交接出去了;近日听说他们部门又采购了一个风控系统,我的也不用了。问原因说:只有后台,没页面。
没页面开发一个就好了,几个页面有什么复杂的,不比招标、采购、熟悉一个新的巨无霸风控系统简单?
我估计真实的原因是:技术不好意思说自己没弄懂,毕竟代码写的还是有点复杂的。于是我就又写了一个极简的风控系统,大概就不到20个类,代码不超过1000行。
虽然较小,功能可不弱
1,,支持接入任何业务,配置任何规则,接入任何数据
2,性能很高,单机tps1.5w+(我个人的2核,小米pro笔记本)
——————先讲风控系统指标要求和特点——————————————————————————————
如果要给风控系统打个标签的话,区别于其他业务系统的核心标签有几个
标签1,博弈,快速变化,高灵活性
标签2,高性能,高并发,大数据
风控的性质本身就是博弈,就是一个变化的过程。风控系统必须如实反映,适应这种性质。只有将规则的变化成本降到最低,才能满足博弈的快速变化的要求。所以多数的风控系统本身都是基于规则的,规则都是可配的。并且我们要求这种可配置的规则,能够满足任何可能的逻辑表达式。
风控的业务性质决定了他是一个请求,检测N个规则,需要M笔数据(每条规则m笔),从交易到规则到数据是一个IO和计算不断放大的过程,而风控系统本身是一个长尾系统,它最多有核心系统零点几或者几倍但不可能有超出核心系统N*M倍的预算,这意味着我们对风控系统的性能要求也是很高的——我们不能以核心系统同样的代码质量和技术设计来要求他,必须精心设计它,它才能满足大数据,高并发,高性能和低成本低预算的要求。
但是,对于大多数的公司来说,TPS就那么几个,数据就那么几条,即使放大N*M倍也不过几百,随便找台机器做风控硬件基础都绰绰有余。即使代码写的稍微烂一点,加点机器也抗的住。但是要招一个精通风控,同时能优化性能几倍几十倍的技术架构,是比较难的,是不划算的,也是没意义的。
同时,在这种情况下,反而风控系统的可伸缩性要求很高。风控系统能不能适应这种只部署一台或者几台的极简化部署?
对于这99%的需求,现有市面上的绝大多数风控系统,都显得的太臃肿了。他们吹嘘了太多无用的概念,带来了太高的运维,和扩展成本,而这些,对于这99%的用户,基本毫无意义。
————————————————————————核心设计图
里面关键几个设计部分是
1,基于事件,数据和规则|逻辑分离(VarHandler接口和varNotify皆苦)。这样这个系统才有很高的性能
2,基于引擎接口,实现各种简单|复杂,效率和功能的平衡适配
3,统一了传统风控系统中的事前事中事后风控系统设计,三者合一(即使ResultProcessor部分设计)————事实上在技术上根本不用区分什么事前,事后。只需要指定一个timeout,如果在timeout以前检测出风险了,不需要区分这个规则是事前规则还是时候规则,实时返回就可以了,这就是所谓的事前。这里的本质是:检测速度到底能有多快,多准确。如果所有规则都能100%命中,纳秒级返回,那所有规则都可以是实时。这是传统风控一个误区很大的地方。是技术被业务误导很严重的地方。
——————————————核心代码
package wanglin.inspect; import lombok.extern.slf4j.Slf4j; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.Set; import java.util.concurrent.*; @Service @Slf4j public class InspectServiceImpl extends BaseService implements InspectService { @Autowired RuleService ruleService; ConcurrentMap<InspectContext, CountDownLatch> locks = new ConcurrentHashMap<>(); @Override public Object inspect(String bizType, Object request) throws TimeoutException { Object result = null; InspectContext context = createInspectContext(bizType, request); try { log.info("受理检测请求{},{},{}", context.uuid, context.bizType, context, request); execute(context); result = getInspectResult(context); return result; } catch (TimeoutException e){ log.info("检测请求超时{},{},{}", context.uuid, e); throw e; } finally { log.info("检测请求结果{},{},{}", context.uuid, result); } } @Override public void varValueNotify(String uuid, String varName, Object value) { log.info("收到变量回调{},{},{}", uuid, varName,value); InspectContext context = getInspectContext(uuid); ruleService.varNotify(uuid, varName, context); //set request result configuration.getResultProcessor(context.bizType.resultProcessorName).process(context); //notify实时线程 if (null != context.result) { if (null != locks.get(context)) { locks.get(context).countDown(); } } } private Object getInspectResult(InspectContext context) throws TimeoutException { CountDownLatch cdl = new CountDownLatch(1); try { locks.put(context, cdl); if (cdl.await(context.bizType.timeout, TimeUnit.MILLISECONDS)) { return context.result; } else { throw new TimeoutException("交易检测超时"); } } catch (InterruptedException e) { throw new RuntimeException("交易检测侧异常"); } finally { locks.remove(context); } } private void execute(InspectContext context) { context.vars.forEach((varName, var) -> { configuration.getVarHandler(var).handle(context.uuid, varName, context.bizType, context.request); }); } private InspectContext createInspectContext(String bizType, Object request) { BizType bzType = configuration.getBizType(bizType); Set<Rule> rules = configuration.getRules(bizType); Set<Var> vars = configuration.getVars(bizType); return new InspectContext(bzType, request, rules, vars); } }
package wanglin.inspect; import org.springframework.stereotype.Service; import java.util.Set; @Service public class RuleService extends BaseService { public void varNotify(String uuid, String varName,InspectContext context) { Set<Rule> rules = referRules(uuid, varName); rules.forEach(rule -> { try { EngineService engine = configuration.getEngine(rule.engine); Object ruleContext = engine.buildRuleContext(context); Object ruleResult = engine.execute(rule, ruleContext); context.getRule(rule).setResult(ruleResult); }catch (Exception e){ context.getRule(rule).setException(e); } }); } private Set<Rule> referRules(String uuid, String varName) { return null; } }
其他类不贴了,这两个类包含了一个极简化的风控核心的逻辑。
任何一个设计良好的系统的核心代码,都是极简的。dubbo其实原型就几十行,其他都是在进行一些工程上的处理。
这个核心麻雀虽小,五张俱全。
可以通过VarHandler接口进行数据扩展
可以通过EngineService进行引擎扩展
可以通过ResultProcessor接口进行结果处理方式的扩展
比如业务需要规则,任何用户非大学毕业就拒绝交易
假设请求数据{username:xxx,cardid:xxxxxxx....}biztype为testBizType
1,设定规则 $user. schooling < 4 //1小学,2初中,3,高中,4,大学
2,写一个UserHandler 实现 VarHandler
public UserHandler implements VarHandler{
InspectService inspectService;
public void handle(...){
//从第三方获取用户学历
inspectService.varNotify(uuid,"user",xxxx);
}
}
EngineService如下
public interface EngineService { /** * 解析规则中需要的变量 * @param rule * @return */ Set<String> analyze(Rule rule); /** * 执行规则 * @param rule * @param ruleContext * @return */ Object execute(Rule rule, Object ruleContext); /** * 构建引擎上下文 * @param inspectContext * @return */ Object buildRuleContext(InspectContext inspectContext); }
一般上会有两个实现:EL,脚本。
脚本可以扩展出很多其他类型,比如决策树,积分卡等,现在比较火的机器学习用这个实现一个扩展就可以了。
哦,漏放接口了
如图