InnoDB 锁机制

219 阅读10分钟

共享锁和排它锁

共享锁和排它锁

InnoDB 实现了标准的行级锁,其中有两种锁类型:共享锁(S)和排它锁(X)。

共享锁(S) 允许持有锁的事务读取一行数据。

排它锁(X) 允许持有锁的事务更新或删除一行数据。

如果事务 T1 在行 r 上持有共享锁(S),则来自不同事务 T2 对行 r 的锁请求处理如下:

• T2 请求共享锁(S)可以立即授予,结果是 T1 和 T2 都持有行 r 的共享锁。

• T2 请求排它锁(X)不能立即授予。

如果事务 T1 在行 r 上持有排它锁(X),那么来自不同事务 T2 对行 r 的任何类型的锁请求都不能立即授予。事务 T2 必须等待事务 T1 释放该行的锁。

意图锁

InnoDB 支持多粒度锁定,允许行锁和表锁共存。例如,像 LOCK TABLES ... WRITE 这样的语句对指定的表进行排它锁(X 锁)。为了使多粒度锁定变得可行,InnoDB 使用意图锁。意图锁是表级锁,用于指示事务稍后对表中的行需要何种类型的锁(共享或排它)。有两种类型的意图锁:

意图共享锁(IS) 表示事务打算对表中的单行设置共享锁。

意图排它锁(IX) 表示事务打算对表中的单行设置排它锁。

例如,SELECT ... FOR SHARE 会设置意图共享锁(IS),而 SELECT ... FOR UPDATE 会设置意图排它锁(IX)。

意图锁协议如下:

• 在事务可以对表中的一行获取共享锁之前,必须首先在表上获取意图共享锁(IS)或更强的锁。

• 在事务可以对表中的一行获取排它锁之前,必须首先在表上获取意图排它锁(IX)。

表级锁类型兼容性总结如下:

image.png 如果请求的锁与现有的锁兼容,则授予该请求;如果与现有锁冲突,则无法授予锁,事务将等待冲突的锁被释放。如果锁请求与现有锁冲突并且无法授予(因为可能引发死锁),则会发生错误。

意图锁不会阻塞除完全表锁请求(如 LOCK TABLES ... WRITE)之外的任何操作。意图锁的主要目的是显示某个事务正在锁定行,或即将锁定行。

在 SHOW ENGINE INNODB STATUS 和 InnoDB 监视器输出中,意图锁的事务数据类似如下:

TABLE LOCK table `test`.`t` trx id 10080 lock mode IX

记录锁

记录锁是对索引记录的锁。例如,

SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 

会阻止其他事务插入、更新或删除值为 10 的行。

即使表没有定义索引,记录锁也会锁定索引记录。对于这种情况,InnoDB 会创建一个隐藏的聚集索引,并使用该索引进行记录锁定。

在 SHOW ENGINE INNODB STATUS 和 InnoDB 监视器输出中,记录锁的事务数据类似如下:

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`

trx id 10078 lock_mode X locks rec but not gap

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0

 0: len 4; hex 8000000a; asc     ;;

 1: len 6; hex 00000000274f; asc     'O;;

 2: len 7; hex b60000019d0110; asc        ;;

间隙锁

间隙锁是对索引记录之间的间隙或对索引记录前后间隙的锁。例如,

SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; 

会阻止其他事务在 t.c1 列中插入值 15,无论该列中是否已经存在值 15,因为锁定了所有现有值之间的间隙。

间隙可能跨越单个索引值、多重索引值,甚至为空。

间隙锁是性能与并发之间的权衡,在某些事务隔离级别下使用,而在其他级别下则不使用。

对于使用唯一索引搜索唯一行的语句,不需要间隙锁。(这不包括搜索条件仅包含多列唯一索引的某些列的情况;在这种情况下,确实会发生间隙锁。)例如,如果 id 列有一个唯一索引,则以下语句仅使用索引记录锁定 id 值为 100 的行,而不管其他会话是否在前面的间隙中插入行:

SELECT * FROM child WHERE id = 100;

如果 id 没有索引或者有一个非唯一索引,那么该语句确实会锁定前面的间隙。

这里还需要注意的是,不同事务可以在同一个间隙上持有冲突的锁。例如,事务 A 可以在一个间隙上持有共享间隙锁(gap S-lock),同时事务 B 可以在同一个间隙上持有独占间隙锁(gap X-lock)。允许冲突的间隙锁的原因是,如果某个记录被从索引中清除,那么不同事务在该记录上持有的间隙锁必须合并。

在 InnoDB 中,间隙锁是“纯粹的抑制性锁”,这意味着它们的唯一目的是防止其他事务向该间隙插入数据。间隙锁可以共存。一个事务获取的间隙锁并不会阻止另一个事务在同一个间隙上获取间隙锁。共享间隙锁和独占间隙锁没有区别,它们不会互相冲突,并执行相同的功能。

间隙锁可以被显式禁用。这会发生在你将事务隔离级别更改为 READ COMMITTED 时。在这种情况下,间隙锁在搜索和索引扫描时被禁用,只在外键约束检查和重复键检查时使用。

使用 READ COMMITTED 隔离级别还会有其他影响。对于不匹配的行,记录锁会在 MySQL 评估 WHERE 条件后释放。对于 UPDATE 语句,InnoDB 执行“半一致性”读取,返回最新的已提交版本给 MySQL,这样 MySQL 就可以判断该行是否与 UPDATE 的 WHERE 条件匹配。

Next-Key 锁

Next-key锁是记录锁与间隙锁的组合,它锁定了索引记录和紧接该记录前的间隙。

InnoDB 执行行级锁定的方式是,当它搜索或扫描表索引时,会对遇到的索引记录设置共享锁或独占锁。因此,行级锁实际上是索引记录锁。索引记录上的next-key锁(next-key lock)还会影响该索引记录之前的“间隙”。也就是说,next-key锁是一个索引记录锁加上一个位于该索引记录前面的间隙锁。如果一个会话在索引中的记录 R 上持有共享锁或独占锁,那么另一个会话就不能在索引顺序中紧接着 R 之前的间隙插入新的索引记录。 默认情况下,InnoDB 在 REPEATABLE READ 事务隔离级别下操作,在这种情况下,InnoDB 会使用下一键锁来进行搜索和索引扫描,从而避免了幻影行问题。

假设一个索引包含值 10、11、13 和 20。该索引的可能的下一个键锁(next-key lock)覆盖以下区间,其中圆括号表示不包括区间端点,方括号表示包括端点:

(negative infinity, 10]
(10, 11] 
(11, 13]
(13, 20] 
(20, positive infinity)

对于最后一个区间,next-key 锁锁定了索引中最大值上方的间隙以及一个“上确界”(supremum)伪记录,该伪记录的值高于索引中任何实际存在的值。上确界并不是一个真实的索引记录,因此,实际上这个下一个键锁仅锁定了索引中最大值之后的间隙。

默认情况下,InnoDB 采用 REPEATABLE READ 事务隔离级别。在这种情况下,InnoDB 在搜索和索引扫描时使用下一个键锁(next-key lock),这可以防止幻影行的出现(参见第 17.7.4 节,“幻影行”)。

RECORD LOCKS space id 58 page no 3 n bits 72 index `PRIMARY` of table `test`.`t`
trx id 10080 lock_mode X
Record lock, heap no 1 PHYSICAL RECORD: n_fields 1; compact format; info bits 0
  0: len 8; hex 73757072656d756d; asc supremum;;

Record lock, heap no 2 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
  0: len 4; hex 8000000a; asc     ;;
  1: len 6; hex 00000000274f; asc     'O;;
  2: len 7; hex b60000019d0110; asc        ;;

插入意图锁

插入意向锁是一种由 INSERT 操作在插入行之前设置的间隙锁。这种锁表示插入意图,允许多个事务在同一个索引间隙中插入数据时,如果它们不在间隙中的相同位置插入,彼此无需等待。例如,假设有两个索引记录,值为 4 和 7。尝试分别插入 5 和 6 的独立事务,在获取排他锁之前,每个事务都会在 4 和 7 之间锁定该间隙,并且它们不会相互阻塞,因为这两行数据不冲突。

以下示例演示了一个事务在获取排他锁之前,如何首先获得插入意向锁。示例涉及两个客户端,A 和 B。

客户端 A 创建一个包含两个索引记录(90 和 102)的表,并启动一个事务,该事务对 ID 大于 100 的索引记录加排他锁。排他锁包括记录 102 前的间隙锁:

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
Query OK, 0 rows affected (0.01 sec)

mysql> INSERT INTO child (id) values (90),(102);
Query OK, 2 rows affected (0.01 sec)

mysql> START TRANSACTION;
Query OK, 0 rows affected (0.00 sec)

mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id  |
+-----+
| 102 |
+-----+
1 row in set (0.00 sec)

客户端 B 开始一个事务,准备在间隙中插入一条记录。在等待获取排他锁时,该事务会先获取一个插入意向锁。

mysql> START TRANSACTION;
mysql> INSERT INTO child (id) VALUES (101);

插入意向锁的事务数据在 SHOW ENGINE INNODB STATUS 和 InnoDB 监控输出中显示类似如下内容:

RECORD LOCKS space id 31 page no 3 n bits 72 index `PRIMARY` of table `test`.`child`
trx id 8731 lock_mode X locks gap before rec insert intention waiting
Record lock, heap no 3 PHYSICAL RECORD: n_fields 3; compact format; info bits 0
  0: len 4; hex 80000066; asc    f;;
  1: len 6; hex 000000002215; asc     " ;;
  2: len 7; hex 9000000172011c; asc     r  ;;

AUTO-INC 锁

AUTO-INC 锁是一个特殊的表级锁,由插入到包含 AUTO_INCREMENT 列的表中的事务设置。在最简单的情况下,如果一个事务正在向表中插入值,任何其他事务都必须等待才能进行插入,以确保第一个事务插入的行接收连续的主键值。 innodb_autoinc_lock_mode 变量控制用于自动递增锁定的算法。它允许您选择在预测的自动递增值序列和插入操作的最大并发性之间进行权衡。

有关更多信息,请参见第 17.6.1.6 节,“InnoDB 中的 AUTO_INCREMENT 处理”。

空间索引的谓词锁

InnoDB 支持对包含空间数据的列进行 SPATIAL 索引(参见第 13.4.9 节,“优化空间分析”)。

为了处理涉及 SPATIAL 索引的操作,next-key 锁在支持 REPEATABLE READ 或 SERIALIZABLE 事务隔离级别时效果不佳。因为在多维数据中没有绝对的排序概念,因此不清楚哪个是“下一个”键。

为了支持具有 SPATIAL 索引的表的隔离级别,InnoDB 使用谓词锁。SPATIAL 索引包含最小边界矩形(MBR)值,因此 InnoDB 通过对用于查询的 MBR 值设置谓词锁来强制执行一致性读取。其他事务不能插入或修改与查询条件匹配的行。 翻译自dev.mysql.com/doc/refman/…