关于秒杀系统中订单库存扣减的实践

一、背景

一般在日常开发中经常会遇到打折促销,秒杀活动,就如拼多多最近的4999抢券买爱疯11促销活动,毕竟谁的钱也不是大风刮来的,有秒杀有促销必定带来大量用户,而这类活动往往支撑着公司重要营销策略,所以保证系统在高并发下不出异常非常关键,这其中棘手的便是如何在高并发下高效的处理库存数据。

现在处理这种场景存在多种方案。但是要保证高性能和高可用,大部分方案并不满足,今天就来聊聊高并发下库存加减那些事儿。

二、方案

1. 历史数据库的事务特性和唯一主键

基于数据库的事务,扣减库存的操作方法同一个事务中进行库存扣减,事务中任何操作失败,执行回滚操作。从而保证原子性。单纯靠数据库的事务,只能在单体的项目中。如何要分布式的项目中,就无法保证单线程操作了。

那如何在多进程中实现单线程扣减库存呢?我们可以利用数据库的唯一索引。具体操作步骤:

  1. 新建立一张表:t_lock_order,同时将商品ID作为唯一索引;
  2. 进行扣减库存之前在表中插入商品ID,然后进行数据库更新;
  3. 更新结束后删除刚才插入数据库中的记录。

A线程进程扣减库存时候,插入了该商品的ID,当B线程扣减该商品的库存的时候,同样也会在数据库中插入该商品ID,A线程没有执行完B线程插入同一个商品ID就会报主键重复的错误,这样就扣减库存失败。

这种方案,功能上是可以实现;但是过分依赖数据库,无法满足其性能要求,而且存在很多获取锁失败的情况,用户体验差。

2. 分布式锁

Redis 或者 ZooKeeper 来实现一个分布式锁,以商品维度来加锁,在获取到锁的线程中,按顺序去执行商品库存的查询和扣减,这样就同时实现了顺序性和原子性。其实这个思路是可以的,只是不管通过哪种方式实现的分布式锁,都是有弊端的。

以 Redis 的实现来说,通过超时时间来控制锁的失效时间,不太靠谱,比如在有些场景中,一个线程 A 获取到了锁之后,由于业务代码执行时间可能比较长,导致超过了锁的超时时间,自动失效,后续线程 B 又意外的持有了锁,当线程 A 再次恢复后,通过 del 命令释放锁,就错误的将线程 B 中同样 key 的锁误删除了。
在这里插入图片描述

所以,如果锁的超时时间设置过长,会影响性能,如果设置的超时时间过短,有可能业务阻塞没有处理完成,能否合理设置超时时间,是基于缓存实现分布式锁很难解决的一个问题

那么如何合理设置超时时间呢? 你可以基于续约的方式设置超时时间:先给锁设置一个超时时间,然后启动一个守护线程,让守护线程在一段时间后,重新设置这个锁的超时时间。实现方式就是:写一个守护线程,然后去判断锁的情况,当锁快失效的时候,再次进行续约加锁,当主线程执行完成后,销毁续约锁即可。不过这种方式实现起来相对复杂,我建议你结合业务场景,所以针对超时时间的设置,要站在实际的业务场景中进行衡量。

3. Redis + lua 脚本
Redis 单线程支持顺序操作,而且性能优异,但是不支持事务回滚。但是通过 Redis + lua 脚本可以实现 Redis 操作的原子性。这种方案同时满足顺序性和原子性的要求了。能帮我们实现 Redis 执行 Lua 脚本的命令可以采用EVALSHA,接下来用代码实现它。

1) 核心思路

首先我们根据库存扣减核心操作,完成核心 Lua 脚本的编写。其主要实现的功能就是查询库存并判断库存是否充足,如果充足,则做相应的扣减操作,脚本内容如下:
在这里插入图片描述

2) 业务逻辑
然后我们将 Lua 脚本转成字符串,并添加脚本预加载机制。

预加载可以有多种实现方式

  1. 一个是外部预加载好,生成了 sha1 然后配置到配置中心,这样 Java 代码从配置中心拉取最新 sha1 即可;
  2. 另一种方式是在服务启动时,来完成脚本的预加载,并生成单机全局变量 sha1。

我们这里先采取第二种方式,代码结构如下图所示:

在这里插入图片描述

以上是将 Lua 脚本转成字符串形式,并通过 @PostConstruct 完成脚本的预加载。然后新增 EVALSHA 方法,如下图所示:

在这里插入图片描述
方法入参为活动商品库存 key 以及单次抢购数量,并在内部调用 Lua 脚本执行库存扣减操作。看起来是不是很简单?在写完底层核心方法之后,我们只需要在下单之前,调用该方法即可,具体如下图所示:

在这里插入图片描述

三、总结
最后,我们从技术的角度分析了库存超卖发生的两个原因:

  1. 一个是库存扣减涉及到的两个核心操作,查询和扣减不是原子操作;
  2. 另一个是高并发引起的请求无序。

在秒杀场景下,因为查询缓存要比查询数据库快,一般将库存数放在缓存中,直接在缓存中扣减库存。在上面的三个方案中,小编建议是采用redis+lua的方案,即利用Redis的单线程原理,以及提供的原生 EVALSHA 和 SCRIPT LOAD 命令来实现库存扣减的原子性和顺序性,并且经过实测也确实能达到我们的预期,且性能良好,从而有效地解决了秒杀系统所面临的库存超卖挑战。

最后,如果我的文章对你有所帮助或者有所启发,欢迎关注公众号(微信搜索公众号:首席架构师专栏),里面有许多技术干货,也有我对技术的思考和感悟,还有作为架构师的验验分享;关注后回复 【面试题】,有我准备的面试题、架构师大型项目实战视频等福利 , 小编会带着你一起学习、成长,让我们一起加油!!!

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

首席架构师专栏

喜欢请点赞,么么哒

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

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

余额充值