SELECT ... FOR UPDATE 必须走主键或唯一索引,否则退化为表锁;需确保事务显式开启、索引有效、更新原子执行,三者缺一不可。

SELECT ... FOR UPDATE 必须走主键或唯一索引,否则锁表
直接在 WHERE 条件里用 id = ? 是安全的;但用 name = ? 或 status = ? 这类非索引字段,InnoDB 会退化为表锁——所有后续的 SELECT ... FOR UPDATE 都会被阻塞,哪怕查的是不同商品,整个商品表就卡住了。
- 检查执行计划:
EXPLAIN SELECT * FROM good WHERE name = 'iPhone' FOR UPDATE,如果type是ALL或index,说明没走行锁 - 确保查询条件命中主键、唯一索引或覆盖索引(比如给
sku加唯一索引) - 避免在事务中先
SELECT *再判断,而应把库存校验逻辑写进 SQL 条件里,减少锁持有时间
事务必须显式开启,且不能跨请求生命周期
SELECT ... FOR UPDATE 只在事务内生效;一旦 commit 或 rollback,锁立刻释放。常见错误是:在 Web 请求里开了事务,但没等扣减完成就提前提交,或者忘了开启事务,导致锁根本没加。
- Spring Boot 中要用
@Transactional包裹整个扣减逻辑(查库存 + 更新库存),不能只加在 DAO 层 - Phalcon、ThinkPHP 等框架需手动调用
$connection->begin()和$connection->commit(),漏掉任一环节都会失效 - 不要在 HTTP 请求里“开事务 → 返回响应 → 异步更新库存”,锁会在响应后立即释放,起不到保护作用
扣减库存不能靠应用层计算,必须原子更新
查出 total = 100,然后 PHP/Java 里算 99 再 UPDATE,这中间仍有窗口期:另一个事务可能已扣减一次,你写的 99 就是错的。
- 正确做法是:在
UPDATE语句里直接做减法,例如UPDATE good SET total = total - 1 WHERE id = 1 AND total >= 1 - 配合
SELECT ... FOR UPDATE使用时,更新语句也必须在同一个事务中,且 WHERE 条件要再次校验库存(防脏读后的二次超卖) - 返回影响行数,如果
UPDATE影响 0 行,说明库存不足,不是程序异常,而是业务拒绝
FOR UPDATE 会阻塞其他 FOR UPDATE,但不阻塞普通 SELECT
这是关键认知偏差:很多人以为加了 FOR UPDATE 就“别人完全读不了这行”,其实普通 SELECT(不带锁)仍可读,只是不能加锁或修改。阻塞只发生在“下一个想对同一行加锁”的事务上。
- 这意味着:监控类查询(如后台看板)不会被阻塞,但并发下单请求会排队等待
- 如果发现大量请求 hang 在
SELECT ... FOR UPDATE,优先查是不是锁范围过大(比如没走索引)、事务过长(比如中间调了慢 HTTP 接口)、或死锁 - 生产环境建议加超时:
SET innodb_lock_wait_timeout = 5,避免一个卡住的事务拖垮整条链路
事务边界、索引有效性、SQL 原子性——这三个点只要漏掉一个,FOR UPDATE 就形同虚设。实际线上问题里,八成出在没确认索引是否生效,或者把锁和更新拆到了两个事务里。










