高并发场景下,库存问题解决方案

背景:

业务场景:库存扣减场景,消费方调用dubbo服务,扣减订单的库存

Dubbo配置:5s超时时间,未配置默认重试次数

消费方:对商品加redis分布式锁,防止并发扣减,失效与超时时间为10s

问题:用户对相同商品的订单,同时发起扣减库存请求,造成了未扣减,或同一订单多扣减的情况

疑惑:未扣减可能是失败了,但为什么会同一订单多扣减?明明加了redis锁啊!

但问题,就出在redis锁上

复现:

1.大量相同商品扣减请求发出

2.消费方对一笔订单A的商品加锁,其他订单请求进来后redis等待

4.当订单量巨大,必然会造成订单B的redis等待超时(10s),此时dubbo请求超时(5s)

5.另一方面,由于当前大量线程不断进入并持续等待,造成cpu迟缓,当前订单A库存的操作时间变长,便可能造成扣减库存变长(7s),订单Adubbo请求超时(5s),而此时订单A仍在扣减库存

6.订单B重试2次,均未抢到锁,扣减失败

7.订单A扣减1次后,由于重试,再次扣减1次,甚至2次

分析:

经过情景复现,可以知道,由于

1.dubbo的默认重试机制

2.redis锁的等待超时

最终造成了这种情况,如何避免呢?

避免:

dubbo:

1.对非幂等性接口,dubbo不进行重试

2.对于库存扣减这种操作,消费方不关心操作结果,操作成败与否的重任交给服务方,所以dubbo请求超时时间可以定位100ms甚至更低

消费方:

1.服务方接口添加幂等性校验:如通过redis锁对订单号加为期10s的锁(时间可结合业务),尽量防止重复请求与并发

2.引入失败重试机制,对redis超时或扣减失败的订单数据进行记录,利用定时任务等机制重试1-2次。提高系统的容错性与性能

问题:

上面的复现中,我们提到了可能的重复扣减的可能性场景,但结合我所遇到的问题,依然遗留下两个问题:

1.dubbo默认重试机制是2次,加上第一次的请求,一共是3次,但我看到共有4次该订单的请求,十分不解!

2.在【复现】中模拟的重复扣减场景里,按理重复的扣减应该是间隔1-5s左右的时间间隔,但重复的扣减记录却是相同的扣减时间,难道在redis锁下,并发了?不解...

===============================================

经过将近一年的运行,业务整体平稳,但也遇到了一些新问题,并进行了解决

在这里更新记录一下主要的问题

1.

【问题:】出现了库存成功扣减为0,但库存状态仍为【正常】的情况

【复现:】多个订单对应相同系统商品去打印发货扣减商品时,会出现这种情况。比如:库存剩余8,用8个订单去打印扣减,一般都会大概率复现这个问题

【分析:】我们目前是利用redis锁,对某一个系统商品/规格锁定后进行扣减库存操作,然后再释放锁。所以反馈的用户很少(半年内两次)。

我便初步认为应该是redis锁失效导致两个线程都同时进行了库存扣减操作导致。所以对redis锁机制替换为利用setnx(之前是自己实现的redis锁机制)。

但在测试期间又出现了这种情况,那问题就不是redis锁的问题了,日志也反应出A线程操作时,B线程确实在等待,那为什么会出现两个线程扣减后状态未变更的情况呢?

答案就是:【事物】!

【解决:】扣减库存操作是先查询现有库存,然后在内存中根据库存计算出库存状态,然后再修改库存的SQL中拿这个内存计算好的库存状态去修改。这时,如果事物没控制好,就会出问题

我们项目全局开启了事物支持——@EnableTransactionManagement

【一个方法默认只有当全部执行完成后才会真正提交SQL到数据库。】

所以问题就是这个原因:即使用redis对库存修改方法加锁。在执行完库存修改方法后释放锁,但事物并不会立刻提交,而是要等到整个方法执行完才会提交。所以A线程释放锁,B线程获取锁,去查库存,此时如果A还没执行完。B线程查询到的还是A之前的库存。就会导致B计算的库存状态正确

所以我们需要重新注入service的Bean,然后再调用库存修改方法,然后对库存修改方法要指定事物传播机制为@Transactional(propagation = Propagation.REQUIRES_NEW)

当库存修改完后,立刻提交!然后再释放锁。然后B获取锁,然后去查库存时就会最新的库存。计算的库存状态就是正确的!

【总结:】这个问题本质看来是很简单,就是我在开发时没有意识到事务传播机制的问题。另外我对事物传播默认机制理解也有问题,官方文档是:【支持当前事务,如果没有事务会创建一个新的事务。】

我理解的是判断新的bean方法有没有事物,有食物就用当前bean方法的事物。恰恰相反!

是看调用这个Bean方法的A方法有没有事物,有事务就用A方法的事物。所以就算利用重新注入Bean再调方法,其实事务和外面的方法还是一个整体!果然理解很重要!要和多求证!

2.

【问题:】有用户反馈扣减库存延迟,导致用户多发货的问题

【复现:】多个订单对应一笔库存进行打印发货时,就很容易触发异步扣减库存

【分析:】因为多个订单会拆分成几组发送多个dubbo请求。到进销存后,一方面可能会触发dubbo请求限流走异步。另一方面就是在实际扣减时发生redis锁竞争导致走异步

【解决:】第一种情况比较好解决,目前dubbo请求的阀值是4,很低了,提高阀值即可。

第二种情况就要多考虑一下。
在【优化前】的锁竞争机制是:会尝试竞争5s,如果5s内持续未获取到锁,就会走异步

这种方式一般情况是满足了,但我觉得可以多提供几种可配置的锁竞争方式:

(1.)持续锁竞争,竞争失败就睡50ms,然后再次竞争,最多竞争持续10s

注释:这种情况是对应平时情况,尽可能让库存实时扣减,不走异步。如果10s都未获取到锁,那么可能redis或者代码出问题了

(2.)竞争两次,如加锁失败一次,则等待50s后再试一次。

注释:这种应对用户请求量增大或者大促期间。多提供一次机会,如果两次都失败,就走异步

(3.)竞争一次,失败就直接异步

注释:这种应对大促极限情况,尽可能的走异步。出现竞争并发就走异步。

通过这种方式,基本应对了普遍情况,但实际效果还是要看用户反馈实时跟进

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值