深入MySQL:走进存储引擎-InnoDB[#3]

997 阅读17分钟

前言

接着上一篇sql语句的执行过程原理,我们开始接触到了存储引擎,同时也已经了解存储引擎的作用,它是数据存储与查询的底层支撑。

我们知道MySQL的存储引擎设计是基于插件式架构,它支持了很多不同存储引擎的实现,像InnoDB、MyISAM、CSV、Memory等等,而他们各自的优缺点及适用场景前文也已经介绍过了。

InnoDB

虽然存储引擎的种类比较多,但是我们不用每个都深入了解,先挑一个重点且优先级高的来,举一反三,所以接下来我们会基于InnoDB存储引擎来深入了解其架构原理。

那为什么会先选择它呢?

因为它是MySQL 5.5.8及之后版本的默认存储引擎,这也代表了相对于其他存储引擎,它是更高可靠性和更高性能的通用存储引擎。而实际开发中我们大多数业务场景下的表设计都是使用它作为数据的底层支撑,所以要对MySQL进行性能调优的话,对它一知半解是不行,必须弄懂它,搞透它。

重要特性

在深入分析InnoDB之前,首先我们得先知道它实现的存储引擎具有哪些特性,这样我们才更容易理解其中原理实现的目的。以下摘自官方文档(MySQL版本5.7):

特性是否支持
B树索引
备份/时间点恢复(在服务器中实现,而不是在存储引擎中。)
集群数据库支持
聚集索引
压缩数据
数据缓存
加密数据是(通过加密函数在服务器中实现;在 MySQL 5.7 及更高版本中,支持静态数据加密。)
外键支持
全文检索索引是(MySQL 5.6 及更高版本支持 FULLTEXT 索引。)
地理空间数据类型支持
地理空间索引支持是(MySQL 5.7 及更高版本支持地理空间索引。)
哈希索引否(InnoDB 在内部利用哈希索引来实现其自适应哈希索引功能。)
索引缓存
锁定粒度
MVCC
复制支持(在服务器中实现,而不是在存储引擎中。)
存储限制64TB
T树索引
事务

除此之外,还有其他一些版本迭代中新增、废弃或删除的内容,这里就不详列了,感兴趣可以查阅官方文档

同时相信这些特性实际开发中我们或多或少都已经接触或使用过。不过归类总结下来,最为直接关注的就是以下几个点:

  • 事务
  • 索引
  • 数据复制
  • 数据备份

这些功能的原理后面都会一一深入,但是在开始这些之前,我们必须先得了解InnoDB的基础架构设计。

架构设计

我们先来看看InnoDB的架构图,如下:

image-20211029102230676

首先不说能不能看懂它的架构组成以及里面的执行流程,但是我们最为直观的了解就是:原来数据是从内存结构通过操作系统缓存(从用户态到内核态的切换),再写到磁盘结构进行存储的。

内存结构

通过架构图我们能看到,在数据落入到磁盘之前,会先经过InnoDB的内存结构进行操作,其中内存里面有下面几种重要的结构。

**Buffer Pool:**缓冲池,首先InnoDB的磁盘存储数据的最小单位为页,包括索引页和数据页等。而对于数据的读写,如果直接操作磁盘则速度过慢,所以这里使用了缓冲池的技术来提升性能。读操作时,会先将磁盘读到的页数据保存在缓冲池中,下一次读取如果缓冲池中存在的则直接返回,无需再访问磁盘。写操作时,会先写缓冲池中的页数据(这时缓冲池中的数据会和磁盘上的数据不一致,称之为脏页),然后InnoDB中会有专门的后台线程(Master Thread)每隔一定时间(Checkpoint机制)再把缓冲池中的页数据一次性异步刷新写入到磁盘中(刷脏)。

而缓存池中缓存的数据页类型包括:索引页、数据页、undo页、自适应哈希索引(Adaptive Hash Index)、写缓冲(Change buffer)、锁信息等等,其中索引页和数据页占用空间较大。这里我们先介绍下自适应哈希索引和写缓冲,

  • Adaptive Hash Index: 首先我们知道通过哈希索引去查找数据速度是非常快的,因为它的时间复杂度为O(1),仅需要一次查找。那什么是自适应呢?很好理解,InnoDB会根据访问的频率和模式自动为这些页建立哈希索引,前提是对这个页的连续访问模式(查询条件)必须是一样的,且只能用于等值查询。所以它的作用不明而喻,就是根据sql的执行频率和条件来自动为这些访问的页建立哈希索引,优化缓冲池中页的查找速度。

  • Change buffer: MySQL5.5版本之前叫做Inert buffer(插入缓冲),现在也支持delete和update操作,包含了insert buffer、update buffer、delete buffer,所以称为写缓冲。它作用的前提是:数据的索引不是唯一(unique)的,也就是说它是对于具有二级索引(非主键唯一索引外的其他索引,又叫辅助索引)的数据的优化。再对数据进行频繁insert、update、delete操作(密集性工作且写多读少)时,如果数据表含有较多的二级索引,直接写操作磁盘,会带来大量的I/O消耗用于更新二级索引,所以通过将数据二级索引的更改缓存在内存中进行记录,之后后台线程(Master Thread)会进行合并异步写入到磁盘中,从而减少大量额外的开销。

    其中满足以下三种条件时都会触发合并写入:1、后台线程定时刷盘;2、数据库shut down;3、redo log写满时。

Log Buffer: 日志缓冲区,又叫redo log buffer。前面已经知道了几种不同buffer的作用,其实这里也是一样,都是避免频繁地直接操作磁盘带来大量的I/O开销,从而降低数据操作的效率,显著提升吞吐量;而这里的Log buffer则是用来针对于redo log的操作优化。那redo log又是什么呢?这里暂时先不说,等下面讲完磁盘中的redo log结构,我们就知道它的作用了。

这里log buffer可以通过参数innodb_flush_log_at_trx_commit设置三种不同的刷盘机制:

  1. 默认为1时,日志在每次事务提交时写入并刷新到磁盘,保证事务ACID中的持久性。
  2. 设置为0时,每秒将日志写入并刷新到磁盘一次。
  3. 设置为2时,日志在每次事务提交后写入操作系统缓存,并每秒刷新到磁盘一次。

image-20211102152853508

其中对于设置为0和2时,都可能在数据库宕机的情况下导致未刷盘的日志丢失,但是设置为2时的区别在于,如果数据库宕机时但服务器未宕机,依然可以通过操作系统本身的系统缓存中的日志来恢复数据;而丢失的数据范围则根据日志的刷新频率值innodb-flush-log-at-timeout相关。

综上所述,我们能明白这些结构都是分布在内存空间中,那在某些场景下必然都会受限于所分配的内存空间大小,那如果我们需要对其进行优化,就可以通过一些它们的相应参数设置来调整不同buffer的空间大小从而节省磁盘I/O;除此之外,内存不够的情况下为了保证缓存的利用效率,也会进行内存淘汰,我们知道内存淘汰机制有很多种,随机、LRU、LFU等等,而MySQL这里是基于LRU算法的变体来实现的。同时由于都是通过后台线程进行异步刷盘,那么根据不同的场景下能够满足的需求进行相应刷盘策略的调整来提升吞吐量也是优化的一种手段,而具体支持的参数设置可以参考官方文档说明。

磁盘结构

说完内存结构,接下来我们来看看InnoDB的磁盘结构,既然是磁盘结构,那么我们得明白这些磁盘内不同的结构的存在必然是以文件形式进行展现。通过InnoDB存储引擎的架构图,我们能看到这里面主要可以两类:表空间和Redo log。

redo log: 我们先来说下redo log,它叫做重做日志。而前面我们知道了Buffer pool的作用和原理,这里我们假设一种情况,当Buffer pool中缓存的脏页数据还没有被后台线程异步写到磁盘上或写到一半时,数据库宕机,因为这时候数据还是缓存在内存中,那这种情况下数据岂不是会丢失不可用?

所以为了避免这个问题,InnoDB会把所有对页的修改操作单独写入到一个日志文件进行记录,以便于数据库重启恢复时,可以从这个日志文件进行恢复操作,进而利用它来实现crash-safe机制,保证事务的持久性。

同时这种先写日志,再写磁盘的设计,其实就是MySQL的WAL(Write-Ahead Logging)预写日志机制,那同样是写入到磁盘,为什么不直接写db文件,而要先写日志文件呢?这其实是因为写db文件是随机IO,而写日志文件是顺序IO,这样设计不但解决直接写db文件效率慢,进而导致吞吐量降低的问题,还为数据提供了可恢复的副本。

如果你打开对应存放的/mysql目录下,你能看到命名为ib_logfile0的日志文件就是我们的Redo log,默认每个文件为48M,超过大小则会顺序创建下一个ib_logfile文件。

表空间: 再看看表空间,对于InnoDB存储引擎来说,它非常重要,是所有数据存储的地方,我们能够看到它分为几种不同的表空间,分别为系统表空间、独占表空、通用表空间、临时表空间、Undo Log表空间,而他们的意义也很容易理解。

  • 系统表空间:默认创建的一个共享表空间,对应文件为/mysql/ibdata1,它也是数据字典、双写缓冲区、写缓冲区和Undo log的存储区域,这代表了它是其他表空间的基础,保存了数据的一些重要基本信息。
  • 独占表空间:它是默认的表空间类型,可以通过参数innodb_file_per_table开启和关闭,每张表在创建时会隐式使用,都会单独创建占用一个表空间,而非共享一个表空间,以user表为例,会生成一个user_innodb.ibd文件去单独保存user表中的索引和数据,但是其他类的数据,像undo log、双写缓冲区、change buffer还是保存在系统表空间中。
  • 通用表空间:它与系统表空间类似,都是一种共享的表空间,但是它可以指定创建不同的表来共享同一个表空间,而这些表的数据也将保存在同一个.ibd文件中,同时不同表空间的表数据也是可以移动的。
  • 临时表空间:它是用来存储用户创建和磁盘内部的临时表数据,生成的文件格式为ibtmp,而临时表空间在MySQL正常关闭时会被删除,同时每次服务器启动时都会重新创建。
  • Undo Log表空间:它是用来保存undo log数据的,默认是存储在系统表空间中的,但是因为系统表空间的大小是无法收缩的(无用数据所占用的空间只会被标记为可用空间,而不是回收),所以我们可以通过innodb_undo_directory配置指定存储在一个或多个的undo表空间中,文件命名为undo_001

我们能知道这些不同的表空间其实就是用来存储不同数据的区域或者说文件,那么这些表空间的内部逻辑存储结构又是怎么样呢?

其实表空间是由segment(段)、extent(区)、page(页)、row(行)4个部分组成的,从前往后,依次是包含关系,它们的组成图如下:

image-20211103090950410

  • segment段:表空间由各个segment组成,像数据段、索引段、回滚段等,其中数据段就是B+树索引的叶子结点,索引段为B+树索引的非叶子结点(这个InnoDB的索引结构相关,后续会深入索引原理),回滚段则是存储undo log的区域。
  • extent区:InnoDB一次最多可以在一个segment中分配4个extent,默认每个extent大小为1MB,而扩展大小根据页的大小有关。
  • page页:InnoDB中磁盘存储的最小单位,默认每个page大小为16KB,所以默认一个extent中有64个page,还可以通过参数innodb_page_size将page的大小减少为8KB或者4KB,这样的话,每个extent就对应128个或256个page。
  • row行:最终落实到具体的数据则是按行进行存储的,而行的数量受限于page的大小限制,同时我们在设计表时会给每个字段定义不同的数据格式,通过每个字段所占的字节大小计算总和,我们就能大概计算出一个page下能最多保存多少row的数据。

通过上面的介绍,我们已经非常清楚InnoDB存储引擎的内部结构及组成,但是系统表空间中的存储的一些重要基础数据我们还没深入了解,像数据字典、双写缓冲区、Change buffer和Undo log,那他们的作用是什么呢?

  • 数据字典:是用来存储一些元数据的内部系统表,包括用户定义的表、列、索引等信息。
  • 双写缓冲区:它的作用是配合redo log一起来保障数据的恢复,从而保证数据的持久性。而redo log的作用我们已经知道了,为什么还需要双写缓冲区去配合呢?这是因为,我们知道InnoDB的磁盘存储的结构是页,默认大小是16KB,但是操作系统的文件管理的数据页并不是这样,而是大小只有4K,这意味着每个InnoDB页的写满需要操作系统进行4次写入操作。假设操作系统正在往InnoDB的页写入数据时,比如刚写了4K就崩溃宕机了,这种情况会造成InnoDB页数据的损坏失效,这时候通过redo log进行恢复时也无法恢复刚刚失效的那个页所存储的数据,所以这里需要通过双写缓冲来提前写入一个页的副本,那这种情况下就可以通过redo log配合副本页来完成数据的恢复,这样才能可靠的保证数据的持久性。 这种机制在InnoDB中是默认启用的,虽然这样保证了数据恢复的可高性,但是你可能有个疑问,这样没份数据都要写入两份,岂不是需要两倍的IO操作,不会明显降低系统吞吐量吗?其实不然,它和Log buffer一样,一部分在内存中,一部分在磁盘上,这种写入操作是顺序IO,所以不会占用很大的IO资源开销。
  • Change buffer:上面已经详细介绍过了。
  • Undo log:回滚日志,它的作用是记录事务提交之前的数据操作记录,如果事务提交前发生异常(不管是业务层异常还是服务器宕机),就可以利用undo log来回滚数据到事务开始前的状态,进而保证数据的原子性。所以它其实是和事务相关的,等到深入事务原理的时候我们再来谈谈它。

更新sql的执行

通过上面的深入分析,相信我们已经很清楚InnoDB的内部架构设计,对我们来说,它已经不再神秘,但是跳出来想想,我们好像对这些架构在sql的实际执行过程中是怎么串起来的还是有点模糊。在上一篇文章中,我们以查询sql为例,了解了sql的执行原理,同时其中关于存储引擎方面的并没有细说,那么接下来我们就以更新sql为例,来把InnoDB的架构设计串联起来。

我们知道,更新sql和查询sql一样,都是会经过解析器解析处理,再到优化器优化转换生成执行计划,最后交给查询执行引擎来调用存储引擎API执行。而他们的区别就在于存储引擎不同的实现处理。

假设现在需要执行了一个update语句,

update user set name = "风动草" where id = 1

那么它的执行流程具体是怎么样的呢?我们先看图,

image-20211103135107300

大概流程说明如下:

  1. 首先MySQL的server层会隐式调用存储引擎API去开启事务;
  2. 接着调用存储引擎执行sql的API,先尝试命中Buffer pool中的需要修改的原数据的自适应索引,无法命中则从数据页中查找,如果依然没有,则会调用磁盘结构中的表空间查找,并缓存在Buffer pool的数据页中;
  3. 然后先修改Buffer pool的数据页中的数据,再将操作记录写入到Log buffer,Log buffer中的数据再通过异步刷盘到磁盘的redo log文件中,此时的数据状态为prepare;
  4. 更新完成后会通知server层,这次会将数据记录到binlog中,然后调用存储引擎API来提交事务;
  5. 提交事务后,会再次写入数据到Log buffer,并通过异步刷盘到磁盘的redo log文件中,此时的数据状态为commit;
  6. 最后事务提交完成后也就代表更新sql执行结束,server层返回给客户端更新成功的消息。
  7. 而内存Buffer pool中存储的数据页、索引页、change buffer等都会有专门的后台线程定时异步刷盘到磁盘的表空间中。

这其中server层涉及到了另一个日志文件:binlog,它是以事件的形式记录了所有的DDL和DML语句,可以通过开启生成二进制的binlog文件,我们可以利用它进行数据的恢复,同时主从复制的功能就是依赖于它实现的。

总结

InnoDB存储引擎的原理不仅仅就只有这些,还有很多特性的实现原理还没有深入,但是通过上面这些,我们其实已经掌握了InnoDB的基础架构原理,相信再去看在其基础上封装和扩展的功能会更加融会贯通。

文章中如有不妥之处,请批评指正,非常感谢。


把一件事做到极致就是天分!