select … for update 在 rr 级别下默认加间隙锁+记录锁,导致阻塞;改用 rc 级别或确保走索引可避免间隙锁;无索引查询会退化为表锁;通过 performance_schema.data_locks 等视图定位锁争用。

为什么 SELECT ... FOR UPDATE 一用就堵住其他事务?
根本原因是默认在可重复读(RR)隔离级别下,SELECT ... FOR UPDATE 会走间隙锁(Gap Lock)+ 记录锁(Record Lock),不仅锁住命中行,还锁住索引间隙。哪怕只查一个主键,若该主键不存在,也会锁住前后两个值之间的空隙,导致看似无关的插入被阻塞。
实操建议:
- 确认是否真需要当前读:如果只是查数据做展示,别加
FOR UPDATE或LOCK IN SHARE MODE - 尽量用主键或唯一索引等值查询:避免范围条件(如
WHERE id > 100),否则锁住整个范围 - 在业务允许前提下,把事务拆小:长事务 = 长时间持锁,尽早
COMMIT - 检查是否隐式升级为临键锁:比如
WHERE name = 'alice'但name没有索引,mysql 会全表扫描并锁所有行
怎么让 MySQL 只锁行、不锁间隙?
核心是关闭间隙锁,但不能直接关——它和 RR 隔离级别强绑定。真正可控的方式是降级隔离级别,或主动加索引抑制间隙锁触发。
实操建议:
- 改用读已提交(RC)隔离级别:
SET session TRANSACTION ISOLATION LEVEL READ COMMITTED。此时SELECT ... FOR UPDATE只锁匹配到的记录,不锁间隙(但注意:RC 下幻读可能产生) - 确保查询条件走索引:尤其是
WHERE中的字段必须有有效索引,否则即使 RC 级别也会退化为全表锁 - 避免
UPDATE ... WHERE无索引条件:这种语句在任何隔离级别下都会升级为表级锁(type: ALL执行计划)
什么情况下 MySQL 会退化成表锁?
不是“想锁表才锁表”,而是优化器判断无法安全使用行锁时自动降级。典型场景包括:
- 对非唯一索引字段做范围更新,且该索引选择性极差(比如
status只有 0/1 两个值) -
UPDATE t SET x=1 WHERE y LIKE '%abc%'—— 索引失效,走全表扫描 - 表没主键:InnoDB 要求每张表有聚簇索引,没定义主键时会自建隐藏
ROW_ID,但无法用于高效定位,容易锁多行甚至整表 - 执行
ALTER table加列或改类型(尤其大表),8.0 前默认阻塞 DML;8.0+ 支持部分 ALGORITHM=INSTANT,但并非全部操作都免锁
如何快速定位谁在持有锁、谁被堵住了?
别只看 SHOW PROCESSLIST,它不反映锁状态。关键看三张元数据表:
-
SELECT * FROM performance_schema.data_locks:看到当前每个事务持有的锁类型、锁对象(如PRIMARY、GEN_CLUST_INDEX)、锁模式(RECORD/INSERT_INTENTION) -
SELECT * FROM performance_schema.data_lock_waits:直接列出阻塞链,BLOCKING_TRX_ID和REQUESTING_TRX_ID一目了然 -
SELECT * FROM information_schema.INNODB_TRX:结合TRX_STATE(如LOCK WAIT)和TRX_STARTED判断事务是否卡太久 - 临时启用死锁日志:
SET GLOBAL innodb_print_all_deadlocks = ON,错误日志里会输出完整冲突 SQL
锁粒度调得再细,也架不住一个没索引的 UPDATE 把整张表扫一遍。真正要盯的不是“怎么调锁”,而是“哪条 SQL 正在破坏锁的边界”。