需求点
近期开发抽奖功能,涉及到的问题以及相应的需求点列举如下:
-
能够根据奖品初始设置的抽中概率进行分配
-
防止前端并发操作产生的超中情况
-
避免频繁抽中单一奖品,导致抽中奖项种类不均匀
-
抽中的概率能够根据实时库存进行相应变化,便于灵活的增加减少奖品数量
-
奖品分为几等奖,每等奖中奖品数量不为一,并且同一等奖的抽中概率相同
-
保证每次抽奖均抽中
方案设计
将抽中奖品的逻辑实现放在后台,前端只负责相应的交互与结果展示,这样可以降低前端逻辑,更加可控
mock数据
首先模拟奖品数据,包括奖品id,奖品名称,奖品分类,奖品抽中概率,奖品库存
id | 名称 | 分类 | 抽中概率(0-100) | 库存 |
---|---|---|---|---|
1 | 奖品1 | 一等奖 | 1 | 10 |
2 | 奖品2 | 二等奖 | 5 | 50 |
3 | 奖品3 | 三等奖 | 34 | 100 |
4 | 奖品4 | 三等奖 | 34 | 100 |
5 | 奖品5 | 三等奖 | 34 | 150 |
6 | 奖品6 | 四等奖 | 60 | 300 |
7 | 奖品7 | 四等奖 | 60 | 300 |
8 | 奖品8 | 四等奖 | 60 | 350 |
9 | 奖品9 | 四等奖 | 60 | 500 |
mock的数据满足以上列举的需求,即可以设置初始抽中概率值,库存可以灵活的去设置修改,每等奖中奖品数量不为一且相同等奖的抽中概率相同
实现设计
引入区间的概念,即每个奖品的区间距离为抽中概率乘以库存数量,并且单个奖品的区间左值为前面奖品区间距离之和,如上mock的数据为例:
id | 名称 | 分类 | 抽中概率(0-100) | 库存 | 区间 |
---|---|---|---|---|---|
1 | 奖品1 | 一等奖 | 1 | 10 | [0,10) |
2 | 奖品2 | 二等奖 | 5 | 50 | [10,260) |
3 | 奖品3 | 三等奖 | 34 | 100 | [260,3660) |
4 | 奖品4 | 三等奖 | 34 | 100 | [3660,7060) |
5 | 奖品5 | 三等奖 | 34 | 150 | [7060,12160) |
6 | 奖品6 | 四等奖 | 60 | 300 | [12160,30160) |
7 | 奖品7 | 四等奖 | 60 | 300 | [30160,48160) |
8 | 奖品8 | 四等奖 | 60 | 350 | [48160,69160) |
9 | 奖品9 | 四等奖 | 60 | 500 | [69160,99160) |
附:若存在未中奖的情况,则需要添加未中奖区间,该区间可以为[区间最右值,区间最右值 乘n-区间最右值),n为抽几次中一次奖的数量
接着在区间范围内取随机值,落在哪个区间就获得相应的奖品
int randNum = new Random().nextInt(区间最右值);
对于防止奖品超中,分两方面进行控制,即查询奖品信息时,过滤掉库存为0的奖品
select id,ratio,num,type from prize where num > 0
抽中奖品进行写库时,加上CAS乐观锁机制,防止并发操作导致奖品超中
update prize set num = num -1 where id = #{prizeId} and num > 0
即保证只有num大于0的情况下才会减库成功
设计总结
-
奖品区间大小为库存*抽中概率,则抽中概率越大,区间会越大,满足需求1
-
在读取奖品信息时过滤库存为0,并且写库使用乐观锁控制,可以防止超中,满足需求2
-
当抽中奖品后,导致库存减少,相应的区间距离变小,所以随着抽中次数增多,抽中的概率会变小,会避免单一奖品频繁被抽中,满足需求3
-
可以灵活的进行改库修改库存数量,相应的区间距离会变化,抽中概率也会变化,满足需求4
逻辑实现
代码如下:Java实现
class IntervalBean {
private int front;
private int end;
private int type;
//构造函数与Get Set 略
}
public Map<String, Object> getWinInfo() {
try {
// 从数据库读取奖品信息列表
List<WinCalDO> l = luckDrawDAO.getCalList();
int totalWeight = 0;
Map<Long, IntervalBean> interval = new HashMap<>();
int prev = 0;
// 计算最右区间值以及奖品对应的区间
for (WinCalDO item : l) {
int spacing = item.getNum() * item.getRatio();
totalWeight += spacing;
interval.put(item.getPrizeId(), new IntervalBean(prev, prev + spacing, item.getType()));
prev = prev + spacing;
}
// 产生随机数
int randNum = new Random().nextInt(totalWeight);
long prizeId = -1;
int type = -1;
for (Map.Entry<Long, IntervalBean> m : interval.entrySet()) {
int front = m.getValue().getFront();
int end = m.getValue().getEnd();
if (randNum >= front && randNum < end) {
prizeId = m.getKey();
type = m.getValue().getType();
break;
}
}
Map<String, Object> res = new HashMap<>();
res.put("prizeId", prizeId);
res.put("type", type);
return res;
} catch (Exception e) {
LOGGER.error("计算获奖出错");
}
}
待完善点
-
因为计算奖品逻辑会动态根据库存等信息变化,所以每次计算都会读取数据库,获取所有奖品列表信息,后续考虑同步到缓存中进行,提高计算速度
-
若考虑到未中奖的情况,奖品全部抽完的次数不确定,可以加入保证抽几次必中一次的逻辑,确保奖品可以都抽完