java redis订单减库存6,secKill项目 --- Redis预减库存的存在的问题 及 库存补偿

本文建立在已完成可靠性传输的基础上,建议先阅读另一篇博文,不然可能难以理解。secKill项目 — 可靠性传输的实现 及 易错点总结

先看看秒杀接口的源代码:

@RequestMapping(value="/{path}/do_miaosha",method=RequestMethod.POST)

@ResponseBody

public Result doMiaosha(Model model,MiaoshaUser user,

@RequestParam(value="goodsId",defaultValue="0") long goodsId,

@PathVariable("path")String path) {

model.addAttribute("user", user);

//0.判断用户是否非空、秒杀路径是否正确

// 1. 查看内存标记,看是否已结束

boolean over = localOverMap.get(goodsId);

if (over) {

return Result.error(CodeMsg.MIAO_SHA_OVER);

}

//2.预减少redis的库存

long stock=redisService.decr(GoodsKey.getMiaoshaGoodsStock,""+goodsId);

//3.判断减少数量1之后的stock,减少到0一下,则代表之后的请求都失败,直接返回

if(stock<0) {

//进行内存标记

localOverMap.put(goodsId, true);

return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);

}

//4.判断这个秒杀订单形成没有,判断是否已经秒杀到了,避免一个账户秒杀多个商品

MiaoshaOrder order = orderService.getMiaoshaOrderByUserIdAndCoodsId(user.getId(), goodsId);

if (order != null) {// 查询到了已经有秒杀订单,代表重复下单

return Result.error(CodeMsg.REPEATE_MIAOSHA);

}

//5.正常请求,入队,发送一个秒杀message到队列里面去,入队之后客户端应该进行轮询。

MiaoshaMessage mms=new MiaoshaMessage();

mms.setUser(user);

mms.setGoodsId(goodsId);

mQSender.sendMiaoshaMessage(mms);

//返回0代表排队中

return Result.success(0);

}

其中的第三步:

if(stock < 0) {

//进行内存标记

localOverMap.put(goodsId, true);

return Result.error(CodeMsg.MIAOSHA_OVER_ERROR);

}

乍一看没问题,但其实是需要进行redis的库存补充的,代码为redisService.incr(GoodsKey.getMiaoshaGoodsStock, "" + goodsId);

为什么需要补充库存?

原因有几点:

不补充redis的库存,就会无法保证数据库和redis的最终一致性。这里说的是最终一致性,而不是说强一致性。很简单的道理,redis的数量会是负数,而数据库的最多就是 0

即使不考虑最终一致性,那如果有订单重复、或者消费失效时,这些地方对redis库存进行了释放了(操作还是incr,不过"释放"好理解)。但此处不做回补处理,那显然stock还是负数,这样就造成:数据库还有库存,但redis已经没有库存了,而这显然是不合理的。

综上,此处是需要进行库存的补充的。

当然,更优的做法,是将第2、3步组合起来:

先判断redis库存是否 > 0 ,是才将库存减一。

但需要注意用Lua脚本实现,才能保证原子性。直接在代码用if判断是不行了。

其他需要补充的库存的地方

(原项目完全没有进行处理)

生产者:

上述第四步的判断重复秒杀

其实更好的做法是 4 、2 互换,先判断是否重复秒杀,再进行预减库存。

这样就不用补充库存,就减少了对redis的访问了。笔者采用的就是这种

重发消息超过限制

消费者:

判断已经秒杀到

下单时,发现重复订单,违反唯一约束

消费者可以判定是消费失败时

这里说法比较"模糊",因为如何判定消费失败,是与业务相关的

以项目为准进行说明,就是最后的兜底 + 重试超限后,根据标记,决定是否补充库存

道理也很简单,try外定义一个boolean ,初始为false ,在秒杀接口后,设置为true

异常来源:

秒杀接口之上的代码,补偿库存

秒杀接口内部,由事务保证,进行回滚。下单失败,补偿库存

秒杀借口之下(当前项目没有)。此时下单已成功。而消费者并没有开启事务,下单操作不会进行回滚。因此不能补充库存。

这种情况虽然异常,但仍应视为消费成功

这里还需要注意:消费端发现消息已消费,或者库存不足时,是不需要而且不能进行redis库存的补充。

伪代码如下,( 去掉了可靠性传输的相关代码,方便强调库存操作)

@RabbitHandler

@RabbitListener(queues = MQConfig.MIAOSHA_QUEUE)

public void receive(String message, Channel channel, Message messages) throws Exception {

boolean miaoshaSuccess = false;

// try住整个代码,防止漏异常。如果catch需要的try中的变量,则提前声明就好了

try {

// 消息幂等性的处理

String correlatonId = messages.getMessageProperties().getCorrelationId();

//根据correlationId判断消息是否被消费过

if (消息已经被消费过) {

return;

}

if (库存不足) {

return;

}

if (已经秒杀到) {// 重复下单

补充库存

return;

}

//进行原子操作:1.库存减1,2.下订单,3.写入秒杀订单--->是一个事务

miaoshaService.miaosha(user, goodsvo);

miaoshaSuccess = true;

} catch (DuplicateKeyException e) {

补充库存

} catch (Exception e) {

if(重试超出限制){

if(miaoshaSuccess == true){

补充库存

}

}

}

}

拓展思考:

进行redis的库存补充这个操作,能不能替换成更新redis库存为数据库库存,这样不是更能保证一致吗?

首先需要查询数据库,这就多增加了压力了

其次,与直观感觉相反,这样恰恰是会违反一致性的。因为引入了MQ,redis的库存一直都是 <= 数据库的库存,如果直接更新为数据库库存,就会造成"假情报"。

注意这里其实是不会影响正确性的,因为秒杀sql中,用了 >0。无论如何都不会发生超卖。但是自然的,这些无效的消息也是浪费资源的。所以这样处理是不对的。

关于消费者代码中的,if (库存不足) {return;} 这段代码的必要性的思考

因为得益于redis的单线程,我们其实可以保证进入MQ的消息数量就是商品的数量。

当然这里面有"水分",这些消息中可能有同一个用户的多条消息。但这难道不是意味着:MQ的消息消耗的商品,是 <= 数据库的库存。 如果是这样,那这个if应当是可以省略的。

而且经笔者测试,这个if也确实一次都没有触发。

其实此处还可以延伸到miaoshaService.miaosha(user, goodsvo) 的代码:

@Transactional

//减库存 下订单 写入秒杀订单 的原子操作

public void miaosha(MiaoshaUser user, GoodsVo goods) {

// 小优化:可以先下订单,再减库存。这样在订单重复的时候,可以不用回滚库存的表操作

boolean success = goodsService.reduceStockWithPessimisticLock(goods);

if (success) {

orderService.createOrder(user, goods);

} else {

setGoodsOver(goods.getId());

}

}

这里的success 也是同理,看数据库库存还有没有来返回。所以其实setGoodsOver 方法是无效的。(已经过测试)

因此这里的success应该换成查询数据库库存是否为0。代码如下:

@Transactional

public void miaosha(MiaoshaUser user, GoodsVo goods) {

orderService.createOrder(user, goods);

// 但其实success 必为true, setGoodsOver 永远不会被触发。

goodsService.reduceStockWithPessimisticLock(goods);

int stock = goodsService.getGoodsVoByGoodsId(goods.getId()).getStockCount();

// 其实只会 == 0 ,但安全起见,还是<=

if (stock <= 0) {

setGoodsOver(goods.getId());

}

}

当然了,这是基于redis单机的情况下说的,集群环境下我们是无法做到上述的保证的,因此这些代码还是需要的。

不知道笔者理解有没有错。

至此,库存的补偿方案完成了。

redis库存和数据库库存,现在已经可以保证最终一致性了。

但这并不等同于项目的正确性。

详见另一篇博文:secKill项目 — 限制同一用户只有一个请求生效

本文完,有误欢迎指出

  • 1
    点赞
  • 3
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值