以前給不少公司和部門作過風控系統的技術分享,聽的時候你們都很過癮,不過不久之後好像都忘記了,沒一我的記得。java
去年公司風控部門採購的風控系統很差用了,要換,我幫他們寫了一個新的。後來部門調整,我不負責了,就交接出去了;近日據說他們部門又採購了一個風控系統,個人也不用了。問緣由說:只有後臺,沒頁面。spring
沒頁面開發一個就行了,幾個頁面有什麼複雜的,不比招標、採購、熟悉一個新的巨無霸風控系統簡單?架構
我估計真實的緣由是:技術很差意思說本身沒弄懂,畢竟代碼寫的仍是有點複雜的。因而我就又寫了一個極簡的風控系統,大概就不到20個類,代碼不超過1000行。併發
雖然較小,功能可不弱運維
1,,支持接入任何業務,配置任何規則,接入任何數據機器學習
2,性能很高,單機tps1.5w+(我我的的2核,小米pro筆記本)ide
——————先講風控系統指標要求和特色——————————————————————————————高併發
若是要給風控系統打個標籤的話,區別於其餘業務系統的核心標籤有幾個性能
標籤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,腳本。
腳本能夠擴展出不少其餘類型,好比決策樹,積分卡等,如今比較火的機器學習用這個實現一個擴展就能夠了。
哦,漏放接口了
如圖