乐观锁与悲观锁:库存扣减怎么写
库存扣减是并发控制的经典案例:两个人同时下单,只剩一件库存,怎么保证不超卖。
悲观锁:先锁再改
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;
失败就重试或返回「已售罄」。适合冲突率低的场景:大多数时候一次就成功,没有锁开销。
超卖的完整解法
单靠数据库行,只能保证「不扣成负数」,还需要整条链路配合:
- 下单先创建订单再扣库存,或者用 Redis 预扣减(
DECR判负则回滚),数据库异步对账; - 幂等设计:同一用户重复提交只扣一次;
- 限流前置:把明显超出库存的请求挡在队列外。
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 挡量 → 乐观锁落库 → 对账兜底」的组合。