InnoDB锁和事务模型 —— 锁

本篇介绍InnoDB使用的锁类型,并提供了一些例子来更加深入的介绍了锁。

共享锁和排它锁

我们都知道InnoDB实现了行级锁,行级锁分为两类:共享和排他。

  • 共享锁(Sharded Lock, S)允许持有锁的事务读取行
  • 排它锁(Exclusive Lock, X)允许持有锁的事务更新或删除行

顾名思义,共享锁可以在多个事务中共享,例如事务T1在行r获取一个共享锁,另外一个事务T2再次在行r上请求共享锁,会立马获取到。这样T1和T2都持有在行r上的共享锁。

如果t2请求在行r上的排它锁,将不能立即获取到。反之如果T1获取r上的排它锁,则后续不管T2是什么类型的锁,都不会立即获取。下面的例子中我们先从让事务T1获取共享锁,然后让另个一事务T2分别一次获取共享锁、排它锁,是否如以上描述的一样。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
-- 首先我们创建一张测试表`user`
+-------+-------------+------+-----+---------+-------+
| Field | Type | Null | Key | Default | Extra |
+-------+-------------+------+-----+---------+-------+
| id | int(11) | NO | PRI | NULL | |
| name | varchar(20) | YES | | NULL | |
+-------+-------------+------+-----+---------+-------+

-- 插入一些测试数据
insert into user values(1, 'zhao'), (2, 'qian'), (3, 'sun'), (4, 'li'), (8, 'zhou'), (12, 'wu');

-- 打开一个mysql客户端,执行下面语句来获取id=1的共享锁
START TRANSACTION;

SELECT * FROM `user` WHERE id = 1 LOCK IN SHARE MODE;
+----+------+
| id | name |
+----+------+
| 1 | zhao |
+----+------+

-- 然后打开另一个客户端,执行与上面相同语句,再次获取id=1的共享锁,我们会立刻获得该锁
START TRANSACTION;
SELECT * FROM `user` WHERE id = 1 LOCK IN SHARE MODE;
+----+------+
| id | name |
+----+------+
| 1 | zhao |
+----+------+

-- 最后我们尝试获取id=1的排它锁,我们将无法获取到该锁
START TRANSACTION;

SELECT * FROM `user` WHERE id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

意图锁

InnoDB支持多个粒度锁,允许行锁和表锁共存。为了实现多个粒度级别的锁,InnoDB使用意图锁。意图锁是表锁,用来表明事务稍后对表中的行所需的锁类型(共享或独占)。意图锁分为两种:

  • 意图共享锁(IS)表明事务打算在表中的个别行设置共享锁。
  • 意图排他锁(IX)表示事务打算在表中的个别行上设置独占锁。

在事务可以获取表中某行的S锁之前,它必须首先在表上获取IS或更强的锁。同样在事务可以获取表中某行的X锁之前,它必须首先获取IX锁。

意图锁不会阻止除表的X锁(例如,LOCK TABLES … WRITE)请求之外的任何内容。意图锁的主要目的是表明事务将要锁定表中的行。

表级锁的兼容性如下,可以将列想象为事务T1已获取到的表级锁,行为事务T2要请求的表锁。

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

如果锁与现有锁兼容,则向请求事务授予锁,但如果它与现有锁冲突,则不授予锁。事务等待直到冲突的现有锁被释放。

记录锁(Record Lock)

记录锁是索引记录上的锁。索引必须是主键索引唯一索引。InnoDB的行锁实现方式是锁定索引记录,而不是行数据。例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; 防止任何其他事务插入,更新或删除t.c1的值为10的行。

如果一个查询条件没有索引,则将会使用表锁。

1
2
3
4
5
6
7
8
9
10
11
12
13
START TRANSACTION;

select * from user where name = 'zhao' for update;
+----+------+
| id | name |
+----+------+
| 1 | zhao |
+----+------+

START TRANSACTION;

SELECT * FROM `user` WHERE id = 1 for update;
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

以上例子中,第一个事务占有name=’zhao’的排它锁,但是第二个事务却无法获取到id=1的排它锁,所以id=1也被第一个事务锁定。造成这个问题的原因是因为name上没有索引存在,会进行全表扫描,所以就对整张表进行锁定。

间隙锁(Gap Lock)

间隙锁是锁定索引记录之间的间隙,或锁定在第一个之前或最后一个之后索引记录的间隙上。例如,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未被索引或具有非唯一索引,则语句确实锁定前面的间隙。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
START TRANSACTION;

-- 锁住4-12的间隙
select * from user where id between 4 and 12 for update;
+----+------+------+
| id | name | age |
+----+------+------+
| 4 | li | 18 |
| 8 | zhou | 40 |
| 12 | wu | 52 |
+----+------+------+

-- 阻止插7的记录
insert into user values (7, 'wang', 23);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

这里也值得注意的是,不同的事务可以在间隙上持有冲突锁。例如,事务A可以在间隙上持有共享间隙锁(gap S-lock),而事务B可以在相同的间隙上持有独占间隙锁(gap X-lock)。允许冲突间隙锁定的原因是,如果从索引中清除记录,则必须合并由不同事务保留在记录上的间隙锁定。

注:
上述所描述的不同事务,可以在相同的间隙上持有冲突锁,没有验证成功。如果有朋友能看到这篇文章,知道原因可留言解答,非常感谢。

InnoDB中的间隙锁是“纯粹的抑制”,这意味着它们只能阻止其他事务插入间隙。它们不会阻止不同的事务在同一间隙上进行间隙锁定。 因此,间隙X锁具有与间隙S锁相同的效果。

可以显式禁用间隙锁。如果将事务隔离级别更改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现已弃用),则会发生这种情况。在这些情况下,对于搜索和索引扫描禁用间隙锁定,并且仅用于外键约束检查和重复键检查。

使用READ COMMITTED隔离级别或启用innodb_locks_unsafe_for_binlog还有其他影响。MySQL评估了WHERE条件后,将释放不匹配行的记录锁。对于UPDATE语句,InnoDB执行“半一致”读取,以便将最新提交的版本返回给MySQL,以便MySQL可以确定该行是否与UPDATE的WHERE条件匹配。

Next-Key锁

Next-Key锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁的组合。该索引记录为非唯一性索引。

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

假设索引包含值10,11,13和20。此索引的可能的Next-Key锁包括以下间隔,其中圆括号表示区间终点的排除,方括号表示包含端点:

1
2
3
4
5
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

对于最后一个间隔,next-key锁锁定索引中最大值以上的间隔和“上界”伪记录,其值大于索引中实际的任何值。上界不是一个真正的索引记录,因此,实际上,这个next-key锁只锁定最大索引值之后的间隙。

默认情况下,InnoDB在可重复读事务隔离级别运行。在这种情况下,InnoDB使用next-key锁进行搜索和索引扫描,这可以防止幽灵行。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
-- 修改上表,添加age字段,并设置age字段为索引。
select * from user order by age;
+----+------+------+
| id | name | age |
+----+------+------+
| 40 | wang | 10 |
| 16 | wang | 19 |
| 6 | zhao | 20 |
| 8 | li | 35 |
| 12 | wu | 35 |
| 30 | sun | 35 |
| 50 | feng | 43 |
| 10 | zhou | 65 |
| 20 | qian | 65 |
+----+------+------+

-- 下面事务设置了age=35的索引记录X锁,同时也设置了20-43的间隙锁
START TRANSACTION;
SELECT * FROM `user` WHERE age = 35 FOR UPDATE;

-- 在插入范围20-43内的数据需要等待锁
INSERT INTO `user` VALUES(31, 'wang', 21);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
INSERT INTO `user` VALUES(32, 'wang', 42);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction

-- 如果插入的id不在该范围内,会立即返回成功
INSERT INTO `user` VALUES(32, 'wang', 44);
Query OK, 1 row affected (0.01 sec)
INSERT INTO `user` VALUES(33, 'wang', 15);
Query OK, 1 row affected (0.01 sec)

-- 当插入age=20的记录时,首先我们可以看到age为20最大的id为6,则如果插入的age为20,并且id > 6则无法插入,< 6可以插入
INSERT INTO `user` VALUES(34, 'wang', 20);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
INSERT INTO `user` VALUES(4, 'wang', 20);
Query OK, 1 row affected (0.02 sec)

-- 当插入age=43时,age为43最小id是50,则插入age为20,并且id > 50可以插入,< 50无法插入。
INSERT INTO `user` VALUES(36, 'wang', 43);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
INSERT INTO `user` VALUES(51, 'wang', 43);
Query OK, 1 row affected (0.01 sec)

插入意图锁

插入意图锁是在行插入之前由INSERT操作设置的一种间隙锁。该锁表示以这样的方式插入的意图:如果插入到相同索引间隙中的多个事务不插入间隙内的相同位置,则不需要等待彼此。假设存在值为4和7的索引记录。尝试分别插入值5和6的单独事务,在获取插入行上的独占锁之前,每个用插入意图锁锁定4和7之间的间隙,但不会因为行不冲突而相互阻塞。

下面的示例演示了一个事务,在获取插入记录上的独占锁之前使用插入意图锁。这个示例涉及两个客户端A和B。

客户端A然后启动一个事务,在ID大于30的索引记录上放置一个独占锁。独占锁包括记录102之前的间隙锁定:

1
2
START TRANSACTION;
SELECT * FROM user WHERE id > 30 FOR UPDATE;

客户端B开始一个事务以将记录插入间隙。 该事务在等待获取独占锁时采用插入意图锁。

1
2
START TRANSACTION;
INSERT INTO user (id, name, age) VALUES (33, 'wei', 29);

客户端B开始一个事务以将记录插入间隙。 该事务在等待获取独占锁时采用插入意图锁。

AUTO-INC锁

AUTO-INC锁是由插入到具有AUTO_INCREMENT列的表中的事务所采用的特殊表级锁。在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务必须等待对该表执行自己的插入,以便第一个事务插入的行接收连续的主键值。

innodb_autoinc_lock_mode配置选项控制用于自动增量锁定的算法。它允许您选择如何在可预测的自动增量值序列和插入操作的最大并发之间进行权衡。

以下术语用于描述innodb_autoinc_lock_mode设置:

  • INSERT-like语句

在表中生成新行的所有语句,包括INSERT, INSERT … SELECT, REPLACE, REPLACE … SELECT,和LOAD DATA。包括”simple-inserts”, “bulk-inserts”, “mixed-mode”插入。

  • Simple inserts

可以预先确定要插入的行数的语句(最初处理语句时)。包括单行和多行不包括嵌套子查询INSERT和REPLACE语句,但并不是INSERT … ON DUPLICATE KEY UPDATE语句。

  • Bulk inserts

预先不知道要插入的行数(以及所需的自动增量值的数量)的语句。包括 INSERT … SELECT, REPLACE … SELECT, and LOAD DATA语句,当不是平常的INSERT。在处理每一行时,InnoDB一次为AUTO_INCREMENT列分配一个新值。

  • Mixed-mode inserts

这些是“Simple inserts”语句,指定了某些(但不是全部)新行的自动增量值。下面例子中,c1是表t1 AUTO_INCREMENT列

INSERT INTO t1 (c1,c2) VALUES (1,’a’), (NULL,’b’), (5,’c’), (NULL,’d’);

另一种类型“mixed-mode insert” 是INSERT … ON DUPLICATE KEY UPDATE,在最坏的情况下,实际上是INSERT后跟UPDATE,其中在更新阶段可能会或可能不会使用AUTO_INCREMENT列的分配值。

innodb_autoinc_lock_mode配置参数有三种可能的值。0,1,2分别代表 “traditional”, “consecutive”, “interleaved”

  • innodb_autoinc_lock_mode = 0

该锁模式下,所有“INSERT-like”语句都获得一个特殊的表级AUTO-INC锁,用于插入具有AUTO_INCREMENT列的表。此锁通常保持在语句的末尾(而不是事务的结尾),以确保为给定的INSERT语句序列以可预测且可重复的顺序分配自动增量值,并确保自动增量 任何给定语句分配的值都是连续的。

  • innodb_autoinc_lock_mode = 1 (“consecutive” lock mode)

默认的锁模式。在此模式下,“Bulk inserts”使用特殊的AUTO-INC表级锁定并保持它直到语句结束。应用到所有的INSERT … SELECT, REPLACE … SELECT, 和LOAD DATA语句。一次只能执行一个持有AUTO-INC锁定的语句。

“Simple inserts”(预先知道要插入的行数)通过在互斥锁(轻量级锁定)的控制下获取所需数量的自动增量值来避免表级AUTO-INC锁定 只在分配过程的持续时间内持有,而不是在语句完成之前。除非另一个事务持有AUTO-INC锁,否则不使用表级AUTO-INC锁。 如果另一个事务持有AUTO-INC锁,则“简单插入”等待AUTO-INC锁定,就像它是“批量插入”一样。

这种锁定模式确保在INSERT语句存在的情况下,事先不知道行数(以及在语句进行时分配自动增量数),所有自动增量值都由任何“INSERT-like”分配 语句是连续的,操作对于基于语句的复制是安全的。

空间索引的谓词锁

InnoDB支持对包含空间列的列进行空间索引.

为了处理涉及空间索引的操作的锁定,next-key锁不能很好地支持可重复读或可序列化的事务隔离级别。多维数据中没有绝对的排序概念,因此不清楚哪个是“next” key。

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

总结

InnoDB的锁都是基于索引的,InnoDB是对索引记录加锁,而不是行数据。如果是查询唯一索引,间隙只会锁住查询范围内的那些索引记录,而如果查询的是非唯一索引,则innoDB会采用Next-key Locking,不仅会锁定查询范围那些记录,还会锁定该范围之前和之后的记录。

坚持原创技术分享,更多深度分析、实践代码,您的支持将鼓励我继续创作!