人人都会AI编程

6.4 行锁算法:记录锁、间隙锁、临键锁

更新时间:2026-07-10

InnoDB 的行锁并不是简单地“锁住某一行”这么粗暴,而是用了一套精细的算法来决定到底锁哪些范围。这套算法由三种基本锁构成:记录锁、间隙锁、临键锁。它们共同的目标,是在并发下保证数据一致性,同时尽可能减少锁冲突。尤其在可重复读隔离级别下,这三种锁配合 MVCC,可以很大程度上避免幻读。

理解这些锁的最好方式,是先建立一个具体的数据模型。假设有一张表 t,主键 id 为整数,当前表中的记录为:

id: 10, 20, 30

InnoDB 在锁定时并不只看到这三条记录,还会看到它们之间的间隙以及两端的伪记录

  • 负无穷到 10 之间的左间隙
  • 10 和 20 之间的间隙
  • 20 和 30 之间的间隙
  • 30 到正无穷之间的右间隙

下面就以这个模型逐一说明。

6.4.1 记录锁:精准锁定一行

记录锁锁定的是索引中的某一条具体记录,而不是这一行所在的整个数据页或间隙。当一条 SQL 通过唯一索引精确命中某一行时,InnoDB 就会对该行加记录锁。

比如:

-- 事务 A
SELECT * FROM t WHERE id = 20 FOR UPDATE;

这条语句会锁定 id = 20 这条记录。事务 B 尝试更新或删除 id = 20 时会被阻塞,但更新 id = 19id = 21 完全不受影响。这就是记录锁的粒度优势。

记录锁主要用于等值查询且命中唯一索引记录的场景。它不会封锁任何间隙,所以其他事务插入 id = 15 的记录是允许的——这也意味着如果隔离级别是读已提交,仅仅记录锁是无法防止幻读的(因为它允许在间隙插入新数据)。

不过如果唯一索引由多列构成,且 WHERE 条件中使用了全部列,那么等值命中的行也会被加记录锁。如果只用了部分列,行为会退化为间隙锁或临键锁。

6.4.2 间隙锁:锁住范围,阻止插入

间隙锁不锁任何具体记录,而是锁定两条索引记录之间的间隙。间隙锁的唯一作用就是阻止其他事务在间隙中插入新记录,从而在可重复读级别下防止“幻读”——也就是防止同一个事务内两次查询读到不同的行(往往是新插入的行)。

一个关键特性:间隙锁之间是兼容的。多个事务可以同时持有同一个间隙的间隙锁,因为它们的目的都是防止插入,而不是排斥对方。这也意味着间隙锁本身不会造成死锁,除非搭配其他锁。

举例说明:

-- 事务 A
SELECT * FROM t WHERE id BETWEEN 15 AND 18 FOR UPDATE;

当前表中没有 id 在 15 到 18 之间的记录,所以 InnoDB 无法加任何记录锁。但它会锁住 10 到 20 之间的间隙(因为这是第一个包含该范围的已有间隙)。事务 B 此时尝试执行:

INSERT INTO t (id) VALUES (16);  -- 被阻塞,因为间隙被锁住

但可以插入 id = 25,因为这个间隙未被封锁。再看看另一个例子:

-- 事务 A
SELECT * FROM t WHERE id = 15 FOR UPDATE;  -- 当前无记录

这条语句没有命中任何记录,但为了防止其他事务插入 id = 15 而造成幻读,InnoDB 会对 10 到 20 之间的间隙加间隙锁。此时事务 B 插入 id = 14id = 19 都会被阻塞。

不过要特别注意的是,间隙锁只在可重复读及以上隔离级别生效。如果将隔离级别降为读已提交,间隙锁会被禁用。这也意味着在读已提交隔离级别下,虽然并发度更高,但无法杜绝幻读。

6.4.3 临键锁:记录锁 + 间隙锁的组合

临键锁是记录锁和间隙锁的结合体,它锁定一个左开右闭的区间,即 (前一条记录, 当前记录]。这既能防止其他事务修改这条记录(记录锁),又能防止在这个记录前的间隙中插入新记录(间隙锁)。

在可重复读隔离级别下,InnoDB 的行锁默认就是临键锁。也就是说,大多数情况下你实际加上的不是单纯的记录锁,而是临键锁。它会根据查询条件和索引唯一性进行适当退化。

以表 t 为例,假如执行:

-- 事务 A
SELECT * FROM t WHERE id = 20 FOR UPDATE;  -- 主键唯一索引,命中

由于条件完全命中唯一索引记录,优化器可以安全地把临键锁退化为记录锁(不需要锁间隙,因为主键唯一,不可能再插入另一个 id = 20)。所以实际上加的是记录锁。

如果是非唯一索引:

-- 假设 name 列是普通索引,有重复值
SELECT * FROM t WHERE name = 'abc' FOR UPDATE;

name = 'abc' 可能匹配多行。InnoDB 会对所有命中的索引记录加临键锁(即锁住记录本身和它前面的间隙),并且还会在最后一个命中的记录之后再加一个间隙锁,锁住到下一个索引记录的间隙。这种复杂的锁范围是为了防止其他事务插入新的 name = 'abc' 记录,彻底杜绝幻读。

范围查询更直观:

SELECT * FROM t WHERE id > 15 FOR UPDATE;

这条范围查询会锁住 (15, 20]、(20, 30] 和 (30, 正无穷) 这些临键锁区间(具体与查询边界和已有记录有关)。事务 B 想要插入 id = 25 会被阻塞。

6.4.4 三种锁在实际开发中的表现

理解这些算法后,再看日常开发中的典型场景就会清晰很多。

场景一:唯一索引等值查询命中行
加记录锁。对业务并发最友好,两个事务更新不同的行互不影响。

场景二:唯一索引等值查询未命中行
加间隙锁,锁住目标值应该落入的那个间隙。防止在这个间隙中插入等值记录。

场景三:非唯一索引等值查询已命中行
对命中的每一行加临键锁,并在最后一行间隙后加一个间隙锁。这些锁范围更宽,可能造成意外的锁等待。比如一张用户表,status 列有索引,对 status = 0 加锁可能影响后续 status = 0 的插入,即使插入的主键不同。

场景四:范围查询(无论唯一还是非唯一索引)
通常会加多个临键锁覆盖整个范围,直到满足条件的第一条不匹配记录为止。范围查询的锁往往很宽,容易引发锁冲突甚至是死锁,设计时要尽量避免对不必要的大范围加锁。

常见问题:死锁
临键锁和间隙锁虽然解决了幻读,但也增加了死锁概率。比如两个事务分别先加间隙锁,然后试图获得对方的记录锁,就可能死锁。这类死锁通常是逻辑层面的问题,InnoDB 的死锁检测器一般会自动回滚其中一个事务,应用层需要做重试。使用 SHOW ENGINE INNODB STATUS 可以查看最近一次死锁的详细信息,包括参与的事务和锁等待的 SQL。

在 MySQL 8.0 中,可以通过查询 performance_schema.data_locks 表实时查看当前持有的锁与等待的锁,对于排查线上的锁等待问题非常实用。

6.4.5 隔离级别对行锁算法的影响

最后有必要强调隔离级别的决定作用:

  • 读未提交读已提交隔离级别下,间隙锁被禁用,InnoDB 只会使用记录锁。这提升了并发性,但也引进了幻读风险。
  • 可重复读隔离级别下,间隙锁和临键锁完整启用,幻读被基本杜绝。
  • 串行化级别下,所有读操作都会被隐式转为 SELECT ... FOR SHARESELECT ... FOR UPDATE,锁范围最广,并发性最差。

因此,如果你需要调整隔离级别来提升并发,也要接受幻读的可能性,并在应用层做逻辑补偿。大多数的线上系统使用可重复读已经能满足数据一致性要求,并依靠合理的索引设计来缩小锁范围,从而兼顾并发和正确性。