结论是排队主因在于SELECT FOR UPDATE使用不当:事务未及时提交、查询未走索引、裹挟非DB操作,而非语句本身;应优先用唯一索引+INSERT ON DUPLICATE KEY UPDATE等轻量方案替代。

直接说结论:排队不是因为用了 SELECT FOR UPDATE,而是它被用在了不该用的地方,或用法不规范。真正卡住的从来不是 SQL 本身,是锁没及时释放、查询没走索引、事务裹挟了非数据库操作。
为什么FOR UPDATE会引发严重排队
典型现象是 SHOW PROCESSLIST 里一堆线程卡在 Updating 或 Locked 状态,慢查询日志里反复出现同一句 SELECT ... FOR UPDATE 耗时上百毫秒甚至几秒——但 CPU 和磁盘 IO 并不高。这说明不是算力瓶颈,是纯排队等锁。
- 事务开启后执行
SELECT ... FOR UPDATE,但后续还做了 HTTP 调用、日志写入、JSON 解析等耗时操作,锁一直挂着不放 -
WHERE条件没命中主键或唯一索引,MySQL 升级为间隙锁(Gap Lock)或全表扫描加锁,锁住远超预期的行 - 多个事务对同一行反复执行
SELECT ... FOR UPDATE+UPDATE,而中间夹杂了应用层判断逻辑,导致锁持有时间不可控 - 默认隔离级别
REPEATABLE READ下,SELECT ... FOR UPDATE会加 next-key 锁,对“不存在的记录”也锁住插入间隙,进一步扩大阻塞面
必须立刻检查的三个执行点
别改业务逻辑,先看这三处是否踩坑:
-
SELECT ... FOR UPDATE后面有没有紧跟UPDATE?如果中间穿插了if判断、循环、外部 API 调用,必须拆出去——锁只该在 DB 层最小闭环内持有 - 执行
EXPLAIN FORMAT=TRADITIONAL看这条语句是否走了type: const或type: eq_ref;若显示type: ALL或type: range且rows值很大,说明锁住了大量无关行 - 事务是否显式
BEGIN/COMMIT?别依赖AUTOCOMMIT=1,否则FOR UPDATE可能意外延长到下一条语句才提交
替代方案:什么情况下不该用FOR UPDATE
不是所有“防并发”都得靠悲观锁。以下场景优先换轻量方案:
- 防重复下单:建
UNIQUE KEY (order_no),用INSERT INTO orders (...) VALUES (...) ON DUPLICATE KEY UPDATE status = 'created',冲突即说明已存在 - 计数类更新(如点击量、埋点):直接
INSERT INTO counter (key, value) VALUES ('page_home', 1) ON DUPLICATE KEY UPDATE value = value + VALUES(value) - 任务分发(MySQL 8.0+):改用
SELECT * FROM tasks WHERE status = 'pending' ORDER BY id ASC LIMIT 1 FOR UPDATE SKIP LOCKED,避免 Worker 争抢同一条 - 热点行更新(如用户余额):把变更推到 Redis 队列,后台消费者聚合后批量落库,MySQL 只承担最终状态写入
最常被忽略的一点:很多所谓“必须强一致”的字段,其实业务上根本容忍几秒延迟。先确认“这行数据是否真要实时精确”,比急着加锁更关键。锁是手段,不是目的。


















