V2EX = way to explore
V2EX 是一个关于分享和探索的地方
现在注册
已注册用户请  登录
312ybj
V2EX  ›  推广

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

  •  
  •   312ybj · 2021-11-04 10:34:26 +08:00 · 1867 次点击
    这是一个创建于 1142 天前的主题,其中的信息可能已经有所发展或是发生改变。

    背景

    有天产品找我,“保健,帮我做个需求呗,根据用户的多维度条件来制定 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

    image.png

    
    
    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

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

    OP 对 Drools 动态更新规则有更进一步的了解了吗?
    关于   ·   帮助文档   ·   博客   ·   API   ·   FAQ   ·   实用小工具   ·   2117 人在线   最高记录 6679   ·     Select Language
    创意工作者们的社区
    World is powered by solitude
    VERSION: 3.9.8.5 · 23ms · UTC 00:24 · PVG 08:24 · LAX 16:24 · JFK 19:24
    Developed with CodeLauncher
    ♥ Do have faith in what you're doing.