目录
MySQL索引、事务、锁、MVCC简述
一、索引
1.1 执行计划 Explain
1.2 索引结构
1.2.1 Hash
1.2.2 二叉搜索树
1.2.3 平衡二叉搜索树(AVL)
1.2.4 多叉平衡搜索树
1.2.4.1 B-Tree
1.2.4.2 B+Tree
1.2.5 B-Tree与B+ Tree的区别
1.3 Myisum与InnoDB的区别
1.3.1 Myisum
1.3.2 InnoDB
1.4 名词解释
二、事务
2.1 事务的定义
2.2 不同事物隔离级别效果
2.2 事务中的隔离性实现
2.3 锁机制
2.3.1 常见的锁
2.3.2 锁的分类
2.3.2.1 锁升级情况
2.3.4 Innodb下行级锁原理
2.4 MVCC
2.4.1 快照读、当前读
2.4.1.1 快照读
2.4.1.2 当前读
2.4.2 MVCC的实现原理
2.4.2.1 三要素
2.4.2.2 具体流程
三、常见面试题
3.1 不可重复读、幻读的主要区别
3.2 间隙锁与临键锁为什么能避免幻读
3.3 MVCC与锁的关系
3.4 MVCC 一定可以解决幻读吗
3.5 解决幻读的终极杀器
一、索引
1.1 执行计划 Explain
可以查看之前的一篇博客,这里简单会回忆一下
https://www.jianshu.com/p/3ea49fd0067e
Explain可以查看一条语句的执行计划,显示出比较重要的几个字段:
id
select_type
table
-
type
级别从左侧到右侧效果越来越低下
system > const > eq_ref > ref > range > index > all
一般来说SQL语句的优化最好优化到range级别
这里着重说一下index、all级别的区别:
-
index
只通过索引树的全表扫描
-
all
不通过索引树的全表扫描
疑问?
看了官网对index与all的对比,直到现在还是有点不太明白index级别和all级别的区别。比如,在MySQL Innodb引擎下,哪来的数据文件,所有的数据也都会捆绑在了聚簇索引文件上,因此index、all究竟有啥区别?
-
-
possible_keys
SQL语句在执行前期,MySQL服务层选出可能会用到的索引,需要注意的是,这些索引并不一定会在执行期间真正的甬道
-
keys
在SQL执行期间真正使用到的索引
-
key_len
使用到的索引占用空间的大小,如int类型占用4字节,varchar类型索引占用n个字节...,将这些索引占用的字节数相加即为ken_len的值
-
rows
查出来的数据量多少,该值也是一个预估值
extra
1.2 索引结构
1.2.1 Hash
特点:数据的存储与取数都是通过计算Hash值去定位
优势:适合等值查询
弊端:
- 范围查询效率低
- Hash冲突时硬伤
1.2.2 二叉搜索树
特点:是一颗树且左子树的Value小于根且根小于右子树。
优势:查询效率要高于Hash方式
弊端:在某些情况下,如表记录的ID使用自增主键,这样的情况下,二叉搜索树会退化成单链表,导致树的深度过深
1.2.3 平衡二叉搜索树(AVL)
特点:为了解决二叉搜索树的深度可能会过深,平衡二叉搜索树(AVL树)增加了一些约束,如:左右子树的高度差不能大于1,尽可能的让二叉搜索树变得平衡。常见的平衡二叉搜索树有:红黑树、数堆
弊端:虽然平衡二叉搜索树解决了二叉搜索树有可能退化成单链表的情况,但是树毕竟属于二叉,因此树的深度还是会比较深。
1.2.4 多叉平衡搜索树
1.2.4.1 B-Tree
特点:如果采用平衡二叉搜索树,因为属于二叉树,大量的数据还是会导致树的深度过深,因此树中的一个节点就不能只有2个子节点,应该允许M个子节点(M>2),B-Tree就是为了解决这个问题,B- Tree的结构如下:
- B-Tree作为平衡多路搜索树,它的每个节点包含M个子节点,M称为B树的阶,如果每个节点中包含X个关键字,则每个节点有
X+1
个指针。 - 所有叶子节点都在同一层。
- 每个节点除了存储关键字之外,还会存储该关键字对应的数据记录。
优势:一定程度上解决了树的深度
弊端:因为所有的节点都会存储行记录,因此每一个节点可以表示的状态较少,导致树的深度仍然比较高。
1.2.4.2 B+Tree
特点:同B-Tree的结构相似,但是非叶子节点除了存储关键字之外不会存储行记录。除此之外,所有的叶子节点除了在同一层的状态之外,还会使用指针进行连接,形成叶子节点的单链表。
优势:
- 因为节点不存储记录,因此节点可以表示更多的状态,从根本上解决了树的深度高的问题,是MySQL索引默认的索引结构
- 因为叶子节点是形成单链表的,因此非常适合范围查询
1.2.5 B-Tree与B+ Tree的区别
- B树的关键字和记录是放在一起的,而B+树只有叶子节点才存储记录数据
- B+树的叶子节点形成单链表,利于范围查询
- B树的查询具有不稳定性,而B+树的查询具有稳定性,因为在B树中,所有节点都存储数据,若一些查询碰巧只需要查询几个节点就能获取到数据,而有的数据需要一直检索到叶子节点,因此就形成了查询不稳定性。
1.3 Myisum与InnoDB的区别
1.3.1 Myisum
支持的索引:主键索引、唯一索引、普通索引、复合索引、全文索引
这里顺带提一下MyIsum的文件结构:
一个数据表会有三种类型文件:
-
.FRM
存放表结构
-
.MYI
存放索引数据
-
.MYD
存放实际表数据,
可以看出Myisum存储引擎下数据和索引还是分开存储的,索引文件中的节点存储的都是实际数据的地址指针。
1.3.2 InnoDB
支持的索引:主键索引、唯一索引、普通索引、复合索引、全文索引(高版本)
InnoDB的文件结构:
-
.FRM
存放表结构
-
.IDB
存放数据和索引的文件
看得出InnoDB下会将数据与索引存储在一起
1.4 名词解释
在InnoDB引擎下,有多少个索引就有多少个.idb后缀的索引树文件
-
聚簇索引
该索引有一个特点就是非重复、非Null,比如常用的Primary Key,MySQL就会对Primary Key进行建立聚簇索引。
关于聚簇索引的选择,选择过程从上至下:
- 表记录中的Primary Key
- 唯一索引
- 表记录隐藏记录
rowid
MySQL在InnoDB引擎下,所有的表记录都会与聚簇索引存储在一个文件中
-
非聚簇索引
聚簇索引之外的所有索引都称为非聚簇索引
非聚簇索引的叶子节点只会存储一部分数据与聚簇索引的关键字
-
索引覆盖
因为非聚簇索引只存储记录中的一部分记录(对应的索引记录值),因此如果一条查询需要从非聚簇索引中进行查询数据并且查询的字段都在非聚簇索引的叶子节点中了,那么这种现象就称为索引覆盖,索引覆盖对应的是回表
-
回表
若查询的字段不在非聚簇索引中,则需要拿着非聚簇索引中的聚簇索引关键字值去聚簇索引中查询,这样的过程称为回表
-
索引下推
看一个SQL语句:
-- 其中name、age都是普通索引 select * from name = 'zcy' and age > 20
在不使用索引下推的情况下:
会先在name索引树中查询name为'zcy'的所有记录,
然后再拿到这些记录对应的聚簇索引关键字值回表进行查询,查询出的所有结果
在MySQL Server层将第二步查询出的结构进行根据
age > 20
进行对比
在使用索引下推的情况下:
第三个步骤中根据age字段进行比较的工作也会下推到存储引擎层进行。所以这里的下推指的是将比较的任务继续下发给存储引擎进行。
二、事务
2.1 事务的定义
说到事务,大家都会想到ACID,但ACID其实本身与事务的定义并没有关系
事务,其实是值一组操作,这一组操作包含了多个操作。事务的概念可以应用于任意一款存储中间件,比如MySQL、Redis,甚至Kafka中也有这样的概念。
但是什么样的事务称为好的事务呢?我们常用转账案例去解释什么是好的事务。
好的事务是能够满足ACID的四个特性,我们就称之为该事务是好的事务,是完美的事务。
- A:原子性
- C:一致性
- I:隔离性
- 读未提交
- 读已提交(RC)
- 可重复读(RR)
- 串行读
- D:持久性
其中ACID中,C是最重要的目标,其它三个都是为了满足C的特性。
对于事务中,隔离性也非常的重要,隔离性决定着不同事务进行并发的时候,事务之间的数据隔离强度的问题,隔离性分为四个级别,读未提交是隔离性最弱的一种情况,串行读是隔离强度最强的,串行读完全保证了在事务并发的情况下,数据一定是安全的、一致的,但是因为每一步操作都会加锁,导致并发情况下的效率低下。
2.2 不同事物隔离级别效果
事务进行并发过程中,在未经过特殊处理的情况下势必会产生脏读、不可重复读、幻读,而不同强度的事务隔离级别能够解决的问题自然也不同
读未提交:脏读、不可重复读、幻读
读已提交:不可重复读、幻读
可重复读:幻读
串行化读:解决了所有的问题
上述文字对应关系说明:左侧代表不同的事务隔离级别,右侧代表这种隔离级别下仍然会有很多问题。
2.2 事务中的隔离性实现
其实ACID的提出,包括ACID中的四种强度的隔离性都是由ISO提出的标准,而这些也只是标准而已,具体的实现还是要一句不同的数据库厂商。
对于MySQL而言,MySQL是通过如下两种方式的配合去实现事务的隔离性:
- 锁机制
- MVCC机制(在InnoDB引擎下,且在RC、RR隔离级别下工作)
这两者实现方式是相辅相成的,MVCC更侧重于DQL语句的事务隔离,而锁机制不论是DQL还是DML都能够有效隔离。
但如果所有语句都需要使用锁机制的话,这也会大大减少了事务的并发性,基于这样的想法,就有了MVCC机制。而锁机制与MVCC机制并非通过加锁来保证事务隔离型。
两者的关系就类似于Java中的悲观锁于CAS之间的关系。
在下文中,会具体阐述锁于InnoDB下的MVCC机制。
2.3 锁机制
通过前文的分析,锁是实现事务隔离性的一种重要手段。而锁的分类很多,根据锁的作用范围又可以分为库锁、表锁、临键锁、间隙锁、行锁,而根据锁是否具有独占、排他的性质,又分为共享锁、排他锁(独占锁)
2.3.1 常见的锁
-
共享锁(RC、RR都支持)
类比读写锁中的读锁
select 后面加上lock in share mode.
-
排他锁(RC、RR都支持)
类比读写锁中的写锁
默认所有的DML操作都是排他锁
如果显示的加,则在语句后面加上 for update
-
独占锁
其功能等效于排他锁,只是在不同的存储引擎中的名称不一样而已
在MyIsum中称呼为独占锁
-
间隙锁(只有RR支持,RC某些情况下也会存在)
用于范围方式的上锁方式,但需要注意的是:间隙锁不同于排他锁(写锁),间隙锁读读可以并发,不是互斥的
-
临键锁(只有RR支持,RC某些情况下也会存在)
间隙锁+行锁
2.3.2 锁的分类
从粒度上来分:行锁(只有Innodb才有)、表锁、数据库锁
从锁操作上分:读锁、写锁
从实现方式上分:乐观锁、悲观锁
2.3.2.1 锁升级情况
在工作中我们虽然不会去写lock关键字,但是很多时候会无意上升为表锁,
如DML语句,因为DML默认的话就会加行级排他锁,而刚好检索的字段不是索引列,就自动上升成为表锁
- 对未加索引的字段作为检索的字段进行上锁操作(select 手动指定锁、或者DML自动加锁)
有一张tt表,tt中的name字段不是索引
如:select * from tt where name = 'zcy'(不会有锁,不论是行锁还是表锁)、
select * from tt where name = 'zcy' lock in share mode (会由行级共享锁上升为表锁)
select * from tt where name = 'zcy' for update (会由行级排他锁上升为表锁)
update tt set age = 1 where name = 'zcy' (会由行级排他锁上升为表锁)
delete from tt where name = 'zcy' (会由行级排他锁上升为表锁)
insert into tt values ('zcy', 22)(会由行级排他锁上升为表锁)
- 加锁的语句索引失效
2.3.4 Innodb下行级锁原理
对主键(聚簇索引)记录加行锁:直接锁定索引记录中某个叶子节点即可
对于唯一键记录加锁:要锁定该非聚簇索引叶子节点,还要根据叶子节点中聚簇索引的主键值去锁定聚簇索引中对应的叶子节点,以避免修改其它非当前索引的字段(回想一下索引覆盖的概念)
非唯一键加锁:会升级为表锁
解决疑惑:
上文说事物隔离级别是通过锁或者MVCC两种实现方式去完成的,
我个人觉得这句话有问题,我觉得这两个是相辅相成的去完成的,
比如,事务隔离级别中,幻读是通过间隙锁/临键锁区避免幻读的(Innodb下),这种其实就是通过加锁机制保证不同事务之间的数据隔离性,而MVCC是侧重于读而且是快照读,因此所说:数据的DML是通过加锁方式避免幻读,真正在读取过程中(快照读方式)是通过MVCC的机制去避免幻读,所以锁是侧重DML,让事务写形成一种带阻塞状态,一个事务操作,另一个事务就无法操作,自然不会有幻读了,而MVCC是侧重于读,因为虽然你已经有了加锁机制保证了避免幻读,但是我们一般写select的时候,并不会主动加锁,因此读的操作并不会自动纳入到锁机制当中去完成幻读,因此就需要对这些我们大多数常用的快照读作特殊处理,才有了MVCC。
2.4 MVCC
根据上文可知,MVCC是InnoDB引擎下的一款事务隔离级别的具体实现方式,因为MVCC并不需要加锁读因此其吞吐量要优于锁机制,MVCC中文意思为:多版本控制协议。
MVCC工作在InnoDB引擎下的RC、RR两个事务隔离级别中
2.4.1 快照读、当前读
在具体了解锁与MVCC之前,最好了解一下什么是快照读、什么是当前读。
2.4.1.1 快照读
快照读工作在MVCC机制下。
指的是读取快照中的数据,而并不一定是当前最新的数据,常见的不加锁的DQL都是快照读
但有一点需要注意,不同事务隔离级别生成快照读的时机不一样,因为快照读主要是通过MVCC去实现的,因此快照读主要研究在RC、RR两种事务隔离级别下,快照读的生成时机。
-
RC
在每次不加锁的DQL语句下都会重新生成一遍快照,这也是为什么MVCC机制下RC无法做到可重读
-
RR
只在第一次不加锁的DQL语句后生成
除此之外,如果事务中使用了一次当前读,那么快照也会跟着被重建。
2.4.1.2 当前读
当前读是利用锁机制去实现的,保证每次读取到的一定是最新值。
指的是通过加锁方式,让DQL语句在检索中始终读取记录表中的最新数据。
常见的加锁DQL与DML都属于当前读。
2.4.2 MVCC的实现原理
MVCC的实现原理主要通过三个要素,分别是:行记录隐藏字段、Undolog历史版本链、ReadView
2.4.2.1 三要素
-
行记录隐藏字段
在MySQL中每一行都有三个隐藏字段,分别是:db_trx_id(操作当前记录的最后一次事务ID)、db_row_id(每一条行记录都有,回忆一下之前讲述的聚簇索引的建立)、db_row_rt(指向undolog的指针)
-
Undolog
一条记录的历史版本链,每一个版本的记录都通过pointer指向下一个历史版本的记录,如下图:
而Undolog日志就作为MVCC历史数据提取的数据来源
- ReadView(快照读的基础)
ReadView就是快照读SQL执行时MVCC从UndoLog中提取数据的依据
而在RC、RR中,ReadView生成的时机不一样:
RC
每次快照读都会生成一个ReadView
RR
只有在第一次进行快照读的时候才会生成ReadView
除此之外,如果事务中使用了一次当前读,那么快照也会跟着被重建。
也正是因为生成时机不一样,因此才会有了RC、RR的不同,即:RC未能解决不可重复读的问题(就是因为每次进行快照读,ReadView都会重新生成,导致根据MVCC规则,从Undolog都能拿到新一点的数据,而RR就只能拿到旧数据),而RR解决了不可重复读就和他的ReadView总是复用第一次生成的有关, 1⃣️在Undolog不变的情况下,2⃣️ReadView相同,3⃣️判断规则相同,那么读取到的Undolog数据肯定一致。
组成部分:
m_ids:当前活跃的事务编号集合
min_trx_id:最小活跃事务编号
max_trx:id:预分配事务编号,当前最大事务编号+1
creator_trx_id:ReadView创建者的事务编号
2.4.2.2 具体流程
在理解了MVCC三要素之后,这里通过一个小案例去说明MVCC的具体工作流程
先查看以下案例:
前提:事务隔离级别RC情况下。 这是个非常重要的前提,因为RC与RR中MVCC生成ReadView的时机不相同
一共有两个DQL语句,而根据ReadBView的生成规则,可以分别写出右侧两个SelectReadView。
MVCC工作流程总结:
其实也非常容易理解:
- 先判断当前查询是不是属于当前DML事务,如果DML、DQL发生在一个事务,并且前后两句话中没有额外的事务操作,那么这条数据就是可读的;
- 如果undolog当前版本数据事务id小于最小活跃ID,则表明上一次操作该条记录的事务早已提交,则当前DQL可以进行读取;
- 如果undolog当前版本数据事务id大于ReadView中最大活跃事务ID,则说明当前undolog数据是在当前DQL开始事务之后才开启的,属于未来性数据,这样的undolog数据不允许访问;
- 如果undolog当前版本属于ReadView中活跃事务区间内,并且不属于ReadView的活跃区间,则说明Undolog数据的上一次操作事务已经提交,则可以读取。
三、常见面试题
3.1 不可重复读、幻读的主要区别:
不可重复读主要针对:Update操作;
幻读主要针对:Insert、Delete操作,即两次相同的查询,数据的总量发生了变化
3.2 间隙锁与临键锁为什么能避免幻读
间隙锁与临键锁的工作原理一样,就是锁住索引关键字的前后两半部分区间,使得整个区间无法进行其它操作,以此来避免幻读;
这个地方你可能会有疑问:为什么只需要锁定索引关键字前后两个区间,而不是锁住整张表。
看下面的案例:
结论:首先前提是需要认知到叶子节点会形成一个单链表,因此在DML或者DQL当前读方式操作某个节点的时候,间隙锁或临键锁会锁住记录前后两个区间,如果对于普通索引这样的关键字是被允许关键字重复的,因此如果另一个事务插入一个相关关键字的记录,那么反应到B+树的结构来说,其必然会落到前后两个区间之中;而在一开始就使用锁机制锁住了前后两个区间,这就导致insert或者delete语句会被阻塞直到超时或者案例中的事务A提交,这样一来就可以避免幻读。
3.3 MVCC与锁的关系
相辅相成的关系,MVCC可以看成是乐观锁,锁是悲观锁,MVCC的效率更高
3.4 MVCC 一定可以解决幻读吗
不可以!
MVCC避免幻读的原理与3.2 间隙锁与临键锁为什么能避免幻读有着极大的不同,这是因为MVCC使用快照方式,去避免了幻读的出现,但是!在MVCC的机制下如果前后两次快照读之间有一次当前读,当前读必然导致了重建ReadView,这就会使得前后两次查询的快照不同,而这之间恰巧有其它事务进行了Insert或者Delete操作,那么依然会导致幻读的出现。
3.5 解决幻读的终极杀器
在分析了3.2 间隙锁与临键锁为什么能避免幻读与3.4 MVCC 一定可以解决幻读吗两个面试题之后,可以发现只有通过间隙锁、临键锁的方式才能更加安全的避免幻读的出现。
参考:
《IT老齐架构三百讲》
MySQL官网:https://dev.mysql.com/doc/internals/en/
注:文章文字较多,编写与整理的过程稍显仓促,若有错误,欢迎纠错。