InnoDB中的锁 - MySQL 8.0官方文档笔记(一)

背景

最近看MySQL官方文档比较多,在此开坑翻译部分篇章,并附上一些旁注,用于展示实操结果,或者表达个人理解。

文档版本:8.0
来源:innodb-locking

此类形式为旁注。
本篇主要介绍InnoDB中的各类锁,而锁触发条件和应用场景不全在此篇中提及,后续会单独成篇进行讲解。

共享锁 & 独占锁

InnoDB 实现了两种类型的标准行锁:共享(S)锁和独占(X)锁。(下文简称S锁和X锁)

  • S锁允许持有该锁的事务读取一行记录
  • X锁允许持有该锁的事务更新或删除一行记录

如果事务 T1 持有行 R 的S锁,另一个事务 T2 在行 R 上尝试获取锁,会有如下情景:

  • T2 请求 S 锁,可以直接获得。此时 T1 和 T2 都持有行 R 的S 锁。
  • T2 请求 X 锁,不能直接获得。

如果T1 持有行 R 的 X锁,另一个事务 T2 在行 R 上尝试获取任何一种锁,都不能直接获得。T2 必须等待T1 释放行R 上的锁。

这里讲到的S/X锁更倾向于在描述锁的模型:即锁的获取方式、资源控制能力和锁之间的交互。

接下来所讲到的各类锁是基于S/X模型来实现的,不同在于粒度、强弱等。

意向锁

InnoDB支持多粒度锁:即行锁与表锁共存。例如语句LOCK TABLES ... WRITE 获取表的X锁。InnoDB使用 意向锁 实现在多个粒度
上加锁。意向锁是表锁,用于指明一个事务稍后要获取哪种类型的行锁(S or X)。意向锁有两种类型:

  • 共享意向锁(IS):指明事务将要获取行的共享锁
  • 独占意向锁(IX):指明事务将要获取行的独占锁

例如,SELECT ... FOR SHARE获取了 IS 锁,SELECT ... FOR UPDATE 获取了 IX 锁。

注意5.6\5.7版本获取IS锁的语句有所不同:SELECT ... LOCK IN SHARE MODE

意向锁的使用原则:
一个事务若要获取行的 S 锁,必须先获取该表的 IS 锁或更强级别的锁。
一个事务若要获取行的 X 锁,必须先获取该表的 IX 锁。

表锁之间的兼容性总结如下:


X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

事务请求的锁必须和目前已产生的锁兼容,否则无法获取,直到冲突锁释放。而等待释放的过程如果InnoDB检测到存在死锁,则会抛出错误。

意向锁不会阻塞其他锁请求,除了表锁(如 LOCK TABLES ... WRITE)。意向锁是为了表明事务正在尝试获取,或将要获取行锁。

如果要查看当前数据库中的意向锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

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

如果在输出日志中看不到锁的相关信息,需要开启如下参数:
SET GLOBAL innodb_status_output_locks=ON;
见 : innodb-enabling-monitors

记录锁

记录锁用于锁住一条索引值。例如语句 SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 会防止其他事务针对t.c1=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 8000000 a; asc ;;
 1 : len 6 ; hex 00000000274 f; asc 'O;;
 2 : len 7 ; hex b60000019d0110; asc ;;

trx id 10078 lock_mode X locks rec but not gap 意味记录锁只锁住了单条记录而没有锁定任何间隙,这也是通常主键查询的结果,关于间隙的概念下文中会介绍到。

针对查询条件没有覆盖索引时的情况,进行实验:
1.在一个表中加入自增主键,插入若干记录
2.删除主键列的索引
3.以原主键列的值作为查询条件执行SELECT FOR UPDATE

监视器输出:RECORD LOCKS space id 3 page no 6 n bits 320 index GEN_CLUST_INDEX of table `test`.`t` trx id 2131 lock_mode X
也就是使用了隐式生成的聚簇索引。
在这种情况下即使查询条件中的列在值上是唯一的,也会锁定全表记录(因为走了全表扫描)。
此时开启另一个事务,对另一条记录执行主键加锁查询(SELECT FOR UPDATE),根据S/X锁标准将被阻塞,经验证确实如此。

间隙锁

间隙锁用于锁定索引记录之间的间隙,或者一组索引值两端的间隙。比如语句 SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
可以防止其他事务在t.c1列上插入 15 (因为在10-20之间),无论 15 是不是列上已有的值,因为在 BETWEEN 所指定的区间都被锁住了。

所谓的间隙可以覆盖一个值,多个值,甚至是 0 个。

关于间隙的准确含义此处引用官方术语集:
间隙 指能在InnoDB索引数据结构中能被插入的位置。例如用SELECT ... FOR UPDATE 锁住一批行时,InnoDB将锁住条件命中的索引上的值以及它们之间的间隙。比如加锁读所有大于10的值时,间隙锁会防止其他事务插入大于10的值。

间隙锁一定程度上体现了MySQL在性能与并发之间的权衡,在某些特定的事务隔离级别中使用到了间隙锁。

对于使用唯一索引查找的语句,不会用到间隙锁(除非搜索条件中只包含一个多列唯一索引的部分列)例如下列语句中,如果列 id 有唯一索引,则只会用到一个 id=100 的记录锁,并且不会妨碍其它会话在之前的间隙进行插入。

1 SELECT * FROM child WHERE id = 100 ;

但如果id没有索引或者有一个非唯一索引,语句就会锁住之前的间隙。

不同的事务可以在同一段间隙上持有相互冲突的锁。例如,事务A持有一段间隙的共享间隙锁(gap S-lock),同时事务B可以在同一段间隙上持有独占间隙锁(gap X-lock)。因为如果一条索引记录被删除,不同事务针对该记录持有的间隙锁必须被合并。

在InnoDB中,间隙锁的互斥特性被相当程度地抑制了,意思是间隙锁的唯一作用是防止事务在间隙中进行插入操作。间隙锁可以共存。不同事务可以同时持有同一段间隙的间隙锁。共享间隙锁和独占间隙锁没有区别。它们之间不会冲突,且作用相同。

间隙锁可以被显式禁用。通过改变事务隔离级别为 READ COMMITTED 或者开启系统变量 innodb_locks_unsafe_for_binlog(目前已弃用)
来禁用。在这些情况下,间隙锁不再用于搜索和索引扫描,而只用于外键约束检查和重复键检查。

上述的两种设置还有一些“副作用”。在MySQL计算出where条件后,不匹配的行的记录锁会被释放。对于UPDATE语句,InnoDB会执行一个“半一致性”读,从而向MySQL返回最新提交的版本号,用于挑选出匹配WHERE条件的行。

针对最后一段提到关于 READ COMMITTED 会释放不匹配的记录锁进行实验。

SET SESSION TRANSACTION ISOLATION LEVEL READ COMMITTED; 
start TRANSACTION;  
-- id未加索引  
SELECT * from test.t where id =1 for update;

监视器输出:

RECORD LOCKS space id 3 page no 6 n bits 320 index >GEN_CLUST_INDEX of table `test`.`t` trx id 2178 lock_mode X locks rec but not gap

可以看到虽然走聚簇索引,但事务最终只占有符合筛选的记录锁。
隔离级别改回REPEATABLE READ,执行相同语句。
监视器输出:

3 lock struct(s), heap size 1136, 372 row lock(s)  
RECORD LOCKS space id 3 page no 6 n bits 320 index GEN_CLUST_INDEX of table `test`.`t` trx id 2179 lock_mode X

占有全表记录锁。

邻键锁

邻键锁是记录锁以及在索引记录之前间隙上的间隙锁的组合。

InnoDB在搜索或扫描索引时,对遇到的每一条索引记录设置共享/独占锁,以此实现行锁。因此,所谓的行锁实际就是记录锁。而邻键锁不仅仅锁住一条索引记录,还会影响记录之前的“间隙”。也就是说邻键锁可以表示为一个记录锁加上记录之前间隙的间隙锁。如果某个会话持有记录R上索引的共享/独占记录锁,对于其它会话,如果插入的值小于记录R上索引值(按索引排序规则),则不能直接插入,必须等待锁释放。

设想一个索引包含值 10 , 11 , 13 , 20 。那么所有可能的邻键锁区间如下,圆括号代表不包含,方括号代表包含:

 (负无穷, 10 ]  
 ( 10 , 11 ]  
 ( 11 , 13 ]  
 ( 13 , 20 ]  
 ( 20 , 正无穷)  

对于最后一个区间,邻键锁锁定一段大于最大索引值的间隙,并使用一个虚拟的纪录表示上界。这个上界并不是真实的索引值,所以实际上这个邻键锁没有携带记录锁,只有大于当前索引最大值的间隙锁。

InnoDB默认使用 REPEATABLE READ 隔离级别。在这个级别下,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 10080 lock_mode X
 Record lock, heap no 1 PHYSICAL RECORD: n_fields 1 ; compact format; info bits 0
 0 : len 8 ; hex 73757072656 d756d; asc supremum;;
 Record lock, heap no 2 PHYSICAL RECORD: n_fields 3 ; compact format; info bits 0
 0 : len 4 ; hex 8000000 a; asc ;;
 1 : len 6 ; hex 00000000274 f; asc 'O;;
 2 : len 7 ; hex b60000019d0110; asc ;;

邻键锁主要用于解决幻行问题:事务因其它事务的插入操作导致两次读取的结果集不一致。邻键锁解决了这一问题,可帮助应用实现插入值唯一(加锁读->获取邻键锁->只允许本会话插入)。

插入意向锁

插入意向锁是一种特殊的间隙锁,在插入操作中执行行插入之前获得。用于标志插入的意向,从而使多个事务在同一段间隙执行插入时,如果对方不在同一个索引值位置上插入,则无需互相等待。例如,当前索引值有 4 和 7 。两个事务分别准备插入 5 和 6 ,在获取被插入行的独占锁之前,它们会各自获取 4 至 7 之间间隙的插入意向锁,且不会互相阻塞,因为插入值没有冲突。

下面通过一个例子来演示事务在获取记录的独占锁之前,获取插入意向锁的过程。案例中涉及两个客户端,分别是A和B。

客户端A创建一张表,包含两条索引值( 90 和 102),然后开启一个事务,获取ID大于100的所有记录的独占锁。独占锁将包含一个 id<102 的间隙锁:

即邻键锁,区间 =(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开启一个事务,在间隙内执行插入记录的命令。事务会先获取一个插入意向锁,然后等待获取独占锁。

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

如果要查看当前数据库中的插入意向锁,执行 SHOW ENGINE INNODB STATUS , InnoDB 的监视器会输出如下内容:

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

自增锁

自增锁是一种特殊的表锁,事务在带有自增列的表中执行插入会获取自增锁。在最简单的情形中,如果某个事务在向表中插入数据,其它事务必须等待其插入完毕才能执行自己的插入,以此来保证主键值是连续的。

配置项 innodb_autoinc_lock_mode 用于控制自增锁使用的算法,以帮助你在自增序列的可预测性和插入的并发能力之间权衡。

针对空间索引的断言锁

InnoDB支持对空间行建立索引。
在处理涉及到空间索引的操作时,邻键锁在 REPEATABLE READSERIALIZABLE 两个隔离级别上不能很好的工作。因为对于多维数据没有绝对的排序规则,所以并不能明确谁才是“邻键”。

posted @ 2020-11-08 00:16  d1zzyboy  阅读(486)  评论(2编辑  收藏  举报