目录
1、MySQL中锁的前置知识
1.1 MySQL中锁的分类
1.2 MySQL支持三种层级的锁定
1.2.1 表级锁
1.2.2 页级锁
1.2.3 行级锁定
1.3 如何学习MySQL中的锁知识
1.3.1 了解MySQL中大致有哪些锁,建立脑图分类
1.3.2 站在存储引擎的角度,具体学习存储引擎支持的锁
1.3.3 重点研究InnoDB下行锁的具体实现算法
1.3.4 了解InnoDB下锁与事务、锁与索引的联系
1.4 事务持有锁的SQL
2、MyISAM存储引擎支持的锁
2.1 MyISAM下的表锁
2.2 MyISAM存储引擎下的共享锁和排他锁
2.2.1 MyISAM下的共享锁和排他锁
2.2.2 表共享读锁和表独占写锁的兼容性
2.3 知识总结
3、InnoDB存储引擎支持的锁
3.1 官方列举的InnoDB下的锁
3.2 InnoDB下的表锁和行锁
3.2.1 表锁和行锁的概念
3.2.2 表锁和行锁的分类
3.2.3 InnoDB实现的行锁
3.3 InnoDB下的共享锁和排他锁
3.3.1 什么是共享锁和排他锁
3.3.2 表/行层次下的共享锁和排他锁
3.3.3 共享锁和排它锁的兼容性
3.3.4 怎么显式的加共享锁或排他锁
3.4 InnoDB下的意向锁
3.4.1 为什么会有意向锁
3.4.2 什么是意向锁
3.4.3 意向锁的兼容性
3.4.4 怎么使用意向锁
3.4.5 详细的说说意向锁有什么作用
意向锁好处之一:
意向锁好处之二:
3.4.6 IX、IS与表级X/S与行级X/S锁的关系
3.5 InnoDB下的记录锁、间隙锁、临键锁
3.5.1 什么是记录锁- record lock
另:此行锁非彼行锁?
3.5.2 什么是间隙锁-gap lock
3.5.3 什么是临键锁-NK lock
3.6 InnoDB下的记录锁/间隙锁/临键锁的关系
3.7 排他锁/共享锁与记录锁/间隙锁/临键锁的关系
3.8 InnoDB下的插入意向锁
4、验证锁与索引之间的“锁哪里”关系
99、参考
首先声明,MySQL的测试环境是MySQL8.0。
MySQL中锁的名称很多,分类也很繁杂,下图为MySQL中部分存储引擎所支持的锁。
我们都知道,在关系型数据库MySQL中支持三种层级的锁,分别为:表级锁、页级锁、行级锁。
表级锁,是MySQL中锁定粒度最大的一种锁,表示当前操作对整张表加锁,它实现简单,资源消耗较少,被MySQL中的大部分存储引擎支持。最常使用的MyISAM与InnoDB都支持表级锁定。
页级锁,是MySQL中锁定粒度介于行级锁和表级锁中间的一种锁。表级锁速度快,但冲突多;行级锁冲突少,但速度慢。所以,取了表级锁和行级锁的折衷方案—— 页级锁,一次锁定相邻的一组记录。BDB存储引擎支持页级锁。
行级锁,是MySQL中锁定粒度最细的一种锁,表示只针对当前操作的行进行加锁。行级锁能大大减少数据库操作的冲突。其加锁粒度最小,但加锁的开销也最大。
从上到下,锁的粒度逐渐细粒化,但实现锁的开销逐渐增大。 同时,我们也要知道,【 表锁、页锁、行锁 】并不是一个具体的锁,仅代表将数据库某个层级上的数据进行锁定。具体怎么去锁这个数据,还要看具体的锁的实现是什么。
MySQL中锁的知识,在初学的时候,真是挺复杂的。主要是概念多,分类也多,还有各种乱七八糟的名词。所以,学习MySQL中各种锁时,要从简单的入手,繁琐的名词暂时不要管。等有经验之后,自然会懂得。
(1)知道MySQL支持三个层级的锁定,但是,要根据不同的存储引擎来判断数据库支持哪个层级的锁。
(2)知道什么是表锁、行锁、页锁。
(1)知道不同存储引擎支持哪个层级的锁,主要研究InnoDB和MyISAM两种存储引擎。
(2)研究MyISAM下的表锁。
(3)研究InnoDB下的表锁和行锁。
(4)研究InnoDB下的排他锁和共享锁。
(5)研究InnoDB下的意向锁。
(1)研究InnoDB存储引擎下的记录锁、间隙锁、临键锁。
(1)了解InnoDB下,RR级别下是怎么避免幻读的?
(2)了解锁与事务之间的联系、二段锁提交。
了解以上内容,我想应该大致也能够应付很多日常的开发了。剩下的内容比较细致和繁琐,个人觉得还是需要实际的社会实践,才能更加的深有体会并记住。
要认识事务、SQL 与锁的知识,最好的办法就是调试。所以,我这里提供给大家 MySQL 8.0 以及 MySQL 8.0以下 的查询事务持有锁情况的SQL语句。
MySQL 8.0 以下:
- show engine innodb status;
解释:在TRANSACTION位置可以看到事务持有锁的情况,但是,需要经验分析,名词比较难懂。
- select * from information_schema.innodb_locks;
解释:查看InnoDB下,所有表、所有事务的加锁情况,无法分别 gap、record、next-key锁的具体类型。没有冲突情况下,则无法显示持有的锁。
- select * from information_schema.innodb_locks_wait;
解释:查看InnoDB下,所有表、所有事务因锁的阻塞情况,谁被谁block住了。
MySQL 8.0:
- show engine innodb status;
解释:在TRANSACTION位置可以看到事务持有锁的情况,但是,需要经验分析,名词比较难懂。
- select * from performance_schema.data_locks;
解释:查看数据库中所有表、所有事务的加锁情况。没有冲突,也能看到持有了什么锁,具有具体的分类,简单好用,更容易分析。
- select * from performance_schema.data_locks_wait;
解释:查看数据库中,所有表、所有事务因锁的阻塞情况,谁被谁block住了。
MyISAM存储引擎只支持表锁,不支持行锁和页锁。
MyISAM存储引擎支持的锁不多,也就是表级的共享锁和排他锁而已,官方资料上也没有专门介绍MyISAM锁的章节。
表锁并不是一个真正的锁,只是代表对数据库表层级的数据进行锁定。具体以什么形式锁定,则要看具体的锁实现。
由上图可知,MyISAM存储引擎下,表锁的具体实现:表共享读锁(又叫 表级共享锁)和表独占写锁(又叫 表级排他锁)。
我们知道,MyISAM存储引擎只支持在表级对数据进行锁定(即:只支持表锁)。那么,表级具体的加锁实现有什么?即:表级共享锁和表级排他锁。
(1)表共享读锁(又叫 表级共享锁):获得表共享读锁的事务,可以 读取 该表的任意数据。MyISAM存储引擎在执行 select 语句前,会自动给涉及的表加表共享读锁。
(2)表独占写锁(又叫 表级排他锁):获得表独占写锁的事务,可以 插入、删除、更新 该表中任意数据。在执行 insert、delete、update 语句前,会自动给涉及的表加表独占写锁。
只是很多场合下,更喜欢把表级共享锁和表级排他锁叫做表共享读锁和表独占写锁,实际上它们的意思是一样的。
从上表中可以知道:MyISAM下的表锁可以做到:【读读共享、读写互斥、写写互斥】的功能。
值得注意的是:
The LOCAL modifier enables nonconflicting INSERT statements (concurrent inserts) by other sessions to execute while the lock is held. (See Section 8.11.3, “Concurrent Inserts”.) However, READ LOCAL cannot be used if you are going to manipulate the database using processes external to the server while you hold the lock. For InnoDB tables, READ LOCAL is the same as READ.
译文:MyISAM 在一定程度上还是可以支持并发的进行查询和插入操作的。即:如果MyISAM 表中没有空洞(表的中间没有被删除的行),MyISAM 允许在一个线程读表的同时,另一个线程从表尾插入记录。这属于MyISAM存储引擎的特性,所以,InnoDB存储引擎是不支持的。
(1)虽然MySQL支持【表锁、页锁、行锁】三级锁定,但是,MyISAM存储引擎只支持表级的数据锁定:表锁。
(2)相对而言,MyISAM的加锁开销低,但数据操作的并发性能相对不高。但是,如果写操作都是尾插入,那么,还是可以支持它自己特有的一定程度的读写并发。
(3)MyISAM存储引擎下的数据操作如果导致加锁行为,那么,该锁肯定是一个表锁,会对全表数据进行加锁。但是,也要看具体是什么形式的表锁,不同形式的表锁有着不同的特性。同时,从MyISAM存储引擎所支持的锁中也可以看出:MyISAM是一个支持【读读并发】,但不支持【通用读写并发】、【写写并发】的数据库引擎,所以,它更适合用于读多写少的应用场合。
InnoDB存储引擎的功能非常强大,MySQL官方还专门一个栏目来介绍InnoDB引擎所支持的锁。【链接是:MySQL官网:InnoDB的锁详解】
从图中大概可以看出如下这么几种锁:(红色部分是我们本文关注的锁)
InnoDB存储引擎,不仅仅支持表级锁定,还支持行级锁定。说白了,就是不仅仅支持表锁,还支持MyISAM所不支持的行锁。
是不是行级锁一定比表级锁要好呢?那到未必,锁的粒度越细,加锁和释放锁的开销越高,相比表级锁在表的头部直接加锁,行级锁还要扫描找到对应的行对其加锁,这样的代价其实是比较高的。总之,锁的粒度越细,需要实现的开销越大,所以,表锁和行锁各有好处。
InnoDB存储引擎下,表锁与行锁并不是一把具体的锁,仅仅代表要在数据库的那个层次结构上对数据进行锁定,这也代表了表锁与行锁还可以被细分出以下具体的锁实现,很绕,需要慢慢领略。
InnoDB实现的行锁,是通过给索引上的索引项加锁来实现的。这一点MySQL与Oracle不同,后者则是通过在数据块中对相应的数据行加锁来实现的。
InnoDB这种行锁的实现也意味着,只有通过索引条件检索数据时,InnoDB才会使用行级锁,否则,InnoDB将使用表级锁。
参考:InnoDB行锁实现
(1)共享锁,Shared Lock,又称 S锁、读锁。一个事务拿到某一行记录的S锁,才可以读取这一行的数据,共享锁可以实现【读读共享】、【读写互斥】。
(2)排他锁,Exclusive Lock,又称 X锁、写锁、独占锁。一个事务拿到某一行记录的X锁,才可以修改或者删除这一行的数据,排他锁可以实现【读写互斥、写写互斥】。
因为InnoDB存储引擎支持表锁和行锁,所以,在数据库层次结构的表级和行级,都可以对数据进行锁定,具体以什么方式进行锁定呢?共享锁和排他锁就是本节要讨论的具体的锁实现。
(1)表级共享锁:又叫表共享读锁,在表的层级上,对数据加共享锁,实现【读读共享】。
(2)表级排他锁:又叫表独占写锁,在表的层级上,对数据加排他锁,实现【读写互斥、写写互斥】。
(3)行级共享锁:在行的层级上,对数据加共享锁,实现对该行数据的【读读共享】。
(4)行级排他锁:在行的层级上,对数据加排他锁,实现对该行数据的【读写互斥,写写互斥】。
一句话,就是【读读共享、读写互斥、写写互斥】。这里要理解一个概念:上表仅仅是同一层级的共享锁和排他锁之间的兼容性,并非是表锁和行锁共存的情况。
(1)select * from table lock in share mode; —— 为table的所有数据加上共享锁,即 表共享读锁。
(2)select * from table for update; —— 为table的所有数据加上排他锁,即 表独占写锁。
(3)select * from table where id = 1 lock in share mode; —— 为table中id为1的那行数据加上共享锁,即 行级共享锁。
(4)select * from table where id = 1 for update; —— 为table中id为1的那行数据加上排他锁,即 行级排他锁。
当然,以上加行锁的前提是:id为主键且查询命中,否则,行锁将会退化为表锁。
InnoDB supports multiple granularity locking which permits coexistence of row locks and table locks. For example, a statement such as LOCK TABLES … WRITE takes an exclusive lock (an X lock) on the specified table. To make locking at multiple granularity levels practical, InnoDB uses intention locks.Intention locks are table-level locks that indicate which type of lock (shared or exclusive) a transaction requires later for a row in a table.
译文:通常情况下,表锁和行锁是相互冲突的。获得了表锁,就无法再获得该表具体行的行锁;反之亦然。
但是,有的时候,表锁和行锁实现部分的共存有利于更细粒度的对锁进行控制,以便得到更好的并发性能。
所以,InnoDB存储引擎支持多粒度(granular)锁定,这种锁定允许一个事务中同时存在行锁和表锁。
所以,InnoDB存储引擎为了实现行锁和表锁共存的多粒度锁机制特性,InnoDB存储引擎也就支持了一种额外的锁方式,以此对多粒度锁机制进行支持,称之为意向锁(Intention Lock)。
《MySQL技术内幕》:若将上锁对象看作是一颗树,那么,对最下层的对象进行加锁,也就是对最细粒度的对象进行加锁,那么首先就需要先对上层的粗粒度的对象进行上锁。
意向锁的具体实现,又可以分为以下两种:
(1)意向共享锁,intention shared lock,IS锁:事务打算给表中的某些行加行级共享锁(S锁),事务在给某些行加 S锁 前必须先取得该表的 IS锁。
(2)意向排他锁,intention exclusive lock,IX:事务打算给表中的某些加行级排他锁(X锁),事务在给某些行加 X锁 前必须先取得该表的 IX锁。
总之,所谓意向锁,就是提前表明了一个要加行锁的【意向】。
意向锁的维护,是由InnoDB存储引擎隐式帮我们做了,不需要程序员操心。
(1)意向锁,是由InnoDB存储引擎自己维护的,用户无法手动操作意向锁。
(2)在为数据行加共享锁(S锁)、排他锁(X锁)之前,InooDB存储引擎会先获取该数据行所在表的对应的意向共享锁(IS锁)、意向排它锁(IX锁)。
举个应用场景的粟子:
事务A 向student表申请某一行记录的X锁(行级)。
事务A 申请完毕后,事务B 向student表整个表的X锁(表级)。
因为事务A 已经获得了student表中某行的行级X锁,所以,数据库需要阻塞事务B 的表级X锁的申请,直到事务A 释放了行级X锁。
那么,数据库如何去判断这个冲突呢?分两步,第一步OK的话,才继续进行下一步:
- 判断student表是否已经让其他事务获得了表锁?
- 判断表中的每一行是否已经让其他事务获得了行锁?(本质上是逐行遍历索引项)。
从上两个判断可以得知,表锁和行锁是互不兼容的,不能共存的,这是其一。在不存在表锁的情况下,就会到步骤二中一行一行的判断是否存在行锁,这样的效率十分低下。
如果意向锁存在的话,那么步骤如下:
- 判断student表是否已经让其他事务获得了表级S锁、表级X锁?
- 判断该表是否存在意向锁( IS锁、IX锁 ):如果有意向锁,则说明表中存在行锁,所以阻塞事务B 的表级X锁申请。
所以,我们知道存在意向锁时,判断冲突步骤就会变得非常快了,因为避免了 需要逐行判断是否有行锁 这样一个非必须的操作。
(1)事务A 向student表申请某个行记录的S锁(行级),所以,首先要为student表上 IS锁。
(2)事务A 获得锁完毕后,事务B 向student表申请全表的S锁(表级)。于是,数据库开始锁冲突的判定。
(3)数据库首先判断事务B 申请表锁前,student表是否已经存在表级S锁、表级X锁,发现没有。
(4)数据库再判断student表是否已经存在意向锁IS、意向锁IX,然后,发现之前有事务A 申请过IS锁。
(5)根据表级S锁和意向IS锁的兼容性策略,两者是兼容的,所以,事务B 直接就获得了对student表的表级S锁。
(6)此时,student表已经共存了行级S锁、IS锁、表级S锁。
所以,意向锁的出现,也让表锁和行锁得到一定的共存,提高了部分的并发性能。
这里有一个小例子:
(1)事务A 向student表申请a行的行级S锁,所以,存储引擎自动为该表先加上 IS锁。
(2)事务B 向student表申请全表的表级X锁,因为表级X锁与IS锁 不兼容,所以,事务B 的表级X锁申请会被事务A阻塞。
(3)事务C 向student表申请b行的行级X锁,所以,存储引擎自动为该表先加上 IX锁。因为IX锁和IS锁 互相兼容,且事务A 与事务C 针对的行记录不同,所以 事务C成功获得行级X锁。
从上面的例子中,可以大致感受到意向IX、IS锁与表级X/S锁与行级X/S锁之间的关系了。
行锁,根据锁的算法实现又可以分为三种锁:记录锁
、间隙锁
、临键锁。
【MySQL官网——记录锁】
A record lock is a lock on an index record. For example,SELECT c1 FROM t WHERE c1 = 10 FOR UPDATE; prevents any other transaction from inserting, updating, or deleting rows where the value of t.c1 is 10.
记录锁,Record Lock,就是我们最单纯认知的行锁,只锁住一条行记录,准确的说是一条索引记录。InnoDB的行锁,是依赖索引实现的,其锁住某行数据的本质是锁住行数据对应在聚集索引中的索引记录。
例如:某个事务执行了 select * from t where id=1 for update; 语句,就相当于它会在 id = 1的索引记录上加上一把锁,以阻止其他事务插入、更新、删除 id = 1 的这一行记录。
通常情况下,字面意思上的行锁,代表对一行记录加锁。但是实际上,行锁是一个抽象,行锁(1) 还分记录锁(record lock)、间隙锁(Gap Lock)、临键锁(Next-Key Lock)等具体的行锁算法实现。但是,我们通常意义上单纯的 行锁(2) ,实际上说的就是记录锁。所以,此 行锁(1) 非彼 行锁(2)。
行锁(1) 更倾向是一个锁分类,代指一系列行锁 。
行锁(2) 更接地气更单纯,就是指锁单条记录的意思,随着不同场景不同语义,所代表的意思可能有些许不同。
通常情况下,都会被混在一起,所以也不用太介意,自己要清楚就好。
【官网 GAP Locks】 A gap lock is a lock on a gap between index records, or a lock on the gap before the first or after the last index record. For example,
SELECT c1 FROM t WHERE c1 BETWEEN 10 and 20 FOR UPDATE; prevents other transactions from inserting a value of 15 into column t.c1, whether or not there was already any such value in the column, because the gaps between all existing values in the range are locked.
间隙锁,Gap Lock,顾名思义,它会封锁索引记录中的“间隙”,不让其他事务在“间隙”中插入数据。 它锁定的是一个不包含索引本身的左右开区间范围( index1, index2 )。
间隙锁是锁定索引记录之间的间隙、或者锁定第一条索引记录之前的范围、又或者锁定最后一条索引记录之后的范围。说白了,间隙锁的目的就是为了防止索引间隔被其他事务 “插入”。
间隙锁,是InnoDB权衡性能和并发性后研究出来的特性。
【MySQL官网——临键锁】A next-key lock is a combination of a record lock on the index record and a gap lock on the gap before the index record.
临键锁,Next-Key Lock,其实并不能算是一把新的行锁,它实际上就是 记录锁(Record Lock)和间隙锁(Gap Lock)的组合,既锁住了"间隙",又锁住了“索引本身”,即 组合起来构成了一个左开又闭的区间的锁范围。即:临键锁 锁的是索引记录本身,以及索引记录之前的间隙( index1, index2 ]。
比如,某表有4行数据,主键ID分别为 10、11、13、20。那么该表可能存在的临键锁锁住聚簇索引的区间如下:
(negative infinity, 10]
(10, 11]
(11, 13]
(13, 20]
(20, positive infinity)
每两个索引项之间就是一个临键锁的锁定区域,通常规则是 左开右闭
,除了最后一个锁区间是全开区间之外
,它代表锁住索引最大项之后的区域。
我们知道,InnoDB的行锁默认是基于B+树的。所以,行锁依赖的索引是有序的。
的确,我们一直都说行锁是一个抽象,只代表在行层级对数据进行锁定。我们也说,行锁实际有具体的实现,比如排他锁和共享锁。为什么这个时候又冒出了一些记录锁、间隙锁、临键锁的概念?
其实【排他锁、共享锁】与【记录锁、间隙锁、临键锁】本不应该混淆在一起,因为他们虽然都属于行锁的范畴,但却是不同维度的概念。
所以,它们只是不同角度分类下的概念,比如我们可以这么认为,如果一把行锁在锁粒度上的实现是一个记录锁,那么该行锁就是一把记录锁,而这个记录锁在功能上的实现是一把共享锁,那么这把记录锁就是一把共享锁。在这个角度上分析,此时,这把行锁即是记录锁,也是共享锁。
参考:论 MySql InnoDB 如何通过插入意向锁控制并发插入、插入意向锁博文、MySQL官网中插入意向锁。
InnoDB的行锁是基于索引实现的,就比如记录锁(record lock)实际锁的并不是数据行,而是数据行对应的索引记录。
说是这么说,但是“行锁,锁的是索引记录”这句话到底要怎么理解呢?我们知道一个表,可以有多个索引,每个索引都有对应行的索引记录。那么,行锁要对一行数据加锁,锁的到底是哪个索引中的索引记录呢?
(1)我们假设一张student表,sid是主键索引、name是唯一索引、tel与email是组合索引。
(2)首先,事务A 执行 select * from student where sid = 1 for update ,未提交事务。 对student执行了查询,where条件是一个主键,for update 使我们为该表加了X锁,本质上就是给student表中 sid = 1 的行数据上行级排他锁。
(3) 然后,事务B 执行 select * from student where age= 22 for update, 未提交事务。同样对student表进行查询,where条件是一个普通索引列。事务A 和事务B 其实查询的都是同一条行记录,但是,两个SQL的where条件分别是两种索引列。
(4) 因为事务A 给行数据加了排他锁,后面事务B 又给同一条行记录加排他锁,因为事务A 还没有提交事务,所以事务B 自然就被阻塞了。
此时,我们执行 select * from infomation_schema.innodb_locks,来看一下当前存储引擎下的加锁状态。
会发现行锁是一个记录锁,锁的是PRIMARY索引,代表的就是主键索引。
(5) 此时,我们将两个事务都rollback一下,删除主键索引,让student此时只剩下name唯一索引、tel与email的组合索引。
重复步骤2和步骤3 ,然后再看看加锁状态。发现,此时行锁锁的索引变成了以name列生成的唯一索引。
(6) 此时,我们将两个事务都rollback一下,再删除唯一索引name,让student此时只剩下tel与email的组合索引。
重复步骤2和步骤3 ,然后再看看加锁状态。发现,此时行锁锁的不是tel与name的组合索引,而是 InnoDB内部生成的隐式索引GEN_CLUST_INDEX。
所以,可以得出结论:行锁是基于聚簇索引实现的,不管该表存在多少个索引,行锁所锁的索引记录都是属于该表的聚簇索引的。
即:InnoDB的行锁锁住某行数据的本质是 锁住行数据对应在聚簇索引上的索引记录。
想想也是,InnoDB存储引擎下,一个表的数据本身就是存储在该表聚簇索引的叶子结点上的。锁住某行数据的另一个含义,不就是锁在聚簇索引的叶子结点的索引记录上嘛,其实两者也没有太大区别。
(1)MySQL锁机制1:https://blog.csdn.net/cmm0401/article/details/107299458
(2)MySQL锁机制2:https://blog.csdn.net/cmm0401/article/details/115860762
(3)来自于文章,感谢:https://blog.csdn.net/SnailMann/article/details/88353099
(4)InnoDB中意向锁有什么用:https://www.zhihu.com/question/51513268
(5)MySQL 8.0 官方:https://dev.mysql.com/doc/refman/8.0/en/innodb-locking.html