详细介绍MySQL InnoDB存储引擎各种不同类型的锁

本文中,我们详细介绍mysql innodb存储引擎各种不同类型的锁,以及不同sql语句分别会加什么样的锁。
阅读提示
1、本文所参考的mysql文档版本是8.0,做实验的mysql版本是8.0.13
2、本文主要参考了mysql官方文档 innodb锁定和事务机制
3、本文还参考了何登成的 mysql加锁处理分析、一个最不可思议的mysql死锁分析 以及阿里云rds-数据库内核组的 常用sql语句的mdl加锁源码分析
4、mysql是插件式的表存储引擎,数据库的锁是和存储引擎相关的,本文讨论的锁都是innodb存储引擎的锁
文章正文开始
“加什么样的锁”与以下因素相关
1、当前事务的隔离级别
2、sql是一致性非锁定读(consistent nonlocking read)还是dml(insert/update/delete)或锁定读(locking read)
3、sql执行时是否使用了索引,所使用索引的类型(主键索引,辅助索引、唯一索引)
我们先分别介绍这几个因素
一、隔离级别(isolation level)
数据库事务需要满足acid原则,“i”即隔离性,它要求两个事务互不影响,不能看到对方尚未提交的数据。数据库有4种隔离级别(isolation level),按着隔离性从弱到强(相应的,性能和并发性从强到弱)分别是
1、read uncommitted。下面简称ru
2、read committed。下面简称rc
3、repeatable read(mysql的默认隔离级别)。下面简称rr
4、serializable
“i”即隔离性正是通过锁机制来实现的。提到锁就会涉及到死锁,需要明确的是死锁的可能性并不受隔离级别的影响,因为隔离级别改变的是读操作的行为,而死锁是由于写操作产生的。
-- 查看事务的 全局和session 隔离级别( mysql 5.7.19及之前使用tx_isolation)
select @@global.transaction_isolation, @@session.transaction_isolation;
-- 设置 全局 事务隔离级别为repeatable read
set global transaction isolation level repeatable read
-- 设置 当前session 事务隔离级别为read uncommitted
set session transaction isolation level read uncommitted
事务隔离级别设置和查看的详细语法请见:
https://dev.mysql.com/doc/refman/8.0/en/set-transaction.html
二、一致性非锁定读和锁定读
innodb有两种不同的select,即普通select 和 锁定读select。锁定读select 又有两种,即select ... for share 和 select ... for update;锁定读select 之外的则是 普通select 。
不同的select是否都需要加锁呢?
1、普通select 时使用一致性非锁定读,不加锁;
2、锁定读select 使用锁定读,加锁;
3、此外,dml(insert/update/delete)时,需要先查询表中的记录,此时也使用锁定读,加锁;
for share 语法是 mysql 8.0 时加入的,for share 和 lock in share mode 是等价的,但,for share 用于替代 lock in share mode,不过,为了向后兼容,lock in share mode依然可用。
1、一致性非锁定读(consistent nonlocking read)
innodb采用多版本并发控制(mvcc, multiversion concurrency control)来增加读操作的并发性。mvcc是指,innodb使用基于时间点的快照来获取查询结果,读取时在访问的表上不设置任何锁,因此,在事务t1读取的同一时刻,事务t2可以自由的修改事务t1所读取的数据。这种读操作被称为一致性非锁定读。这里的读操作就是普通select。
隔离级别为ru和serializable时不需要mvcc,因此,只有rc和rr时,才存在mvcc,才存在一致性非锁定读。
一致性非锁定读在两种隔离级别rc和rr时,是否有什么不同呢?是的,两种隔离级别下,拍得快照的时间点不同
1、rc时,同一个事务内的每一个一致性读总是设置和读取它自己的最新快照。也就是说,每次读取时,都再重新拍得一个最新的快照(所以,rc时总是可以读取到最新提交的数据)。
2、rr时,同一个事务内的所有的一致性读 总是读取同一个快照,此快照是执行该事务的第一个一致性读时所拍得的。
2、锁定读(locking read)
如果你先查询数据,然后,在同一个事务内 插入/更新 相关数据,普通的select语句是不能给你足够的保护的。其他事务可以 更新/删除 你刚刚查出的数据行。innodb提供两种锁定读,即:select ... for share 和 select ... for update。它俩都能提供额外的安全性。
这两种锁定读在搜索时所遇到的(注意:不是最终结果集中的)每一条索引记录(index record)上设置排它锁或共享锁。此外,如果当前隔离级别是rr,它还会在每个索引记录前面的间隙上设置排它的或共享的gap lock(排它的和共享的gap lock没有任何区别,二者等价)。
看完背景介绍,我们再来看一下innodb提供的各种锁。
三、innodb提供的8种不同类型的锁
innodb一共有8种锁类型,其中,意向锁(intention locks)和自增锁(auto-inc locks)是表级锁,剩余全部都是行级锁。此外,共享锁或排它锁(shared and exclusive locks)尽管也作为8种锁类型之一,它却并不是具体的锁,它是锁的模式,用来“修饰”其他各种类型的锁。
mysql5.7及之前,可以通过information_schema.innodb_locks查看事务的锁情况,但,只能看到阻塞事务的锁;如果事务并未被阻塞,则在该表中看不到该事务的锁情况。
mysql8.0删除了information_schema.innodb_locks,添加了performance_schema.data_locks,可以通过performance_schema.data_locks查看事务的锁情况,和mysql5.7及之前不同,performance_schema.data_locks不但可以看到阻塞该事务的锁,还可以看到该事务所持有的锁,也就是说即使事务并未被阻塞,依然可以看到事务所持有的锁(不过,正如文中最后一段所说,performance_schema.data_locks并不总是能看到全部的锁)。表名的变化其实还反映了8.0的performance_schema.data_locks更为通用了,即使你使用innodb之外的存储引擎,你依然可以从performance_schema.data_locks看到事务的锁情况。
performance_schema.data_locks的列lock_mode表明了锁的类型,下面在介绍各种锁时,我们同时指出锁的lock_mode。
1、共享锁或排它锁(shared and exclusive locks)
它并不是一种锁的类型,而是其他各种锁的模式,每种锁都有shard或exclusive两种模式。
当我们说到共享锁(s锁)或排它锁(x锁)时,一般是指行上的共享锁或者行上的排它锁。需要注意的是,表锁也存在共享锁和排它锁,即表上的s锁和表上的x锁,表上的锁除了这两种之外,还包括下面将会提到的意向共享锁(shard intention locks)即is锁、意向排它锁(exclusive intention locks)即ix锁。表上的锁,除了这四种之外,还有其他类型的锁,这些锁都是在访问表的元信息时会用到的(create table/alter table/drop table等),本文不讨论这些锁,详细可见:常用sql语句的mdl加锁源码分析。
数据行r上共享锁(s锁)和排它锁(x锁)的兼容性如下:
假设t1持有数据行r上的s锁,则当t2请求r上的锁时:
1、t2请求r上的s锁,则,t2立即获得s锁。t1和t2同时都持有r上的s锁。
2、t2请求r上的x锁,则,t2无法获得x锁。t2必须要等待直到t1释放r上的s锁。
假设t1持有r上的x锁,则当t2请求r上的锁时:
t2请求r上的任何类型的锁时,t2都无法获得锁,此时,t2必须要等待直到t1释放r上的x锁
2、意向锁(intention locks)
表锁。含义是已经持有了表锁,稍候将获取该表上某个/些行的行锁。有shard或exclusive两种模式。
lock_mode分别是:is或ix。
意向锁用来锁定层级数据结构,获取子层级的锁之前,必须先获取到父层级的锁。可以这么看innob的层级结构:innodb所有数据是schema的集合,schema是表的集合,表是行的集合。意向锁就是获取子层级(数据行)的锁之前,需要首先获取到父层级(表)的锁。
意向锁的目的是告知其他事务,某事务已经锁定了或即将锁定某个/些数据行。事务在获取行锁之前,首先要获取到意向锁,即:
1、事务在获取行上的s锁之前,事务必须首先获取 表上的 is锁或表上的更强的锁。
2、事务在获取行上的x锁之前,事务必须首先获取 表上的 ix锁。
事务请求锁时,如果所请求的锁 与 已存在的锁兼容,则该事务 可以成功获得 所请求的锁;如果所请求的锁 与 已存在的锁冲突,则该事务 无法获得 所请求的锁。
表级锁(table-level lock)的兼容性矩阵如下:
对于上面的兼容性矩阵,一定注意两点:
1、在上面的兼容性矩阵中,s是表的(不是行的)共享锁,x是表的(不是行的)排它锁。
2、意向锁is和ix 和任何行锁 都兼容(即:和行的x锁或行的s锁都兼容)。
所以,意向锁只会阻塞 全表请求(例如:lock tables ... write),不会阻塞其他任何东西。因为lock tables ... write需要设置x表锁,这会被意向锁is或ix所阻塞。
innodb允许表锁和行锁共存,使用意向锁来支持多粒度锁(multiple granularity locking)。意向锁如何支持多粒度锁呢,我们举例如下
t1: select * from t1 where i=1 for update;
t2: lock table t1 write;
t1执行时,需要获取i=1的行的x锁,但,t1获取行锁前,t1必须先要获取t1表的ix锁,不存在冲突,于是t1成功获得了t1表的ix锁,然后,又成功获得了i=1的行的x锁;t2执行时,需要获取t1表的x锁,但,t2发现,t1表上已经被设置了ix锁,因此,t2被阻塞(因为表的x锁和表的ix锁不兼容)。
假设不存在意向锁,则:
t1执行时,需要获取i=1的行的x锁(不需要获取t1表的意向锁了);t2执行时,需要获取t1表的x锁,t2能否获取到t1表的x锁呢?t2无法立即知道,t2不得不遍历表t1的每一个数据行以检查,是否某个行上已存在的锁和自己即将设置的t1表的x锁冲突,这种的判断方法效率实在不高,因为需要遍历整个表。
所以,使用意向锁,实现了“表锁是否冲突”的快速判断。意向锁就是协调行锁和表锁之间的关系的,或者也可以说,意向锁是协调表上面的读写锁和行上面的读写锁(也就是不同粒度的锁)之间的关系的。
3、索引记录锁(record locks)
也就是所谓的行锁,锁定的是索引记录。行锁就是索引记录锁,所谓的“锁定某个行”或“在某个行上设置锁”,其实就是在某个索引的特定索引记录(或称索引条目、索引项、索引入口)上设置锁。有shard或exclusive两种模式。
lock_mode分别是:s,rec_not_gap或x,rec_not_gap。
行锁就是索引记录锁,索引记录锁总是锁定索引记录,即使表上并未定义索引。表未定义索引时,innodb自动创建隐藏的聚集索引(索引名字是gen_clust_index),使用该索引执行record lock。
4、间隙锁(gap locks)
索引记录之间的间隙上的锁,锁定尚未存在的记录,即索引记录之间的间隙。有shard或exclusive两种模式,但,两种模式没有任何区别,二者等价。
lock_mode分别是:s,gap或x,gap。
gap lock可以共存(co-exist)。事务t1持有某个间隙上的gap lock 并不能阻止 事务t2同时持有 同一个间隙上的gap lock。shared gap lock和exclusive gap lock并没有任何的不同,它俩并不冲突,它俩执行同样的功能。
gap lock锁住的间隙可以是第一个索引记录前面的间隙,或相邻两条索引记录之间的间隙,或最后一个索引记录后面的间隙。
索引是b+树组织的,因此索引是从小到大按序排列的,在索引记录上查找给定记录时,innodb会在第一个不满足查询条件的记录上加gap lock,防止新的满足条件的记录插入。
上图演示了:innodb在索引上扫描时,找到了c2=11的记录,然后,innodb接着扫描,它发现下一条记录是c2=18,不满足条件,innodb遇到了第一个不满足查询条件的记录18,于是innodb在18上设置gap lock,此gap lock锁定了区间(11, 18)。
为什么需要gap lock呢?gap lock存在的唯一目的就是阻止其他事务向gap中插入数据行,它用于在隔离级别为rr时,阻止幻影行(phantom row)的产生;隔离级别为rc时,搜索和索引扫描时,gap lock是被禁用的,只在 外键约束检查 和 重复key检查时gap lock才有效,正是因为此,rc时会有幻影行问题。
gap lock是如何阻止其他事务向gap中插入数据行的呢?看下图
索引是b+树组织的,因此索引是从小到大按序排列的,如果要插入10,那么能插入的位置只能是上图中标红的区间。在10和10之间插入时,我们就认为是插入在最后面的10的后面。如果封锁了标红的区间,那么其他事务就无法再插入10啦。
问题一:当t2要插入 10时,上图哪些地方允许插入(注意:索引是有序的哦)?
答:(8, 10)和(10,11)。在10和10之间插入,我们就认为是插入在最后的10后面。
只要封锁住图中标红的区间,t2就无法再插入10啦。上面这两个区间有什么特点吗?对,这两个区间就是:满足条件的每一条记录前面的间隙,及,最后一条不满足条件的记录前面的间隙。innodb使用下一个键锁(next-key locks)或间隙锁(gap locks)来封锁这种区间。
问题二:gap lock是用来阻塞插入新数据行的,那么,t2, insert into g values('z', 9) 会被阻塞吗?插入('z', 8),('z', 10),('z', 11)呢?
答:上图中,t1的update设置的gap lock是 (8, 10)和(10,11),而,insert intention lock的范围是(插入值, 向下的一个索引值)。insert intention lock的详细介绍请见下面的6. 插入意向锁(insert intention locks)。
于是,对于上面这些插入值,得到的insert intention lock如下:
插入 ('z', 8)时,insert intention lock 是 (8, 10) -- 冲突,与gap lock (8, 10)重叠了
插入 ('z', 9)时,insert intention lock 是 (9, 10) -- 冲突,与gap lock (8, 10)重叠了
插入 ('z', 10)时,insert intention lock 是 (10, 11) -- 冲突,与gap lock (10, 11)重叠了
插入 ('z', 11)时,insert intention lock 是 (11, 15) -- 不冲突
事实是不是这样呢,看下图
是的,和我们分析的一致,为了看的更清楚,我们把结果列成图表如下
问题三:“gap是解决phantom row问题的”,插入会导致phantom row,但更新也一样也会产生phantom row啊。
例如,上图的t1和t2,t1把所有i=8的行更新为108,t2把i=15的行更新为8,如果t2不被阻塞,t1的where条件岂不是多出了一行,即:t1出现了phantom row?
答:nice question。我们自己来分析下t1和t2分别加了哪些锁
t1加的锁:idx_i上的next-key lock (5, 8],primary上的'b',以及idx_i上的gap lock (8,10)
t2加的锁:idx_i上的next-key lock (11, 15],primary上的'f',以及idx_i上的gap lock (15,108),最后这个gap lock是因为t1在idx_i上加了新值108
根据上面的分析,t1和t2的锁并没有重叠,即我们分析的结果是:t2不会被阻塞。
但,上图清楚的表明t2确实被阻塞了,原因竟然是:t2 insert intention lock和t1 gap lock(8, 10)冲突了。很奇怪,t2是更新语句,为什么会有insert intention lock呢?
我不知道确切的原因,因为我没找到文档说这事。根据我的推断,update ... set 成功找到结果集然后执行更新时,在即将被更新进入行的新值上设置了insert intention lock(如果找不到结果集,则就不存在insert intention lock啦),因此,t2在idx_i上的新值8上设置了insert intention lock(8, 10)。最终,t2 insert intention lock(8, 10) 与 t1 gap lock(8, 10)冲突啦,t2被阻塞。
因此,update ... set 成功找到结果集时,会在即将被更新进入行的新值上设置 index record lock 以及 insert intention lock。如前所述,insert intention lock的范围是(插入值,下一个值),如果t2是 update g set i=9 where i=15; 那么update ... set 所设置的新值是9,则t2 insert intention lock就是(9, 10)啦,它依然会和 t1 gap lock(8, 10)冲突,是这样吗?确实是的,感兴趣的同学可以试试。
5、下一个键锁(next-key locks)
next-key lock 是 (索引记录上的索引记录锁) + (该索引记录前面的间隙上的锁) 二者的合体,它锁定索引记录以及该索引记录前面的间隙。有shard或exclusive两种模式。
lock_mode分别是:s或x。
当innodb 搜索或扫描索引时,innodb在它遇到的索引记录上所设置的锁就是next-key lock,它会锁定索引记录本身以及该索引记录前面的gap(gap immediately before that index record)。即:如果事务t1 在索引记录r 上有一个next-key lock,则t2无法在 紧靠着r 前面的那个间隙中 插入新的索引记录(gap immediately before r in the index order)。
next-key lock还会加在“supremum pseudo-record”上,什么是supremum pseudo-record呢?它是索引中的伪记录(pseudo-record),代表此索引中可能存在的最大值,设置在supremum pseudo-record上的next-key lock锁定了“此索引中可能存在的最大值”,以及 这个值前面的间隙,“此索引中可能存在的最大值”在索引中是不存在的,因此,该next-key lock实际上锁定了“此索引中可能存在的最大值”前面的间隙,也就是此索引中当前实际存在的最大值后面的间隙。例如,下图中,supremum pseudo-record上的next-key lock锁定了区间(18, 正无穷),正是此next-key lock阻止其他事务插入例如19, 100等更大的值。
supremum pseudo-record上的next-key lock锁定了“比索引中当前实际存在的最大值还要大”的那个间隙,“比大还大”,“bigger than bigger”
6、插入意向锁(insert intention locks)
一种特殊的gap lock。insert操作插入成功后,会在新插入的行上设置index record lock,但,在插入行之前,insert操作会首先在索引记录之间的间隙上设置insert intention lock,该锁的范围是(插入值, 向下的一个索引值)。有shard或exclusive两种模式,但,两种模式没有任何区别,二者等价。
lock_mode分别是:s,gap,insert_intention或x,gap,insert_intention。
insert intention lock发出按此方式进行插入的意图:多个事务向同一个index gap并发进行插入时,多个事务无需相互等待。
假设已存在值为4和7的索引记录,事务t1和t2各自尝试插入索引值5和6,在得到被插入行上的index record lock前,俩事务都首先设置insert intention lock,于是,t1 insert intention lock (5, 7),t2 insert intention lock (6, 7),尽管这两个insert intention lock重叠了,t1和t2并不互相阻塞。
如果gap lock或next-key lock 与 insert intention lock 的范围重叠了,则gap lock或next-key lock会阻塞insert intention lock。隔离级别为rr时正是利用此特性来解决phantom row问题;尽管insert intention lock也是一种特殊的gap lock,但它和普通的gap lock不同,insert intention lock相互不会阻塞,这极大的提供了插入时的并发性。总结如下:
1、gap lock会阻塞insert intention lock。事实上,gap lock的存在只是为了阻塞insert intention lock
2、gap lock相互不会阻塞
3、insert intention lock相互不会阻塞
4、insert intention lock也不会阻塞gap lock
insert插入行之前,首先在索引记录之间的间隙上设置insert intention lock,操作插入成功后,会在新插入的行上设置index record lock。
我们用下面三图来说明insert intention lock的范围和特性
上图演示了:t1设置了gap lock(13, 18),t2设置了insert intention lock(16, 18),两个锁的范围重叠了,于是t1 gap lock(13, 18)阻塞了t2 insert intention lock(16, 18)。
上图演示了:t1设置了insert intention lock(13, 18)、index record lock 13;t2设置了gap lock(17, 18)。尽管t1 insert intention lock(13, 18) 和 t2 gap lock(17, 18)重叠了,但,t2并未被阻塞。因为 insert intention lock 并不阻塞 gap lock。
上图演示了:t1设置了insert intention lock(11, 18)、index record lock 11;t2设置了next-key lock(5, 11]、primary上的index record lock 'b'、gap lock(11, 18)。此时:t1 index record lock 11 和 t2 next-key lock(5, 11]冲突了,因此,t2被阻塞。
7、自增锁(auto-inc locks)
表锁。向带有auto_increment列 的表时插入数据行时,事务需要首先获取到该表的auto-inc表级锁,以便可以生成连续的自增值。插入语句开始时请求该锁,插入语句结束后释放该锁(注意:是语句结束后,而不是事务结束后)。
你可能会想,日常开发中,我们所有表都使用auto_increment作主键,所以会非常频繁的使用到该锁。不过,事情可能并不像你想的那样。在介绍auto-inc表级锁之前,我们先来看下和它密切相关的sql语句以及系统变量innodb_autoinc_lock_mode
insert-like语句
1、insert
2、insert ... select
3、replace
4、replace ... select
5、load data
外加,simple-inserts, bulk-inserts, mixed-mode-inserts
simple-inserts
待插入记录的条数,提前就可以确定(语句初始被处理时就可以提前确定)因此所需要的自增值的个数也就可以提前被确定。
包括:不带嵌入子查询的 单行或多行的insert, replace。不过,insert ... on duplicate key update不是
bulk-inserts
待插入记录的条数,不能提前确定,因此所需要的自增值的个数 也就无法提前确定
包括:insert ... select, replace ... select, load data
在这种情况下,innodb只能每次一行的分配自增值。每当一个数据行被处理时,innodb为该行auto_increment列分配一个自增值
mixed-mode-inserts
也是simple-inserts语句,但是指定了某些(非全部)自增列的值。也就是说,待插入记录的条数提前能知道,但,指定了部分的自增列的值。
insert into t1 (c1,c2) values (1,'a'), (null,'b'), (5,'c'), (null,'d');
insert ... on duplicate key update也是mixed-mode,最坏情况下,它就是insert紧跟着一个update,此时,为auto_increment列所分配的值在update阶段可能用到,也可能用不到。
再看一下系统变量innodb_autoinc_lock_mode,它有三个候选值0,1,和2
8.0.3之前,默认值是1,即“连续性的锁定模式(consecutive lock mode)”;8.0.3及之后默认值是2,即“交织性锁定模式(interleaved lock mode)”
a. 当innodb_autoinc_lock_mode=0时,insert-like语句都需要获取到auto-inc表级锁;
b. 当innodb_autoinc_lock_mode=1时,如果插入行的条数可以提前确定,则无需获得auto-inc表级锁;如果插入行的条数无法提前确定,则就需要获取auto-inc表级锁。因此,simple-inserts和mixed-mode inserts都无需auto-inc表级锁,此时,使用轻量级的mutex来互斥获得自增值;bulk-inserts需要获取到auto-inc表级锁;
c. 当innodb_autoinc_lock_mode=2时,完全不再使用auto-inc表级锁;
我们生产数据库版本是5.6.23-72.1,innodb_autoinc_lock_mode=1,而且,我们日常开发中用到大都是simple-inserts,此时根本就不使用auto-inc表级锁,所以,auto-inc表级锁用到的并不多哦。
lock_mode:auto-inc表级锁用到的并不多,且,auto-inc锁是在语句结束后被释放,较难在performance_schema.data_locks中查看到,因此,没有进行捕获。感兴趣的同学可以使用insert ... select捕获试试。
8、空间索引(predicate locks for spatial indexes)
我们平时很少用到mysql的空间索引。所以,本文忽略此类型的锁
到此为止,mysql innodb 8种类型的锁我们就介绍完了。我们以一个例子结束8种类型的介绍。
t1先执行,事务id是8428;t2后执行,事务id是8429
上图演示了:
1、任何事务,在锁定行之前,都需要先加表级锁intention lock,即:第三行的ix和第一行的ix。
2、idx_c是辅助索引,innodb扫描idx_c时遇到了c=222,于是,在idx_c上加了next-key lock,即:第四行的x。next-key lock就是 index record lock+gap lock,于是此next-key lock锁定了idx_c上值为222的索引记录,以及222前面的间隙,也就是间隙(22, 222)。
3、idx_c是辅助索引,在主键索引之外的任何索引上加index record lock时,都需要在该行的主键索引上再加index record lock,于是,又在primary上添加了index record lock,即:第五行的x,rec_not_gap。
4、innodb扫描完c=222后,又扫描到了c=2222,这是idx_c上,第一个不满足索引扫描条件的索引记录,于是innodb在c=2222上加gap lock,c=2222上的gap lock锁定的范围是“idx_c上2222前面的间隙”,这本应该是(222, 2222),但,t1即将在idx_c上插入c=224,于是,c=2222上的gap lock锁定的范围是(224, 2222)。即:第六行的x,gap。
5、innodb即将在idx_c上插入c=224,224也是不满足c=222的,于是innodb在c=224上加gap lock,该gap lock锁定了224前面的间隙,也就是(222, 224),即,第七行的x,gap。
6、t2执行insert成功后,会在新插入行的加index record lock,但,t2在插入之前,首先要作的是得到表级锁intention lock以及设置表的每个索引的insert intention lock,该锁的范围是(插入值, 向下的一个索引值),于是,在设置idx_c上的insert intention lock范围就是(226, 2222),这个范围和事务t1第六行gap lock范围(224, 2222)重叠。于是,事务t2被阻塞了,t2必须等待,直到t1释放第六行的gap lock。
performance_schema.data_locks表中并不能看到t2的全部锁,比如,t2也得在iux_b上设置insert intention lock,但,performance_schema.data_locks中并没有这个锁。关于performance_schema.data_locks中显示了哪些锁,请见本文最后一段。
把这些锁及其范围列出来如下图所示
四、不同的sql加了什么样的锁?
ok,我们已经了解了innodb各种不同类型的锁,那么,不同sql语句各加了什么样的锁呢
我们用最朴素的想法来思考一下,用锁作什么呢?锁要作的就是达到事务隔离的目的,即:两个并发执行的事务t1和t2,如果t1正在修改某些行,那么,t2要并发 读取/修改/插入 满足t1查询条件的行时,t2就必须被阻塞,这是锁存在的根本原因。index record lock, gap lock, next-key lock都是实现手段,这些手段使得锁既能达到目的,还能实现最大的并发性。所以,当我们考虑事务t1中的sql上加了什么锁时,就想一下,当t1执行时,如果并发的事务 t2不会触及到t1的行,则t2无需被阻塞,如果t2的要 读取/修改/插入 满足t1条件的行时,t2就得被t1阻塞。而t1阻塞t2的具体实现就是:t1在已存在的行上加index record lock使得t2无法触碰已存在的行,以及,t1在不存在的行上加gap lock使得t2无法插入新的满足条件的行。
前面我们说过“加什么样的锁”与以下因素相关
1、当前事务的隔离级别
2、sql是一致性非锁定读(consistent nonlocking read)还是dml或锁定读(locking read)
3、sql执行时是否使用了索引,所使用索引的类型(主键索引,辅助索引、唯一索引)
我们来看一下,不同的隔离级别下,使用不同的索引时,分别加什么锁。在讨论之前,我们先剔除无需讨论的情况
首先,普通select 使用一致性非锁定读,因此根本不存在锁。无需讨论;
再者,作为开发者,我们几乎从来不会使用到隔离级别ru和serializable。这两个隔离级别无需讨论。
于是,剩下的就是 给定锁定读select或dml(insert/update/delete)语句,在不同隔离级别下,使用不同类型的索引时,分别会加什么样的锁?直接给出答案,其加锁原则如下
一、rr时,如果使用非唯一索引进行搜索或扫描,则在所扫描的每一个索引记录上都设置next-key lock。
这里“所扫描的每一个索引记录”是指当扫描执行计划中所使用的索引时,搜索遇到的每一条记录。where条件是否排除掉某个数据行并没有关系,innodb并不记得确切的where条件,innodb倔强的只认其扫描的索引范围(index range) 。
你可能觉得innodb在设置锁时蛮不讲理,竟然不管where条件排除掉的某些行,这不是大大增加了锁的范围了嘛。不过,等我们了解了mysql执行sql时的流程,这就好理解了。mysql的执行计划只会选择一个索引,使用一个索引来进行扫描,mysql执行sql语句的流程是,先由innodb引擎执行索引扫描,然后,把结果返回给mysql服务器,mysql服务器会再对该索引条件之外的其他查询条件进行求值,从而得到最终结果集,而加锁时只考虑innodb扫描的索引,由mysql服务器求值的其他where条件并不考虑。当然,mysql使用index_merge优化时会同时使用多个索引的,不过,这个时候设置锁时也并不特殊,同样,对于所用到的每一个索引,innodb在所扫描的每一个索引记录上都设置next-key lock。
加的锁一般是next-key lock,这种锁住了索引记录本身,还锁住了每一条索引记录前面的间隙,从而阻止其他事务 向 索引记录前面紧接着的间隙中插入记录。
如果在搜索中使用了辅助索引(secondary index),并且在辅助索引上设置了行锁,则,innodb还会在 相应的 聚集索引 上设置锁;表未定义聚集索引时,innodb自动创建隐藏的聚集索引(索引名字是gen_clust_index),当需要在聚集索引上设置锁时,就设置到此自动创建的索引上。
二、rr时,如果使用了唯一索引的唯一搜索条件,innodb只在满足条件的索引记录上设置index record lock,不锁定索引记录前面的间隙;如果用唯一索引作范围搜索,依然会锁定每一条被扫描的索引记录前面的间隙,并且再在聚集索引上设置锁。
三、rr时,在第一个不满足搜索条件的索引记录上设置gap lock或next-key lock。
一般,等值条件时设置gap lock,范围条件时设置next-key lock。此gap lock或next-key lock锁住第一个不满足搜索条件的记录前面的间隙。
四、rr时,insert在插入新行之前,必须首先为表上的每个索引设置insert intention lock。
每个insert intention lock的范围都是(待插入行的某索引列的值, 此索引上从待插入行给定的值向下的第一个索引值)。只有当insert intention lock与某个gap lock或next-key lock冲突时,才能在performance_schema.data_locks看到insert intention lock。
五、rc时,innodb只在完全满足where条件的行上设置index record lock。
六、rc时,禁用了gap lock。
正因为此,rc时不存在gap lock或next-key lock。这是为什么呢?我们想一想啊,gap lock是用来解决phantom row问题的,gap lock封锁的区间内不能插入新的行,因为插入时的insert intention lock会和gap lock冲突,从而阻止了新行的插入。但,隔离级别rc是允许phantom row的,因此rc时gap lock是被禁用的。
七、rr或rc时,对于主键或唯一索引,当有重复键错误(duplicate-key error)时,会在 重复的索引记录上 设置 shared next-key lock或shared index record lock。这可能会导致死锁。
假设t1, t2, t3三个事务,t1已经持有了x锁,t2和t3发生了重复键错误,因此t2和t3都在等待获取s锁,这个时候,当t1回滚或提交释放掉了x锁,则t2和t3就都获取到了s锁,并且,t2和t3都请求x锁,“t2和t3同时持有s锁,且都在请求x锁”,于是死锁就产生了。
好了,规则都列出来了,是时候实践一把了。下面在展示锁时,我们同时指出了当前所使用的隔离级别,表上的索引以及事务的sql语句。
实践一:搜索时无法使用索引,即全表扫描时,innodb在表的全部行上都加锁
上图演示了:搜索条件无法使用索引时,innodb不得不在表的全部行上都加锁。所以,索引实在太重要了,查询时,它能加快查询速度;更新时,除了快速找到指定行,它还能减少被锁定行的范围,提高插入时的并发性。
实践二:唯一索引和非唯一索引、等值查询和范围查询加锁的不同
搜索时使用 唯一索引 作等值查询时,innodb只需要加index record lock;搜索时使用 唯一索引作范围查询时 或 使用非唯一索引作任何查询时 ,innodb需要加next-key lock或gap lock。
示例1演示了:使用非唯一索引 idx_c 搜索或扫描时,innodb要锁住索引本身,还要锁住索引记录前面的间隙,即next-key lock: x 和 gap lock: x,gap。next-key lock既锁住索引记录本身,还锁住该索引记录前面的间隙,gap lock只锁住索引记录前面的间隙。等值条件时,在最后一个不满足条件的索引记录上设置gap lock。
示例2演示了:使用唯一索引 iux_b 的唯一搜索条件,即,使用唯一索引执行等值查找时,innodb只需锁住索引本身,即index record lock: x, rec_not_gap,并不锁索引前面的间隙。
示例3演示了:使用唯一索引 iux_b 进行范围扫描时,依然需要锁定扫描过的每一个索引记录,并且锁住每一条索引记录前面的间隙,即next-key lock: x。范围条件时,在最后一个不满足条件的索引记录上设置next-key lock。
实践三:不同隔离级别加锁的不同
无论何种隔离级别,sql语句执行时,都是先由innodb执行索引扫描,然后,返回结果集给mysql服务器,mysql服务器再对该索引条件之外的其他查询条件进行求值,从而得到最终结果集。
上图中,在不同的隔离级别下,执行了相同的sql。无论何种隔离级别,primary上的index record lock总是会加的,我们不讨论它。在idx_b上,隔离级别为rc时,innodb加了index record lock,即:x,rec_not_gap,隔离级别为rr时,innodb加了next-key lock,即x。注意:rc时没有gap lock或next-key lock哦。
上图演示了:事务的隔离级别也会影响到设置哪种锁。如我们前面所说,gap lock是用来阻止phantom row的,而rc时是允许phantom row,所以,rc时禁用了gap lock。因此,上图中,rc时没有在索引上设置gap lock或next-key lock。
实践四:操作不存在的索引记录时,也需要加锁
上图中,idx_b上并不存在b=266的索引记录,那么,当更新b=266的记录时,是否需要加锁呢?是的,也需要加锁
无论b=266是否存在,rr时,innodb在第一个不满足搜索条件的索引记录上设置gap lock或next-key lock。一般,等值条件时设置gap lock,范围条件时设置next-key lock。上图中是等值条件,于是innodb设置gap lock,即上图的x,gap,其范围是(226, 2222),正是此gap lock使得并发的事务无法插入b列大于等于266的值,rc时,由于gap lock是被禁止的,因此,并不会加gap lock,并发的事务可以插入b列大于等于266的值。
上图演示了:操作不存在的索引记录时,也需要加锁。
实践五:重复键错误(duplicate-key error)时,会加共享锁。这可能会导致死锁。
对于主键或唯一索引,当有重复键错误(duplicate-key error)时,会在 重复的索引记录上 设置 shared next-key lock或shared index record lock。这可能会导致死锁。
上图演示了:t1在主键1上设置exclusive index record lock。t2和t3插入时,会产生重复键错误,于是t2和t3都在主键1上设置了shared next-key lock。如上图所示
如果此时,t1 rollback释放掉其所持有的index record lock,则t2和t3等待获取的shared next-key lock都成功了,然后,t2和t3争夺主键1上的index record lock,于是t2和t3就死锁了,因为它俩都持有shard next-key lock,双方谁都不会放弃已经得到的shared next-key lock,于是,谁都无法得到主键1的index record lock。
需要明确的是死锁的可能性并不受隔离级别的影响,因为隔离级别改变的是读操作的行为,而死锁是由于写操作产生的。死锁并不可怕,mysql会选择一个牺牲者,然后,在系统变量innodb_lock_wait_timeout指定的秒数达到后,自动回滚牺牲者事务;从mysql5.7开始,新加入了系统变量innodb_deadlock_detect(默认on),如果开启此变量,则mysql不会再等待,一旦探测到死锁,就立即回滚牺牲者事务。
上图演示了:在上图的状态下,当t1 commit时,t1释放了主键1上的index record lock,于是t2和t3等待获取的shared next-key lock都成功了,然后,t2和t3争夺主键1上的index record lock,于是t2和t3死锁了,因为它俩都持有shard next-key lock,双方谁都不会放弃已经得到的shared next-key lock,于是,谁都无法得到主键1的index record lock。
五、performance_schema.data_locks中能看到全部的锁吗?
显而易见,performance_schema.data_locks并未显示全部的锁,那么,它显示了哪些锁呢?很不幸,我并未找到文档说这事,尽管文档(https://dev.mysql.com/doc/refman/8.0/en/innodb-information-schema-transactions.html)说:“事务持有的每一个锁 以及 事务被阻塞的每一个锁请求,都在该表中占据一行”,但,我们很多例子都表明,它并未显示全部的锁。根据我的试验,我猜测performance_schema.data_locks显示的是where条件所触碰到的索引上的锁,“where条件所触碰到的索引”是指sql实际执行时所使用的索引,也就是sql执行计划的key列所显示的索引,正因为此,insert时看不到任何锁,update g set a=a+1 where b=22时只看到idx_b上的锁。需要强调的是,这是我自己试验并猜测的,我并未在文档中看到这种说法。
假设t1和t2两个事务操作同一个表,先执行t1,此时尽管performance_schema.data_locks中只显示t1的where条件所触碰到的索引上的锁,但是,事实上在t1的where条件触碰不到的索引上,也是会设置锁的。尽管表的索引idx并未被t1所触碰到,即performance_schema.data_locks显示t1在索引idx并没有设置任何锁,但,当t2执行 锁定读/插入/更新/删除 时触碰到了索引idx,t2才恍然发现,原来t1已经在索引idx上加锁了。
我们来看下面的三个例子
“performance_schema.data_locks无法看到全部锁”示例一
上图演示了:t1执行时,只触碰到了索引idx_b,t1执行完后,在performance_schema.data_locks中只能看到idx_b上的锁,看起来t1并未在idx_a上设置任何锁;但,当t2执行触碰到了索引idx_a时,t2才恍然发现,原来t1已经在idx_a上设置了index record lock啦。
“performance_schema.data_locks无法看到全部锁”示例二
插入新行时,会先设置insert intention lock,插入成功后再在插入完成的行上设置index record lock。
上图演示了:t1插入了新行,但,在performance_schema.data_locks中,我们既看不到t1设置的insert intention lock,也看不到t1设置的index record lock。这是因为t1的where条件并未触碰到任何索引(t1根本不存在where条件),因此我们看不到t1的这两个锁;但,当t2要删除t1新插入的行时,t2才恍然发现,原来t1已经在索引c2上设置了index record lock啦。
“performance_schema.data_locks无法看到全部锁”示例三
插入新行时,本来是不会在performance_schema.data_locks中显示insert intention lock的,因为插入时where条件并未触碰到任何索引(插入时根本不存在where条件)。
上图演示了:t2插入新行时的insert intention lock 和 t1的gap lock冲突了,于是,我们得以在performance_schema.data_locks中观察到t2插入新行时需要请求insert intentin lock。

PyTorch教程-14.11. 全卷积网络
联想zuk edge正式发布:颜值不输小米MIX,配置堪比华为mate9!
锂离子电池聚合物电解质导电机理
应用无处不在,发觉你身边的MEMS传感器
水下机器人如何构建精确的地图
详细介绍MySQL InnoDB存储引擎各种不同类型的锁
PMP物理内存保护介绍
英飞凌TRENCHSTOP™ IGBT6将紧凑型电机控制器总损耗减少20%以上
能为电子系统提供动力的电源模块介绍
QForm锻造、挤压、热处理仿真软件简析
了解PCB布局技巧
【泵类维修】脱硫浆液循环泵防腐耐磨保护方案分享
美国空军大力部署5G网络
工控机的特点
Origin Q一周速览:希腊设立量子计算理学硕士学位
德承DX-1100 成功演绎自动驾驶汽车的核心运算角色
基于FP共振剪裁面板的屏障穿透超声成像
磐石测控:深圳插拔力试验机PS-1220S系列有哪些介绍?
笔记本电脑能上飞机吗?以后飞美国航班都将禁止使用笔记本
Amphenol Industrial LPT系列连接器在贸泽开售 为一般工业环境和恶劣环境而设