next-key lock是innodb根据where条件是否精准匹配唯一索引直接决定的锁类型,非“升级”:唯一索引等值查加record lock,非唯一索引等值或范围查则直接加next-key lock锁区间。

Next-Key Lock不是“升级”,而是执行计划决定的默认行为
MySQL在RR隔离级别下,UPDATE语句根本不存在“从行锁升级为Next-Key Lock”的过程。所谓“升级”是误解——InnoDB根据WHERE条件能否**精准定位唯一索引记录**,直接选择加Record Lock还是Next-Key Lock。没有中间态,也不回退。
常见错误现象:SHOW ENGINE INNODB STATUS\G里看到大量lock_mode X locks gap before rec insert intention waiting,误以为是“锁变重了”,其实是执行一开始就没走对索引路径。
- 主键或唯一索引等值查询(如
WHERE id = 100)→ 只加Record Lock - 非唯一索引等值查询(如
WHERE status = 'pending',status有普通索引)→ 直接加Next-Key Lock,锁住整个(prev_value, current_record]区间 - 范围条件(
WHERE created_at > '2024-01-01')→ 不管有没有索引,只要不是唯一联合索引覆盖,就锁后缀间隙,比如(2024-01-01, +∞) - 没索引 or 索引失效(
EXPLAIN显示type = ALL或key = NULL)→ 每扫描一行聚簇索引,都加一次Next-Key Lock,效果等同锁全表
批量UPDATE时Next-Key Lock的“范围膨胀”怎么发生的
你以为只更新100行,结果锁住了10万行的间隙,问题不在“批量”,而在**索引扫描方式**。InnoDB按B+树结构查找,一旦不能用唯一索引精确定位,它就必须向右/向左遍历,而遍历过程中访问到的所有索引节点和间隙,都会被加锁。
例如:UPDATE orders SET flag = 1 WHERE status = 'pending',status是普通索引,且当前有10万条pending记录:
- InnoDB会先定位到第一个
status = 'pending'的索引项,加Next-Key Lock → 锁(?, first_pending] - 然后顺着索引链向右扫描,每碰到一个pending记录,都加Next-Key Lock → 实际锁住的是整段
(first_non_pending, last_pending],中间所有间隙全被封死 - 如果另一事务正尝试
INSERT INTO orders (...) VALUES (...,'pending',...),就会卡在insert intention waiting
这不是锁“变大”,是执行计划一开始就决定了要扫多远、锁多宽。
如何验证当前UPDATE到底锁了哪些间隙
别猜,查performance_schema.data_locks。这是MySQL 8.0+最准的实时锁视图,比INNODB STATUS更细粒度、不丢静默持有者。
关键字段含义:
-
LOCK_TYPE = 'RECORD'→ 行级锁(含Next-Key) -
INDEX_NAME = 'PRIMARY'或辅助索引名 → 看锁在哪张索引上 -
LOCK_DATA→ 显示具体锁住的索引值,如3表示锁record=3;3, 5可能表示锁(3,5]区间(需结合LOCK_MODE判断) -
LOCK_MODE含GAP字样 → 当前是纯间隙锁;含NEXT-KEY→ 是临键锁
执行完UPDATE后立刻查:
SELECT LOCK_TRX_ID, LOCK_MODE, LOCK_TYPE, INDEX_NAME, LOCK_DATA FROM performance_schema.data_locks WHERE OBJECT_SCHEMA = 'your_db' AND OBJECT_NAME = 'your_table';
注意:该视图只显示当前活跃事务持有的锁,已COMMIT的事务不会出现。
真正可控的解法只有两个方向
想让批量UPDATE不引发大面积间隙阻塞,必须绕过InnoDB对“不确定性范围”的防御逻辑。没有银弹,只有取舍。
-
用主键范围分批:改写成
WHERE id BETWEEN ? AND ?,确保EXPLAIN中type = range且key = PRIMARY。每批100–500行,用MAX(id)续查,每次COMMIT释放锁 -
先查ID再单点更新:两阶段操作——
SELECT id FROM t WHERE status = 'pending' LIMIT 500拿到ID列表,再UPDATE t SET flag = 1 WHERE id IN (1,2,3,...)。此时每个id = ?都是唯一等值查询,只加Record Lock,零间隙锁
后者看似多一次查询,但锁粒度干净、死锁概率低、业务可预测性强。容易被忽略的是:如果你的“先查ID”用了FOR UPDATE,那它本身也会触发Next-Key Lock——所以查ID阶段务必不用锁定读,只做快照读。











