抽奖系统
营销平台 - 抽奖系统
抽奖系统
系统流程
抽奖算法
例: 设 A 奖品概率 20%、 B 奖品概率 30%、 C 奖品概率 50%。
总体概率抽奖算法 算法描述:分别把A、B、C对应的概率值转换成阶梯范围值,A=(0~0.2」、B=(0.2-0.5」、C=(0.5-1.0」, 当使用随机数方法生成一个随机数后,与阶梯范围值进行循环比对找到对应的区域,匹配到中奖结果。o(n) 具体操作是:将 A 转为 20, B 为 30, C 为 50,与随机数进行比较确认即可。 (从 A - C 开始遍历)
单项概率抽奖算法 根据概率值存放到斐波那契散列数组进行存放结果,根据概率值直接定义中奖结果,时间复杂度由 o(n)降低到 o(1)。 具体操作:同样是将 A 转为 20, B 为 30, C 为 50; 创建长度 127 的数组,A、B、C 生成的 1-100 值 会散列到 127 个数组中,根据斐波那契散列算法的特点,在不超过 128 的值生成的散列值不会发生冲突, 虽然 127 长度的数组中会有 null 值,但在获取时也不会散列到这些 null 值位置上,因此概率不会受到影响。
/** * HASH_INCREMENT = 0x61c88647 * 斐波那契散列增量,逻辑:黄金分割点:(√5 - 1) / 2 = 0.6180339887,Math.pow(2, 32) * 0.6180339887 = 0x61c88647 * RATE_TUPLE_LENGTH = 128 */ protected int hashIdx(int val) { int hashCode = val * HASH_INCREMENT + HASH_INCREMENT; return hashCode & (RATE_TUPLE_LENGTH - 1); }
总体概率算法,如同分割一块儿蛋糕,即使某项奖品概率非常低,比如 0.0001,那么将各奖品概率均转为 1, ... 即可,时间复杂度不会发生变化 单项概率算法,若要在 o(1) 完成抽奖,那么就需要以空间换时间,创建更多的数组来实现。如果奖品概率很低,那么采取总体概率 算法进行计算即可,只是不需要调整概率,但会是 o(n) 的时间复杂度。 思考:若是希望能更快完成抽奖,以一定空间换取,可以考虑进行多次的随机值获取,然后要保证多次都能够抽到相同的奖。
执行抽奖
IDrawExec:定义执行抽奖 DrawConfig:配置抽奖策略,SingleRateRandomDrawAlgorithm、DefaultRateRandomDrawAlgorithm DrawStrategySupport:提供抽奖策略数据支持,便于查询策略配置、奖品信息。通过这样的方式隔离职责。 AbstractDrawBase:抽象类定义模板方法流程,在抽象类的
doDrawExec
方法中,处理整个抽奖流程,并提供在流程中需要使用到的抽象方法,由DrawExecImpl
服务逻辑中做具体实现。
- 执行抽奖流程:
- 根据入参策略ID获取抽奖策略配置
- 校验和处理抽奖策略的数据初始化到内存
- 获取那些被排除掉的抽奖列表,这些奖品可能是已经奖品库存为空,或者因为风控策略不能给这个用户薅羊毛的奖品
- 执行抽奖算法
- 包装中奖结果
发奖
执行完成抽奖后得到相应的中将信息,此信息会作为奖品订单写入数据库中,之后更新订单中的状态完成发奖。
id 的生成策略
关于 ID 的生成因为有三种不同 ID 用于在不同的场景下;
- 订单号:唯一、大量、订单创建时使用、分库分表
- 活动号:唯一、少量、活动创建时使用、单库单表
- 策略号:唯一、少量、活动创建时使用、单库单表
活动领域
活动的创建
活动的创建操作主要包括:添加活动配置、添加奖品配置、添加策略配置、添加策略明细配置,在同一个注解事务配置下进行处理
活动状态的变更
活动状态的审核,【1编辑、2提审、3撤审、4通过、5运行、6拒绝、7关闭、8开启】等
活动领取
项目中最为重要的部分,一是要保证事务问题、二是要提高系统的并发
@Test
void testIncre() {
Set<Long> set = new HashSet<>();
ExecutorService executorService = Executors.newFixedThreadPool(100);
// 模拟 10000 个线程同时对 Redis 进行 increment 操作
for (int i = 0; i < 10000; i++) {
executorService.execute(() -> {
Long value = redisTemplate.opsForValue().increment("key-incre", 1);
set.add(value);
});
}
// 关闭线程池
executorService.shutdown();
try {
// 等待所有线程执行完成
executorService.awaitTermination(1, TimeUnit.MINUTES);
System.out.println("set size:" + set.size());
} catch (InterruptedException e) {
e.printStackTrace();
}
}
// set size: 9918 测试多次总会小于 10000,redis 中 val 是 10000
// 因此在并发情况下是存在多个线程获得相同的自增值的,仅靠 incre 无法保证并发安全,需要增加加锁逻辑
规则引擎
使用组合模式搭建用于量化人群的规则引擎,用于用户参与活动之前,通过规则引擎过滤性别、年龄、首单消费、消费金额、忠实用户等各类身份来量化出具体可参与的抽奖活动。通过这样的方式控制运营成本和精细化运营。
- 增加规则引擎开发需要的相关的配置类表:rule_tree、rule_tree_node、rule_tree_node_line
- 运用组合模式搭建规则引擎领域服务,包括:logic 逻辑过滤器、engine 引擎执行器
决策树的处理流程:在进行组和形成一棵树后,由 LogicFilter 定义决策的过滤器,LogicFilter 定义逻辑决策器和获取决策值,具体的执行过程由执行引擎 IEngine 实现
Long filter(String matterValue, List<TreeNodeLink> treeNodeLineInfoList);
String matterValue(Long treeId, String userId, Map<String, String> decisionMatter);
BasicLogic 实现 LogicFilter ,提供最基本的通用的方法
filter:根据决策值以及节点之间的链接条件得到满足条件的下一个节点 Id
matterValue:获取决策值的方法定义为抽象方法,由具体的逻辑实现类提供,比如:UserAgeFilter,UserGenderFilter
IEngine 定义决策的统一的接口
EngineResult process(final Long treeId, final String userId, TreeRich treeRich, final Map<String, String> decisionMatter);
- EngineConfig :决策节点的配置,即添加执行引擎要用到的 filter,如 UserAgeFilter ...
- EngineBase:基础决策引擎功能,提供决策树流程的处理过程
- 定义 process 抽象方法
- engineDecisionMaker :具体的决策过程,返回最终果实
- TreeEngineHandle 继承 EngineBase 实现 process 并返回决策结果 (模板模式)
- 这里只做了调用 EngineBase 中的 engineDecisionMaker 并将结果封装
在进行扩展时只需要添加新的 Filter 以及将该 Filter 配置到执行引擎当中即可
::: 决策值:需要进行比较的条件,比如性别,年龄
::: 决策物料:最终寻找节点所依据的条件值,(包括决策值和具体条件)比如,年龄:29,性别:男
::: 子叶:表示条件的类型,同决策值
::: 果实:最终结果
活动编排
xxl-job 定时扫描库表
压测
@RestController
@RequestMapping("/api")
public class LotteryController {
@DubboReference(interfaceClass = ILotteryActivityBooth.class, url = "dubbo://127.0.0.1:20880")
private ILotteryActivityBooth lotteryActivityBooth;
@GetMapping("/draw")
public DrawRes doDraw() {
DrawReq drawReq = new DrawReq();
drawReq.setActivityId(100001L);
drawReq.setuId("dyy-01");
return lotteryActivityBooth.doDraw(drawReq);
}
}
RT: 163 ms , 数据包:386 B
jmeter: ...