Bmarket Platman

Bmarket Platman

Bmarket Platman

抽奖策略表设计

总体是策略 + 奖品 + 规则的表设计,把抽奖系统配套成概率体系,所有玩法都可以用配置组合出来。


  • 抽奖策略总表:一条策略 = 一套概率配置 + 一套奖品列表 + 一套规则
  • 策略奖品表:每个策略 ID 会对应多个奖品,每个奖品独立拥有:概率 + 库存 + 排序(前端显示)
  • 策略规则:规则是策略的附加条件,用来限制或扩展抽奖的行为
    • 规则分为整套抽奖规则和针对某个奖品
    • 规则模型

抽奖算法

抽奖算法 = 根据权重,从多个奖品中选出一个。 这个过程,要么靠预处理数据(空间换时间),要么靠实时计算(时间换空间)

空间换时间

  • 空间换时间提前构造一个概率分布映射表:
    奖品A: 10% (1 ~ 10)
    奖品B: 20% (11 ~ 30)
    奖品C: 70% (31 ~ 100)
    
    提前构造一个长度为 100 的数组:
    // [ A, A, ..., B, ..., C, ..., C ]  // 每个奖品出现次数对应它的概率
    int rand = random.nextInt(100);
    Prize prize = prizeArray[rand]; // 直接命中,不用计算
    
  • 问题:本地内存 vs Redis
    • 本地内存最快,但是在多节点服务时,维持节点数据一致较为复杂。
    • Redis 稍慢一点,但天然支持分布式,只要更新 Redis 所有节点都能读到。

时间换空间

  • 时间换空间在抽奖时遍历奖品概率列表,用随机值来找属于哪个区间:
    double rand = Math.random(); // 0~1之间
    double sum = 0;
    for (Prize p : prizeList) {
        sum += p.getProbability();
        if (rand <= sum) {
            return p;
        }
    }
    
    • 选型

      如果构造出来的概率数组太大(比如长度 1,000,000),那就不值得用空间换时间了; 注意这里的概率值是指工程上。工程上通常将概率乘以一个倍数转换为整数,以便用数组、号段等方式实现抽奖。100 万概率数组实在是太大了!

策略概率装配处理

类名 / 接口作用及所在层说明
StrategyAwardEntitydomain/实体类抽奖策略对应的奖品实体,保存奖品ID、库存、中奖概率等。
IStrategyRepositorydomain/仓储接口领域层定义的仓储接口,抽象数据访问相关操作(设计DDD规范)。
StrategyRepositoryinfrastructure/基础设施实现仓储接口的实现,真正操作数据库和缓存(DAO + Redis)。
StrategyArmorydomain/领域服务(策略工厂)负责装配策略(根据策略ID加载奖品列表、构建概率区间、抽奖)。

策略工厂

装配阶段

把“抽奖规则”转化成一个可快速随机查找的 概率查找表,并存储到 Redis。

流程:

  1. 查询策略配置:从数据库查出当前策略下的所有奖品信息(概率、库存等)。
  2. 找到最小概率单位,这是为了后续把概率转换成整数区间,例如最小 0.0001 代表 1 万分位。
  3. 计算概率总和,验证所有奖品概率是否合理。
  4. 算出整数概率范围,用 totalAwardRate ÷ minAwardRate 得到总的“整数槽位数”。
  5. 构建概率查找表,按奖品概率占用相应的槽位,把奖品 ID 填进去。
  6. 乱序:打乱奖品分布,避免按顺序分布导致规律性。
  7. 转成 Map
  8. 存到 Redis:Redis 持久化概率表 + 槽位总数,方便分布式节点共享。

抽奖阶段

基于事先准备好的概率表,快速返回中奖奖品 ID。

流程:

  1. 从 Redis 取出 rateRange(整数范围)。
  2. 生成 0 ~ rateRange-1 的随机数。
  3. 用 strategyId + 随机数 从 Redis 查找概率表的对应奖品 ID。
  4. 返回奖品 ID(后续还要去扣库存、触发发奖等)

概率问题

  1. 问题一:为什么概率不一定要求等于 1?
  • 业务上:概率字段更多是“抽签份额”,并不强制 100% 填满。
  • 系统健壮性:如果运营配置错了,比如填了 1.01 或 0.98,系统不能直接报错崩掉,而是自动适配。

举个例子: 故意让总和 > 1(压缩空奖概率) ,假设抽奖系统需要“空奖”:

  • 如果总和 = 1 → 没空奖的概率。
  • 如果总和 = 1.2 → 等于把空奖概率减少(超出的部分全分给有奖品)。
  • 总和 = 1.3 → 等于“压缩空奖概率”,中奖几率超过 100%(从数学角度不合理,但业务上可行,比如兜底奖品)。

在举个例子:故意让总和 < 1(保留空奖概率),比如想让用户有 20% 的概率抽空:

  • 总和 = 0.8 → 剩下 0.2 就是空奖概率
  1. 为什么要用 总概率 ÷ 最小概率 得到概率范围?

把最小概率当作刻度单位,把整个概率区间转成整数格子来方便随机:

例子: 最小概率 0.0001,总概率 1.01,那么:1.01 ÷ 0.0001 = 10100 → 代表整个区间有 10100 个整数格,每个格子对应一个奖品或空奖。

  1. 为什么要向上取整?

因为整数格子数必须能容纳所有奖品份额,如果向下取整会导致最小概率的奖品直接“没格子可放”,概率被截断为 0。

向上取整虽然可能导致“多出一点点区间”,但最多是超出索引范围,属于可控的问题。

简历:

在策略抽奖装配中,针对高精度概率导致查找表内存爆炸的问题,设计并实现基于最小概率小数位动态计算放大倍数的方案,有效限制查找表大小,避免OOM风险。