需求:举办一场活动(activity),可以设置多个场次(设置的场次,即session,每隔一段时间举行一场)进行秒杀,每个场次可以秒杀多个奖品(prize)。
方案:redis存储奖品的库存,使用incr命令扣库存, redis扣成功的情况下,再mysql扣库存。
说明:设置redis有效期>=场次时间(秒杀时间)非常重要,不然redis层面无法解决redis扣库存超发的问题,只能通过mysql解决。
简化的代码:
// 预扣减redis奖品库存(设置的有效期 >= 一个场次的有效期)。 // 如果有效期小于场次的有效期,极端情况下会出现: // (1)redis设置的有效期失效,导致部分奖品发不出去,原因:redis incr命令 扣减库存时,在key失效的情况下,把key的值默认设置为0。 // (2)redis回滚库存会出现多加库存的情况,当然可以通过后续的mysql扣库存防止超发的情况。
AND id = #{id, jdbcType=INTEGER}
}catch (Exception ex){ log.error("抽奖异常"); // 没抽到奖,则回滚redis的奖品剩余库存 this.increasePrizeStock(activityId,sessionId,prizeId); throw ex; }
主要方法:
/** * @desc : 预扣减redis奖品库存(有效期1分钟) * @author : 毛会懂 * @create: 2021/8/26 10:53:00 **/ private Boolean decreasePrizeStock(Integer activityId,Integer sessionId,Integer prizeId){ Integer leftStock = this.getPrizeLeftStock(activityId,sessionId,prizeId); if(leftStock <= 0){ log.info("没有库存了,prizeId={}",prizeId); return Boolean.FALSE; } String prizeCountKey = RedisKeyManagement.getKey(RedisKeyManagement.ACTIVITY_SESSION_PRIZE_INFO, Arrays.asList(activityId.toString(),sessionId.toString(),prizeId.toString())); leftStock = (int)redisService.decrease(prizeCountKey,1);
if(leftStock < 0){ // 回滚多扣减的库存 情况1:最后1个库存,多个user同时扣减 情况2:redis失效,redis的值被初始化为0,永久有效 log.error("回滚多扣减的库存,prizeId={}",prizeId); this.increasePrizeStock(activityId,sessionId,prizeId); return Boolean.FALSE; } return Boolean.TRUE; }
/** * @desc : 回滚预扣减的redis奖品库存 * @author : 毛会懂 * @create: 2021/8/26 10:53:00 **/ private Boolean increasePrizeStock(Integer activityId,Integer sessionId,Integer prizeId){ String prizeCountKey = RedisKeyManagement.getKey(RedisKeyManagement.ACTIVITY_SESSION_PRIZE_INFO, Arrays.asList(activityId.toString(),sessionId.toString(),prizeId.toString())); Long expire = redisDubboService.getExpire(prizeCountKey); if(expire == null || expire <= 0){ // 避免 redis 扣库存,redis的值被初始化为0,永久有效的问题 redisService.delete(prizeCountKey); return Boolean.TRUE; } redisDubboService.increment(prizeCountKey,1); return Boolean.TRUE; }
/** * @desc : 获取某奖品的库存 * @author : 毛会懂 * @create: 2021/8/27 14:58:00 **/ private Integer getPrizeLeftStock(Integer activityId,Integer sessionId,Integer prizeId){ String prizeCountKey = RedisKeyManagement.getKey(RedisKeyManagement.ACTIVITY_SESSION_PRIZE_INFO, Arrays.asList(activityId.toString(),sessionId.toString(),prizeId.toString())); Integer leftStock = (Integer)redisDubboService.get(prizeCountKey); if(leftStock == null){ synchronized (this){ leftStock = (Integer) redisDubboService.get(prizeCountKey); // 这里最好加分布式锁(在多个实例下,会出现以下代码同时执行的情况,有可能会出现读取的leftStock不正确,比如,两个实例分别读到的是20,19,set时却是先set 19,再set 20) if(leftStock == null){ // 读mysql表中的剩余库存 ActivityPrizeDO prizeDO = prizeService.getById(prizeId); leftStock = prizeDO.getLeftStock(); redisService.set(prizeCountKey,leftStock,Constant.ONE_MINUTE); } } } return leftStock; }
|
请发表评论