前言
我的优惠券作为营销玩法的一种运营工具,在营销活跃场中起到很至关重要的作用。如何更加高效的赋能业务,助理业务发展,灵活扩展业务,是我们一直追求和思考的方向
一、背景
1.1 现状
营销中台作为券的“供应链端”,控制券的所有类型。
我的优惠券作为工具,提供用户已有优惠券的展示列表,不同类型的券利益点不同,运营会提供各自展示规则。
谋略作为用户触达方,为了提高券的核销率,会对用户做过期提醒push,同时触达文案要求跟券的营销文案一致。

1.2 挑战点
-
1、营销中台每次新增券类型,都需要运营指定营销文案后,由研发硬编码实现。能不能支持业务运营人员根据需求灵活扩展,动态配置营销文案,并且能够及时生效呢?
-
2、消息中心的push提醒文案需要跟营销展示文案一致,那就由业务侧研发硬编码实现一套,消息中心侧实现一套,并且还得保证两处的规则逻辑一致才可以。
能不能将这种相同的规则抽取出来,以订阅的方式下发到订阅者上去,既保证规则的唯一性,也能够做到规则共享?
二、解决方案
鉴于上述场景的痛点,我们接入ZCube平台来解决
2.1 接入ZCube
官网地址:https://zcube.jr.jd.com文章介绍:《ZCube:会员权益体系规则引擎原理介绍 【一】》
首先,在ZCube平台接入我的优惠券应用

搭建优惠券运营玩法规则

展示逻辑映射规则

发布知识包

2.2 应用系统接入SDK
maven坐标依赖
1<!-- 规则引擎客户端SDK --> 2<dependency> 3 <groupId>com.jd.jdt.rule.core</groupId> 4 <artifactId>rule-core-client-spring</artifactId> 5 <version>1.0.1-SNAPSHOT</version> 6</dependency> 7
properties参数配置
1rule.env=prod 2rule.appName=jrm_member_center 3rule.secret=xxx 4rule.packages=xxx 5
spring xml配置
1<?xml version="1.0" encoding="UTF-8"?> 2<beans xmlns="http://www.springframework.org/schema/beans" 3 xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" 4 xsi:schemaLocation="http://www.springframework.org/schema/beans 5 http://www.springframework.org/schema/beans/spring-beans-3.1.xsd" 6 default-lazy-init="false"> 7 8 <bean id="ruleExecuteService" class="com.jd.jdt.rule.core.client.service.impl.RuleExecuteServiceImpl"> 9 <property name="knowledgeCacheService" ref="knowledgeCachePushService"/> 10 </bean> 11 <bean id="knowledgeCachePushService" class="com.jd.jdt.rule.core.client.service.impl.KnowledgeCachePushService"> 12 <property name="cloudFileInnerService" ref="cloudFileInnerService"/> 13 </bean> 14 <bean id="cloudFileInnerService" class="com.jd.jdt.rule.core.client.service.impl.CloudFileInnerService"> 15 <constructor-arg index="0" value="#{frozenParamBean['rule.env']}"/> 16 </bean> 17 <bean id="ruleClient" class="com.jd.jdt.rule.core.client.etcd.RuleClient"> 18 <constructor-arg index="0" value="#{frozenParamBean['rule.appName']}"/> 19 <constructor-arg index="1" value="#{frozenParamBean['rule.env']}"/> 20 <constructor-arg index="2" value="#{frozenParamBean['rule.packages']}"/> 21 <constructor-arg index="3" ref="knowledgeCachePushService"/> 22 </bean> 23</beans> 24
api调用
1RuleExecutionResult ruleExecutionResult = ruleExecuteService.fireRules(knowPackageName, param); 2
2.3 AB方式灰度上线
为了保证现有功能的稳定,我们采用AB方式灰度上线,配置在白名单内的用户走规则引擎执行的逻辑,否则走原硬编码逻辑。

三、结果展示
C端页面展示

SGM执行方法监控,tp99基本在1ms,cpu及内存较稳定,对系统原业务逻辑基本无影响


四、改造后的优势
1、营销中台新增品后,只需要在策略中心可视化配置,0代码
2、策略规则支持热部署,发布审批即生效
3、业务规则以知识库形式存储,所见即所得,便于业务侧优惠券资源治理
五、压测
5.1 压测对象
| -- | 描述 | 备注 | |
|---|---|---|---|
| 1 | 压测案例项目 | SDK客户端示例项目 | fin-rule-client-example |
| 2 | 压测机器 | 用户持有优惠券接口 | 接口:com.jd.jdt.rule.engine.core.example.facade.IRuleExeFacade 别名:fin-rule-client-example |
| 3 | 执行知识包01 exePackage01 执行知识包03 exePackage03 执行aviator01 exeRule03 | SDK客户端示例项目 | fin-rule-client-example |
| 4 | 压测机器 | 指定预发机器 | 11.248.242.133 100.99.122.24 |
5.2 压测平台
压测脚本:
| 压测方法 | 压测脚本 | |
|---|---|---|
| 1 | exePackage01 | exePackage01.groovy |
| 2 | exePackage03 | exePackage03.groovy |
| 3 | exeRule03 | exeRule03.groovy |
exePackage01
1package jsf 2import com.jd.fastjson.JSON 3import com.jd.fastjson.JSONObject 4import com.jd.forcebot.engine.TestUtils 5import com.jd.forcebot.engine.groovy.Lifecycle 6import com.jd.forcebot.engine.groovy.RatePolicy 7import com.jd.forcebot.engine.groovy.TestCase 8import com.jd.forcebot.engine.groovy.TestSuite 9import com.jd.forcebot.toolkit.parameterized.latest.AsciiFileAccessArbitrarily 10import com.jdd.test.performance.common.GenericServiceInvoker 11import org.slf4j.Logger 12 13@TestSuite(value = "forcebot", lifecycle = Lifecycle.THREAD, ratePolicy = RatePolicy.STANDARD) 14class TestQueryRightsCardDetail { 15 public final Logger logger = TestUtils.LOGGER; 16 public static GenericServiceInvoker genericServiceInvoker; 17 public static AsciiFileAccessArbitrarily realPinFile = new AsciiFileAccessArbitrarily("test_1w_pin.txt"); 18 static { 19 genericServiceInvoker = new GenericServiceInvoker(); 20 genericServiceInvoker.initService("com.jd.jdt.rule.engine.core.example.facade.IRuleExeFacade","fin-rule-client-example"); 21 } 22 23 String getPin() { 24 return realPinFile.readLine().trim(); 25 } 26 27 @TestCase( record = false) 28 void queryRightsCardDetail() { 29 // 真实pin 30 String pin = getPin(); 31 logger.info("pin: {}", pin); 32 33 String tranName = "exePackage01"; 34 35 JSONObject param = new JSONObject(); 36 param.put("pin", pin); 37 param.put("clientIP", "127.0.0.1"); 38 39 String strParam = JSON.toJSONString(param); 40 41 42 43 44 try { 45 TestUtils.transactionBegin(tranName); 46 Object result = genericServiceInvoker.invoke("exePackage01",new String[]{"java.lang.Integer","java.lang.Integer"},new Object[]{45,45}); 47 logger.info("{} result: {}", tranName, result); 48 boolean ret = resultCheck(result, tranName); 49 } catch (Exception ex) { 50 TestUtils.transactionFailure(tranName); 51 logger.error(ex.getMessage(), ex); 52 } 53 } 54 55 boolean resultCheck(Object result, String tranName) { 56 if (result == null) { 57 logger.error("{} exec error, result is null.", tranName); 58 return false; 59 } 60 61 if (!(result instanceof Map)) { 62 logger.error("{} exec error, result is {}.", tranName, result); 63 return false; 64 } 65 66 Map<String, Object> resultMap = result; 67 logger.info(tranName + " result: {}.", resultMap); 68 69 if (resultMap == null || resultMap.size() == 0) { 70 logger.warn("{} exec error, result map size is zero.", tranName); 71 TestUtils.transactionFailure(tranName); 72 return false; 73 } 74 TestUtils.transactionSuccess(tranName); 75 // logger.info("{} exec succeed, result: {}.", tranName, resultMap); 76 return true; 77 } 78 79 80} 81
exePackage03
1package jsf 2import com.jd.fastjson.JSON 3import com.jd.fastjson.JSONObject 4import com.jd.forcebot.engine.TestUtils 5import com.jd.forcebot.engine.groovy.Lifecycle 6import com.jd.forcebot.engine.groovy.RatePolicy 7import com.jd.forcebot.engine.groovy.TestCase 8import com.jd.forcebot.engine.groovy.TestSuite 9import com.jd.forcebot.toolkit.parameterized.latest.AsciiFileAccessArbitrarily 10import com.jdd.test.performance.common.GenericServiceInvoker 11import org.slf4j.Logger 12 13@TestSuite(value = "forcebot", lifecycle = Lifecycle.THREAD, ratePolicy = RatePolicy.STANDARD) 14class TestQueryRightsCardDetail { 15 public final Logger logger = TestUtils.LOGGER; 16 public static GenericServiceInvoker genericServiceInvoker; 17 public static AsciiFileAccessArbitrarily realPinFile = new AsciiFileAccessArbitrarily("test_1w_pin.txt"); 18 static { 19 genericServiceInvoker = new GenericServiceInvoker(); 20 genericServiceInvoker.initService("com.jd.jdt.rule.engine.core.example.facade.IRuleExeFacade","fin-rule-client-example"); 21 } 22 23 String getPin() { 24 return realPinFile.readLine().trim(); 25 } 26 27 @TestCase( record = false) 28 void queryRightsCardDetail() { 29 // 真实pin 30 String pin = getPin(); 31 logger.info("pin: {}", pin); 32 33 String tranName = "exePackage03"; 34 35 JSONObject param = new JSONObject(); 36 param.put("pin", pin); 37 param.put("clientIP", "127.0.0.1"); 38 param.put("orderType", "ssahhh"); 39 param.put("bizCode", "qwerrtta"); 40 41 String strParam = JSON.toJSONString(param); 42 43 44 try { 45 TestUtils.transactionBegin(tranName); 46 Object result = genericServiceInvoker.invoke("exePackage03","com.jd.jdt.rule.engine.core.example.domain.MqVar",strParam); 47 logger.info("{} result: {}", tranName, result); 48 boolean ret = resultCheck(result, tranName); 49 } catch (Exception ex) { 50 TestUtils.transactionFailure(tranName); 51 logger.error(ex.getMessage(), ex); 52 } 53 } 54 55 boolean resultCheck(Object result, String tranName) { 56 if (result == null) { 57 logger.error("{} exec error, result is null.", tranName); 58 return false; 59 } 60 61 if (!(result instanceof Map)) { 62 logger.error("{} exec error, result is {}.", tranName, result); 63 return false; 64 } 65 66 Map<String, Object> resultMap = result; 67 logger.info(tranName + " result: {}.", resultMap); 68 69 if (resultMap == null || resultMap.size() == 0) { 70 logger.warn("{} exec error, result map size is zero.", tranName); 71 TestUtils.transactionFailure(tranName); 72 return false; 73 } 74 TestUtils.transactionSuccess(tranName); 75 // logger.info("{} exec succeed, result: {}.", tranName, resultMap); 76 return true; 77 } 78 79 80} 81
execRule03
1package jsf 2import com.jd.fastjson.JSON 3import com.jd.fastjson.JSONObject 4import com.jd.forcebot.engine.TestUtils 5import com.jd.forcebot.engine.groovy.Lifecycle 6import com.jd.forcebot.engine.groovy.RatePolicy 7import com.jd.forcebot.engine.groovy.TestCase 8import com.jd.forcebot.engine.groovy.TestSuite 9import com.jd.forcebot.toolkit.parameterized.latest.AsciiFileAccessArbitrarily 10import com.jdd.test.performance.common.GenericServiceInvoker 11import org.slf4j.Logger 12 13@TestSuite(value = "forcebot", lifecycle = Lifecycle.THREAD, ratePolicy = RatePolicy.STANDARD) 14class TestQueryRightsCardDetail { 15 public final Logger logger = TestUtils.LOGGER; 16 public static GenericServiceInvoker genericServiceInvoker; 17 public static AsciiFileAccessArbitrarily realPinFile = new AsciiFileAccessArbitrarily("test_1w_pin.txt"); 18 static { 19 genericServiceInvoker = new GenericServiceInvoker(); 20 genericServiceInvoker.initService("com.jd.jdt.rule.engine.core.example.facade.IRuleExeFacade","fin-rule-client-example"); 21 } 22 23 String getPin() { 24 return realPinFile.readLine().trim(); 25 } 26 27 @TestCase( record = false) 28 void queryRightsCardDetail() { 29 // 真实pin 30 String pin = getPin(); 31 logger.info("pin: {}", pin); 32 33 String tranName = "execRule03"; 34 35 JSONObject params = new JSONObject(); 36 params.put("orderType","1"); 37 params.put("orderType","1"); 38 params.put("bizCode",1); 39 params.put("subBizCode",0); 40 params.put("merchantCode","200022"); 41 params.put("subMerchantCode","110597078001"); 42 43 JSONObject param = new JSONObject(); 44 param.put("ruleId", "bt"); 45 param.put("params", params); 46 47 String strParam = JSON.toJSONString(param); 48 49 50 try { 51 TestUtils.transactionBegin(tranName); 52 Object result = genericServiceInvoker.invoke("execRule03","com.jd.jdt.rule.engine.core.example.domain.RuleRequest",strParam); 53 logger.info("{} result: {}", tranName, result); 54 boolean ret = resultCheck(result, tranName); 55 } catch (Exception ex) { 56 TestUtils.transactionFailure(tranName); 57 logger.error(ex.getMessage(), ex); 58 } 59 } 60 61 boolean resultCheck(Object result, String tranName) { 62 if (result == null) { 63 logger.error("{} exec error, result is null.", tranName); 64 return false; 65 } 66 67 if (!(result instanceof Map)) { 68 logger.error("{} exec error, result is {}.", tranName, result); 69 return false; 70 } 71 72 Map<String, Object> resultMap = result; 73 logger.info(tranName + " result: {}.", resultMap); 74 75 if (resultMap == null || resultMap.size() == 0) { 76 logger.warn("{} exec error, result map size is zero.", tranName); 77 TestUtils.transactionFailure(tranName); 78 return false; 79 } 80 TestUtils.transactionSuccess(tranName); 81 // logger.info("{} exec succeed, result: {}.", tranName, resultMap); 82 return true; 83 } 84 85 86} 87
模拟调用:easyone接口调用
5. 3 压测指标监控
SGM监控:
sgm方法监控
5. 4 结论
demo
| tps峰值 | tp99 | tp999 | 成功率 | 内存使用率 | cpu使用率 | |
|---|---|---|---|---|---|---|
| 1 | 10000 | 1 | 1 | 100% | 12.6% | 31.6% |
| 2 | 19500 | 1 | 1 | 100% | 12.6% | 53.7% |
白条规则
| tps峰值 | tp99 | tp999 | 成功率 | 内存使用率 | cpu使用率 | |
|---|---|---|---|---|---|---|
| 1 | 2500 | 1 | 1 | 100% | 12.5% | 9.88% |
| 2 | 3000 | 1 | 1 | 100% | 12.5% | 11.1% |
| 3 | 3500 | 1 | 1 | 100% | 12.6% | 13.0% |
| 4 | 4000 | 1 | 1 | 100% | 12.5% | 13.9% |
| 5 | 5000 | 1 | 1 | 100% | 12.5% | 17.9% |
| 6 | 6300 | 1 | 1 | 100% | 12.5% | 21.6% |
| 7 | 8400 | 1 | 1 | 100% | 12.5% | 26.9% |
| 8 | 10500 | 1 | 1 | 100% | 12.5% | 33.7% |
| 9 | 14500 | 1 | 1 | 100% | 12.5% | 46.1% |
| 10 | 19200 | 1 | 1 | 100% | 12.5% | 59.2% |
白条规则 aviator执行
| tps峰值 | tp99 | tp999 | 成功率 | 内存使用率 | cpu使用率 | |
|---|---|---|---|---|---|---|
| 1 | 6100 | 1 | 1 | 100% | 12.4% | 18.4% |
| 2 | 10500 | 1 | 1 | 100% | 12.4% | 29.6% |
| 3 | 20000 | 1 | 1 | 100% | 12.5% | 51.9% |
| 压测tps指标 | 执行方式 | SGM监控 | JDOS实例监控 | |
|---|---|---|---|---|
| 1 | 6000 | aviator执行 | ![]() | ![]() |
| 2 | 6000 | package执行 | ![]() | ![]() |
| 3 | 10000 | aviator执行 | ![]() | ![]() |
| 4 | 10000 | package执行 | ![]() | ![]() |
| 5 | 20000 | aviator执行 | ![]() | ![]() |
| 6 | 20000 | package执行 | ![]() | ![]() |
作者:京东科技 王芳
来源:京东云开发者社区 转载请注明来源












