0%

译文:Innodb的锁机制

翻译自MySQL官方文档:InnoDB Locking

本文介绍Innodb使用到的锁的类型:

  • 共享锁和排它锁
  • 意向锁
  • 记录锁
  • 间隙锁(Gap locks)
  • Next-Key Lock
  • 插入意向锁
  • 自增锁(Auto-INC Locks)
  • 空间索引的谓词锁

共享锁和排它锁

Innodb实现了2种标准的行级锁,即共享锁(S锁)和排它锁(X锁)。

  • 持有共享锁的事务可以读取记录。
  • 持有排它锁的事务可以更新、删除记录。

如果事务T1持有记录r的共享锁,则事务T2:

  • 也可以马上获得记录r的共享锁。T1T2可以同时持有共享锁。
  • 但是不能马上获得记录r的排它锁。

如果事务T1持有记录r的排它锁,则事务T2不能马上获取记录r的排它锁或共享锁,事务T2需要等待,直到T1释放锁。

意向锁

Innodb支持多粒度锁,即表锁和行锁。例如,可以使用LOCK TABLES .... WRITE语句给表格加上表级的排它锁。Innodb使用意向锁来实现多粒度锁定。意向锁是表级锁,它表示稍后事务会申请这个表格的哪些类型的行锁。意向锁有2种类型:

  • 意向共享锁(IS),表示事务准备在某些记录上加上共享锁。
  • 意向排它锁(IX),表示事务准备在某些记录上加上排它锁。

例如,SELECT ... LOCK IN SHARE MODE会加上一个意向共享锁,SELECT ... FOR UPDATE会加上一个意向排它锁。

意向锁的规则如下:

  • 事务在获取某些行的共享锁之前,必须先获取该表格的意向共享锁(或更强的锁)。
  • 事务在获取某些行的排它锁之前,必须先获取该表格的意向排它锁。

各种表级锁的兼容性如下:

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

如果事务申请的锁,跟其它事务已经获取的锁是兼容的,那么就可以马上申请到;否则出现锁冲突时,申请锁的事务会被阻塞,直到其它事务把锁释放掉。如果事务申请锁而导致死锁出现时,会产生一个错误。

意向锁只会阻塞对整个表加锁的请求,例如LOCK TABLES ... WRITE。意向锁的主要用途是:表明其它事务锁住了、或者即将表格种的某些行。

通过SHOW ENGINE INNODB STATUS语句输出事务信息时,意向锁的输出类似于:

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

记录锁

记录锁是加在索引上的锁。例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE会阻止其它事务插入、更新或删除t.c1=10的记录。

如果表格没有定义任何索引,也没有指定主键索引时,Innodb会自动创建一个隐藏的列作为主键索引。

通过SHOW ENGINE INNODB STATUS语句输出事务信息时,记录锁的输出类似于:

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 ;;

间隙锁

间隙锁,用于锁住索引之间的间隙,或者索引第一条记录之前、最后一条记录之后的区间。例如,执行SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;语句会阻止其它事务插入t.c1=15的记录,不管目前表格中有没有t.c1=15的记录,因为间隙锁会锁住已有记录之间的间隙。

一个间隙可能锁住索引的一个值、多个值,也可能0个值。

间隙锁是用于权衡性能和并发性的一个方案,只在某些隔离级别下会用到。

当使用唯一索引来查找一行唯一的记录时,不会用到间隙锁。但是如果唯一索引由多个字段组成,WHERE条件中只有部分字段,这种情况还是会用到间隙锁。例如,id字段是唯一的,下面的语句会在id=100的记录上加上行锁,但不会阻止其它事务往id=100之前的间隙插入记录:

1
SELECT * FROM child WHERE id = 100;

如果id字段没有索引,或者不是唯一索引,这条SQL会锁住id=100之前的记录。

值得注意的是,不同的事务可以同时持有冲突的间隙锁。例如,事务A持有一个共享的间隙锁(gap S-lock),事务B可以同时持有同一个间隙的排它间隙锁(gap X-lock)。间隙锁允许冲突的原因是:如果所有中的一条记录被删除,则必须合并不同事务持有的间隙锁。

间隙锁唯一的用处是:阻止其它事务往间隙中插入记录。间隙锁可以共存,事务获取到一个间隙锁后,并不会阻止其它事务获取同一个间隙的间隙锁。共享间隙锁和排它间隙锁没有区别,它们也不会互相阻塞。

间隙锁可以被显式的禁用:通过把隔离级别设置为读已提交(READ-COMMITTED),或者启用配置项innodb_locks_unsafe_for_binlog(现在不推荐使用这个配置)。这时,索引查找、索引扫描都不会使用间隙锁,只有检查外键约束、唯一索引时才会使用。

把隔离级别设置为读已提交,或者修改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可能锁定的区间如下:

(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)

对于最后一个区间,Next-key锁住了索引中的最大值到正无穷的区间,虽然索引中并没有“正无穷”这个值。

默认情况下,Innodb使用可重复读(REPEATABLE READ)隔离级别。在这个隔离级别下,Innodb会在查找时,给遇到的记录加上Next-key锁,用来防止幻读。

通过SHOW ENGINE INNODB STATUS语句输出事务信息时,Next-Key锁的输出类似于:

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语句插入数据之前会加上插入意向锁。插入意向锁的用处:向同一个索引间隙中插入数据的事务,如果它们没有插入到间隙中的相同位置,就不需要互相等待。假设现在索引中有4和7两个值,有2个事务分别想插入5和6,则每个事务在获得插入行的排它锁之前,都会使用插入意向锁锁住4~7之间的间隙,但是插入意向锁并不会互相阻塞,因为2个事务插入的是不同的位置。

下面的例子演示了一个事务在获取插入行的排它锁之前,获取了插入意向锁。例子中一共有2个客户端A和B。

客户端A首先创建一个表格,插入2行记录:90和102, 然后开启一个事务,获取了id>100的记录的排它锁,以及一个id<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 STATUS语句可以看到以下输出:

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)

自增锁是一个特殊的表级锁,向带有自增字段的表格插入数据时会用到。一个最简单的例子:如果一个事务正在向表格中插入数据,其它往同一个表格插入数据的事务必须等待,这样生成的自增ID才是连续的。

通过innodb_autoinc_lock_mode参数可以控制自增锁的算法,你可以选择更好的并发性,但是放弃自增字段的连续性。

空间索引的谓词锁

Innodb支持给空间类型的字段增加空间索引。

对于空间索引来说,Next-key并不能很好的支持可重复读和可序列化隔离级别,因为在空间索引中,没有绝对的排序的概念,不能确定哪一个是索引的下一个值。

Innodb使用谓词锁来支持空间索引的各个事务隔离级别。空间索引包含最小边界矩形(MBR)值,因此Innodb给用于查询的MBR值加上谓词锁来实现对空间索引进行一致性读取,其它事务不能修改、插入与当前事务查询条件匹配的行。