web学习笔记编程技术学习与分享

乐观锁与悲观锁:库存扣减怎么写

2026 年 08 月 18 日约 3 分钟阅读 后端与数据库

库存扣减是并发控制的经典案例:两个人同时下单,只剩一件库存,怎么保证不超卖。

悲观锁:先锁再改

BEGIN;
SELECT stock FROM sku WHERE id = 1 FOR UPDATE;   -- 行锁,其他事务等待
UPDATE sku SET stock = stock - 1 WHERE id = 1;
COMMIT;

逻辑直观,适合冲突率高的场景。代价是锁等待:热点商品大促时,大量事务排队,连接被占满,可能拖垮整个库。

乐观锁:先试再改

不提前加锁,提交时用版本号或条件判断来确认数据没被改过:

UPDATE sku SET stock = stock - 1
WHERE id = 1 AND stock > 0;          -- 受影响行数为 0 说明扣减失败

或者带版本号:

UPDATE sku SET stock = stock - 1, version = version + 1
WHERE id = 1 AND version = 5;

失败就重试或返回「已售罄」。适合冲突率低的场景:大多数时候一次就成功,没有锁开销。

超卖的完整解法

单靠数据库行,只能保证「不扣成负数」,还需要整条链路配合:

  1. 下单先创建订单再扣库存,或者用 Redis 预扣减(DECR 判负则回滚),数据库异步对账;
  2. 幂等设计:同一用户重复提交只扣一次;
  3. 限流前置:把明显超出库存的请求挡在队列外。

Redis 预扣减的骨架:

$ok = $redis->eval("if redis.call('get',KEYS[1]) + 0 <= 0 then return 0 end
                    redis.call('decr',KEYS[1]) return 1", ['stock:1'], 1);
if (!$ok) return '售罄';
// 创建订单 → 发 MQ → 消费者落库 → 失败时 INCR 回补

怎么选

  • 冲突少、逻辑简单:乐观锁(条件更新),代码量最小;
  • 冲突激烈、事务里还要改多张表:悲观锁,缩短事务时间;
  • 极端热点(秒杀):Redis 挡量 + MQ 削峰,数据库只承接削峰后的稳定流量。

三个方案不是互斥的,实际系统往往是「Redis 挡量 → 乐观锁落库 → 对账兜底」的组合。

MySQL并发设计