简介:作者:iceman1952 来源:https://blog.csdn.net/iceman1952/article/details/85504278 本文中,我们详细介绍MySQL InnoDB存储引擎各种不同类型的锁,以及不同SQL语句分别会加什么样的锁。阅读提示1. 本文所参考的MyS ...

作者:iceman1952来源:https://blog.csdn.net/iceman1952/article/details/85504278


本文中,我们详细介绍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

二、一致性非锁定读和锁定读

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)的兼容性矩阵如下:


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


对于上面的兼容性矩阵,一定注意两点:

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,防止新的满足条件的记录插入。


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


上图演示了: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中插入数据行的呢?看下图


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


索引是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) -- 不冲突

事实是不是这样呢,看下图


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


是的,和我们分析的一致,为了看的更清楚,我们把结果列成图表如下


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


问题三:“gap是解决phantom row问题的”,插入会导致phantom row,但更新也一样也会产生phantom row啊。


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


例如,上图的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”


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


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的范围和特性


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


上图演示了:T1设置了gap lock(13, 18),T2设置了insert intention lock(16, 18),两个锁的范围重叠了,于是T1 gap lock(13, 18)阻塞了T2 insert intention lock(16, 18)。


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


上图演示了: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。


MySQL InnoDB 锁介绍及不同 SQL 语句分别加什么样的锁(上)


上图演示了: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被阻塞。



本文仅代表作者个人观点,不代表巅云官方发声,对观点有疑义请先联系作者本人进行修改,若内容非法请联系平台管理员,邮箱2522407257@qq.com。更多相关资讯,请到巅云www.rzxsoft.cn学习互联网营销技术请到巅云建站www.rzxsoft.cn。