InnoDB在UPDATE范围查询时锁住不该锁的行,是因为其按索引B+树结构连续扫描并加Next-Key Lock,不仅锁命中记录,还锁扫描路径上的所有间隙(如WHERE id BETWEEN 10 AND 20实际锁(9,30]),以防止幻读;索引失效则退化为全表加锁。

为什么UPDATE WHERE id BETWEEN ...会锁住不该锁的行
InnoDB 在范围更新时不是只锁 WHERE 条件命中的行,而是按索引结构“连续扫描+加锁”,只要扫描路径上经过的索引记录(包括间隙),都可能被加上 Next-Key Lock。比如执行 UPDATE orders SET status = 1 WHERE created_at > '2026-09-01',即使表里只有 3 行满足条件,但 created_at 索引中该范围覆盖的整个 B+ 树区间(含间隙)都会被锁住。
间隙锁(Gap Lock)让 UPDATE 实际锁住空洞
在 REPEATABLE READ 隔离级别下,InnoDB 对范围条件自动启用间隙锁,目的是防止幻读。这意味着:即使某条记录不存在,它所在的“间隙”也会被锁。例如:
SELECT * FROM users WHERE age BETWEEN 25 AND 30;
如果当前表中没有 age = 28 的记录,(25, 30) 这个开区间仍会被锁住——后续任何 INSERT INTO users (age, ...) VALUES (28, ...) 都会被阻塞。
这种行为在 UPDATE 中同样生效,且不会报错、不显式提示,只表现为其他事务卡在 INSERT/UPDATE 上。
- 间隙锁只在有索引的列上生效;全表扫描时退化为表级锁
-
READ COMMITTED级别下默认不加间隙锁(仅 Record Lock),可显著减少锁范围 - 唯一索引的等值查询(如
WHERE id = 123)不会触发间隙锁,但WHERE id > 123仍会
索引失效会让范围更新直接锁全表
一旦 WHERE 条件没走索引,InnoDB 就无法定位扫描边界,只能全表扫描——这时所有行(甚至整张表)都会被加上 X 锁。典型诱因包括:
面向 CRM 系统的 MySQL 8 数据库模式设计。当用户需要为客户关系管理(CRM)系统设计、评审、优化或生成数据库模式时,应使用此项技能。
- 对字符串字段用数字比较:
WHERE phone = 13800138000(phone是VARCHAR) - 函数包裹字段:
WHERE DATE(created_at) = '2026-09-29' - 隐式类型转换导致索引失效(如传入 JSON 字符串匹配
JSON字段) - 联合索引顺序错配:
INDEX (a, b),却查WHERE b = 1
验证方式很简单:对相同 WHERE 条件跑 EXPLAIN,如果 type 是 ALL 或 index,就说明正在扫全表加锁。
如何确认当前 UPDATE 到底锁了哪些位置
不能靠 SHOW PROCESSLIST 或慢日志猜,得查 InnoDB 内部视图:
- 先定位等待事务:
SELECT * FROM INFORMATION_SCHEMA.INNODB_TRX WHERE trx_state = 'LOCK WAIT'; - 再查谁在堵它:
SELECT * FROM INFORMATION_SCHEMA.INNODB_LOCK_WAITS; - 最后看锁在哪:
SELECT lock_trx_id, lock_rec_id, lock_mode, lock_type FROM INFORMATION_SCHEMA.INNODB_LOCKS;
lock_rec_id 是关键,格式类似 0 123456,前半部分是 space_id,后半是 page_no + heap_no,结合 INFORMATION_SCHEMA.INNODB_BUFFER_PAGE 可反查具体行。不过多数时候,直接看 lock_mode 值更实用:X,GAP 表示间隙锁,X,REC_NOT_GAP 是行锁,X,NEXT_KEY 是两者混合。
真正容易被忽略的是:间隙锁本身不对应任何真实数据行,但它实实在在阻止写入——排查时若只盯着“有数据的行”,就会漏掉这个空洞里的锁。










