「MySQL高级篇」详解InnoDB存储引擎

404 阅读16分钟

开启掘金成长之旅!这是我参与「掘金日新计划 · 12 月更文挑战」的第六天,点击查看活动详情

大家好,我是Zhan,一名个人练习时长一年半的大二后台练习生,最近在学MySQL高级篇,欢迎各路大佬一起交流讨论

👉本篇速览

在前面对存储引擎的的学习中,我们学习到了MySQL数据库服务器的体系结构、各种存储引擎的特点、区别以及使用场景,当时我们也提到了:InnoDB是我们最常用、默认的存储引擎,也介绍了InnoDB存储引擎的一些特点:能保证事务、支持外键、支持行锁,其中行锁我们在锁的那一篇有讲到,而事务原理以及InnnoDB的架构是什么样的呢,而本篇将从以下三个方面,深入了解InnoDB存储引擎:

  • 1️⃣ 首先回顾并补充一下InnoDB存储引擎的逻辑存储结构
  • 2️⃣ 然后谈谈InnoDB存储引擎的内存、磁盘结构以及线程管理
  • 3️⃣ 最后就从底层来看看,InnoDB存储引擎是如何实现事务

1️⃣ 逻辑存储结构

在讲存储引擎以及索引的时候,我们有提到过InnoDB存储引擎的逻辑存储结构,这里我们回顾并补充一下InnoDB逻辑存储机构的相关知识:

  1. Tablespace:表空间,也就是最后会生成一个IBD文件,我们在存储引擎那一章节有讲过,一个MySQL实例可以对应多个表空间,用于存储记录、索引等数据
  2. Segment:段,分为数据段、索引段、回滚段,InnoDB的索引我们也有聊过,它使用的B+Tree结构,其中分支结点就是索引段,叶子结点就是数据段,段用来管理多个区
  3. Extent:区,表空间的单元结构,每个区的大小为1M,在默认情况下,InnoDB存储引擎的页的大小默认为16K,也就是说,一个区里面一共有64个连续的页
  4. Page:页,是InnoDB存储引擎磁盘管理的最小单元,每隔页的大小默认为16KB。为了保证页的连续性,InnoDB存储引擎每次从磁盘申请4-5个区。
  5. Row:行,InnoDB存储引擎中的数据就是按行进行存放的。在行中我们会发现有两个隐藏字段:Trx_id,Roll_pointer,其中Trx_id就是最后一次事务的id,Roll_pointer类似于一个指针,通过这个指针我们能找到在增删改之前的数据,我们会在后续MVCC中详细讲解。

2️⃣ InnoDB架构

MySQL5.5版本开始,默认使用InnoDB存储引擎,因为它擅长事务处理以及崩溃恢复的特性,在日常的使用中使用十分广泛,让我们一起走进InnoDB存储引擎的架构:

该图为InnoDB存储引擎的架构图,左侧的是内存结构,其中有许多的缓冲区。右侧为磁盘结构,其中有表空间、Redo Log、双写缓冲区……。我们从内存结构开始了解:


⛏ 内存结构

对于一台MySQL服务器,通常来说,80%的内存都会分配给缓冲区,以提高MySQL的执行效率

单看内存结构,内存结构中标注了四块区域:Buffer Pool缓冲池、Change Buffer缓冲区、Log Buffer日志缓冲区、Adaptive Hash Index自适应哈希索引。我们分别来谈谈这四块区域分别的作用:

🔴 Buffer Pool

Buffer Pool:缓冲池是主内存中的一块区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改查操作的时候,先操作的就是内存中缓冲池的数据,如果缓冲池没有数据,就从磁盘加载然后缓存起来,然后以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度。

如果没有缓冲池,那么我们进行增删改查的时候,每次数据库都需要操作磁盘空间,就会存在大量的磁盘IO,在业务系统比较复杂的场景,产生的IO还是随机IO

在图中我们可以发现里面有一个一个的块,实际上它是一个一个的页,底层它采用链表数据结构管理Page。根据状态,可以把Page分为三种类型(也就是图中的不同的三种颜色):

  1. free page:空闲page,即申请了这部分空间,但是还没有使用
  2. clean page:尽管该Page被使用过,但是它的数据没有被修改,即还可用
  3. dirty page:脏页,数据被修改过,页中的数据与磁盘的数据不一致


🟠 Change Buffer

Change Buffer:更改缓冲区,实际上是在8.0引入的,之前是Update,它针对于非唯一的二级索引,即主键索引、唯一索引,不会操作该区域。

当我们要更改一条数据,而该数据不在Buffer Pool中存在的时候,我们不会直接操作磁盘,而会把数据变更存储在更改缓冲区中,在后续数据被读取的时候,再把数据合并恢复到Buffer Pool中,再将合并后的数据刷新到磁盘中

为什么它只针对于非唯一的二级索引呢Change Buffer存在的意义又是什么呢?我们以下面这个场景来介绍一下:

与聚集索引不同的是,在一般情况下,二级索引并不唯一,并且插入索引的顺序并不确定,很有可能乱序,同时,删除和更新会影响到索引树中不相邻的二级索引页,如果每一次都操作磁盘,就会造成大量的磁盘IO,有了Change Buffer存在,就可以在缓冲池中做合并处理、减少磁盘IO。


🟡 Log Buffer

Log Buffer:日志缓冲区,用来保存要写入磁盘中的log日志数据(redo log, undo log),这两个日志十分重要,下面我们会详细讲解。该缓冲区的默认大小为16MB,日志缓冲区的日志会定期刷新到磁盘中。如果需要更新、插入、删除许多行的事务,增加日志缓冲区的大小可以节省磁盘IO。

参数

  1. innodb_log_buffer_size:默认缓冲区大小
  2. innodb_flush_log_at_timeout:日志刷新的时机,默认为1
    1. 1:表示日志在每次事务提交时写入并刷新到磁盘
    2. 0:表示每秒把日志写入并刷新到磁盘一次
    3. 2:表示日志在每次事务提交后写入,并每秒刷新到磁盘一次

🟢 Adaptive Hash Index

在之前讲解索引的时候,我们就提到过:InnoDB中具有自适应Hash功能,hash索引是存储引擎根据B+Tree索引在指定条件下自动构建的。这里来填坑:

Adaptive Hash Index:自适应Hash索引,用于优化Buffer Pool数据的查询。InnoDB存储引擎会监听各个表上索引页的查询,如果观察到用Hash索引可以提升速度,则建立Hash索引,因此我们称它为自适应Hash索引

自适应Hash索引默认是开启的,它的开关的参数为adaptive_hash_index,默认为ON


⚒ 磁盘结构

磁盘结构看图的时候可能看起来比较复杂比较多,实际上它确实比较复杂比较多hhh,我们还是和内存结构一样,一一来了解它的区域

🌳 System Tablespace

System Tablespace:系统表空间,是更改缓冲区Change Buffer的存储区域,同样的,它也是在MySQL8.0后重新规划的,在MySQL5.x的版本中还包含InnoDB数据字典、undolog等信息。

如果InnoDB引擎中每张表的独立表空间都关闭的话,那么所有的表的数据和索引都存储在系统表空间。

参数

  1. innodb_data_file_path:系统表空间文件路径

☘ File-Per-Table Tablespaces

File-Per-Table Tablespaces:每个表的文件表空间包含单个InnoDB表的数据和索引,并存储在文件系统中单个数据文件中,也就是我们之前提到的IBD文件

参数:

  1. innodb_file_per_table:表示每张表一个独立表空间的开关。如果开启了,就代表每张表有一个独立的表空间,不会存放在系统表中。

🌲 General Tablespaces

General Tablespaces:通用表空间,需要通过CREATE TABLESPACE命令来创建通用表空间,在创建的时候,可以指定该表的空间:

CREATE TABLESPACE 表空间名字 ADD DATATILE 表空间关联的表空间文件 ENGINE = 存储引擎

在这里做一个示范: CREATE TABLESPACE Zhan ADD DATAFILE 'Zhan.ibd' ENGINE = innodb

这样我们就创建好了一个表空间,那么这个通用表空间有什么用呢?

我们可以在创建表的时候指定一个表的空间,语法为:

CREATE TABLE xxx TABLESPACE 表空间名字

例如:CREATE TABLE user(id int primary key auto_increment, name varchar(10)) TABLESPACE Zhan


🥦 Undo Tablespaces

Undo Tablespaces:撤销表空间,MySQL实例在初始化的时候会自动创建两个默认的undo表空间(初始大小为16M),用于存储undo log日志,这是我们第二次提到undo log,这对于我们的事务有很重要的作用,我们会在下面的事务原理中具体介绍。


🍀 Temporary Tablespaces

Temporary Tablespaces:InnoDB使用会话临时表空间和全局临时表空间,主要存储用户在会话中创建的临时表


🌴 Doublewrite Buffer Files

Doublewrite Buffer Files:双写缓冲区,我们在讲内存结构的时候提到了Buffer Pool,而把数据从缓存刷到磁盘前,首先把数据写入双写缓冲区文件中,便于系统异常时恢复数据,保证数据的安全性。

双写缓冲区文件#ib_16384_0.dblwr#ib_16384_1.dblwr


🎄 Redo Log

Redo Log:重做日志,是用来实现事务的持久性,我们在下文中讲事务原理时会具体讲解如何实现。

该日志由两部分组成:重做日志缓冲以及重做日志文件,前者在内存中,后者在磁盘中。

当事务提交之后会把所有修改信息存在该日志中,用于在刷新脏页到磁盘时发生错误,进行数据恢复使用。它不会永久保存,会隔一段时间清掉长时间没有使用的文件。


🛠 后台线程

我们了解了内存结构和磁盘结构,而内存中的数据需要刷新到磁盘中,它是怎么从内存到磁盘刷新的呢?这就不得不提到MySQL的后台线程:

后台线程的作用就是把InnoDB存储引擎缓冲池中的数据在合适的时间刷到磁盘的存储文件中,根据后台线程的作用,我们可以把它分为四类:

  1. Master Thread:核心后台线程,负责调度其他线程,还负责把缓冲池中的数据异步刷新到磁盘中,保证数据的一致性,同时它的作用还有:脏页的刷新、合并插入缓存、undo页的回收
  2. IO Thread:在InnoDB存储引擎中,使用的是AIO(异步非阻塞IO)来处理IO请求,这样可以极大的提高数据库的性能,而IO Thread主要负责这些IO请求的回调,主要有下面这四类: | 线程类型 | 默认个数 | 职责 | | -------------------- | -------- | -------------------------- | | Read Thread | 4 | 负责读操作 | | Write Thread | 4 | 负责写操作 | | Log Thread | 1 | 负责把日志缓冲区刷新到磁盘 | | Insert buffer thread | 1 | 负责把写缓冲区内容刷新到磁盘 |

我们可以通过查看InnoDB存储引擎状态来查看这四个线程:

  1. Pure Thread:主要用于回收事务已经提交了的undo log,上面我们有提到,在事务提交后undo log就没有意义了,于是我们就用这个线程来回收。
  2. Page Cleaner Thread:协助 Master Thread 刷新脏页到磁盘的线程,它可以减轻 Master Thread的工作压力,减少阻塞

3️⃣ 事务原理

InnoDB存储引擎很重要的一个特性就是:支持事务,但是它底层具体是怎么实现的呢?我们将在这里详细介绍,在具体讲解之前我们先做一个简单的回顾:

🎯 知识点回顾

  1. 事务:事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或者回滚,也就是说要么同时成功,要么同时失败。
  2. 事务的四大特性:ACID
    1. 原子性(Atomicity):事务是一个不可分割的最小工作单元,要么全部成功、要么全部失败
    2. 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态
    3. 隔离性(Isolation):数据库系统提供隔离机制,保证事务在不受外部并发操作影响的独立环境下运行
    4. 持久性(Durability):事务一旦提交,它对数据库的数据的改变就是永久的

其实我们要支持事务,那么就要保证事务的四大特性,我们这里简单把四种特性的实现分个类:

  • MySQL底层的原子性、一致性、持久性是由redo log, undo log实现的
  • 隔离性我们在看到的时候就很容易想到,当然除此之外还需要一个,就是MVCC,这是今天第很多次提到MVCC了,我们在明天的文章中会详解MVCC 今天的文章,我们先讲解redo log、undo log怎么去实现事务的原子性、一致性、持久性,明天我们再一起来探究MVCC与事务的隔离性

📗 redo log

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

那么它是如何做到在刷新脏页到磁盘出错时做出补救措施的呢?

我们先看看刷新脏页到磁盘这个过程发生错误的后果:

我们在对缓冲池做更新操作后,数据页变成了脏页,InnoDB存储引擎会定期去把脏页的数据刷到磁盘中,但是,如果刷新到磁盘中这个过程出了问题,就会出现:已提交的事务修改的数据实际上没有改变数据库磁盘中的文件,也就无法保证事务的持久性,那么此时就需要redo log。有了redo log,这个流程就变成了:

当我们对缓冲区的数据做了增删改后,首先会把增删改的数据记录在Redo log buffer中,在事务提交的时候,会把Redo log buffer中的数据页变化刷新到磁盘中,持久化的保存在磁盘中

如果现在脏页刷新出错,我们可以通过Redo log进行恢复,因为Redo Log中记录了单次数据的变化,因此我们可以通过它进行数据的恢复

那我们为什么每次提交的时候要把Redo Log刷新到磁盘当中?为什么不直接把变更的数据刷新到磁盘呢?

如果这么做了,会存在严重的性能问题,在事务当中,进行一组操作的时候,那么会操作很多条记录,这些记录会随机的操作数据页,造成大量的随机磁盘IO,性能很低

如果我们进行操作的时候用到了Redo Log,在事务提交的时候先异步刷新Redo Log,由于它是Log 日志文件,日志文件是追加的,那么它就是顺序磁盘IO,性能高于直接把变更的数据刷新到磁盘。这种机制我们称为:WAL(Write-Ahead Logging)


由于在脏数据提交后,日志文件就不再有意义,因此我们在磁盘中是循环写的方式,以达到定期清理日志文件的目的:


📘 undo log

undo log:回滚日志,用于记录数据被修改前的信息,作用有两个:提供回滚和MVCC

undo log 和 redo log记录物理日志不一样,它是逻辑日志。比如说:当删除一条记录的时候,undo log会记录一条插入信息,也就是恢复该数据的SQL语句,在执行ROLLBACK的时候,可以从undo log中的逻辑记录读取到响应的内容并进行回滚。

Undo Log销毁:undo log在事务执行时产生,事务提交是,并不会立即删除undo log,因为这些日志还可能用于MVCC

Undo Log存储:undo log采用段的方式进行管理和记录,存放在前面介绍的ROLLBACK Segment中,内部包含1024个undo log segment


💬 总结

本篇我们首先回顾了MySQL的逻辑存储结构,补充了MySQL的表空间、段、区、页、行的知识点。

然后分别从内存、磁盘、线程三个方面解读了InnoDB的架构,并以那张图为基点,分别去分析图中的各个区域的作用,了解到InnoDB存储引擎的存储数据、更新数据磁盘、内存的关系。

然后讲了InnoDB最大的特性:支持事务,然后讲解了事务了持久性、原子性、一致性的底层原理,也就是两个日志的底层。


🎁 下期预告

在本文中我们一直提到MVCC,在最后我们也提到,事务的隔离性是由锁和MVCC共同维护的,在明天的文章中我们将详细聊聊,这个一直出现的神秘角色~


🍁 友链


✒写在最后

都看到这里啦~,给个点赞再走呗~,也欢迎各位大佬指正,在评论区一起交流,共同进步!也欢迎加微信一起交流:Goldfish7710。咱们明天见~

求赞.jpeg