跳至主要內容

抽奖系统

博识笔记大约 6 分钟projectlottery

营销平台 - 抽奖系统

抽奖系统

项目演示open in new window

系统流程

抽奖算法

例: 设 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);
        }
    

    参考open in new window

    总体概率算法,如同分割一块儿蛋糕,即使某项奖品概率非常低,比如 0.0001,那么将各奖品概率均转为 1, ... 即可,时间复杂度不会发生变化 单项概率算法,若要在 o(1) 完成抽奖,那么就需要以空间换时间,创建更多的数组来实现。如果奖品概率很低,那么采取总体概率 算法进行计算即可,只是不需要调整概率,但会是 o(n) 的时间复杂度。 思考:若是希望能更快完成抽奖,以一定空间换取,可以考虑进行多次的随机值获取,然后要保证多次都能够抽到相同的奖。

执行抽奖

IDrawExec:定义执行抽奖 DrawConfig:配置抽奖策略,SingleRateRandomDrawAlgorithm、DefaultRateRandomDrawAlgorithm DrawStrategySupport:提供抽奖策略数据支持,便于查询策略配置、奖品信息。通过这样的方式隔离职责。 AbstractDrawBase:抽象类定义模板方法流程,在抽象类的 doDrawExec 方法中,处理整个抽奖流程,并提供在流程中需要使用到的抽象方法,由 DrawExecImpl 服务逻辑中做具体实现。

  • 执行抽奖流程:
  1. 根据入参策略ID获取抽奖策略配置
  2. 校验和处理抽奖策略的数据初始化到内存
  3. 获取那些被排除掉的抽奖列表,这些奖品可能是已经奖品库存为空,或者因为风控策略不能给这个用户薅羊毛的奖品
  4. 执行抽奖算法
  5. 包装中奖结果

发奖

执行完成抽奖后得到相应的中将信息,此信息会作为奖品订单写入数据库中,之后更新订单中的状态完成发奖。

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

总结