1.抽奖功能及难点
功能: (1)每种奖品设立每日库存,每天指定时间刷新库存 (2)奖品有消费门槛,例如消费过10000元的用户优先抽实体物品,并且是尽量每个消费够的用户尽量都能抽到实物 (3)每个人只能抽中一次实体奖品 (4)所有人都有概率抽中谢谢惠顾,并且每日也是有限额的 难点: (1)业务需求实现 (2)避免人多抢购并发请求导致数据与预期值不一致,也就是如秒杀导致库存数据错误
2.伪业务代码的实现
@ApiOperation("进行抽奖")
@LoginLimitSubmit(lockTime = 3000) //防重复提交防护
@PostMapping(value = "/go")
public Rest<> go() {
//一系列校验检查:活动时间,用户身份,是否存在抽奖次数等
checkResidueCount();
//由于用户可能有多次抢购次数,根据用户id,进行防抖处理,3秒内只能请求一次
if (!RedisLockUtil.tryLock("/raffle/go" + userIdInt, WfIdWorkerUtil.getIdStr(), 3000)) {
return Rest.error("您点的太快了,请稍后再试~");
}
//活动暂停判断,预防突发问题,可随时终止抽奖
Integer h5RafflePause = distributionService.queryCacheDistributionByKey("h5_raffle_pause").getValue();
if (h5RafflePause != null && h5RafflePause == 1) {
return Rest.error("活动太火爆了,请稍后再试~");
}
//随机抽到谢谢参与
if (isRandomThank()) {
saveThanksLog(userIdInt);
WfLuckyEntity thankEntity = new WfLuckyEntity();
thankEntity.setId(-1);
thankEntity.setGood("谢谢参与");
vo.setWfLuckyEntity(thankEntity);
return Rest.ok(vo);
}
//进行抽奖奖品
WfLuckyEntity wfLuckyEntity = randomRaffle();
//进行发放虚拟或保存实物奖品,以下代码忽略...
}
private boolean isRandomThank() {
//缓存中获取抽奖谢谢参与概率及次数
String configStr = distributionService.queryCacheDistributionByKey("h5_raffle_config").getRemark();
if (StringUtils.isBlank(configStr)) {
return false;
}
//每天抽中次数
int thankCount = 0;
//概率
double thankProbability = 0;
try {
JSONObject jsonObject = JSON.parseObject(configStr);
thankCount = jsonObject.getIntValue("thankCount");
thankProbability = jsonObject.getDoubleValue("thankProbability");
} catch (Exception e) {
log.error("转换失败", e);
return false;
}
if (thankCount < 1 || thankProbability < 0.1) {
return false;
}
WfLuckyEntity thankLucky = new WfLuckyEntity();
thankLucky.setId(-1);
thankLucky.setEveryDayCount(thankCount);
//此处默认thankProbability=0.3,及大概百分之30的人抽中,与1-10随机数除以10进行大于等于对比
if (!(thankProbability >= RandomUtil.randomInt(1, 11) / 10f)) {
return false;
}
return checkDistributeAndIncr(thankLucky);
}
//进行抽奖
public WfLuckyEntity randomRaffle() {
//从缓存中获取奖品
List<WfLuckyEntity> luckys = luckyService.queryCache();
//根据用户消费金额,筛选可以抽中的礼物,优先实物还是虚拟,如果实物还要筛选去除用户已经抽中过的实物
List<WfLuckyEntity> userLuckys = queryLuckysByUser();
//缓存判断实物商品当前剩余库存,尽可能的让消费够的用户抽中其中一款实物,此处逻辑为每3秒从redis中获取一次实时抽奖数量
List<WfLuckyEntity> newLuckList = WfLocalCacheTimeUtil.get(3, "getHaveList" + JSON.toJSONString(userLuckys), List.class, () -> {
List<WfLuckyEntity> newList = new ArrayList<>();
try (Jedis jedis = jedisPool.getResource()) {
jedis.select(6);
for (WfLuckyEntity luckyEntity : userLuckys) {
WfLuckyEntity newLuck = new WfLuckyEntity();
String key = "talkshow-raffle:" + luckyEntity.getId() + "-" + LocalDateTimeUtil.getTimeStr(LocalDateTime.now(), "yyyy-MM-dd");
String countStr = jedis.get(key);
if (StringUtils.isBlank(countStr)) {
countStr = "0";
}
BeanUtils.copyProperties(luckyEntity, newLuck);
newLuck.setCurrentCount(Long.parseLong(countStr));
newList.add(newLuck);
}
} catch (Exception e) {
}
return newList;
});
//其中省略虚拟物品抽奖,虚拟物品中有设置库存无限的商品,即用户最后至少能获得虚拟物品
//将还有库存的商品进行抽奖
return userLuckys.get(RandomUtil.randomInt(0, userLuckys.size()));
}
//减库存判断抽中逻辑,核心扣减逻辑
public boolean checkDistributeAndIncr(WfLuckyEntity wfLuckyEntity) {
//奖品id
Integer luckyEntityId = wfLuckyEntity.getId();
//每日奖品数量
Integer everyDayCount = wfLuckyEntity.getEveryDayCount();
if (everyDayCount == null) {
log.error("没有库存" + everyDayCount);
return false;
}
//使用redis对日期和奖品组成的key,进行递增处理,判断递增后的值是否不大于当日最大抽奖数,如不大于则代表抽成功
String key = "talkshow-raffle:" + luckyEntityId + "-" + LocalDateTimeUtil.getTimeStr(LocalDateTime.now(), "yyyy-MM-dd");
try (Jedis jedis = jedisPool.getResource()) {
jedis.select(6);
Long incr = jedis.incr(key);
if (incr == null) {
return false;
}
return incr <= everyDayCount;
} catch (Exception e) {
}
return false;
}
3.代码解析
(1)做好接口防护,前后端都要做好防重复提交和防抖处理,还可加入限流; (通过业务方式前期拦截大部分用户抽奖或者伪抽奖方式此处不谈) (2)其中查询的数据能用缓存就用缓存,优先运行时缓存,其次使用redis等
(3)此处避免库存超抢,用到了redis的原子性保障的递增,也可以用递减方法; 原理就是先用redis进行操作库存后,那么后续所有操作都不存在并发数据问题,当然此方法要保障redis的可靠性;
本文由 GY 创作,采用 知识共享署名4.0 国际许可协议进行许可
本站文章除注明转载/出处外,均为本站原创或翻译,转载前请务必署名
最后编辑时间为:
2023/12/06 14:54