背景
有天产品找我,“保健,帮我做个需求呗,根据用户的多维度条件来制定ES打分策略, 咱们来做AB test”
我。。。。。心中一万个草泥马飘过。 这个产品是出了名的多变,需求变得相当快,我相信每个公司都有个产品大爷,需求永远跟夏天的天气一样多变,一会想要水里有的,一会想要天上飞的。
需求分析
对于“用户多维度条件判断”,这一句话埋的坑就非常大,用户维度有很多
- 用户Id
- 用户性别
- 用户角色
- 用户部门
- 等等等
如果我全部写完,我估计三天三夜都写不完,总之一句话,这个维度非常广。如果我想着用面条式代码 if else 去写,本期需求也能完成。但是以后呢,这个就是个超级无敌大的坑,我得为自己接下的活负责。
drools流程引擎
在咨询组长后, 我总算是找到了一个解决方案:drools流程引擎。这个技术我也是第一次见,听组长说能代替复杂的if else,速度很快, 而且在我们之前合规检测中使用过(快速检验素材是否合规)。当时组长还让我学学,以后说不定有用。
这个家伙号称只有产品想不到的需求,没有它实现不了的需求。就这么神奇。
简介
- 定义
drools是一个易于调整和管理的开源业务规则引擎。不是业务流程。
- 优点:
速度快,兼容java
- 使用场景
公司开发充值发放优惠券活动,具体规则如下:
100元,送10元优惠券
200元,送25元优惠券
300元,送40元优惠券
Java后端攻城师在代码利用if-else代码将业务逻辑实现了功能,这样看似完全没有必要引入什么鬼规则引擎;
但问题出现了:几天后业务人员发现充值的人还是很少,就想修改发放优惠券活动:100元送15元优惠券等......
这时候攻城师忍气吞声修改后端代码,并经过一大堆发布流程进行上线; 一段时间过后客户量多了,业务人员评估后有要减少优惠券的发放金额.......这时候,一场硝烟滚滚而来
所以适用场景为
- 传统代码开发比较复杂繁琐
- 没有优雅的算法
- 经常变化的业务
- 问题虽然不复杂,但是用传统的代码开发比较脆弱,需要经常修改
- 有很多业务专家在(或者多变的产品经理),但是不懂技术
实践
-
基础实践
-
pom配置
<dependency>
<groupId>org.kie</groupId>
<artifactId>kie-api</artifactId>
<version>7.58.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-core</artifactId>
<version>7.58.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-compiler</artifactId>
<version>7.58.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-decisiontables</artifactId>
<version>7.58.0.Final</version>
</dependency>
<dependency>
<groupId>org.drools</groupId>
<artifactId>drools-templates</artifactId>
<version>7.58.0.Final</version>
</dependency>
-
先建立Person对象
package com.tezign.intelligence.search.entity;
import lombok.Data;
import java.math.BigDecimal;
import java.util.List;
@Data
public class Person {
private int age;
private int salary;
}
-
编写rule规则
drl文件建立在resources/rules 文件夹下
package rules;
dialect "mvel"
import com.tezign.intelligence.search.entity.Person
import com.tezign.intelligence.search.entity.Age
import java.math.BigDecimal
rule "rule1"
salience 3
when
$person:Person( age > 25 )
then
$person.salary = 3000;
end
-
配置读取drl文件
读取resources/rules 文件夹下的drl文件
package com.tezign.intelligence.search.config;
import org.kie.api.KieBase;
import org.kie.api.KieServices;
import org.kie.api.builder.KieBuilder;
import org.kie.api.builder.KieFileSystem;
import org.kie.api.builder.KieModule;
import org.kie.api.builder.KieRepository;
import org.kie.api.builder.ReleaseId;
import org.kie.api.runtime.KieContainer;
import org.kie.api.runtime.KieSession;
import org.kie.internal.io.ResourceFactory;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.support.PathMatchingResourcePatternResolver;
import org.springframework.core.io.support.ResourcePatternResolver;
import org.springframework.core.io.Resource;
import java.io.IOException;
@Configuration
public class KiaSessionConfig {
private static final String RULES_PATH = "rules/";
@Bean
public KieFileSystem kieFileSystem() throws IOException {
KieFileSystem kieFileSystem = getKieServices().newKieFileSystem();
for (Resource file : getRuleFiles()) {
kieFileSystem.write(ResourceFactory.newClassPathResource(RULES_PATH + file.getFilename(), "UTF-8"));
}
return kieFileSystem;
}
private Resource[] getRuleFiles() throws IOException {
ResourcePatternResolver resourcePatternResolver = new PathMatchingResourcePatternResolver();
final Resource[] resources = resourcePatternResolver.getResources("classpath*:" + RULES_PATH + "**/*.*");
return resources;
}
@Bean
public KieContainer kieContainer() throws IOException {
final KieRepository kieRepository = getKieServices().getRepository();
kieRepository.addKieModule(new KieModule() {
public ReleaseId getReleaseId() {
return kieRepository.getDefaultReleaseId();
}
});
KieBuilder kieBuilder = getKieServices().newKieBuilder(kieFileSystem());
kieBuilder.buildAll();
return getKieServices().newKieContainer(kieRepository.getDefaultReleaseId());
}
private KieServices getKieServices() {
return KieServices.Factory.get();
}
@Bean
public KieBase kieBase() throws IOException {
return kieContainer().getKieBase();
}
@Bean
public KieSession kieSession() throws IOException {
return kieContainer().newKieSession();
}
}
-
进行规则测试
@Test
public void test(){
Person person = new Person();
person.setAge(26);
session.insert(person);
session.fireAllRules();
log.info("formula {}",person.toString());
}
-----
输出结果
formula Person(age=26, salary=3000)
-
读取数据库的rule
这里规则我放入到数据库中,便于以后上线修改。这里梳理了如何获得KieSession
public KieSession getKieSession(){
InternalKnowledgeBase knowledgeBase = KnowledgeBaseFactory.newKnowledgeBase();
List<SearchFactorFormulaRuleEntity> searchFactorFormulaRuleEntityList = factorFormulaRuleRepository.getFormulaList();
for (int i = 0; ObjectUtils.isNotEmpty(searchFactorFormulaRuleEntityList) && i < searchFactorFormulaRuleEntityList.size(); i++) {
KnowledgeBuilder knowledgeBuilder = KnowledgeBuilderFactory.newKnowledgeBuilder();
SearchFactorFormulaRuleEntity formulaRule = searchFactorFormulaRuleEntityList.get(i);
if(StringUtils.isNoneBlank(formulaRule.getRuleValue())){
StringReader stringReader = new StringReader(formulaRule.getRuleValue());
Resource resource = ResourceFactory.newReaderResource(stringReader);
knowledgeBuilder.add(resource, ResourceType.DRL);
knowledgeBase.addPackages(knowledgeBuilder.getKnowledgePackages());
}
}
return knowledgeBase.newKieSession();
}
-
动态刷新rule
这里我暂时没有特别好的办法,只能通过缓存数据来刷新,从而获得kieSession。
这里获取到的数据如果不在缓存,那么就查询数据库。这样子就实现了数据刷新。
List<SearchFactorFormulaRuleEntity> searchFactorFormulaRuleEntityList = factorFormulaRuleRepository.getFormulaList();
-
如何维护后续rule
- 修改数据库
- 清楚redis中的缓存数据
适应产品的需求
产品的需求是:根据用户的多维度条件来制定ES打分策略。下面是解决方案
- 解决用户不同维度
这里我声明一个对象,里面直接丢个map,想要什么维度我就塞什么维度数据
public class SearchFormulaReq {
private String systemId;
/**
* 用于存储不定参数, 用map是为了方便升级
*/
private Map<String,Object> parameters;
}
- 解决不同维度的打分方案
这里我用公式id表示选择的打分公式,这块的逻辑没有用if else包装,而是用drools来做,以后上线也方便。直接上SQL脚本,往MAP里丢数据就行了
package rules;
dialect "mvel"
import com.tezign.intelligence.tenant.manager.search.domain.SearchRuleDomain;
import java.util.ArrayList;
rule "rule1"
when
$ruleDomain:SearchRuleDomain( systemId == "t1" && parameters["userId"] % 2 == 0 )
then
System.out.println("$ruleDomain = " + $ruleDomain);
$ruleDomain.formulaIdList.add(2L);
end
产品一席话
本以为这个需求被我征服了,能够满足产品这个“小小的需求”。至此,本期需求可以帮助产品完成他的需求:根据用户多维度进行条件判断。
但是当我写完后,找到产品,他却来了句“先按照单一需求来。。。不用那么花里胡哨”。我真是一万匹草泥马飘过。。。。。。