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 READ
和 SERIALIZABLE
两个隔离级别上不能很好的工作。因为对于多维数据没有绝对的排序规则,所以并不能明确谁才是“邻键”。