根本原因是查询未走索引,导致innodb无法精准定位单行而退化为表级锁或过大范围间隙锁;必须通过唯一索引精确匹配才能实现真正的行锁。

为什么 SELECT ... FOR UPDATE 没锁住该锁的行
根本原因往往是查询没走索引,导致 MySQL 退化为表级锁或间隙锁范围过大。InnoDB 行锁只在通过**唯一索引(含主键)精确匹配**时才真正锁定单行;否则可能锁住索引区间、甚至整张表。
- 检查
EXPLAIN输出:如果type是ALL或index,说明没走有效索引,行锁大概率失效 - 确认 WHERE 条件字段是否有索引:比如用
name = 'Alice'但name没建索引,就会全表扫描 + 锁所有聚簇索引记录 - 注意隐式类型转换:如
id是INT,但写成WHERE id = '123',MySQL 可能放弃索引,触发全扫描 - 字符串字段要留意字符集和排序规则:
utf8mb4_bin和utf8mb4_0900_as_cs不兼容时,即使有索引也可能无法使用
执行计划里 key 为空但语句明明用了索引字段
常见于索引失效场景——不是“有没有索引”,而是“能不能用上”。MySQL 优化器判断走索引不划算,或条件破坏了索引最左前缀原则,就会跳过索引。
- 复合索引
(a, b, c),只查WHERE b = 1或WHERE c = 1,key一定为空 -
WHERE a > 10 AND b = 2可能只用到a,b变成过滤条件,不参与索引查找 - 对索引字段使用函数或运算:
WHERE YEAR(create_time) = 2024或WHERE status + 0 = 1,索引直接失效 -
OR连接不同字段时,除非所有字段都有独立索引且被合并,否则容易退化为全表扫描
事务中锁住的到底是哪些行:看 INFORMATION_SCHEMA.INNODB_TRX 和 INNODB_LOCKS
别靠猜,得查实时状态。MySQL 8.0+ 已移除 INNODB_LOCKS,改用 performance_schema.data_locks;但老版本仍可依赖前者辅助定位。
- 先查活跃事务:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_state = 'LOCK WAIT' OR trx_state = 'RUNNING'; - 再查锁信息(MySQL 5.7):
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCKS;注意它只显示当前等待/冲突的锁,不是全部 - MySQL 8.0+ 必须用:
SELECT * FROM performance_schema.data_locks;字段OBJECT_NAME、INDEX_NAME、LOCK_DATA能告诉你锁在哪个表、哪个索引、哪条记录(例如LOCK_DATA: 5表示主键值为 5 的行) - 关键提示:
LOCK_DATA显示的是索引值,不是业务字段值;如果是二级索引加锁,LOCK_DATA是二级索引键值,不是主键
UPDATE 带 LIMIT 却还是锁了多行
LIMIT 不影响加锁范围,只控制最终修改几行。InnoDB 在执行前会先定位并锁住所有满足 WHERE 条件的候选行,再逐个判断是否符合 LIMIT。
- 语句
UPDATE t SET x = 1 WHERE status = 0 LIMIT 1:只要status = 0匹配 100 行,就可能锁住这 100 行(取决于执行计划和隔离级别) - 想精准锁单行?必须让 WHERE 精确命中唯一索引,例如
WHERE id = 123,再加LIMIT 1才真正只锁一行 - 高并发下慎用带非唯一条件的
LIMIT更新,容易引发锁竞争甚至死锁 - 如果确实需要“取一个未处理的记录更新”,建议用
SELECT ... FOR UPDATE先查再更,配合唯一索引 + 乐观锁字段(如version)更可控
事情说清了就结束。行级锁失效从来不是锁机制的问题,而是你写的 SQL 没让 InnoDB 看懂你想锁谁。











