InnoDB:InnoDB 锁
关于
- 关于 InnoDB 锁机制的进一步理解,见:InnoDB 锁机制解析
InnoDB 实现标准的行级锁,其中有两种类型的锁:共享(S)锁和排他(X)锁。
- 共享(S)锁允许持有锁的事务读取一行。
- 排他(X)锁允许持有锁的事务更新或删除行。
共享锁/排他锁,即读/写锁。
如果事务 T1 在行 r 上持有共享(S)锁,则来自某些不同事务 T2 的对行 r 的锁请求将按以下方式处理:
- T2 对 S 锁定的请求可以立即获得批准。结果,T1 和 T2 都在 r 上保持 S 锁定。
- T2 对 X 锁定的请求无法立即获得批准。
如果事务 T1 在行 r 上拥有排他(X)锁,则不能立即批准来自某个不同事务 T2 的对 r 上任一类型的锁的请求。相反,事务 T2 必须 await 事务 T1 释放对行 r 的锁定。
意向锁(Intention Locks)【表级锁】
InnoDB 支持多重粒度锁定,它允许行锁和表锁并存。
- 例如,诸如“LOCK TABLES ... WRITE”之类的语句在指定的表上具有排他锁(X锁)。【?】
为了使在多个粒度级别上的锁定切实可行,InnoDB 使用 intention locks。意向锁是表级锁,指示事务稍后对表中的行需要哪种类型的锁(共享锁或排他锁)。有两种类型的意图锁:
- 意向共享锁(IS)表示事务打算对表中的各个行设置“共享”锁。
- 意向排他锁(IX)表示事务打算对表中的各个行设置排他锁。
例如,“SELECT ... LOCK IN SHARE MODE”设置 IS锁(意向共享锁),而“SELECT ... FOR UPDATE”设置 IX锁(意向排他锁)。【!】
意向锁定协议如下:【必须先获取表级锁,再获取行级锁???】
- 在事务可以获取表中某行的共享锁之前,它必须首先获取该表中的 IS 锁或更强的锁。
- 在事务可以获取表中某行的排它锁之前,它必须首先获取该表中的 IX 锁。【?】
其意义是:
- 如果没有意向锁的话,则需要遍历所有整个表判断是否有行锁的存在,以免发生冲突。
- 如果有了意向锁,只需要判断该意向锁与即将添加的表级锁是否兼容即可。因为意向锁的存在代表了,有行级锁的存在或者即将有行级锁的存在。因而无需遍历整个表,即可获取结果。
表级锁类型的兼容性汇总在以下矩阵中:
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
- 此处的“X”、“S”均为表级锁,而非行级锁:IX,IS是表级锁,不会和行级的X,S锁发生冲突。只会和表级的X,S发生冲突。【!!!】
如果锁与现有锁兼容,则将其授予请求的事务,但如果与现有锁冲突,则不授予该请求。事务 await 直到冲突的现有锁被释放。如果锁定请求与现有锁定发生冲突并且由于会导致死锁而无法被授予,则会发生错误。
除了全表请求(例如“LOCK TABLES ... WRITE”)之外,意图锁不会阻止任何其他操作。意图锁定的主要目的是表明有人正在锁定表中的行,或者打算锁定表中的行。
- 【在实际中,手动锁表的情况并不常见,所以意向锁并不常用。特别是之后 MySQL 引入了 MDL 锁,解决了 DML 和 DDL 冲突的问题,意向锁就更不被提起来了。】
意向锁的事务处理数据在“SHOW ENGINE INNODB STATUS”和 InnoDB monitor 的输出如下:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
记录锁(Record Locks)
记录锁是对索引记录的锁定。
- 例如,“SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;”阻止任何其他事务插入,更新或删除 t.c1 的值为 10 的行。
- 【“SELECT FOR UPDATE”主要用于锁定读,会产生排他锁。见:“锁定读取”】
记录锁始终锁定索引记录,即使没有定义索引的表也是如此。在这种情况下,InnoDB 将创建一个隐藏的聚集索引,并将该索引用于记录锁定。【!】
记录锁定的事务数据在“SHOW ENGINE INNODB STATUS”和 InnoDB monitor 的输出如下:
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 Locks)
间隙锁定是对索引记录之间的间隙的锁定,或者是对第一个或最后一个索引记录之前的间隙的锁定。
- 例如,“SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;”防止其他事务将 15 的值插入到 t.c1 列中,无论该列中是否已经有这样的值,因为该范围中所有现有值之间的间隙都被锁定。
- 间隙可能跨越单个索引值,多个索引值,甚至为空。
- 间隙锁是性能和并发性之间权衡的一部分,并且在某些事务隔离级别而非其他级别中使用。【?】
- 对于使用唯一索引锁定行以搜索唯一行的语句,不需要间隙锁定。(不包括搜索条件仅包含多列唯一索引的某些列的情况;在这种情况下,会发生间隙锁定。)【?】
- 例如,如果 id 列具有唯一索引,则以下语句仅使用具有 id 值 100 的行的索引记录锁,其他会话是否在前面的间隙中插入行都没有关系:【?】
SELECT * FROM child WHERE id = 100;
- 如果 id 未构建索引或索引不唯一,则该语句会锁定前面的间隙。
- 在这里还值得注意的是,可以通过不同的事务将冲突的锁保持在间隙上。
- 例如,事务 A 可以在间隙上保留一个共享的间隙锁(间隙 S 锁),而事务 B 可以在同一间隙上保留排他的间隙锁(间隙 X 锁)。允许冲突的间隙锁的原因是,如果从索引中清除记录,则必须合并由不同事务保留在记录上的间隙锁。
- InnoDB 中的间隙锁是“完全抑制”,这意味着它们的唯一目的是防止其他事务插入间隙。【???】
- 间隙锁可以共存。一个事务进行的间隙锁定不会阻止另一事务对相同的间隙进行间隙锁定。共享和专用间隙锁之间没有区别。它们彼此不冲突,并且执行相同的功能。
间隙锁定可以显式禁用。如果将事务隔离级别更改为“READ COMMITTED”或启用“innodb_locks_unsafe_for_binlog”系统变量(现已弃用),则会发生这种情况。在这种情况下,将禁用间隙锁进行搜索和索引扫描,并且仅将其用于外键约束检查和重复键检查。
- 使用“READ COMMITTED”隔离级别或启用“innodb_locks_unsafe_for_binlog”还有其他效果。 MySQL 评估“WHERE”条件后,将释放不匹配行的记录锁。对于“UPDATE”语句,InnoDB 进行“半一致”读取,以便将最新的提交版本返回给 MySQL,以便 MySQL 可以确定该行是否与UPDATE的WHERE条件匹配。
临键锁(Next-Key Locks)【记录锁和间隙锁的组合】
临键锁是“索引记录上的记录锁”和“索引记录之前的间隙上的间隙锁”的组合。
InnoDB 执行行级锁定的方式是:当它搜索或扫描表索引时,它会对遇到的索引记录设置共享或排他锁。因此,行级锁实际上是索引记录锁。索引记录上的“临键锁”也会影响该索引记录之前的“间隙”。即,临键锁是索引记录锁加上索引记录之前的间隙上的间隙锁。
- 如果一个会话在索引中的记录 R 上具有共享或排他锁,则另一会话无法按照索引 Sequences 在 R 之前的间隙中插入新的索引记录。
假定索引包含值 10、11、13 和 20。此索引的可能的临键锁定涵盖以下间隔,其中,圆括号表示排除区间端点,方括号表示包括端点:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
对于最后一个间隔,“临键锁”将锁定索引中最大值之上的间隙,并且“supremum”(上限)伪记录的值高于索引中实际的任何值。
- 上限不是真的索引记录,因此,实际上,此临键锁仅锁定跟随最大索引值的间隙。
默认情况下,InnoDB 以“REPEATABLE READ”事务隔离级别运行。在这种情况下,InnoDB 使用“临键锁”进行搜索和索引扫描,这可以防止幻像行。
临键锁定的事务处理数据在“SHOW ENGINE INNODB STATUS”和 InnoDB monitor 的输出如下:
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 Locks)【???】
插入意图锁定是一种在行插入之前通过“INSERT”操作设置的间隙锁。
此锁表示插入的意图,如果插入到同一索引间隙中的多个事务不在间隙中的同一位置插入,则它们无需等待对方。
- 假设有索引记录,其值分别为 4 和 7。单独的事务分别尝试插入值 5 和 6,在获得插入行的排他锁之前,每个事务都使用插入意图锁来锁定 4 和 7 之间的间隙,但不要互相阻塞,因为行是无冲突的。
示例,演示了在获得对插入记录的排他锁之前,使用插入意图锁的事务。该示例涉及两个 Client 端 A 和 B。
- Client 端 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 | +-----+
- Client B 开始事务以将记录插入空白。事务在等待获得排他锁的同时获取插入意图锁。【?】
mysql> START TRANSACTION; mysql> INSERT INTO child (id) VALUES (101);
插入意图锁定的事务数据在“SHOW ENGINE INNODB STATUS”和 InnoDB monitor 的输出如下:
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 Locks)【表级锁】
AUTO-INC 锁是一种特殊的表级锁,由事务插入具有“AUTO_INCREMENT”列的表中获得。在最简单的情况下,如果一个事务正在向表中插入值,那么任何其他事务都必须 await 自己在该表中进行插入,以便第一个事务插入的行接收连续的主键值。
“innodb_autoinc_lock_mode”配置选项控制用于自动增量锁定的算法。它使您可以选择如何在可预测的自动增量值序列与插入操作的最大并发性之间进行权衡。
空间索引的谓词锁(Predicate Locks for Spatial Indexes)【???】
InnoDB 支持对包含空间列的列进行 SPATIAL 索引(空间索引)。
要处理涉及 SPATIAL 索引的操作的锁定,“临键锁”不能很好地支持“REPEATABLE READ”或“SERIALIZABLE”事务隔离级别。多维数据中没有绝对排序概念,因此不清楚哪个是“下一个”键。
为了支持具有 SPATIAL 索引的表的隔离级别,InnoDB 使用了谓词锁。 SPATIAL 索引包含最小边界矩形(MBR)值,因此 InnoDB 通过在用于查询的 MBR 值上设置谓词锁定来强制对索引进行一致的读取。其他事务不能插入或修改将匹配查询条件的行。