在电商系统中,防止超卖 是无法绕开的一个经典问题,本系列就通过几篇文章,通过自己的案例,来由浅入深,逐步的讲解。
注:主要讨论单机大并发环境下,微服务以后再聊
本文聚焦于单机高并发场景,暂不考虑缓存、限流等中间件,纯粹从数据库层面分析超卖原因,并给出悲观锁(JVM锁)和乐观锁(条件更新)两种基础方案。
正常的下单流程一般是这样的:用户A点击购买按钮,系统调用后台接口查询产品剩余库存,判断是否支持本次购买数量,如果不支持则提示库存不足,如果支持,数据库中将剩余库存字段值减去本次购买数量,返回成功结果。
——在单线程环境下,这个流程完美运行。但一旦有多个用户(即多个线程)同时下单,问题就来了:
此处以“下单即扣库存”为例(支付后扣库存还会涉及占用释放问题,后续再讲)。这种 “查→扣→返回” 的操作,在单线程下是安全的,但在高并发多线程环境下,问题出在“查”和“扣”之间不是原子操作。
举个典型例子:线程A购买2件,查询到库存剩余3(可扣);与此同时,线程B也购买2件,由于时间片轮转,B在线程A还未执行扣减时也查询到了库存剩余3。随后线程A先扣减2(剩余1),线程B再扣减2(剩余-1)。数据库剩余库存变成了负数,明明只有3件库存,却卖出了4件,这就是超卖。
从上面的案例可以看出,超卖问题的根源可以归结为以下三个因素(尤其在并发场景下三者叠加,问题尤为突出):
基于第二点的分析,解决超卖的核心思路无非两条:
悲观锁思路:通过加互斥锁,将并发操作串行化,用阻塞换取数据一致性。
乐观锁思路:不依赖业务锁,通过在更新时附加前置条件(如库存充足判断),用失败重试或放弃来换取并发性能。
将“查库存 → 判断 → 扣库存”这一组操作通过加锁变为原子操作,确保同一时刻只有一个线程能执行。
单机场景:可使用 JVM 内置锁(synchronized、ReentrantLock)。
分布式场景:需借助 Redis 分布式锁或 ZooKeeper 分布式锁。
(本文后续讨论均基于单机场景,分布式锁将在后续系列中展开)
加锁后,线程B必须等待线程A完成扣减并释放锁后才能查询,此时读到的是扣减后的最新库存(剩余1),从而避免超卖。
//每个商品一把锁 锁的是商品
//lockMap 会随商品数量增长而膨胀,生产环境可改用 Guava Cache 或 Caffeine 实现带过期策略的锁缓存,
//避免内存泄漏。
private final ConcurrentHashMap<Long, Object> lockMap = new ConcurrentHashMap<>();
//伪代码
//实际开发中需注意事务提交时机,确保锁的范围覆盖整个事务
public void createOrder(Long productId, Integer buyCount) {
Object lock = lockMap.computeIfAbsent(productId, k -> new Object());
synchronized (lock) {
// 1. 查询库存
Integer stock = productMapper.getStock(productId);
// 2. 判断库存是否充足
if (stock < buyCount) {
throw new InsufficientStockException("库存不足");
}
// 3. 扣减库存
productMapper.decreaseStock(productId, buyCount);
// 4. 创建订单...
}
}不依赖业务锁,而是在 SQL 更新时直接将“库存充足”作为前置条件:
UPDATE product SET stock = stock - #{buyCount}
WHERE id = #{productId} AND stock >= #{buyCount};通过判断 affected rows(影响行数)是否大于 0 来确定是否扣减成功:若为 0,说明库存不足或已被其他线程抢先扣完,业务层返回“库存不足”。
(注:也可引入 version 字段实现乐观锁,但在库存扣减场景下,上述 SQL 方式更直接高效,能同时防止库存为负。)
不管是方案一还是方案二,核心都是通过某种机制保证"查"和"扣"的原子性。
但两种方案的代价截然不同:
悲观锁通过阻塞将并发串行化,虽然简单可靠,但线程排队等待会显著拉长响应时间,高并发下吞吐量急剧下降。
乐观锁虽不加业务互斥锁、不阻塞线程,在冲突率较低的场景下性能优于悲观锁;但在秒杀这类冲突极高的场景下,大量更新冲突导致的重试也会成为新的瓶颈。
两种方案在真正的秒杀级流量下,都有各自的短板。 那么,有没有既能防止超卖、又能兼顾高并发的更优解?
下一篇,我们将引入 Redis 预扣库存 + 同步落库 的思路——利用 Redis 的高速读写能力承担秒级流量的库存扣减,再通过同步方式将最终结果落库,在性能和一致性之间寻找更优雅的平衡点
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。
原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。
如有侵权,请联系 cloudcommunity@tencent.com 删除。