UPDATE没匹配到行仍加锁,因为InnoDB按索引扫描路径加锁,而非是否命中数据;即使WHERE无结果,也会对扫描涉及的索引项及间隙(如Gap Lock、Next-Key Lock)加锁,以防止RR级别幻读。

UPDATE没匹配到行,为什么还加锁?
因为InnoDB的加锁行为不取决于“是否找到数据”,而取决于“扫描了哪些索引记录”。哪怕WHERE条件最终没命中任何行,只要优化器决定走某个索引去扫描,InnoDB就会对扫描路径上访问到的所有索引项(包括间隙)加锁。
典型场景是等值查询但值不存在:比如UPDATE users SET status = 1 WHERE name = 'nonexistent_user',而name字段有普通索引。InnoDB会定位到name索引中该值应处的位置,发现没有对应记录,但仍要锁住这个“不存在的间隙”——这就是Gap Lock。
- 唯一索引(含主键)等值查询时,若值不存在,只锁间隙,不锁记录
- 普通索引等值查询时,即使值不存在,也会锁住对应索引项+相邻间隙,且可能触发
Next-Key Lock -
SELECT ... FOR UPDATE行为一致,不是UPDATE特有
为什么RR隔离级别下必须锁“不存在的行”?
这是为防止幻读设计的底层机制。可重复读(RR)要求事务内多次执行相同查询,结果集必须一致。Gap Lock封锁的是“值的范围”,而非具体记录——它阻止其他事务在该间隙插入新行,从而保证当前事务下次查还能看到同样数量的行。
例如:UPDATE orders SET paid = 1 WHERE order_no = 'ORD9999',order_no有唯一索引但值不存在。InnoDB会在索引中找到'ORD9999'应插入的位置,并对前后两个边界之间的间隙加锁。此时另一个事务执行INSERT INTO orders (order_no, ...) VALUES ('ORD9999', ...)就会被阻塞。
- RC(读已提交)隔离级别下,这类操作不加
Gap Lock,只加Record Lock(但没记录可锁,所以实际不锁) - RR下,即使
EXPLAIN显示rows=0,也不能认为“没加锁” - 可通过
performance_schema.data_locks确认锁类型和范围
如何验证“没匹配到行却加了锁”?
最直接的方式是并发观察:开启事务A执行UPDATE但不提交,再在事务B中尝试插入或更新同一索引范围内的值,看是否被阻塞。配合系统表查锁状态更可靠。
执行以下语句查看当前锁:
SELECT ENGINE_TRANSACTION_ID, OBJECT_NAME, INDEX_NAME, LOCK_TYPE, LOCK_MODE, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_NAME = 'your_table_name';
-
LOCK_MODE为X,GAP或X,REC_NOT_GAP说明已加锁 -
LOCK_DATA显示锁住的具体索引值或间隙边界(如5, 10表示开区间(5,10)) - 注意:MySQL 5.7及更早版本需用
INNODB_TRX+INNODB_LOCKS(已废弃)
哪些写法容易误以为“安全”,实则悄悄加锁?
开发常误判“没数据就没事”,但以下情况仍会触发锁,且不易察觉:
- 使用函数或隐式转换导致索引失效,
WHERE变成全表扫描 → 锁所有聚簇索引记录 + 所有间隙 - 普通索引字段允许
NULL,查询WHERE col IS NULL会锁住所有NULL所在间隙 -
LIKE前缀匹配(如WHERE name LIKE 'abc%')即使没匹配到行,也锁对应索引范围 - 复合索引中只用右部字段(违反最左前缀),导致无法精确定位,扩大扫描范围
真正决定锁行为的,从来不是“UPDATE影响了几行”,而是“优化器选择怎么扫描索引、扫描了哪些位置”。只要扫描发生,锁就跟着落下去——哪怕一行都没改。


















