二倍均值法-抢红包案例

350 阅读4分钟

说明

下面的红包分配逻辑借鉴微信红包,采用二倍均值法随机拆分;

代码

普通使用

     private static Triple<Integer, Integer, Integer> pocket(int minMoneyInt, int remainInt, int remainSize) {
         int red;
         if (remainSize == 1) {
             red = remainInt;
         } else {
             //获取红包的最大值
             red = RandomUtils.nextInt(0, remainInt / remainSize * 2 + 1);
         }
         if (remainInt > red) {
             remainInt -= red;
         } else {
             remainInt = 0;
         }
         return Triple.of(minMoneyInt + red, remainInt, remainSize - 1);
     }
 ​
     public static void main(String[] args) {
         // 总人数
         int totalPeople = 10;
         // 总金额或剩余可分配金额
         BigDecimal remainPocket = BigDecimal.valueOf(10);
         // 红包最小值
         BigDecimal minPocket = BigDecimal.valueOf(0.02);
         // 发放时可分配数量,保证用户一定有最小值的红包
         remainPocket = remainPocket.subtract(minPocket.multiply(BigDecimal.valueOf(totalPeople)));
         // 剩余人数
         int remainSize = totalPeople;
         // 发红包总数量
         BigDecimal total = BigDecimal.ZERO;
 ​
         BigDecimal hundred = new BigDecimal("100");
         for (int i = 1; i <= totalPeople; i++) {
             // 分配红包
             Triple<Integer, Integer, Integer> pocket = pocket(minPocket.multiply(hundred).intValue(), remainPocket.multiply(hundred).intValue(), remainSize);
             BigDecimal packet = BigDecimal.valueOf((double) pocket.getLeft() / 100);
             // 更新剩余红包金额
             remainPocket = BigDecimal.valueOf((double) pocket.getMiddle() / 100);
             // 更新剩余人数
             remainSize = pocket.getRight();
             // 更新已发放红包总金额
             total = total.add(packet);
             System.out.printf("第%d人:抢到%s元,当前红包发放%s元,剩余%s元,剩余%d个红包%n", i, packet, total, remainPocket, remainSize);
         }
         System.out.println("发放红包总金额" + total);
     }

基于Redis的Lua实现

lua脚本

 local minMoneyInt = tonumber(redis.call("HGET", KEYS[1], "minMoneyInt"))
 local remainInt = tonumber(redis.call("HGET", KEYS[1], "remainInt"))
 local remainSize = tonumber(redis.call("HGET", KEYS[1], "remainSize"))
 local red
 --计算红包大小
 if (remainSize == 1) then
     red = remainInt
 else
     math.randomseed(tonumber(tostring(ARGV[1]):reverse()))
     red = math.random(0, remainInt / remainSize * 2)
 end
 --计算剩余金额
 if (remainInt > red) then
     remainInt = remainInt - red
 else
     remainInt = 0
 end
 --设置redis的红包属性
 redis.call("HMSET", KEYS[1], "remainInt", remainInt, "remainSize", remainSize - 1)
 --返回红包金额
 return minMoneyInt + red

Java使用

 @Slf4j
 @SpringBootTest
 @RunWith(SpringRunner.class)
 public class TestLua {
     @Resource
     private StringRedisTemplate stringRedisTemplate;
     @Resource
     private RedisUtils redisUtils;
 ​
     private DefaultRedisScript<Long> pocketLua;
 ​
     @PostConstruct
     public void init() {
         // 执行 lua 脚本
         pocketLua = new DefaultRedisScript<>();
         // 指定 lua 脚本
         pocketLua.setScriptSource(new ResourceScriptSource(new ClassPathResource("pocket.lua")));
         // 指定返回类型
         pocketLua.setResultType(Long.class);
     }
 ​
     @Test
     public void testLua() {
         String lockKey = "test";
         // 总人数
         int totalPeople = 10;
         // 总金额或剩余可分配金额
         BigDecimal remainPocket = BigDecimal.valueOf(100);
         // 红包最小值
         BigDecimal minPocket = BigDecimal.valueOf(9);
         // 发放时可分配数量,保证用户一定有最小值的红包
         remainPocket = remainPocket.subtract(minPocket.multiply(BigDecimal.valueOf(totalPeople)));
 ​
         // 测试代码忽略
         redisUtils.hset(lockKey, "minMoneyInt", minPocket.multiply(BigDecimal.valueOf(100)).intValue());
         redisUtils.hset(lockKey, "remainInt", remainPocket.multiply(BigDecimal.valueOf(100)).intValue());
         redisUtils.hset(lockKey, "remainSize", totalPeople);
         
         // 发红包总数量
         BigDecimal total = BigDecimal.ZERO;
         for (int i = 1; i <= 10; i++) {
             Long result = stringRedisTemplate.execute(pocketLua, Collections.singletonList(lockKey), System.nanoTime() + "");
             BigDecimal packet = BigDecimal.valueOf((double) result / 100);
             System.out.printf("第%d人:抢到%s元%n", i, packet);
             total = total.add(packet);
         }
         System.out.println("发放红包总金额" + total);
     }
 }

示例

  1. minPocket*totalPeople==remainPocket

      // 总人数
     int totalPeople = 10;
     // 总金额或剩余可分配金额
     BigDecimal remainPocket = BigDecimal.valueOf(3.3);
     // 红包最小值
     BigDecimal minPocket = BigDecimal.valueOf(0.33);
     ​
     ​
     // 结果
     第1人:抢到0.33元,当前红包发放0.33元,剩余0.0元,剩余9个红包
     第2人:抢到0.33元,当前红包发放0.66元,剩余0.0元,剩余8个红包
     第3人:抢到0.33元,当前红包发放0.99元,剩余0.0元,剩余7个红包
     第4人:抢到0.33元,当前红包发放1.32元,剩余0.0元,剩余6个红包
     第5人:抢到0.33元,当前红包发放1.65元,剩余0.0元,剩余5个红包
     第6人:抢到0.33元,当前红包发放1.98元,剩余0.0元,剩余4个红包
     第7人:抢到0.33元,当前红包发放2.31元,剩余0.0元,剩余3个红包
     第8人:抢到0.33元,当前红包发放2.64元,剩余0.0元,剩余2个红包
     第9人:抢到0.33元,当前红包发放2.97元,剩余0.0元,剩余1个红包
     第10人:抢到0.33元,当前红包发放3.30元,剩余0.0元,剩余0个红包
     发放红包总金额3.30
    
  2. minPocket*totalPeople!=remainPocket

     // 总人数
     int totalPeople = 10;
     // 总金额或剩余可分配金额
     BigDecimal remainPocket = BigDecimal.valueOf(5);
     // 红包最小值
     BigDecimal minPocket = BigDecimal.valueOf(0.33);
     ​
     ​
     // 结果
     第1人:抢到0.55元,当前红包发放0.55元,剩余1.48元,剩余9个红包
     第2人:抢到0.33元,当前红包发放0.88元,剩余1.48元,剩余8个红包
     第3人:抢到0.58元,当前红包发放1.46元,剩余1.23元,剩余7个红包
     第4人:抢到0.61元,当前红包发放2.07元,剩余0.95元,剩余6个红包
     第5人:抢到0.5元,当前红包发放2.57元,剩余0.78元,剩余5个红包
     第6人:抢到0.38元,当前红包发放2.95元,剩余0.73元,剩余4个红包
     第7人:抢到0.57元,当前红包发放3.52元,剩余0.49元,剩余3个红包
     第8人:抢到0.63元,当前红包发放4.15元,剩余0.19元,剩余2个红包
     第9人:抢到0.35元,当前红包发放4.50元,剩余0.17元,剩余1个红包
     第10人:抢到0.5元,当前红包发放5.00元,剩余0.0元,剩余0个红包
     发放红包总金额5.00
    

补充

  1. 两者实现本质上一样,通过lua脚本和代码就可以看出来,红包计算全部转化为分最后再转为decimal类型的;

  2. 程序变量

    1. 必选参数

      1. 总红包数量totalPeople
      2. 总发放金额remainPocket
    2. 可选参数

      1. 红包最小单位minPocket
  3. minPocket*totalPeople=remainPocket,红包会变为均分红包