当我尝试使用 Drools 引擎计算保险费时,我遇到了潜在的性能/内存瓶颈。
我在我的项目中使用 Drools 将业务逻辑与 Java 代码分开,我决定也将它用于高级计算。
详情如下:
我必须计算给定合同的保险费。
合约配置为
目前,使用以下公式计算保费:
premium := SI * px * (1 + py) / pz
Run Code Online (Sandbox Code Playgroud)
在哪里:
实现 R1、R2 和 R3 后,我将 Java 代码与业务逻辑分离,任何业务分析师 (BA) 都可以修改公式并添加新的依赖项,而无需重新部署。
我有合同域模型,它由类 Contract、Product、Client、Policy 等组成。合约类定义为:
public class Contract {
String code; // contractCode
double sumInsured; // SI
String clientSex; // M, F
int professionCode; // code from dictionary
int policyYear; // 1..5
int clientAge; //
... // etc.
Run Code Online (Sandbox Code Playgroud)
此外,我引入了Var类,它是任何参数化变量的容器:
public class Var {
public final String name;
public final ContractPremiumRequest request;
private double value; // calculated value
private boolean ready; // true if value is calculated
public Var(String name, ContractPremiumRequest request) {
this.name = name;
this.request = request;
}
...
public void setReady(boolean ready) {
this.ready = ready;
request.check();
}
...
// getters, setters
}
Run Code Online (Sandbox Code Playgroud)
最后 -请求类:
public class ContractPremiumRequest {
public static enum State {
INIT,
IN_PROGRESS,
READY
}
public final Contract contract;
private State state = State.INIT;
// all dependencies (parameterized factors, e.g. px, py, ...)
private Map<String, Var> varMap = new TreeMap<>();
// calculated response - premium value
private BigDecimal value;
public ContractPremiumRequest(Contract contract) {
this.contract = contract;
}
// true if *all* vars are ready
private boolean _isReady() {
for (Var var : varMap.values()) {
if (!var.isReady()) {
return false;
}
}
return true;
}
// check if should modify state
public void check() {
if (_isReady()) {
setState(State.READY);
}
}
// read number from var with given [name]
public double getVar(String name) {
return varMap.get(name).getValue();
}
// adding uncalculated factor to this request – makes request IN_PROGRESS
public Var addVar(String name) {
Var var = new Var(name, this);
varMap.put(name, var);
setState(State.IN_PROGRESS);
return var;
}
...
// getters, setters
}
Run Code Online (Sandbox Code Playgroud)
现在我可以在这样的流程中使用这些类:
request = new ContractPremiumRequest(contract)
state == INITpx = request.addVar( "px" )
Var("px")与ready == falsestate == IN_PROGRESSpy = request.addVar( "py" )px.setValue( factor ), px.setReady( true )
pxready == truerequest.check()使得state == READY如果所有瓦尔准备我已经创建了2 个 DRL 规则并准备了3 个决策表(px.xls、py.xls 等),其中包含 BA 提供的因素。
规则 1 - contract_premium_prepare.drl:
rule "contract premium request - prepare dependencies"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.INIT)
then
insert( $req.addVar("px") );
insert( $req.addVar("py") );
insert( $req.addVar("pz") );
$req.setState(ContractPremiumRequest.State.IN_PROGRESS);
end
Run Code Online (Sandbox Code Playgroud)
规则 2 - contract_premium_calculate.drl:
rule "contract premium request - calculate premium"
when
$req : ContractPremiumRequest (state == ContractPremiumRequest.State.READY)
then
double px = $req.getVar("px");
double py = $req.getVar("py");
double pz = $req.getVar("pz");
double si = $req.contract.getSumInsured();
// use formula to calculate premium
double premium = si * px * (1 + py) / pz;
// round to 2 digits
$req.setValue(premium);
end
Run Code Online (Sandbox Code Playgroud)
决策表 px.xls:
决策表py.xls:
KieContainer 在启动时构造一次:
dtconf = KnowledgeBuilderFactory.newDecisionTableConfiguration();
dtconf.setInputType(DecisionTableInputType.XLS);
KieServices ks = KieServices.Factory.get();
KieContainer kc = ks.getKieClasspathContainer();
Run Code Online (Sandbox Code Playgroud)
现在计算给定合约的溢价,我们写:
ContractPremiumRequest request = new ContractPremiumRequest(contract); // state == INIT
kc.newStatelessKieSession("session-rules").execute(request);
BigDecimal premium = request.getValue();
Run Code Online (Sandbox Code Playgroud)
这是发生的事情:
ContractPremiumRequest[INIT]Var对象)ContractPremiumRequest[READY]并使用公式第一次计算,加载和初始化决策表需要大约 45 秒- 这可能会成为问题。
每次计算(在一些热身之后)大约需要0.8 毫秒——这对我们的团队来说是可以接受的。
堆消耗约为 150 MB——这是有问题的,因为我们预计将使用更多的大表。
========== 编辑(2 年后)==========
这是两年后的简短总结。
正如我们预期的那样,我们的系统已经发展得非常快。我们已经结束了 500 多个表(或矩阵),其中包含保险定价、精算因素、覆盖配置等。一些表的大小超过 100 万行。我们使用了drools,但我们无法处理性能问题。
最后我们使用了Hyperon引擎 ( http://hyperon.io )
这个系统是一个野兽——它允许我们在大约 10 毫秒的总时间内运行数百个规则匹配。
我们甚至能够在 UI 字段上的每个 KeyType 事件上触发完整的策略重新计算。
正如我们所了解的,Hyperon 为每个规则表使用快速内存索引,并且这些索引以某种方式被压缩,因此它们几乎不占用内存。
我们现在还有一个好处——所有定价、因素、配置表都可以在线修改(值和结构),这对 Java 代码是完全透明的。应用程序只是继续以新逻辑工作,不需要开发或重启。
然而,我们需要一些时间和精力来充分了解 Hyperon :)
我发现我们团队一年前做了一些比较——它从 jvisualVM 的角度显示了引擎初始化(drools/hyperon)和 100k 的简单计算:
问题在于您为相对少量的数据创建了大量代码(所有规则均来自表)。我见过类似的案例,它们都受益于将表作为数据插入。PxRow、PyRow 和 PzRow 应该这样定义:
class PxRow {
private String gender;
private int age;
private double px;
// Constructor (3 params) and getters
}
Run Code Online (Sandbox Code Playgroud)
数据仍然可以位于(更简单的)电子表格或任何您喜欢由 BA 研究员输入数据的任何其他位置。您将所有行作为事实 PxRow、PyRow、PzRow 插入。那么你需要一两个规则:
rule calculate
when
$c: Contract( $cs: clientSex, $ca: clientAge,
$pc: professionCode, $py: policyYear,...
...
$si: sumInsured )
PxRow( gender == $cs, age == $ca, $px: px )
PyRow( profCode == $pc, polYear == $py,... $py: py )
PzRow( ... $pz: pz )
then
double premium = $si * $px * (1 + $py) / $pz;
// round to 2 digits
modify( $c ){ setPremium( premium ) }
end
Run Code Online (Sandbox Code Playgroud)
忘记流程和所有其他装饰。但您可能需要另一个规则,以防万一您的合同与 Px 或 Py 或 Pz 不匹配:
rule "no match"
salience -100
when
$c: Contract( premium == null ) # or 0.00
then
// diagnostic
end
Run Code Online (Sandbox Code Playgroud)