InnoDB 锁类型

说明

本文译自MySQL 5.7官方文档的章节14.7.1,InnoDB的锁类型

译文

本章节介绍了InnoDB所使用的锁类型。

共享锁和排他锁(Shared and Exclusive Locks)

InnoDB实现了两种标准的行级锁:共享锁(也称为S锁)和排他锁(也称为X锁)。

  • 共享锁允许持有该锁的事务读取一行记录。
  • 排他锁允许持有该锁的事务更新或者删除一行记录。

如果事务T1在记录r上持有一个共享锁,那么,如果此时另一个不同的事务T2发起了申请记录r的锁的请求,将会被这样处理:

  • 如果T2申请的是共享锁,那么将会立即被满足。此时,T1和T2都持有一个r的共享锁。
  • 如果T2申请的是排他锁,那么将不会被立即满足。

如果事务T1持有记录r的排他锁,那么,如果此时另一个不同的事务T2无论发起了申请记录r的哪一类的锁请求,都将不会被立即满足。事务T2必须等待事务T1释放它在记录r上的锁。

意向锁(Intention Locks)

InnoDB支持多种粒度的锁,允许了行级锁和表级锁的共存。例如,类似于LOCK TABLES … WRITE的语句会在指定表上加上排他锁(X锁)。为了使得多种粒度级别的锁变得可行,InnoDB使用了意向锁。意向锁是表级别的锁,它指明了一个事务对表中的记录将需要哪种类型的锁(共享还是排他)。意向锁有两类:

  • 意向共享锁(即IS锁),指明一个事务将会在表的某些行上设置一个共享锁。
  • 意向排他锁(即IX锁),指明一个事务将会在表的某些行上设置一个排他锁。

例如,SELECT … LOCK IN SHARE MODE设置了一个意向共享锁,SELECT … FOR UPDATE设置了一个意向排他锁。

意向锁的协议有以下两点:

  • 一个事务在获取表的一行记录的共享锁之前,它首先必须获取该表的意向共享锁或者更强级别的锁。
  • 一个事务在获取表的一行记录的排他锁之前,它首先必须获取该表的意向排他锁。

表级别的锁类型之间的兼容性可以用下表来表示:

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

一个事务在申请锁时,如果申请的锁与当前存在的锁兼容,那么这个申请将会得到满足。如果锁冲突,则事务会等待已存在的锁被释放。如果一个锁请求与当前存在的锁产生了冲突并且由于会引发死锁而得不到满足,此时就发生了错误。

意向锁不会阻塞全表请求(例如,LOCK TABLES … WRITE)之外的任务东西。意向锁的主要目的是表明有事务正锁着某行记录,或者将要锁住表的某行记录。

意向锁的事务数据在SHOW ENGINE INNODB STATUSInnoDB 监视器输出中的显示内容类似于如下:

1
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的记录进行插入,更新或者删除操作。

记录锁总是锁定索引的记录,即使表里并没有定义索引。在这种情况下,InnoDB创建了一个隐藏的聚簇索引并使用这个索引来锁定记录。详见章节14.6.2.1,“聚簇索引和二级索引”

记录锁的事务数据在SHOW ENGINE INNODB STATUSInnoDB 监视器输出中的显示内容类似于如下:

1
2
3
4
5
6
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的记录行仅使用了索引记录锁,至于其它会话是否在前面的间隙里插入记录则无关紧要:

1
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上持有一个共享或者排他锁,那么其它会话则不能在索引顺序中R前面的间隙中插入新的索引记录。

假设一个索引包含了值10,11,13和20。这个索引可能存在的临键锁包含一下几个区间,其中圆括号表示不包含区间端点,方括号表示包含区间端点:

1
2
3
4
5
(负无穷, 10]
(10, 11]
(11, 13]
(13, 20]
(20, 正无穷)

对于最后一个区间,临键锁锁定索引中最大值上的间隙,并且“最高”伪记录的值高于索引中的任何实际值。这个最高值不是一个真正的记录,所以这个临键锁实际上只锁定了最大索引值之后的间隙。

InnoDB的默认事务隔离级别为REPEATABLE READ(可重复读)。在这种条件下,InnoDB采用临键锁来搜索和索引扫描,从而避免了幻读(详见章节14.7.4,“幻行记录”)。

临键锁的事务数据在SHOW ENGINE INNODB STATUSInnoDB 监视器输出中的显示内容类似于如下:

1
2
3
4
5
6
7
8
9
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之间的间隙,但不会相互阻塞,因为行是不冲突的。

以下这个示例演示了一个在获取到被插入行记录的排他锁之前先获取插入意向锁的事务。示例里包含了两个客户端,A和B。

客户端A创建了一个包含两条索引记录(90和102)的表,然后开启一个事务,该事务在ID大于100的索引记录上放置了一个排他锁。这个排他锁包含了记录102之前的间隙锁:

1
2
3
4
5
6
7
8
9
10
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开启一个在这个间隙里插入一条记录的事务。这个事务在等待获取排他锁时拿到了一个插入意向锁。

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

插入意向锁的事务数据在SHOW ENGINE INNODB STATUSInnoDB 监视器输出中的显示内容类似于如下:

1
2
3
4
5
6
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_INCREMENT列的表中的事务使用。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务如果要向该表进行插入则都必须等待,以便第一个事务插入的行能收到连续的主键值。

innodb_autoinc_lock_mode参数控制着自增锁所采用的算法。这个参数允许你选择如何在可预测的自增值序列和插入操作的最大并发之间做权衡。

更多信息详见章节14.6.1.6,“InnoDB中的自增处理”

空间索引的谓词锁(Predicate Locks for Spatial Indexes)

InnoDB支持对包含空间数据的列进行空间索引(详见章节11.4.8,“优化空间分析”

为了处理涉及空间索引的操作的锁,临键锁定不能很好地支持REPEATABLE READ(可重复读)SERIALIZABLE(序列化)事务隔离级别。多维数据中没有绝对的排序概念,所以无法定义哪个是临键。

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

原文

MySQL 5.7 Reference Manual 14.7.1 InnoDB Locking