一个简单抽奖算法的实现以及如何预防超中
需求java
每一个用户天天有3次抽奖机会;
抽奖奖池一共分为6档内容:现金红包1元,2元,3元,5元,iphone6s,谢谢参与;
支持天天调整和配置抽奖的获奖几率;算法
算法介绍
每种奖品都有一个权重 对应一个区间 若落入该区间就表示中奖 调整区间大小就可改变获奖几率 即调整权重值便可并发
奖品 | 权重 | 区间 | ||
---|---|---|---|---|
1元 | 5000 | [0,5000) | ||
2元 | 1000 | [5000,6000) | ||
3元 | 500 | [6000,6500) | ||
5元 | 100 | [6500, 6600) | ||
iphone6s | 1 | [6600, 6601) | ||
未中奖 | 59409 | [6601,66010) | 假设设定抽10次中一次, 未中奖权重 = 抽检几率导数奖品数-奖品数 = 106601-6601 = 59409 |
抽奖的时候 先生成一个随机值dom
randNum = new Random().nextInt(totalWeight); // totalWeight = 上面权重列之和
判断该随机值在哪个区间 如iphone
randNum = 8944 落在未中奖区间 未中奖 randNum = 944 落在1元区间 中了一元
若是想增大中iphone6s的几率 调整权重值便可 如将权重改成1000, 则区间变为[6600,7600)
同时会为每种奖品设置库存 如spa
日期 | 奖品 | 库存 |
---|---|---|
3.1 | 一元 | 5000 |
中奖后 会减库存 但假如库存只剩1个了 有10个用户同时落入一元区间 如何避免1-10=-9
的状况呢?
解决方法线程
update award_stock set stock = stock - 1 where award_id = ? and stock > 0;
便是否中奖除了落入区间外 还需判断减库存是否成功
若是减库存失败 仍当作未中奖code
一旦一种奖品库存为0 下次计算区间的时候 将它排除 如一元奖品库存已为0 这时各奖品的区间变化为ip
奖品 | 权重 | 区间 | |
---|---|---|---|
2元 | 1000 | [0,1000) | |
3元 | 500 | [1000,1500) | |
5元 | 100 | [1500, 1600) | |
iphone6s | 1 | [1600, 1601) | |
未中奖 | 59409 | [1601,61010) | 61010/1601=38 此时中奖几率变小了 至关于抽38次中一次 |
验证上述算法
看是否能抽完全部奖品 如某天的奖品配置以下 (权重默认等于库存)get
日期 | 奖品 | 权重 | 库存 |
---|---|---|---|
3.1 | 1元 | 5000 | 5000 |
3.1 | 2元 | 1000 | 1000 |
3.1 | 3元 | 500 | 500 |
3.1 | 5元 | 100 | 100 |
3.1 | iphone6s | 1 | 1 |
3.1 | 未中奖 | 59409 | 59409 |
假设日活用户数为3万 每一个用户可抽3次
java代码
final Map<String, Integer> awardStockMap = new ConcurrentHashMap<>(); // 奖品 <--> 奖品库存 awardStockMap.put("1", 5000); awardStockMap.put("2", 1000); awardStockMap.put("3", 500); awardStockMap.put("5", 100); awardStockMap.put("iphone", 1); awardStockMap.put("未中奖", 59409); //6601*10 -6601 //权重默认等于库存 final Map<String, Integer> awardWeightMap = new ConcurrentHashMap<>(awardStockMap); // 奖品 <--> 奖品权重 int userNum = 30000; // 日活用户数 int drawNum = userNum * 3; // 天天抽奖次数 = 日活数*抽奖次数 Map<String, Integer> dailyWinCountMap = new ConcurrentHashMap<>(); // 天天实际中奖计数 for(int j=0; j<drawNum; j++){ // 模拟每次抽奖 //排除掉库存为0的奖品 Map<String, Integer> awardWeightHaveStockMap = awardWeightMap.entrySet().stream().filter(e->awardStockMap.get(e.getKey())>0).collect(Collectors.toMap(e->e.getKey(), e->e.getValue())); int totalWeight = (int) awardWeightHaveStockMap.values().stream().collect(Collectors.summarizingInt(i->i)).getSum(); int randNum = new Random().nextInt(totalWeight); //生成一个随机数 int prev = 0; String choosedAward = null; // 按照权重计算中奖区间 for(Entry<String,Integer> e : awardWeightHaveStockMap.entrySet() ){ if(randNum>=prev && randNum<prev+e.getValue()){ choosedAward = e.getKey(); //落入该奖品区间 break; } prev = prev+e.getValue(); } dailyWinCountMap.compute(choosedAward, (k,v)->v==null?1:v+1); //中奖计数 if(!"未中奖".equals(choosedAward)){ //未中奖不用减库存 awardStockMap.compute(choosedAward, (k,v)->v-1); //奖品库存一 if(awardStockMap.get(choosedAward)==0){ System.out.printf("奖品:%s 库存为空%n",choosedAward); //记录库存为空的顺序 } } } System.out.println("各奖品中奖计数: "+dailyWinCountMap); //每日各奖品中奖计数
输出
奖品:iphone 库存为空 奖品:5 库存为空 奖品:1 库存为空 奖品:2 库存为空 奖品:3 库存为空 每日各奖品中奖计数: {1=5000, 2=1000, 3=500, 5=100, iphone=1, 未中奖=83399}
可知 假如该天抽奖次数能有9万次的话 能够抽完全部的奖品 另外因是单线程未考虑减库存
失败的状况 即并发减库存的状况
抽奖算法2 存在奖品库存的前提下 保证每次中奖的几率恒定 如15% 抽100次有15次中奖
final Map<String, Integer> awardStockMap = new ConcurrentHashMap<>(); awardStockMap.put("1", 3000); awardStockMap.put("2", 2000); awardStockMap.put("3", 1500); awardStockMap.put("5", 1000); awardStockMap.put("10", 100); awardStockMap.put("20", 10); awardStockMap.put("50", 5); awardStockMap.put("100", 2); // 权重默认等于库存 final Map<String, Integer> awardWeightMap = new ConcurrentHashMap<>(awardStockMap); final Map<String, Integer> initAwardStockMap = new ConcurrentHashMap<>(awardStockMap); int drawNum = 50780; // 理论能够抽完全部奖品所需抽奖次数 = 奖品数×中奖几率导数 = 7617*100/15 final int threshold = 15; //中奖几率 15% Map<String, Integer> dailyWinCountMap = new ConcurrentHashMap<>(); // 天天实际中奖计数 for (int j = 0; j < drawNum; j++) { // 模拟每次抽奖 //肯定是否中奖 int randNum = new Random().nextInt(100); if(randNum>threshold){ dailyWinCountMap.compute("未中奖", (k,v)->v==null?1:v+1); continue; //未中奖 } //中奖 肯定是哪一个奖品 //排除掉库存为0的奖品 Map<String, Integer> awardWeightHaveStockMap = awardWeightMap.entrySet().stream().filter(e->awardStockMap.get(e.getKey())>0).collect(Collectors.toMap(e->e.getKey(), e->e.getValue())); if(awardWeightHaveStockMap.isEmpty()){ //奖池已为空 System.out.printf("第%d次抽奖 奖品已被抽完%n",j); break; } int totalWeight = (int) awardWeightHaveStockMap.values().stream().collect(Collectors.summarizingInt(i->i)).getSum(); randNum = new Random().nextInt(totalWeight); int prev=0; String choosedAward = null; for(Entry<String,Integer> e : awardWeightHaveStockMap.entrySet() ){ if(randNum>=prev && randNum<prev+e.getValue()){ choosedAward = e.getKey(); //落入此区间 中奖 dailyWinCountMap.compute(choosedAward, (k,v)->v==null?1:v+1); break; } prev = prev+e.getValue(); } //减少库存 awardStockMap.compute(choosedAward, (k,v)->v-1); } System.out.println("每日各奖品中奖计数: "); // 每日各奖品中奖计数 dailyWinCountMap.entrySet().stream().sorted((e1,e2)->e2.getValue()-e1.getValue()).forEach(System.out::println); awardStockMap.forEach((k,v)->{if(v>0){ System.out.printf("奖品:%s, 总库存: %d, 剩余库存: %d%n",k,initAwardStockMap.get(k),v); }});
输出
第47495次抽奖 奖品已被抽完 每日各奖品中奖计数: 未中奖=39878 1=3000 2=2000 3=1500 5=1000 10=100 20=10 50=5 100=2
可见 实际不用到理论抽奖次数 便可抽完全部奖品