之前两篇文章是去年调研和自研规则引擎的存货,今天是最后一篇,后记。
有人会问,标题不是写的动手撸吗?哪里体现撸了?
其实撸起来一个引擎并不复杂,为了体现架构思想,调研心得和设计思想反而更重要,相信优秀如你写代码没有任何压力的。
那我就和大家聊聊业务背景和引擎要求。
比如[券表],对于字段属性有一定的规则要求,比如券的互斥属性需要做一定的校验,比如change-config是个json,需要进行解析之后和detail信息做规则校验,等其他的一些规则。梳理出来的需要主要设计到字段属性的处理,而没有涉及到复杂的流程,数据问题的处理。
硬编码:适用于规则不易变场景。
优点
缺点
Drools:开源规则引擎
流程:业务分析师编写业务需求文档,开发工程师根据规则进行DSL规则编写,DSL规则入库,Drools引擎根据规则库规则进行解析,动态执行规则。
优点
缺点
规则复杂之后,依然存在不好维护问题,某种程度上甚至比硬编码糟糕。
过多的if,else,when,then不利于维护。
基于Spark数据处理规则引擎
如果场景涉及大部分规则是数据处理,则可以认为此场景规则处理等于数据处理。为商业分析师提供友好可视化规则界面。规则引擎将配置信息解析为Spark作业进行计算。
优点
缺点
基本假设
n个规则输入,一个规则结果输出;规则支持基本的逻辑运算,算数运算,关系运算,属性判断等;多个原子语义规则之间可聚合,可复用,可拆分;
性能要求
可用性要求
规则引擎可降级,不影响主流程;
系统设计要求
功能要求
设计方案
技术点
难点
规则解析
示例
赋值
coupon.setId(100L);
coupon.setAcctId(5L);
coupon.setRemark("备注备注备注备注备注1");
coupon.setType((byte) 5);
coupon.setDetail("{\"an\":\"\",\"charge_detail\":[{\"charge_amount\":100000,\"charge_side\":\"1\",\"order_no\":1,\"setAn\":false,\"setCharge_amount\":true,\"setCharge_side\":true,\"setOrder_no\":true},{\"an\":\"17D03004416N001S0009T187R00493A0080$C1CB4\",\"charge_amount\":20000,\"charge_side\":\"14010\",\"order_no\":2,\"setAn\":true,\"setCharge_amount\":true,\"setCharge_side\":true,\"setOrder_no\":true}],\"charge_detailIterator\":[{\"$ref\":\"$.charge_detail[0]\"},{\"$ref\":\"$.charge_detail[1]\"}],\"charge_detailSize\":2,\"charge_method\":0,\"charge_side\":\"1\",\"charge_type\":1,\"default_charge_side\":\"1\",\"default_side_an\":\"\",\"setAn\":true,\"setCharge_detail\":true,\"setCharge_method\":true,\"setCharge_side\":true,\"setCharge_type\":true,\"setDefault_charge_side\":true,\"setDefault_side_an\":true}");
coupon.setCount(120000L);
规则
rule.setKey("id");
rule.setRule("id > 10");
rule.setErrMessage("id 必须大于 10");
rule.setKey("type");
rule.setRule("acctId == 5 && type == 5");
rule.setErrMessage("acctId = 5 type 必须为5");
rule.setKey("remark");
rule.setRule("string.length(' remark ') > 10"); // 注意表达式都有空格
rule.setErrMessage("remark长度不能小于10");
rule.setKey("detail");
rule.setRule("(<json>detail.charge_detail.(0).charge_amount</json> + <json>detail.charge_detail.(1).charge_amount</json>) == <json>count</json>");
rule.setRuleTypeEnum(CompassRuleTypeEnum.JSON);
rule.setErrMessage("charge_amount值不相等");
Json
{
"an":"",
"charge_detail":[
{
"charge_amount":100000,
"charge_side":"1",
"order_no":1,
"setAn":false,
"setCharge_amount":true,
"setCharge_side":true,
"setOrder_no":true
},
{
"an":"17D03004416N001S0009T187R00493A0080$C1CB4",
"charge_amount":20000,
"charge_side":"14010",
"order_no":2,
"setAn":true,
"setCharge_amount":true,
"setCharge_side":true,
"setOrder_no":true
}
],
"charge_detailIterator":[
{
"$ref":"$.charge_detail[0]"
},
{
"$ref":"$.charge_detail[1]"
}
],
"charge_detailSize":2,
"charge_method":0,
"charge_side":"1",
"charge_type":1,
"default_charge_side":"1",
"default_side_an":"",
"setAn":true,
"setCharge_detail":true,
"setCharge_method":true,
"setCharge_side":true,
"setCharge_type":true,
"setDefault_charge_side":true,
"setDefault_side_an":true
}
方案和核心代码实现起来比较简单,但是一个非业务相关项目可能面对着两个问题,做大和做小。
做小
指的是只满足当前业务场景和需求,这样可最快的实现需求,但后续如果有类似的需求不能满足或不易定制,这样最开始引入这个项目的目的全无。
做大
指的是如果做成一个业务和技术上都可用的项目,达到一个平台的效果,则需要在研发投入更多的时间。
包括代码细节,技术方案,UI界面等,后续在系统稳定性方案也需要投入一些时间,这样做一个非业务相关性的东西投入这么大是否值得,比如需要大概投入了3,4个人力,完整周期持续了小2个月。
规则引擎有多个场景:风控场景,业务场景。
风控场景
属于风控产品线产品,整体上功能比较完备,但是对于一般场景显得重一些,引入了场景,规则,规则因子,appkey,单元测试等很多新的概念,整体上比较重。
业务场景
业务场景比较轻量级一些,对于我们的场景支持的还可以,引入上对于代码入侵可以接受。
决定采用这种低侵入方案进行对接。
调研收获
进行调研之前,自己对于业务场景对于规则引擎的需求进行了一定的设计和代码开发,在考虑上存在一些问题。
通用的方案
调研了一圈,大家大家在实现细节上都很类似,比如基于Aviator的表达式,通过Zk,MQ,DataBus的规则下发,规则放到内存中不存在跨进程调用。
离线方案基于Hive进行分析。
完全避免代码侵入 我自己设计的方案上想对代码做到尽量少的代码入侵,甚至零侵入,比如通过AOP的方式实现零侵入。
但经过深度思考后,发现完全的零侵入会限制规则在程序中的能力,调研了几个方案之后,发现所有的方案都存在代码侵入。
得到的收获是,不要为了某种洁癖达到零侵入,适当的代码侵入更有助于规则引擎的表达,只要做好侵入更友好就可以。
规则沉淀
通过规则管理平台配置和修改规则,基于MySql存储。规则配置采用多租户管理,便于不同团队进行操作。
日志存储
监控告警
对于使用流程进行埋点监控,可视化报表查看。
接入方式
核心功能