产品什么都要, 我差点给他真人修皮,反手就给他掏出个大宝贝

2021-11-04 10:34:26 +08:00
 312ybj

背景

有天产品找我,“保健,帮我做个需求呗,根据用户的多维度条件来制定 ES 打分策略, 咱们来做 AB test”

我。。。。。心中一万个草泥马飘过。 这个产品是出了名的多变,需求变得相当快,我相信每个公司都有个产品大爷,需求永远跟夏天的天气一样多变,一会想要水里有的,一会想要天上飞的。

需求分析

对于“用户多维度条件判断”,这一句话埋的坑就非常大,用户维度有很多

  1. 用户 Id
  2. 用户性别
  3. 用户角色
  4. 用户部门
  5. 等等等

如果我全部写完,我估计三天三夜都写不完,总之一句话,这个维度非常广。如果我想着用面条式代码 if else 去写,本期需求也能完成。但是以后呢,这个就是个超级无敌大的坑,我得为自己接下的活负责。

drools 流程引擎

在咨询组长后, 我总算是找到了一个解决方案:drools 流程引擎。这个技术我也是第一次见,听组长说能代替复杂的 if else ,速度很快, 而且在我们之前合规检测中使用过(快速检验素材是否合规)。当时组长还让我学学,以后说不定有用。

这个家伙号称只有产品想不到的需求,没有它实现不了的需求。就这么神奇。

简介

  1. 定义

drools 是一个易于调整和管理的开源业务规则引擎。不是业务流程。

  1. 优点:

速度快,兼容 java

  1. 使用场景

公司开发充值发放优惠券活动,具体规则如下:
100 元,送 10 元优惠券
200 元,送 25 元优惠券
300 元,送 40 元优惠券
Java 后端攻城师在代码利用 if-else 代码将业务逻辑实现了功能,这样看似完全没有必要引入什么鬼规则引擎;
但问题出现了:几天后业务人员发现充值的人还是很少,就想修改发放优惠券活动:100 元送 15 元优惠券等......
这时候攻城师忍气吞声修改后端代码,并经过一大堆发布流程进行上线; 一段时间过后客户量多了,业务人员评估后有要减少优惠券的发放金额.......这时候,一场硝烟滚滚而来

所以适用场景为

实践

  1. 基础实践

  1. 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>

  1. 先建立 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;



}
  1. 编写 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
  1. 配置读取 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();

    }

}
  1. 进行规则测试



@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)
  1. 读取数据库的 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();

}
  1. 动态刷新 rule

这里我暂时没有特别好的办法,只能通过缓存数据来刷新,从而获得 kieSession 。

这里获取到的数据如果不在缓存,那么就查询数据库。这样子就实现了数据刷新。

List<SearchFactorFormulaRuleEntity> searchFactorFormulaRuleEntityList = factorFormulaRuleRepository.getFormulaList();
  1. 如何维护后续 rule

    1. 修改数据库
    2. 清楚 redis 中的缓存数据

适应产品的需求

产品的需求是:根据用户的多维度条件来制定 ES 打分策略。下面是解决方案

  1. 解决用户不同维度

这里我声明一个对象,里面直接丢个 map ,想要什么维度我就塞什么维度数据



public class SearchFormulaReq {



    private String systemId;



    /**

 * 用于存储不定参数, 用 map 是为了方便升级

 */

 private Map<String,Object> parameters;



}
  1. 解决不同维度的打分方案

这里我用公式 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

产品一席话

本以为这个需求被我征服了,能够满足产品这个“小小的需求”。至此,本期需求可以帮助产品完成他的需求:根据用户多维度进行条件判断。

但是当我写完后,找到产品,他却来了句“先按照单一需求来。。。不用那么花里胡哨”。我真是一万匹草泥马飘过。。。。。。

参考

https://www.jianshu.com/p/de5789b29927

1644 次点击
所在节点    推广
2 条回复
Anshay
2022-02-12 00:37:47 +08:00
最后一句和我们的产品一个口吻😂
giter
263 天前
从数据库加载成 List ,如果不 dispose 的话,后续数据库新增规则记录对当前 kieSession ( stateful ) 不生效(或许我的方式不对)。但如果 dispose 的话,新规则或更新原有规则会立即生效,但频繁创建和释放 kieSession 又会导致开销变大的问题。

OP 对 Drools 动态更新规则有更进一步的了解了吗?

这是一个专为移动设备优化的页面(即为了让你能够在 Google 搜索结果里秒开这个页面),如果你希望参与 V2EX 社区的讨论,你可以继续到 V2EX 上打开本讨论主题的完整版本。

https://www.v2ex.com/t/812938

V2EX 是创意工作者们的社区,是一个分享自己正在做的有趣事物、交流想法,可以遇见新朋友甚至新机会的地方。

V2EX is a community of developers, designers and creative people.

© 2021 V2EX