Skip to main content
 首页 » 数据库

Redis秒杀代码,非lua脚本。

2022年07月18日66think

需求:举办一场活动(activity),可以设置多个场次(设置的场次,即session,每隔一段时间举行一场)进行秒杀,每个场次可以秒杀多个奖品(prize)。

方案:redis存储奖品的库存,使用incr命令扣库存, redis扣成功的情况下,再mysql扣库存。

 说明:设置redis有效期>=场次时间(秒杀时间)非常重要,不然redis层面无法解决redis扣库存超发的问题,只能通过mysql解决。

简化的代码:

// 预扣减redis奖品库存(设置的有效期 >= 一个场次的有效期)。
// 如果有效期小于场次的有效期,极端情况下会出现:
// (1)redis设置的有效期失效,导致部分奖品发不出去,原因:redis incr命令 扣减库存时,在key失效的情况下,把key的值默认设置为0。
// (2)redis回滚库存会出现多加库存的情况,当然可以通过后续的mysql扣库存防止超发的情况。
Boolean isLeftStock = this.decreasePrizeStock(activityId,sessionId,prizeId);
if(!isLeftStock){
log.info("prizeId={},没有奖品了",prizeId);
throw new BusinessException(GameErrorCodeEnum.LOTTY_ERROR.getErrorCode(),"没有抽中奖品");
}

try {
// mysql扣减库存
   // 代码略 主要操作 UPDATE activity_prize SET left_stock = left_stock - 1,version = version + 1 WHERE left_stock > 0 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;
}

本文参考链接:https://www.cnblogs.com/maohuidong/p/15202275.html