基于本文回答
0
评论

电商秒杀场景下,如何利用MySQL的锁机制防止商品库存超卖?

知识点图片

在电商秒杀场景中,高并发极易导致“超卖”问题(即实际卖出的商品数量超过了库存数量)。在使用MySQL作为底层存储时,防止超卖的核心在于保证扣减库存操作的原子性和排他性

以下是利用MySQL锁机制防止超卖的三种核心方案,以及在秒杀场景下的优劣势分析:

方案一:排他锁(悲观锁)—— SELECT ... FOR UPDATE

悲观锁的思想是:在操作数据前,先假定大概率会发生冲突,因此直接把数据锁住,直到操作完成才释放。

实现方式:
依赖 MySQL InnoDB 引擎的行级锁。在开启事务后,查询库存时加上 FOR UPDATE

sql
BEGIN; -- 开启事务

-- 1. 查询库存并加排他锁 (X锁)
SELECT stock FROM goods WHERE id = 1 FOR UPDATE;

-- 2. 业务代码判断库存是否足够 (比如 stock > 0)
-- 3. 执行扣减
UPDATE goods SET stock = stock - 1 WHERE id = 1;

COMMIT; -- 提交事务,释放锁

底层原理:
FOR UPDATE 会对主键 id=1 的记录加上排他锁(X锁)。其他并发的请求执行到这条 SELECT 语句时,会被阻塞,直到当前事务提交释放锁。这就相当于把并发请求变成了串行执行。

  • 优点: 逻辑简单,能绝对保证不超卖,数据强一致。
  • 致命缺点(不适合秒杀): 秒杀场景下并发极高。悲观锁会导致大量请求在数据库层排队等待锁,引发极高的锁竞争。这不仅会导致接口响应极慢、大量请求超时,还极易耗尽数据库连接池,最终导致数据库宕机。

方案二:基于版本号的乐观锁

乐观锁的思想是:假定冲突很少发生,不上锁,只在最后更新数据时,判断数据在此期间有没有被别人修改过。

实现方式:
在商品表中增加一个 version 字段。

sql
-- 1. 先查询库存和当前版本号 (不加锁)
SELECT stock, version FROM goods WHERE id = 1;

-- 2. 业务代码判断库存是否充足
-- 3. 尝试更新库存,条件必须带上刚才查到的 version
UPDATE goods 
SET stock = stock - 1, version = version + 1 
WHERE id = 1 AND version = #{刚才查到的旧version};

底层原理:
如果多个线程同时查到 version = 1,它们同时发起 UPDATE。MySQL 会利用隐式的行锁保证只有一个线程能成功执行,成功执行后 version 变成了 2。其他线程的 UPDATE 因为匹配不到 version = 1 的记录,受影响行数为 0,表示更新失败(此时业务层可以抛出异常或提示抢购失败)。

  • 优点: 读操作不加锁,大大提高了并发读取的性能。
  • 缺点: 在秒杀(写冲突极高)的场景下,会导致大量更新失败。比如1000个人同时发起请求,只有1个人能成功,剩下的999个全部失败(即使库存还有999个)。这就需要业务代码里加重试机制,但疯狂重试同样会给数据库带来巨大压力。

方案三:依赖 UPDATE 隐式行级锁的原子扣减(最优 MySQL 方案)

这是实际业务中最常用、最推荐的 MySQL 纯数据库层防超卖方案。它其实是“乐观锁”的一种变体,但不需要额外的 version 字段。

实现方式:
直接在 UPDATE 语句中加入库存大于0的判断条件。

sql
-- 一条SQL直接完成校验和扣减
UPDATE goods 
SET stock = stock - 1 
WHERE id = 1 AND stock > 0; -- 或者 stock >= 扣减数量

底层原理:

  1. 原子性: MySQL InnoDB 在执行 UPDATE 语句时,会自动对目标记录加上排他锁(行锁)。
  2. 最新读取: InnoDB 在执行 UPDATE 时,会执行“当前读”(Current Read),也就是它读取的 stock 永远是数据库中最新已提交的值,而不是当前事务隔离级别下的一致性视图中的值。
  3. 条件拦截: 当库存被扣减到 0 时,其他正在排队获取行锁的 UPDATE 请求拿到锁后,去执行 stock > 0 的判断发现不成立,就会直接放弃更新(影响行数为0)。
  • 优点:
    • 不需要 SELECT FOR UPDATE 先锁定,减少了网络交互和事务持有时间。
    • 利用了数据库原生的锁机制和原子性,完美防止超卖。
    • 不会出现方案二中“有库存但大量更新失败”的问题(只要有库存,排队拿到锁的就能扣减成功)。
  • 缺点: 虽然是最优 SQL,但在真正的双十一级别秒杀中,几万个请求同时去竞争同一行的行锁,依然会导致 MySQL CPU 飙升,影响其他业务。

总结与真实秒杀架构的补充

在 MySQL 层面,“方案三(UPDATE自带条件原子扣减)” 是防止超卖的最佳实践

但是,真正的高并发秒杀系统,绝对不能把压力全部直接透传给 MySQL。无论 MySQL 的锁用得多么好,单行记录的更新并发极限(TPS)通常也就几千而已。

工业界的秒杀防超卖架构通常是多级拦截的:

  1. Redis 预扣减库存(核心): 将库存提前加载到 Redis 中,利用 Redis 的单线程特性或 Lua 脚本,在内存中进行原子性的库存扣减。拦截掉绝大部分请求(比如 1000万请求只放行 100 个)。
  2. 消息队列(MQ)削峰: Redis 扣减成功的请求,放入 Kafka/RabbitMQ 等消息队列中异步处理。
  3. MySQL 兜底: 消费者从队列取出消息,慢慢落库。此时再使用 方案三 的 SQL (UPDATE ... WHERE stock > 0) 进行最终的数据库防超卖兜底操作,保证数据绝对的准确性。此时 MySQL 面对的并发已经是经过 Redis 过滤后的正常流量了。
右滑查看面试常问