抢购场景优先用悲观锁但须满足单行热点、索引命中、事务极短三条件,否则推荐乐观锁;死锁或变慢主因是锁范围失控,如未走索引致表锁、事务内混入io拉长锁持有时间、一次锁多行等。

抢购场景下,优先用悲观锁(SELECT ... FOR UPDATE),但必须满足“单行热点 + 索引命中 + 事务极短”三个硬条件;否则乐观锁(version字段)更可控,尤其当库存扣减逻辑简单、允许少量重试时。
抢购为什么容易死锁或变慢?
常见错误现象:Deadlock found when trying to get lock 或响应延迟突增,根本原因不是锁本身,而是锁范围失控:
- 没走索引导致
FOR UPDATE升级为表锁,所有抢购请求排队等同一把锁 - 事务里混了 HTTP 调用、日志打印、sleep 等非数据库操作,让锁持有时间从毫秒拉长到秒级
- 一次
FOR UPDATE锁多行(比如查“未支付订单”再扣库存),扩大了锁竞争面
SELECT ... FOR UPDATE 必须满足的三个前提
它不是“加了就安全”,而是“加对才有效”:
- 查询条件必须命中主键或唯一索引,例如
WHERE product_id = 123;WHERE status = 'pending'这类非索引字段会锁全表 - 事务必须显式开启并尽快提交:
BEGIN→SELECT ... FOR UPDATE→UPDATE→COMMIT,中间不能有业务层判断分支或网络 IO - 避免在同一个事务里多次读取同一行再拼 SQL,否则可能读到旧值,又漏掉并发更新
乐观锁 version 字段怎么写才不翻车?
只写 WHERE version = ? 是无效的,真正起作用的是完整闭环:
- 更新语句必须是
UPDATE t SET stock = stock - 1, version = version + 1 WHERE id = ? AND version = ? - 应用层必须检查
ROW_COUNT()或影响行数:为 0 表示已被别人抢先改过,此时应立即失败或重试,不能静默忽略 -
version字段类型必须是INT或BIGINT,禁用TIMESTAMP——毫秒级重复或服务器时钟回拨会导致校验失效
ON DUPLICATE KEY UPDATE 算不算锁?
它本质是隐式悲观锁,但粒度更细、开销更低:
- 只锁住触发冲突的那条记录(或索引间隙),不锁扫描范围
- 适合“用户维度幂等下单”这类场景,例如
UNIQUE KEY (user_id, sku_id),能天然防重复提交 - 但它不能替代库存扣减的原子性判断:如果要先查库存是否充足再下单,仍需
FOR UPDATE或version配合业务逻辑
真正难的不是选乐观还是悲观,而是把“库存扣减”这个动作拆解清楚:它到底是纯数据变更(可用 ON DUPLICATE KEY UPDATE),还是依赖前置业务判断(必须 FOR UPDATE 或 version 校验)?漏掉这层,任何锁都救不了。











