前段时间遇到线上应用报死锁问题,要想解决或避免死锁首先要对锁的概念及其机制有一定的了解,特此根据官网整理锁相关知识。
目录
InnoDB的锁
意向锁(Intention Locks)
记录锁(Record Locks)
间隙锁(Gap Locks)
临界锁(Next-Key Locks)
插入意向锁(Insert Intention Locks)
自增锁(AUTO-INC Locks)
空间索引的谓词锁(Predicate Locks for Spatial Indexes)
InnoDB实现了标准的行级锁,其中有两个类型的锁,共享锁(S)和独占锁(X)(也称为排它锁)。
如果事务T1持有r行的S锁,然后另一个不同的事务T2也请求对r行上锁,则会有如下两种情形:
如果事务T1持有的是r行的X锁,那么另一个不同的事务T2对r上任意一种类型的锁的请求都不能被立即授予。相反,事务T2必须等待事务T1释放其对r行的X锁。
InnoDB支持多粒度锁,允许行锁和表锁共存。可以使用`LOCK TABLES tableName WRITE`给指定表上X锁,可以使用`LOCK TABLES tableName READ`给指定表上S锁。为了使多粒度级别的锁更实用,InnoDB使用了意向锁。意向锁是表级锁,它表示事务要对表中的记录使用哪种类型的锁(S锁或X锁)。
意向锁有两种类型:
可以使用`SELECT ... LOCK IN SHARE MODE`设置IS锁,使用`SELECT ... FOR UPDATE`设置IX锁。
意向锁的协议如下:
表级锁的兼容性如下:
X | IX | S | IS | |
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果某一事务T请求的表级锁B与现有表级锁A兼容,则事务T可以立即获取表级锁B,但如果事务T请求的表级锁B与现有表级锁A冲突,则事务T不会立即获取到锁B。事务T将等待,直到现有锁A被释放。如果锁请求与现有锁冲突,则可能会产生死锁并无法获得该锁。
除了全表请求外(如:`LOCK TABLES ... WRITE`),意向锁不会阻塞其他任何请求。意向锁的主要目的是为了表明某个事务正在锁定表中的一行,或准备锁定表中的一行。
在SHOW ENGINE INNODB STATUS和InnoDB monitor的输出中,意向锁的日志类似如下:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
记录锁是索引记录上的锁。例如使用如下语句给t.c1值为10的行加X锁,以防其他任何事务插入,修改,删除该行。
SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
记录锁总是锁定索引记录,即使表中没有定义索引。对于这种情况,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;
以上语句中,10到20范围内所有存在值之间的间隙已经被锁定,所以,若某事务想往该列中插入值15,无论该列是否已经存在15都不能插入。
一个间隙可以跨越一个索引值,多个索引值,甚至是空的。
间隙锁是性能和并发性之间的折衷,仅适用于某些事务隔离级别。
对于使用唯一索引查找唯一行的语句,不需要使用间隙锁。
对于使用唯一索引锁定行以查找唯一行的语句,不需要使用间隙锁(不包括搜索条件只包括多列唯一索引的一些列的情况,在这种情况下,会发生间隙锁)。例如,如果id列上有唯一索引,下面的语句只对id值为100的行使用索引记录锁,而其他session是否在前面的间隙中插入行并不重要:
SELECT * FROM child WHERE id = 100;
如果id没有索引或有一个非唯一索引,则该语句会锁定前面的间隙。
值得注意的是,不同的事务可以在一个间隙上持有冲突的锁。例如,事务A可以在一个间隙上持有一个共享间隙锁(gap S-lock),而事务B可以在同一个间隙上持有一个排他间隙锁(gap X-lock)。允许冲突锁的原因是,如果从索引中清除了一条记录,则必须合并由不同事务持有的记录上的间隙锁。
InnoDB中的间隙锁是“purely inhibitive”的,这意味着他们的唯一目的是防止其他事务插入到间隙中。间隙锁可以共存。一个事务获得某一间隙上的间隙锁不能阻止另一个事务获得相同间隙上的间隙锁。共享间隙锁和独占间隙锁之间没有区别。它们执行相同的功能,彼此之间不冲突。
间隙锁可以显示禁用。可以通过将事务隔离级别修改为READ COMMITTED或启用innodb_locks_unsafe_for_binlog系统变量(现已弃用)以禁用间隙锁。禁用后对于搜索和索引扫描将不能使用间隙锁,间隙锁仅用于外键约束检查和重复键检查。使用READ COMMITTED隔离级别或启用inndb_locks_unsafe_for_binlog还有其他效果。不匹配行的记录锁在MySQL计算WHERE条件后被释放。对于UPDATE语句,InnoDB执行“半一致”的读取,这样它会返回最新提交的版本给MySQL,以便MySQL可以确定这行是否匹配UPDATE的WHERE条件。
next-key lock是索引记录上的记录锁和索引记录之前间隙上的间隙锁的组合(左开右闭)。InnoDB执行行级锁的方式是这样的:当它搜索或扫描一个表的索引时,它会对找到的索引记录设置共享锁或排它锁。因此,行级锁是索引记录锁。索引记录上的next-key lock也会影响该索引记录之前的间隙。也就是说,next-key锁是一个索引记录锁+该索引记录前间隙的间隙锁。如果一个session持有索引中记录R的共享锁或排他锁,那么另一个session就不能在索引顺序在R之前的间隙中插入一个新的索引记录了。
假设某一索引含有值10、11、13和20。这个索引可能的next-key锁包括以下区间,其中圆括号表示不包含端点,方括号表示包含端点,可以看出,next-key锁锁住的是左开右闭的区间:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
对于最后一个间隔,next-key锁将锁定索引中最大值以上的间隙,并且“supremum”伪记录的值高于索引中实际的任何值。supremum不是一个真正的索引记录,因此,实际上,next-key锁只锁住了实际存在的最大索引值后面的间隙。
默认情况下,InnoDB运行在REPEATABLE READ事务隔离级别。在这种情况下,InnoDB使用next-key锁进行搜索和索引扫描,以防止幻读。
在SHOW ENGIN 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-INCREMENT列的表中插入记录的事务可以获得该锁。在最简单的情况下,如果某一事务正在向表中插入值,那么任何其他事务都必须等待,以便第一个事务插入的行接收连续的主键值。
可以使用innodb_autoinc_lock_mode变量控制AUTO-INC锁的算法。可以通过该变量在自动增量值的可预测序列和插入操作的最大并发性之间进行权衡。
InnoDB支持对包含空间数据的列进行空间索引。
next-key锁在锁设计SPATIAL索引时,在REPEATABLE READ或SERIALIZABLE隔离级别下不能很好地支持。因为在多位数据中没有绝对排序的概念,所以不清楚哪一个是“下一个”键。
InnoDB使用谓词锁以支持具有空间索引的表的隔离级别。空间索引包含最小边界矩形(MBR)值,因此InnoDB通过在用于查询的MBR值上设置谓词锁来强制对索引进行一致性的读取。其他事务不能插入或修改与查询条件匹配的行。
参考:Mysql官网关于5.7版本InnDB锁的介绍