介绍InnoDB的锁类型
Shared and Exclusive 锁
Intention 锁
Record 锁
Gap 锁
Next-Key 锁
Insert Intention 锁
AUTO-INC 锁
Predicate Locks for Spatial Indexes
InnoDB实现标准的行级锁,其中有两种类型的锁,共享(S)锁和独占(X)锁。
共享(S)锁允许持有锁的事务读取行。
独占(X)锁允许持有锁的事务更新或删除行。
如果事务T1在行r上获得共享(S)锁,则来自某个不同事务T2的对行r的锁的请求按如下方式处理:
可以立即授予T2对S锁的请求。 结果,事务T1和T2都在r上都获得S锁。
如果事务T2请求的是X锁,则需等待t1事务释放行r上的s锁
如果事务T1在行r上保持独占(X)锁定,则不能立即授予来自某个不同事务T2的对r上任一类型的锁定的请求。 相反,事务T2必须等待事务T1释放其对行r的锁定。
InnoDB支持多个粒度锁定,允许行锁和表锁共存。例如,LOCK TABLES ... WRITE
等语句在指定的表上采用独占锁(X锁)。为了实现多个粒度级别的锁定,InnoDB使用意向锁。意向锁是表级锁,指示事务稍后对表中的行所需的锁定类型(共享或独占)。意向锁有两种类型:
意向共享锁(IS)表示事务打算在表中的某几行上设置共享锁。
意向排他锁(IX)表示事务打算在表中的某几行上设置独占锁。
例如,SELECT ... LOCK IN SHARE MODE
设置IS锁定,SELECT ... FOR UPDATE
设置IX锁定。
意向锁协议如下:
在事务可以获取表中某一行的共享锁之前,它必须首先在表上获取IS锁或更高级别的锁。
在事务可以获取表中某一行的独占锁之前,它必须首先获取表上的IX锁。
表1 锁的兼容性
X | IX | S | IS | |
---|---|---|---|---|
X | 冲突 | 冲突 | 冲突 | 冲突 |
IX | 冲突 | 兼容 | 冲突 | 兼容 |
S | 冲突 | 冲突 | 兼容 | 兼容 |
IS | 冲突 | 兼容 | 兼容 | 兼容 |
如果事务请求的锁与现有锁兼容,则获得锁,但如果它与现有锁冲突则不会。 事务会等待直到释放冲突的现有锁。 如果锁请求与现有锁冲突而无法授予,因为它会导致死锁,则会发生错误。
意向所不会阻塞除全表扫以外的任何请求(例如,LOCK TABLES … WRITE)。 意向锁的主要目的是为了一个事务中揭示下一行将被请求的锁类型。
意向锁的事务数据与SHOW ENGINE INNODB STATUS和InnoDB监视器输出中的以下内容类似:
TABLE LOCK table `test`.`t` trx id 10080 lock mode IX
在INFORMATION_SCHEMAschema中,有INNODB_TRX、INNODB_LOCKS、INNODB_LOCK_WAITS三张表,可以简单监控当前事务可能存在的锁。
InnoDB INFORMATION_SCHEMA Lock Tables
查看有哪些查询被哪些进程阻塞
SELECT concat('thread ',
b.trx_mysql_thread_id,
' from ',
p.host)
AS who_blocks,
IF(p.COMMAND = "Sleep", p.TIME, 0) AS ilde_in_trx,
max(timestampdiff(second, r.trx_wait_started, now())) AS max_wait_time,
count(*) AS num_waiters
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.INNODB_TRX r
ON r.trx_id = w.requesting_trx_id
LEFT JOIN information_schema.PROCESSLIST p
ON p.ID = b.trx_mysql_thread_id
GROUP BY who_blocks,ilde_in_trx
ORDER BY num_waiters DESC\G
*************************** 1. row ***************************
who_blocks: thread 8 from localhost
ilde_in_trx: 86
max_wait_time: 37
num_waiters: 2
1 row in set, 1 warning (0.00 sec)
说明:
线程8已经空闲了86s,并且至少有一个进程已经等待它释放它的锁长达37s,有2个线程在等待线程8完成它的事务。
显示谁阻塞和谁在等待,以及等待多久的查询。
SELECT r.trx_id AS waiting_trx_id,
r.trx_mysql_thread_id AS waiting_thread,
timestampdiff(second, r.trx_wait_started, current_timestamp)
AS wait_time,
r.trx_query AS waiting_query,
l.lock_table AS waiting_table_lock,
b.trx_id AS blocking_trx_id,
b.trx_mysql_thread_id AS blocking_thread,
p.HOST AS blocking_host,
IF(p.COMMAND = "Sleep", p.TIME, 0) AS ilde_in_trx,
b.trx_query AS blocking_query
FROM information_schema.INNODB_LOCK_WAITS w
INNER JOIN information_schema.INNODB_TRX b
ON b.trx_id = w.blocking_trx_id
INNER JOIN information_schema.INNODB_TRX r
ON r.trx_id = w.requesting_trx_id
INNER JOIN information_schema.INNODB_LOCKS l
ON w.requested_lock_id = l.lock_id
LEFT JOIN information_schema.PROCESSLIST p
ON p.ID = b.trx_mysql_thread_id
ORDER BY wait_time DESC\G
*************************** 1. row ***************************
waiting_trx_id: 440666
waiting_thread: 13
wait_time: 2
waiting_query: update t1 set emp_no=emp_no+1
waiting_table_lock: `employees`.`t1`
blocking_trx_id: 440660
blocking_thread: 12
blocking_host: localhost
ilde_in_trx: 393
blocking_query: NULL
1 row in set, 2 warnings (0.00 sec)
记录锁是对索引记录的锁定。 例如,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE;
防止任何其他事务插入,更新或删除
t.c1的值为10的行。
如果InnoDB存储引擎表在建立的时候么有设置任何一个索引,那么这时InnoDB存储引擎会使用隐式的主键来进行锁定。
记录锁的事务数据在SHOW ENGINE INNODB STATUS和InnoDB监控输出中显示类似于以下内容:
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锁定是锁定索引记录之间的gap,或锁定在第一个或最后一个索引记录之前的gap上。
例如:
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE;
这将阻止其他事务将值15插入到列t.c1
中,无论列中是否存在任何此类值,因为该范围内所有现有值之间的gap都被锁定。
如果c1列上没有索引,此时执行for update
语句将锁定整张表,表进行DML语句将会产生锁等待。
next-key 锁是索引记录上的记录锁和索引记录之前的间隙上的间隙锁定的组合。
InnoDB以这样的方式执行行级锁定:当它搜索或扫描表索引时,它会在遇到的索引记录上设置共享锁或排它锁。 因此,行级锁实际上是索引记录锁。 索引记录上的next-key 锁定也会影响该索引记录之前的“gap”。
也就是说,next-key 锁是索引记录锁加上索引记录之前的间隙上的gap锁。 如果一个会话在索引中的记录R上具有共享锁或独占锁,则另一个会话不能在索引顺序中的R之前的间隙中插入新的索引记录。
假设索引包含值10,11,13,20
.此索引的可能的下一个键锁定包括以下间隔,其中圆括号表示排除,方括号表示包含:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)
对于最后一个间隔,next-key 将gap锁定在索引中最大值之上,而“supremum”伪记录的值高于索引中实际的任何值。 supremum不是真正的索引记录,因此,实际上,此next-key 仅锁定最大索引值之后的间隙。
默认情况下,InnoDB在REPEATABLE READ事务隔离级别运行。 在这种情况下,InnoDB使用next-key锁进行搜索和索引扫描,从而防止幻像行。
下一键锁的事务数据与SHOW ENGINE INNODB STATUS和InnoDB监视器输出中的以下内容类似:
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 ;;
示例1 - 在非唯一索引列上进行事务
会话1
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
mysql> create table t(a int,key idx_a(a))engine =innodb;
Query OK, 0 rows affected (0.05 sec)
msyql> insert into t values(1),(3),(5),(8),(11);
Query OK, 5 rows affected (0.00 sec)
Records: 5 Duplicates: 0 Warnings: 0
root@localhost [test] 15:01:24> select * from t;
+------+
| a |
+------+
| 1 |
| 3 |
| 5 |
| 8 |
| 11 |
+------+
5 rows in set (0.00 sec)
mysql> start transaction;
Query OK, 0 rows affected (0.01 sec)
mysql> select * from t where a = 8 for update;
+------+
| a |
+------+
| 8 |
+------+
1 row in set (0.00 sec)
会话2
mysql> select @@global.tx_isolation, @@tx_isolation;
+-----------------------+-----------------+
| @@global.tx_isolation | @@tx_isolation |
+-----------------------+-----------------+
| REPEATABLE-READ | REPEATABLE-READ |
+-----------------------+-----------------+
1 row in set, 2 warnings (0.00 sec)
root@localhost [test] 14:53:08> begin;
Query OK, 0 rows affected (0.00 sec)
root@localhost [test] 15:01:50> select * from t;
+------+
| a |
+------+
| 1 |
| 3 |
| 5 |
| 8 |
| 11 |
+------+
5 rows in set (0.01 sec)
mysql> insert into t values(2);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(4);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(6);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(7);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(8);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(9);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(10);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(11);
Query OK, 1 row affected (0.00 sec
mysql> insert into t values(12);
Query OK, 1 row affected (0.00 sec)
可以看到插入6,7,8,9,10记录时,都被阻塞了。为什么?
分析:
因为InnoDB对于行的查询都是采用了Next-Key Lock
的算法,锁定的不是单个值,而是一个范围(GAP)。上面索引值有1,3,5,8,11,其记录的GAP的区间如下:是一个左开右闭的空间(原因是默认主键的有序自增的特性,结合后面的例子说明)
(-∞,1],(1,3],(3,5],(5,8],(8,11],(11,+∞)
特别需要注意的是,InnoDB存储引擎还会对辅助索引下一个键值加上gap lock
示例2 - 在唯一索引列上进行事务
会话1
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+----+
| a |
+----+
| 1 |
| 3 |
| 5 |
| 8 |
| 11 |
+----+
5 rows in set (0.00 sec)
mysql> delete from t where a=8;
Query OK, 1 row affected (0.00 sec)
会话2
mysql> begin;
Query OK, 0 rows affected (0.00 sec)
mysql> select * from t;
+----+
| a |
+----+
| 1 |
| 3 |
| 5 |
| 8 |
| 11 |
+----+
5 rows in set (0.00 sec)
mysql> insert into t values(2);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(3);
ERROR 1062 (23000): Duplicate entry '3' for key 'PRIMARY'
mysql> insert into t values(4);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(5);
ERROR 1062 (23000): Duplicate entry '5' for key 'PRIMARY'
mysql> insert into t values(6);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(7);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(8);
ERROR 1205 (HY000): Lock wait timeout exceeded; try restarting transaction
mysql> insert into t values(9);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(10);
Query OK, 1 row affected (0.00 sec)
mysql> insert into t values(11);
ERROR 1062 (23000): Duplicate entry '11' for key 'PRIMARY'
可以看到,此时只阻塞插入8值,其他都能顺利插入。
使用唯一索引的锁定行语句,不会产生gap锁。 (但不包括包含多列唯一索引的某些列的情况;在这种情况下,确实会发生间隙锁定。)
插入意向锁是在行插入之前由INSERT操作设置的一种间隙锁定。
该锁表示以这样的方式插入的意向:如果有多个session插入同一个GAP时,他们无需互相等待,例如当前索引上有记录4和8,两个并发session同时插入记录6,7。他们会分别为(4,8)加上GAP锁,但相互之间并不冲突(因为插入的记录不冲突)。
当向某个数据页中插入一条记录时,总是会调用函数lock_rec_insert_check_and_lock进行锁检查(构建索引时的数据插入除外),会去检查当前插入位置的下一条记录上是否存在锁对象,这里的下一条记录不是指的物理连续,而是按照逻辑顺序的下一条记录。 如果下一条记录上不存在锁对象:若记录是二级索引上的,先更新二级索引页上的最大事务ID为当前事务的ID;直接返回成功。
如果下一条记录上存在锁对象,就需要判断该锁对象是否锁住了GAP。如果GAP被锁住了,并判定和插入意向GAP锁冲突,当前操作就需要等待,加的锁类型为LOCK_X | LOCK_GAP | LOCK_INSERT_INTENTION,并进入等待状态。但是插入意向锁之间并不互斥。这意味着在同一个GAP里可能有多个申请插入意向锁的会话。
以下示例演示了在获取插入记录的独占锁之前采用插入意向锁定的事务。该示例涉及两个客户端,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监视器输出中的以下内容类似:
------------
TRANSACTIONS
------------
Trx id counter 446712
Purge done for trx's n:o < 446709 undo n:o < 0 state: running but idle
History list length 195
LIST OF TRANSACTIONS FOR EACH SESSION:
---TRANSACTION 421545581381008, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421545581378272, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 421545581377360, not started
0 lock struct(s), heap size 1136, 0 row lock(s)
---TRANSACTION 446711, ACTIVE 5 sec inserting
mysql tables in use 1, locked 1
LOCK WAIT 2 lock struct(s), heap size 1136, 1 row lock(s)
MySQL thread id 28, OS thread handle 140067243398912, query id 415 localhost root update
INSERT INTO child (id) VALUES (101)
------- TRX HAS BEEN WAITING 5 SEC FOR THIS LOCK TO BE GRANTED:
RECORD LOCKS space id 548 page no 3 n bits 72 index PRIMARY of table `test`.`child` trx id 446711 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 00000006d0f0; asc ;;
2: len 7; hex a800000004011d; asc ;;
------------------
---TRANSACTION 446709, ACTIVE 39 sec
2 lock struct(s), heap size 1136, 2 row lock(s)
MySQL thread id 27, OS thread handle 140067243931392, query id 411 localhost root
AUTO-INC锁是由插入到具有AUTO_INCREMENT列的表中的事务所采用的特殊表级锁。
在最简单的情况下,如果一个事务正在向表中插入值,则任何其他事务必须等待对该表执行自己的插入,以便第一个事务插入的行接收连续的主键值。
innodb_autoinc_lock_mode配置选项控制用于自动增量锁的算法。 它允许如何在可预测的自动增量值序列和插入操作的最大并发之间进行权衡。
innobase_lock_autoinc
有以下几个值:
为0时
也就是所谓的传统加锁模式(在5.1版本引入这个参数之前的策略),在该策略下,会在分配前加上AUTO_INC锁,并在SQL结束时释放掉。该模式保证了在STATEMENT复制模式下,备库执行类似INSERT … SELECT这样的语句时的一致性,因为这样的语句在执行时无法确定到底有多少条记录,只有在执行过程中不允许别的会话分配自增值,才能确保主备一致。
很显然这种锁模式非常影响并发插入的性能,但却保证了一条SQL内自增值分配的连续性。
为1时
这是InnoDB的默认值。在该锁模式下
普通的 INSERT 或 REPLACE 操作会先加一个dict_table_t::autoinc_mutex
,然后去判断表上是否有别的线程加了LOCK_AUTO_INC锁,如果有的话,释放autoinc_mutex,并使用OLD STYLE的锁模式。否则,在预留本次插入需要的自增值之后,就快速的将autoinc_mutex释放掉。很显然,对于普通的并发INSERT操作,都是无需加LOCK_AUTO_INC锁的。因此大大提升了吞吐量;
但是对于一些批量插入操作,例如LOAD DATA
,INSERT …SELECT
等还是使用OLD STYLE的锁模式,SQL执行期间加LOCK_AUTO_INC锁。
和传统模式相比,这种锁模式也能保证STATEMENT模式下的复制安全性,但却无法保证一条插入语句内的自增值的连续性,并且在执行一条混合了显式指定自增值和使用系统分配两种方式的插入语句时,可能存在一定的自增值浪费。
例如执行SQL:
INSERT INTO t1 (c1,c2) VALUES (1,'a'), (NULL,'b'), (5,'c'), (NULL,’d’)
假设当前AUTO_INCREMENT值为101,在传统模式下执行完后,下一个自增值为103,而在新模式下,下一个可用的自增值为105,因为在开始执行SQL时,会先预取了[101, 104] 4个自增值,这和插入行的个数相匹配,然后将AUTO_INCREMENT设为105,导致自增值103和104被浪费掉。
为2时
这种模式下只在分配时加个mutex即可,很快就释放,不会像NEW STYLE那样在某些场景下会退化到传统模式。因此设为2不能保证批量插入的复制安全性。
有关更多信息,请参见“InnoDB中的AUTO_INCREMENT处理”。
从 MySQL5.7 开始MySQL整合了boost.geometry库以更好的支持空间数据类型,并支持在在Spatial数据类型的列上构建索引,在InnoDB内,这个索引和普通的索引有所不同,基于R-TREE的结构,目前支持对2D数据的描述,暂不支持3D.
R-TREE和BTREE不同,它能够描述多维空间,而多维数据并没有明确的数据顺序,因此无法在RR隔离级别下构建NEXT-KEY锁以避免幻读,因此InnoDB使用称为Predicate Lock的锁模式来加锁,会锁住一块查询用到的被称为MBR(minimum boundingrectangle/box)的数据区域。 因此这个锁不是锁到某个具体的记录之上的,可以理解为一种Page级别的锁。
Predicate Lock和普通的记录锁或者表锁(如上所述)存储在不同的lock hash中,其相互之间不会产生冲突。
Predicate Lock相关代码见lock/lock0prdt.cc文件
https://dev.mysql.com/doc/refman/5.7/en/innodb-locking.html
http://hedengcheng.com/?p=771#_Toc374698321
http://mysql.taobao.org/monthly/2016/01/01/