背景
公司的电商系统下单 操作库存是一个频繁操作,需要高效地扣减库存,把对销售库存的操作抽出来独立设计一个库存中心系统。
功能包括库存的批量添加、获取、下单、支付、回退等的操作。
解决的业务痛点
- 需要高效
- 不超卖
方案
一、使用msql乐观锁
额外增加一个字段,并利用mybatisplus 注解@Version,可以方便地利用乐观锁的方式来操作库存。
这种方式当多个并发碰到一起时,只有一个版本的线程能拿到数据操作,并且要对数据库不断进行查询和修改的io操作,不合适,性能也不好。
二、使用分布式锁
直接加锁本质上是对竞争资源的线程进行排队处理,保证了并发时数据扣减不出错,但直接操作db效率肯定也不高。
三、放在redis操作
大概思路是,把操作库存的动作放在redis操作,(无论是下单扣减,还是回退,添加库存等)redis操作完毕发一个mq消息,然后就可以给上游服务响应了。后续的db扣除操作在消费mq时mysql慢慢消化就行。
上图展示了下单场景时的时序图。支付、撤销、添加库存等操作原理类似。这样的话库存是以redis为准了。
要点:
- 创建商品时,db保存库存,并把库存同步到redis作为预热的缓存。
- 下单时 可以多插入一个【多顶库存记录表】,并开启事务,这个表主要是用来记录操作库存的日志的,用来后续如果有问题可以对照(业务兜底)。因为都是insert 操作,没有update数据库行记录,不会有资源竞争所以效率还好,主键必须设置为长整型,并且自动递增。(这个步骤可以根据业务不增加可以进一步提高并发)。
- 消费mq真正对db的库存操作时,可以先对当前sku的redis缓存增加缓存有效时间,相当于给当前操作的sku缓存续命,防止缓存在处理时失效导致redis的数据和db的库存不一致。
- 消费端消费mq消息时要 对每个sku要加分布式锁的处理,保证数据的正确。
- 我是用lua脚本的方式让库存的操作在redis那边也保证原子执行。因为我的库存有 可用库存、锁定库存、已售库存,所以我用redis的hash数据结构存储:
可以看到每个操作都写了两个lua脚本目的是为了提高下效率,先查询一下(check),因为是批量操作,如果有不符合要求的直接返回了,不用等到执行一半再返回。
- 还有一点要注意因为redis在生产是部署在集群环境,想要key落在同一个槽中,key的名字要加{}即可,否则会报错
- 集群环境下同一个lua脚本中不能操作两个不同槽的key的数据(即两个用不同大括号括起来的key),否则也会报错
其他
- 其实还有其他方式能做到高效原子扣除,比如利用redis的watch机制,当缓存的key发生变化时表明redis已经被扣除过,其他线程不能继续对此key扣除,可以保证不超卖,这个思路就不具体展开了。
还能优化吗
- 其实还可以针对redis进一步优化,因为我们目前这种方式即使集群了也是用到了redis的某个单一数据库。我们可以进一步把库存分散,比如原本100个库存,可以进一步拆分为两个子库存来,把这两个库存分在不同的redis数据库,每个就50,那么也可以进一步利用了redis 提高性能,当然通常需要会对 关键接口进行压测,比如下单接口,从整个链路来追踪性能瓶颈,性能瓶颈一般不在redis而在数据库。
进一步优化流程。
- 我们在流程图可以看到redis扣除库存后,扔消息给mq,尽管已经异步消费了。这里的消费是必须保证一个个消费的,这里是用分布式保证,随着并发的增加 数据库的链接变长会被占用这这个资源链接,当数据库的资源被占用尽后会影响整体数据库的性能,会出现一些慢查询,尽管是一些很简单的查询(不是因为查询慢 而是因为数据库连接池被占用尽了)。怎样解决?可以把这些消息放到延迟队列让redis扣除库存后慢点消费 延迟操作数据?这样可以一定程度来缓解数据库压力,但是等到执行这些延迟消息时 数据库也是会有同样问题,怎么处理呢。
- 这里可以使用(乐观锁 + 延迟消息)的方式,但是在资源竞争时 乐观锁只保证其中一个版本的数据成功,失败的我们不能丢弃的(因为丢弃后 数据库和redis的库存就不一致了,不能多扣也不能少扣的)。
进一步优化方式。
- 解决方式(乐观锁 + 延迟消息):这里的分布式锁可以保留也可以不保留,保留的话锁的时间设置要设置得非常小,比如获取锁时只给10毫秒,10毫秒获取不到锁就 抛出异常获取手动记录失败信息,同时发一个延迟消息,用于操作失败的库存扣减业务。
- 当然如果放到延迟队列的库存扣减业务也有可能失败,通常失败我们需要重试三次,如果三次还失败,那么就只能手动写log或日志记录,后续人工检查操作了。通常如果Mq服务器不挂机,一般不会失败。
Mq服务挂机的业务问题。
- 我们可以看到这个流程其实有一个问题,就是当redis扣减成功,发送Mq消息时,如果这时mq服务器不可用或者挂掉,那么redis数据库和db库存就会不一致了。
- 这里我们需要在代码编写时要注意点,做个业务兜底就行,在redis扣减库存代码后面需要捕捉异常,如果有异常就回退操作库存。
如果觉得此文章对你有帮助,欢迎点赞,收藏,支持。
(本人承接各种小网站、商城、app应用等开发,如果有需求欢迎私聊。)