[译]Innodb中的锁

原文地址InnoDB Locking

本章节描述了InnoDB中使用的锁.

  • 共享锁和排它锁
  • 意向锁
  • 记录锁
  • 间隙锁
  • Next-Key锁
  • 插入意向锁
  • AUTO-INC 锁

共享锁和排它锁

InnoDB实现了标准的行级别锁,包括两种类型:共享锁和排它锁

  • 获取到共享锁(S)的事务可以读取这一行。
  • 获取到排它锁(X)的事务可以更改或删除这一行。

如果事务T1拿到了第r行的S锁,那么另外一个事务T2对第r行的锁请求将会被如下处理:

  • 如果T2请求获取共享锁,那么可以立即成功。结果是T1和T2都拿到了第r行的共享锁。
  • 如果T2请求获取排他锁,那么将不会立即成功。

如果T1拿到了第r行的排它锁,那么T2无论请求哪个类型的锁,都不会立即成功。T2必须等待T1释放第r行的锁,才有可能拿到锁。

意向锁

InnoDB支持多个粒度的锁,它允许行级别锁和表锁同时存在。为了实现这一点,InnoDB使用了额外的锁,叫做意向锁。意向锁是表级别的锁,当事务使用某种类型的意向锁时,说明事务接下来要请求表中的某一行的同类型锁。InnoDB中使用了两种类型的意向锁:

  • 意向共享锁(IS): 事务接下来要对表中某一行加共享锁。
  • 意向排它所(IX): 事务接下来要对表中某些行加排它锁。

举例来说,SELECT ... LOCK IN SHARE MODE加了意向共享锁,SELECT ... FOR UPDATE加了意向排它锁。
意向锁工作机制如下:

  • 事务在获取表t的第r行的共享锁之前,必须先获取t的IS锁,或者更强的锁。
  • 事务在获取表t的第r行的排它锁之前,必须先获取t的IX锁。
X IX S IS
X 冲突 冲突 冲突 冲突
IX 冲突 兼容 冲突 兼容
S 冲突 冲突 兼容 兼容
IS 冲突 兼容 兼容 兼容

这些规则可以总结为一个二维的锁兼容性矩阵,如下:

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

如果事务请求的锁与当前已存在的锁兼容,那么事务将成功获取锁,否则将会等待,直到这个已存在的冲突的锁被释放为止。如果一个与当前存在的锁冲突的锁请求成功了,那么将会导致死锁错误。
因此,意向锁只会阻塞要求获取整个表的那种请求。IX和IS存在的主要目的是说明当前有某个事务锁住了一行数据,或者将要锁住一行数据。
使用SHOW ENGINE INNODB STATUS命令以及InnoDB monitor可以查看意向锁,如下:

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

记录锁

记录锁是加在索引记录上的锁。举例来说,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;阻止了其他事务插入、修改或者删除cl的值为10的这一行记录。

记录锁总是锁在索引上,即使这个表在定义时没有定义索引。这种情况下,InnoDB会创建一个隐藏的组合索引,并使用这个索引来给记录加锁。(查看聚合索引和二级索引)

使用SHOW ENGINE INNODB STATUS命令以及InnoDB monitor可以查看记录锁,如下:

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;阻止了其他事务向表中插入一条c1的值为15的记录,不管是否已经存在这样一条记录,因为这个范围之内的所有记录都被锁住了。

一个间隙可能包括了一个索引值,或者包括多个,也可能一个都没有。

间隙锁是权衡了性能和并发能力的一个折中选择,在某些隔离级别中使用了间隙锁。

当使用一个唯一索引去查找唯一的一行记录时是用不上间隙锁的(不包括那种使用多个列成员来作为查找条件,同时这些列被定义为唯一索引。这种情况下仍然会产生间隙锁)。举例来说,如果id这一列是唯一索引,那么如下的语句将会使用记录锁来锁住id为100的记录,而不关心是否有其他事务在之前的间隙中插入数据:

SELECT * FROM child WHERE id = 100;

如果id不是索引,或者不是唯一索引,那么这条语句就会锁住之前的间隙。

值得注意的是,两种类型冲突的锁是可以同时加在一个间隙上。举例来说,事务A在一个间隙上加了一个共享间隙锁(gap S-lock),同时事务B在同一个间隙上加了一个排它间隙锁(gap X-lock)。这种情况是允许的,因为如果一条记录被删除了,那么不同事务加在这个记录左右两边的的间隙锁必须合并(此处翻译可能有误)。

在InnoDB中,间隙锁是"purely inhibitive"(不知如何翻译),也就是说间隙锁只会阻止其他事务往间隙中插入数据。间隙锁不阻止不同的事务在同一个间隙上加锁。因此,间隙排它锁(gap X-lock)和间隙共享锁(gap S-lock)的效果是一样的。

可以显式禁止间隙锁。将事务隔离级别设置为READ COMMITTED或者将系统配置innodb_locks_unsafe_for_binlog设置生效(不推荐这么搞),可以禁止使用间隙锁。这种情况下,间隙锁只会用来做外键约束检查和重复key检查。

使用READ COMMITTED隔离级别或者将系统配置innodb_locks_unsafe_for_binlog设置生效还有其他两个影响。当MySQL计算完WHERE语句后,会释放记录锁。对于UPDATE语句,InnoDB会做"半一致性(semi-consistent)"读,它返回最近一次提交版本的记录给MySQL,然后MySQL会决定这个记录是否和UPDATE语句的WHERE条件匹配。(这段也不是很懂-_-||)

Next-Key锁

Next-Key Lock是记录锁和间隙锁的结合产物。

在查找和遍历一个表的索引时,InnoDB会做行级别的锁,它对遇到的索引记录加上共享锁或者排它锁。因此行锁实际上就是对索引加的锁。在索引记录上加的next-key lock会影响这个索引之前的间隙。意思就是,一个next-key lock是一个索引上的锁外加这个索引之前那部分间隙的锁。如果一个会话在记录R的索引上加了共享锁或者排它锁,那么其他会话不能在这个记录R之前(按照索引顺序)的间隙中插入记录。

假设索引中包含了10,11,13,20这几个值。对于这个索引,可能的next-key lock会覆盖如下几个间隙,原括弧表示开区间,方括弧表示闭区间:

(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)

对于最后一个间隙,next-key lock锁住了最大的索引值之后的间隙,以及一个伪索引值,这个索引值比所有的索引都大。这个伪索引实际上是不存在的,所以nex-key lock只是锁住了最大索引值后面的间隙。

默认情况下,InnoDB工作在REPEATABLE READ级别下,同时innodb_locks_unsafe_for_binlog没有设置为生效。在这种情况下,InnoDB在查找和遍历索引时会使用next-key lock,也就防止了幻行(查看Phantom Rows)。

使用SHOW ENGINE INNODB STATUS命令以及InnoDB monitor可以查看next-key锁,如下:

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,现在又两个事务分别要插入5和6,那么这两个事务各自给4和7之间的间隙加上插入意向锁,然后给待插入的行上加排它锁,他们不会阻塞彼此,因为插入的行没有冲突。

下面的例子展示了事务在获取行的排它锁之前,先加上了插入意向锁。这个例子包括了两个客户请求,A和B。

客户A创建了一个有索引的表,插入了两条数据(90和102),然后启动一个事务,给ID大于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 monitor可以查看插入意向锁,如下:

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 锁

AUTO-INC锁是一个特殊的表级别锁,当事务向一个带有AUTO_INCREMENT列成员的表中插入数据时,会先加一个AUTO-INC锁。最简单的情况下,如果一个事务正在向表中插入数据,那么其他的想要插入数据的事务必须等待,这样前面那个事务插入的数据才会有一个连续的primary key。

配置项innodb_autoinc_lock_mode控制着AUTO-INC锁使用的算法。允许你自己选择如何在插入操作的可预期自增序列以及并行能力中进行权衡折衷。

你可能感兴趣的:([译]Innodb中的锁)