InnoDB 的锁和事务模型

发布时间 2023-08-18 18:14:36作者: LARRY1024

InnoDB 的锁

InnoDB 使用的锁类型包括:

  • 共享锁(shared lock)和排它锁(exclusive lock)

  • 意向锁(intention lock)

  • 记录锁(record lock)

  • 间隙锁(gap lock)

  • 下一钥匙锁(next-key lock)

  • 插入意向锁(insert-intention lock)

  • AUTO-INC 锁(AUTO-INC lock)

  • 空间索引的断言锁(predicate Locks for spatial indexes)

共享锁和独占锁

InnoDB 实现了标准的行级锁定,其中,有两种类型的锁:

如果事务 T1 在行 r 上持有共享锁,则另一个不同事务 T2 对行 r 的锁的请求将按如下方式处理:

  • T2 请求获取共享锁,T2 能立即持有共享锁,结果,T1 和 T2都持有对行的 r 的共享锁;

  • T2 请求获取排它锁,T2 不能立即持有共享锁。

如果事务 T1 持有行 r 上的排它锁,则另一个不同事务 T2 对行 r 上的任一类型的锁的请求,都不能立即被授予。相反,其他事务必须等待事务 T1 释放在行 r 上的锁定。

意向锁

InnoDB 支持多粒度锁定(multiple granularity locking),允许行锁和表锁共存

例如,在指定的表上执行 LOCK TABLES ... WRITE 语句,将会持有排它锁。

InnoDB 使用了意向锁(intention locks) 实现了多粒度级别的锁定,意向锁是表级锁,表明事务稍后需要对表中的行使用哪种类型的锁(共享锁或排它锁),意向锁有两种类型:

  • 意向共享锁(intention shared lock,IS):表示事务打算在表中的各个行上设置共享锁;

    例如,SELECT ... FOR SHARE 会设置 IS 锁。

  • 意向排它锁(intention exclusive lock,IX):表示事务打算对表中的各个行设置排它锁。

    例如,SELECT ... FOR UPDATE 会设置 IX 锁。

意向锁定协议如下:

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

  • 在事务可以获取表中行的排他锁之前,它必须首先获取表上的 IX 锁。

表级锁类型兼容性如下:

锁的类型 X IX S IS
X \(\times\) \(\times\) \(\times\) \(\times\)
IX \(\times\) \(\checkmark\) \(\times\) \(\checkmark\)
S \(\times\) \(\times\) \(\checkmark\) \(\checkmark\)
IS \(\times\) \(\checkmark\) \(\checkmark\) \(\checkmark\)

注:\(\times\) 表示两种锁会冲突,\(\checkmark\) 表示两种锁会兼容。

如果事务请求的锁与现有的锁兼容,那么,请求的锁将会被授予,但如果它与现有的锁冲突,则请求的锁不会被授予。此时,事务将会等待,直到与现有锁冲突的锁被释放;如果请求的锁与现有锁冲突,并且由于会导致死锁,请求的锁将无法被授予,就会发生错误。

意向锁不会阻塞除全表锁定请求之外的其他任何锁定请求,即意向锁只会阻塞全表锁定请求,意向锁的主要目的是表明有人正在锁定一行,或者将要锁定表中的一行。

意向锁的事务数据可以通过 SHOW ENGINE INNODB STATUS 查看,执行后,会类似如下的输出:

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

记录锁

记录锁(record lock) 是索引记录上的锁。

例如,语句,可以防止任何其他事务插入、更新或删除值为 10 的 t.c1行。

SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;

记录锁始终锁定索引记录,如果表定义时没有指定索引,InnoDB 会创建一个隐藏的聚集索引并使用该索引进行记录锁定。

记录锁的事务数据可以通过 SHOW ENGINE INNODB STATUS 查看,执行后,会类似如下的输出:

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        ;;

间隙锁

间隙锁(gap lock) 是对索引记录之间间隙的锁定,或者对第一个索引记录之前或最后一个索引记录之后的间隙的锁定。间隙可能跨越单个索引值、多个索引值,甚至是空的。

例如,下面的语句可以防止其他事务将值为 15 的行插入到 t.c1 列中,无论该列中是否已经存在任何此类值,因为该范围内的所有现有值之间的间隙已被锁定。

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

间隙锁是性能和并发性之间权衡的一部分,并且在某些事务隔离级别中可以使用,而在其他的事务隔离级别中不能使用。

对于使用通过唯一索引来搜索唯一行的查询语句,就不需要间隙锁定。如果是搜索条件中包含多列组合的唯一索引,就会出现间隙锁。

例如,如果 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 可以确定该行是否匹配该更新语句的 WHERE 条件。

Next-Key 锁

Next-Key Lock 是索引记录上的记录锁索引记录之前的间隙上的间隙锁的组合。

InnoDB 执行行级锁定的方式是,当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享锁或独占锁,因此,行级锁实际上是索引记录锁。索引记录上的 Next-Key Lock 也会影响该索引记录之前的“间隙”,也就是说,Next-Key Lock 是索引的记录锁加上索引记录之前间隙上的间隙锁。因此,如果一个会话对索引中的记录 R 持有共享锁或独占锁,则另一个会话无法在索引顺序中紧邻 R 之前的间隙中插入新索引记录。

例如,假设索引列包含值 10、11、13 和 20,该索引可能的 Next-Key Lock 涵盖以下区间:

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

其中,圆括号表示排除区间端点,方括号表示包含端点。

对于最后一个间隔,Next-Key Lock 下一个键锁锁定索引中最大值之上的间隙,以及具有比索引中实际值更大的值的“最大”伪记录,上界不是真正的索引记录,因此,实际上,Next-Key Lock 仅锁定最大索引值之后的间隙。

在 REPEATABLE READ 事务隔离级别下运行,InnoDB 会使用 Next-Key Lock 进行搜索和索引扫描,这可以防止幻读。

Next-Key Lock 的事务数据可以通过 SHOW ENGINE INNODB STATUS 查看,执行后,会类似如下的输出:

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 intention lock)是一种间隙锁,由 INSERT 操作在记录插入之前设置,如果插入同一索引间隙的多个事务,不会插入间隙内的同一个位置,则无需互相等待。

例如,假设存在值为 4 和 7 的索引记录,分别尝试插入值 5 和 6 的单独事务在获得插入行上的排他锁之前,每个事务都使用插入意向锁锁定 4 和 7 之间的间隙,但不会互相阻塞,因为行不冲突。

例如,以下示例演示了一个事务在获取插入记录上的排它锁之前获取插入意向锁。

客户端 A 创建一个包含两条索引记录(90和102)的表,然后启动一个事务,对 ID 大于100的索引记录设置排他锁,其中,排他锁会包括记录 102 之前的间隙锁:

mysql> CREATE TABLE child (id int(11) NOT NULL, PRIMARY KEY(id)) ENGINE=InnoDB;
mysql> INSERT INTO child (id) values (90),(102);

mysql> START TRANSACTION;
mysql> SELECT * FROM child WHERE id > 100 FOR UPDATE;
+-----+
| id  |
+-----+
| 102 |
+-----+

客户端 B 开启一个事务,尝试将 101 记录插入到间隙中,此时,该事务在等待获取排它锁时获取插入意向锁:

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

插入意向锁的事务数据可以通过 SHOW ENGINE INNODB STATUS 查看,执行后,会类似如下的输出:

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 变量控制用于自动增量锁定的算法。它允许您选择如何在可预测的自动增量值序列和插入操作的最大并发度之间进行权衡。

空间索引的断言锁

InnoDB 支持 SPATIAL 包含空间数据的列的索引。

为了处理涉及 SPATIAL索引的操作的锁定,下一键锁定不能很好地支持REPEATABLE READ或 SERIALIZABLE事务隔离级别。多维数据中不存在绝对的排序概念,因此并不清楚哪个是 “下一个”键。

要支持带 SPATIAL索引的表的隔离级别,InnoDB 请使用谓词锁。索引SPATIAL包含最小边界矩形 (MBR) 值,因此 InnoDB通过在用于查询的 MBR 值上设置谓词锁来强制对索引进行一致读取。其他事务无法插入或修改与查询条件匹配的行。