MySQL进阶:InnoDB引擎(逻辑存储结构、架构、事务原理、MVCC(面试高频))

👨‍🎓作者简介:一位大四、研0学生,正在努力准备大四暑假的实习
🌌上期文章:MySQL进阶:全局锁、表级锁、行级锁总结
📚订阅专栏:MySQL进阶
希望文章对你们有所帮助

MVCC很重要,是大厂面试的高频面试题,学起来也不是很容易,需要瞻前顾后,总之要多总结多思考。
理解MVCC可以帮助你更好的理解事务的性质,大家都知道RC隔离级别的事务会出现不可重复读问题,但是RR隔离级别的事务却解决了不可重复读问题,解决的方法就是通过MVCC。
希望这篇文章能让你醍醐灌顶,不惧面试!

逻辑存储结构

InnoDB的逻辑存储结构如下所示:
在这里插入图片描述
1、表空间

表空间是InnoDB存储引擎逻辑结构的最高层, 如果用户启用了参数 innodb_file_per_table(在8.0版本中默认开启) ,则每张表都会有一个表空间(xxx.ibd),一个mysql实例可以对应多个表空间,用于存储记录、索引等数据。

2、段

段,分为数据段(Leaf node segment)、索引段(Non-leaf node segment)、回滚段(Rollback segment),InnoDB是索引组织表,数据段就是B+树的叶子节点,索引段即为B+树的非叶子节点。段用来管理多个Extent(区)。

3、区

区,表空间的单元结构,每个区的大小为1M。 默认情况下,InnoDB存储引擎页大小为16K, 即一个区中一共有64个连续的页。

4、页

页,是InnoDB 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。为了保证页的连续性InnoDB存储引擎每次从磁盘申请 4-5 个区。

5、行

InnoDB 存储引擎数据是按行进行存放的
在行中,默认有2个隐藏字段:

  • Trx_id:每次对某条记录进行改动时,都会把对应的事务id赋值给trx_id隐藏列(相当于存储了最后一条记录的事务id)
  • Roll_pointer:每次对某条引记录进行改动时,都会把旧的版本写入到undo日志中,然后这个隐藏列就相当于一个指针,可以通过它来找到该记录修改前的信息。

架构

概述

MySQL5.5开始,默认使用InnoDB存储引擎,它擅长事务处理,具有崩溃恢复特性,在日常开发中使用非常广泛。下面是InnoDB架构图,左侧为内存结构,右侧为磁盘结构。
在这里插入图片描述

内存结构

在这里插入图片描述
主要分四大块:
1、Buffer Pool

InnoDB存储引擎基于磁盘文件存储,访问物理硬盘和在内存中进行访问,速度相差很大,为了尽可能弥补这两者之间的I/O效率的差值,就需要把经常使用的数据加载到缓冲池中,避免每次访问都进行磁盘I/O。
缓冲池 Buffer Pool,是主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),然后再以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。
可以看到缓存池是以Page(页)为单位的,底层采用链表数据结构来管理Page。根据状态将Page分为三种类型:

  • free page:空闲page,未被使用。
  • clean page:被使用page,数据没有被修改过。
  • dirty page:脏页,被使用page,数据被修改过,还没有刷到磁盘上,导致数据与磁盘的数据产生了不一致

2、Change Buffer

Change Buffer,更改缓冲区(针对于非唯一二级索引页),在执行DML语句时,如果这些数据Page没有在Buffer Pool中,不会直接操作磁盘,而会将数据变更存在更改缓冲区 Change Buffer中,在未来数据被读取时,再将数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中。

我们可以思考一下Change Buffer存在的意义。首先对于一个非唯一的二级索引页(那就肯定不会是唯一索引或者主键索引了),可能会存在两个字段值是一样的,但是它们肯定的主键id肯定不一致:
在这里插入图片描述
与聚集索引相比较,聚集索引的主键id都是顺序插入的,但是二级索引的叶子结点挂载的主键id并不是顺序的,相对更随机。
那么,删除和更新操作可能操作了多条数据,而在二级索引树中,他们的位置又不是相邻的,这样就会浪费了更多的磁盘IO。
因此,有了ChangeBuffer之后,我们可以在缓冲池中进行合并处理,减少磁盘IO。

3、Adaptive Hash Index

自适应hash索引,用于优化对Buffer Pool数据的查询。MySQL的innoDB引擎中虽然没有直接支持hash索引,但是给我们提供了一个功能就是这个自适应hash索引。因为前面我们讲到过,hash索引在进行等值匹配时,一般性能是要高于B+树的,因为hash索引一般只需要一次IO即可,而B+树,可能需要几次匹配,所以hash索引的效率要高,但是hash索引又不适合做范围查询、模糊匹配等。
InnoDB存储引擎会监控对表上各索引页的查询,如果观察到在特定的条件下hash索引可以提升速度,则建立hash索引,称之为自适应hash索引。
注意:自适应哈希索引,无需人工干预,是系统根据情况自动完成

4、Log Buffer

Log Buffer:日志缓冲区,用来保存要写入到磁盘中的log日志数据(redo log 、undo log)。默认大小为 16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入或删除许多行的事务,增加日志缓冲区的大小可以节省磁盘 I/O。

磁盘结构

在这里插入图片描述
1、System Tablespace

系统表空间是更改缓冲区的存储区域。如果表是在系统表空间而不是每个表文件或通用表空间中创建的,它也可能包含表和索引数据。

2、File-Per-Table Tablespaces

如果开启了innodb_file_per_table开关 ,则每个表的文件表空间包含单个InnoDB表的数据和索引 ,并存储在文件系统上的单个数据文件中。(默认开启)

3、General Tablespaces
通用表空间,需要通过 CREATE TABLESPACE 语法创建通用表空间,在创建表时,可以指定该表空间。
创建表空间:

CREATE TABLESPACE ts_name ADD DATAFILE 'file_name' ENGINE = engine_name;

创建表时指定表空间:

CREATE TABLE xxx ... TABLESPACE ts_name;

4、Undo Tablespaces

撤销表空间,MySQL实例在初始化时会自动创建两个默认的undo表空间(初始大小16M),用于存储undo log日志。

5、Temporary Tablespaces

InnoDB使用会话临时表空间和全局临时表空间。存储用户创建的临时表等数据。

6、Doublewrite Buffer Files

双写缓冲区,innoDB引擎将数据页从Buffer Pool刷新到磁盘前,先将数据页写入双写缓冲区文件中,便于系统异常时恢复数据。

7、Redo Log

重做日志,是用来实现事务的持久性。该日志文件由两部分组成:重做日志缓冲(redo logbuffer)以及重做日志文件(redo log),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都会存到该日志中, 用于在刷新脏页到磁盘时,发生错误时, 进行数据恢复使用。

后台线程

内存中我们所更新的数据要到磁盘当中,需要涉及到一组后台线程。
在这里插入图片描述
1、Master Thread

核心后台线程,负责调度其他线程,还负责将缓冲池中的数据异步刷新到磁盘中, 保持数据的一致性。还包括脏页的刷新、合并插入缓存、undo页的回收。

2、IO Thread

在InnoDB存储引擎中大量使用了AIO来处理IO请求, 这样可以极大地提高数据库的性能,而IOThread主要负责这些IO请求的回调。

线程类型默认个数职责
Read thread4负责读操作
Write thread4负责写操作
Log thread1负责将日志缓冲区刷新到磁盘
Insert buffer thread1负责将写缓冲器内容刷新到磁盘

3、Purge Thread

主要用于回收事务已经提交了的undo log,在事务提交之后,undo log可能不用了,就用它来回收

4、Page Cleaner Thread

协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread 的工作压力,减少阻塞

事务原理

事务基础

MySQL事务相关的知识是很常考的,包括事务的四大特性,这一部分内容可以看我之前的文章:
MySQL进阶:MySQL事务、并发事务问题及隔离级别
简单总结一下,事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。
四大特性:

  • 原子性
  • 一致性
  • 隔离性
  • 持久性

要研究事务的原理,实际上就是研究MySQL的InnoDB引擎是如何保证事务的四大特性的。
对于这都四大特性,实际上分给2个部分:

1、原子性、一致性、持久性:由InnoDB的2份日志保证的,一份是redo log,一份是undo log
2、隔离性:通过数据库的锁以及MVCC来保证的

对于锁机制实现的隔离性,可以看之前的文章:
MySQL进阶:全局锁、表级锁、行级锁总结

这里主要就是来研究一下redolog,undolog以及MVCC。

redo log

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性。
该日志文件由两部分组成:重做日志缓冲(redo log buffer)以及重做日志文件(redo logfile),前者是在内存中,后者在磁盘中。当事务提交之后会把所有修改信息都存到该日志文件中, 用于在刷新脏页到磁盘,发生错误时, 进行数据恢复使用。

若没有redo log:

1、当我们在一个事务中,执行多个增删改的操作时,InnoDB引擎会先操作缓冲池中的数据,如果缓冲区没有对应的数据,会通过后台线程将磁盘中的数据加载出来,存放在缓冲区中,然后将缓冲池中的数据修改,修改后的数据页我们称为脏页。
2、脏页在一定的时机要通过后台线程刷新到磁盘中,从而保证缓冲区与磁盘的数据一致。
3、由于缓冲区的脏页数据并不是实时刷新的,而是一段时间之后将缓冲区的数据刷新到磁盘中,假如刷新到磁盘的过程出错了,而提示给用户事务提交成功,而数据却没有持久化下来,这就出现问题了,没有保证事务的持久性。

若有redo log:

当对缓冲区的数据进行增删改之后,会首先将操作的数据页的变化,记录在redo log buffer中。在事务提交时,会将redo log buffer中的数据刷新到redo log磁盘文件中。
过一段时间之后,如果刷新缓冲区的脏页到磁盘时,发生错误,此时就可以借助于redo log进行数据恢复,这样就保证了事务的持久性。
如果脏页成功刷新到磁盘或者涉及到的数据已经落盘,此时redolog就没有作用了,就可以删除了,所以存在的两个redolog文件是循环写的。

在这里我们看出内存中的redo log buffer和磁盘中的redo log是如何工作的,那么为何每次提交事务的时候,redo log buffer要刷新到磁盘中的redo log中,而不是直接刷新到磁盘中呢?

因为在业务操作中,我们操作数据一般都是随机读写磁盘的,而不是顺序读写磁盘。
而redo log是日志文件,都是顺序写的,顺序写的效率要远大于随机写,这种先写日志的方式,我们称之为WAL(Write-Ahead Logging)

undo log

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚(保证事务的原子性)和MVCC(多版本并发控制)。

undo log和redo log记录物理日志不一样,它是逻辑日志。可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然,当update一条记录时,它记录一条对应相反的update记录。当执行rollback时,就可以从undo log中的逻辑记录读取到相应的内容并进行回滚。

  • Undo log销毁:undo log在事务执行时产生,事务提交时,并不会立即删除undo log,因为这些日志可能还用于MVCC。
  • Undo log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的rollback segment回滚段中,内部包含1024个undo log segment

MVCC(高频面试)

基本概念

1、当前读
读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select … lock in share mode(共享锁),select … for update、update、insert、delete(排他锁)都是一种当前读。
在这里插入图片描述
可以看到,即便在默认的RR隔离级别下,事务A依然可以读取到事务B最新提交的内容,只是因为查询语句的后面加上了lock in share mode共享锁,此时是当前读操作。
同理,加上排他锁的时候,也是当前当前读操作。

2、快照读
简单的select(不加锁)就是快照读,读取的是记录数据的可见版本,很有可能是历史数据,不加锁,那么读操作不会被阻塞。

  • Read Committed:每次select,都生成一个快照读
  • Repeatable Read(默认):开启事务后第一个select语句才是快照读的地方,后续复用
  • Serializable:快照读会退化为当前读

在这里插入图片描述
可以看到,即便事务B提交了数据,事务A也查不到,就是因为select是快照读,在默认的RR隔离级别下,开启事务后第一个select语句才是快照读的地方,后面执行相同的select语句都是从快照中获取数据,可能不是最新数据(这样当然也保证了可重复读,看不懂很正常,读完后面的原理剖析就会明白)。
3、MVCC
全称 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,快照读为MySQL实现MVCC提供了一个非阻塞读功能。MVCC的具体实现,还需要依赖于数据库记录中的三个隐式字段、undo log日志、readView。

隐藏字段

介绍

在这里插入图片描述
当我们创建了上面的这张表,我们在查看表结构的时候,就可以显式的看到这三个字段。 实际上除了这三个字段以外,InnoDB还会自动的给我们添加三个隐藏字段及其含义分别是:

隐藏字段含义
DB_TRX_ID最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID(自增)
DB_ROLL_PTR回滚指针,指向这条记录的上一个版本,用于配合undo log,指向上一个版本
DB_ROW_ID隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段(非必须)

undo log版本链

介绍

回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
当insert的时候,产生的undo log日志只在回滚时需要,在事务提交后,可被立即删除。
而update、delete的时候,产生的undo log日志不仅在回滚时需要,在快照读时也需要,不会立即被删除。

版本链(核心重点)

在这里插入图片描述
对于这张表(加了两个隐藏字段),我们设置四个并发事务(2-5,事务1是创建这张表并赋予上述数据,已经commit了)同时操作这张表:

1、第一步
在这里插入图片描述
当事务2执行第一条修改语句时,会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
在这里插入图片描述
2、第二步
在这里插入图片描述
当事务3执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
在这里插入图片描述
第三步
在这里插入图片描述
当事务4执行第一条修改语句时,也会记录undo log日志,记录数据变更之前的样子; 然后更新记录,并且记录本次操作的事务ID,回滚指针,回滚指针用来指定如果发生回滚,回滚到哪一个版本。
在这里插入图片描述

最终我们发现,不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录。

那么简单的select语句(快照读)到底是怎么发现历史版本的呢?这需要使用ReadView(读视图)来实现

ReadView

这部分先看概念并自己尝试理解,很可能看不懂,也很正常,硬着头皮看下去自然会醍醐灌顶的。
ReadView(读视图)是快照读SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务id(就是还没有提交的事务的id)。
ReadView中包含了四个核心字段:

字段含义
m_ids当前活跃的事务ID集合
min_trx_id最小活跃事务ID
max_trx_id预分配事务ID,即当前最大事务的ID+1(事务ID是自增的)
creator_trx_idReadView创建者的事务ID

而在ReadView中就规定了undo log版本链的访问规则:

条件是否可以访问说明
trx_id==creator_trx_id可以访问该版本成立,说明数据是当前这个事务更改的
trx_id<min_trx_id可以访问该版本成立,说明数据已经提交了
trx_id>max_trx_id不可以访问成立,说明该事务是在ReadView生成后开启的
min_trx<=trx_id<=max_trx_id若trx_id不在m_ids中,则可以访问该版本成立,说明数据已经提交

上面的访问规则可以说是有点复杂的,需要剖析一下。

需要注意的是,不同的隔离级别,生成ReadView的时机是不一样的:

  • READ COMMITTED:在事务中每一次执行快照读时生成ReadView
  • REPEATABLE READ:仅在事务中第一次执行快照读时生成ReadView,后续复用该ReadView

ReadView原理剖析(重难点)

RC隔离级别

RC隔离级别下,在事务中每一次执行快照读时生成ReadView。
在这里,分析下上述例子中的事务5如何进行两次快照读的。

由于隔离级别为Read Committed,所以每一次进行快照读都会生成一个ReadView,那么两次生成的ReadView如下:
在这里插入图片描述
分析第一次快照读:
活跃的事务有3、4、5,显然min_trx_id=3,max_trx_id=6(5+1),当前事务的id为creator_trx_id=5,而此时的记录中的事务id为trx_id=4。
在这里插入图片描述

1、先匹配地址为0x00003这条记录,将4代入匹配规则,可以发现四条规则都不满足
2、匹配第二条,地址为0x00002的这条记录,对应的trx_id=3,代入匹配规则,四条规则都不满足
3、匹配第三条,地址为0x00001的这条记录,对应的trx_id=2,代入匹配规则,发现匹配规则②满足要求,所以,此次快照读的结果就是这条数据
而事实上,此时进行查询的时候,只有事务2是提交了的,因此只能快照读出事务2处理的历史数据。

分析第二次快照读:
在这里插入图片描述

1、先匹配最开始地址为0x00003的这条记录,这条记录对应的trx_id=4,代入匹配规则,都不满足
2、匹配地址为0x00002的这条记录,这条记录对应的trx_id=3,代入匹配规则,满足规则②,那么返回这条记录。
事实上,此次查询前,事务3已经提交了,快照读出来的就是事务3处理过后的数据。

RR隔离级别

RR是可重复读(解决了不可重复读的事务并发问题),也就是说,在一个事务中,执行两次相同的select语句,查询到的结果是一样的。

那么RR隔离级别是如何解决的不可重复读呢?就是因为RR隔离级别下,只在事务中第一次执行快照读时生成ReadView,后续会复用该ReadView:
在这里插入图片描述
那么,容易知道,既然ReadView都一样, ReadView的版本链匹配规则也一样, 那么最终快照读返回的结果也是一样的。

总之,MVCC的实现原理就是通过InnoDB表的隐藏字段、UndoLog 版本链、ReadView来实现的。
而MVCC+锁,则实现了事务的隔离性。而一致性则是由redolog与undolog保证的。

  • 22
    点赞
  • 34
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

布布要成为最负责的男人

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值