UPDATE单行卡住请求是因为多事务竞争同一行导致X锁排队,常见于秒杀等热点场景;提前用SELECT...FOR UPDATE加锁反而延长持锁时间,应避免事务中混入非DB操作,优先用乐观锁或Redis分布式锁解耦。

为什么 UPDATE 单行会卡住一堆请求?
不是语句写错了,而是多个事务同时更新同一行(比如订单状态、库存计数器、用户积分),InnoDB 会对该行加 X 锁,后续请求只能排队等锁释放。常见于秒杀、抢券、支付回调等场景——热点行一旦出现,innodb_row_lock_waits 和 innodb_row_lock_time_avg 会明显升高。
用 SELECT ... FOR UPDATE 提前加锁反而更危险
很多人想“提前占住”,在业务逻辑开头就执行 SELECT id FROM t WHERE id = 123 FOR UPDATE,但实际会让锁持有时间变长:只要事务没提交,锁就一直挂着。如果中间有 RPC 调用、日志写入或异常分支延迟提交,等待队列会指数级膨胀。
- 避免在事务中混入非数据库操作(如 HTTP 请求、文件读写)
- 若必须分步处理,考虑把锁粒度拆到应用层(例如用 Redis 分布式锁控制“是否允许进入 DB 更新流程”)
-
FOR UPDATE后立即做UPDATE,不要中间穿插其他逻辑
批量更新热点行时,WHERE 条件必须命中索引
哪怕只更新一行,如果 WHERE id = ? 中的 id 没走主键或唯一索引,InnoDB 可能升级为间隙锁甚至表锁,导致无关行也被阻塞。用 EXPLAIN 确认执行计划是否走了索引;尤其注意隐式类型转换(比如字段是 VARCHAR,但传了数字参数)会让索引失效。
- 检查
SHOW ENGINE INNODB STATUS\G中的TRANSACTIONS部分,看锁等待具体卡在哪一行 - 对高频更新列(如
status、version)单独建二级索引,避免全表扫描触发锁升级 - 避免在
WHERE中使用函数(如WHERE DATE(create_time) = '2024-01-01')
用乐观锁替代行锁:UPDATE ... SET version = version + 1 WHERE id = ? AND version = ?
适用于冲突不频繁但锁等待代价高的场景。失败时捕获影响行数为 0,由应用重试(带退避)或降级处理。注意:version 字段必须有初始值,且所有更新路径都必须带上该条件,否则乐观锁形同虚设。
- 重试次数建议 ≤ 3,避免雪崩;第 2 次起可加
SELECT ... FOR UPDATE NOWAIT强制快速失败 - 不要把乐观锁和
SELECT ... FOR UPDATE混用在同一事务里,会失去意义 - MySQL 8.0+ 可配合
WAIT n(如FOR UPDATE WAIT 1)限制等待时长,防止无限挂起
真正难处理的不是单次锁等待,而是锁等待引发的连接池耗尽、超时链式传播、以及监控盲区——很多团队只看慢 SQL,却没盯住 innodb_row_lock_time_max 的毛刺突增。










