MySQL 作为一种典型的OLTP数据库事务处理功能必不可少。从上一篇文章中,我们已经知道MySQL 中的引擎是一种插件体系。虽然MySQL有着众多的引擎,但有InnoDB引擎支持事务,随着MySQL的发展,InnoDB 进行了大量的优化,可以说当前 InnoDB 引擎已经可以适合90%以上的 MySQL 使用场景。作为 MySQL 中最重要的引擎,我们有必要对它进行深入的了解。
从文件结构讲起
通过之前的MySQL插件机制我们可以发现:MySQL引擎作用于表,不同的表可以设置不同的引擎。当我们打开mysql 的数据存储文件夹的时候我们可以看到类似这样的文件:

文件类型 | 作用 |
---|---|
auto.cnf | 存储了server-uuid的值,用于标识MySQL实例在集群中的唯一性,一般在主从复制中起唯一标识服务节点的作用 |
ib_buffer_pool | 热点数据缓冲池,MySQL启动的时候会将热点数据预热到该缓冲池中 |
ibdata1 | InnoDB的共享表空间,如若 MySQL 没有开启独享表空间那么表空间将会放在这个文件里面,文件里面记录着表中的数据、索引、Change Buffer、undo 日志、redo 日志等等 |
ib_logfile0,ib_logfile1 | InnoDB 重做日志,ib_logfile0,ib_logfile1组成了一个重做日志组 |
ibtmp1 | 临时表空间,用于存储临时表数据,MySQL5.7 后的新特性 |
VM_16_14_centos | pid 文件 |
t1.frm | 表结构定义文件,记录着表的结构定义 |
t1.ibd | InnoDB 独立表空间 |
InnoDB 逻辑存储结构
从上面我们可以看到 MySQL 中光数据文件已经有这么多种,除了数据文件外还有日志文件如binlog、error.log、slow.log等等。那么他们在 MySQL 中又是如何配合的?要想了解整个过程,我们先来看一下 InnoDB 的逻辑存储结构:

类型 | 作用 |
---|---|
表空间 | 表空间是InnoDB中的最高抽象层次,所有的数据都放在表空间中。里面包含了数据、所有、ChangeBuffer、undo、事务、Double Write Buffer 等等信息。我们知道 InnoDB 采用的是聚集索引来组织数据结构,数据结构为B+ Tree。表空间又可以分为系统表空间、独立表空间、通用表空间、undo 表空间以及临时表空间。 |
段 | 表空间又是由段组成的,段通常又分为数据段、索引段、回滚段等等。对于数据段它就对应于 B+ 树上的的叶子节点、索引段对应于 B+ 树上的非叶子节点。 |
区 | 区是由连续页组成的空间,任何情况下区的大小均为1MB,默认情况下InnDB 引擎页的大小为16KB,因此一个区默认情况下一共有 64 个页。 |
页 | 页是InnDB 磁盘管理的最小单位,默认为16KB。 |
行 | 我们数据库中的数据最终是以行的形式存放在页中,具体格式可以参考:《MySQL InnoDB 行记录格式(ROW_FORMAT)》 |
InnoDB 内存结构
看完上述 InnoDB 的物理结构与逻辑结构,如果每次都进行磁盘数据加载,那么效率必定十分低下, InnoDB 又是如何利用内存的呢?先来看看 InnoDB 的内存结构:

LRU List,Free List 、Flush List
InnoDB 使用了 LRU算法来进行数据淘汰,我们知道 LRU 算法淘汰了最近最少使用的数据,将最新的数据插入到了LRU首部,如果采用经典LRU算法,某次请求加载的页比较多,放在首部会导致热点数据被淘汰,影响缓存使用效率。因此 MySQL 在LRU 上引入了 midpoint 机制,默认 midpoint 为LRU List 的 5/8,即新入的数据插入到了LRU List 的5/8处,来避免热点数据被刷出。
当MySQL刚刚启动的时候LRU List是空的,InnoDB会到 Free List中查找所需页,找到后会将该页从Free List中移动至LRU List。
LRU List 中的页被修改后叫做脏页(Dirty Page),InnoDB 的脏页修改是在内存中进行的, 之后通过 CheckPoint 的机制将脏页刷回磁盘。刷新过程中脏页存放在Flush List中,需要指出的是脏页既存在于LRU List 中,也存在于Flush List中。
Check Point 机制
InnoDB出于性能的考虑,所有的更改操作都是在内存上进行的,之后通过批量处理的方式将脏页刷新回磁盘。InnoDB 数据完整性是通过重做日志来保证,假使我们的 MySQL 运行了一段时间,产生了大量的数据,突然有一天发生了宕机,我们重启数据库的时候 MySQL 就需要根据redo log 来恢复我们的数据,如果这个数据量比较大,那么我们可能需要很长的一个恢复时间,为了解决这个问题,InnoDB 引入了Check Point 技术。当数据库宕机恢复时,不需要依据所有的redo log 进行恢复,只需要找到上一个检查点,从这个检查点开始进行数据恢复。InnoDB中共有如下两种CheckPoint:
类型 | 简介 |
---|---|
Sharp CheckPoint | 全部脏页刷盘,一般发生在数据库关闭时 |
Fuzzy CheckPoint | 部分脏页刷盘,MySQL 会依据实际情况,选取一部分脏页异步刷回磁盘 |
自适应哈希索引
InnoDB 内部会对表上的所有索引页的查询使用情况进行监控统计,如果观察到某些数据访问非常频繁,InnoDB会对这些数据建立哈希索引来提升性能,这种技术叫做自适应哈希索引。这个过程外界是完全透明的并且不可干预。
我们通过MySQL 官方的一张图片再来感受一下InnoDB的整体架构:

InnoDB 线程模型
InnoDB 存储引擎是一个多线程模型,按照类型可分为主线程,Purge 线程,Page Cleaner 线程,IO 线程。接下来我们看看InnoDB中的各个线程又是如何相互配合完成工作的:

线程类型 | 描述 |
---|---|
Master Thread | InnoDB 引擎最为核心的线程,主要负责将缓冲池中的数据异步刷新回磁盘。内部由多个循环组成,主循环、后台循环、刷新循环和暂停循环组成。循环通过线程睡眠实现,因此操作不是精确的,在负载很大的情况下会有延迟 |
Purge Thread | 回收 undo log 线程 |
Page Cleaner Thread | 脏页刷新线程 |
IO Thread | InnoDB 中 AIO 的回调线程 |
主线程工作机制
主线程由主循环、后台循环、刷新循环和暂停循环组成。在主循环中有两种机制,一种是每1秒的操作,另外一种是每10秒的操作。
每1秒的操作包括:
- 总是会进行日志缓冲刷盘,即使这个事务还没有提交也会刷盘。
- 如果IO负载允许,会合并插入缓冲。
- 如果缓冲池脏页比例达到一定阈值默认为75%,至多会将100个脏页刷回磁盘。在1.2.x之后的版本脏页刷盘已经由专门的线程Page Cleaner Thread 负责。
- 如果用户没有任何活动,切换到后台循环。
每10秒的操作包括:
- 总是执行合并至多5个插入缓冲。
- 总是执行日志缓冲刷盘。
- 总是删除无用的undo页。在 1.2.x 之后由Purge Thread 负责。
- 总是刷新 10 个脏页到磁盘,如果磁盘io负载允许,将进行100个脏页刷盘。在 1.2.x 之后的版本脏页刷盘已经由专门的线程Page Cleaner Thread 负责。
InnoDB 关键特性
异步IO
在MySQL中大量使用了系统原生的AIO技术,通过异步化来提高整体性能。
Change Buffer
我们在使用 InnoDB 的过程中,通常情况下数据是按主键递增的顺序进行插入的,这意味着读写磁盘的顺序一般也是顺序读写,这也是我们不太建议主键设置为 UUID 的原因。但是对于普通索引来讲,很难保证索引插入的顺序性。因此我们在新增修改索引的时候,磁盘访问是随机的,我们知道随机访问磁盘在机械硬盘上时间消耗比较大,这无疑会拖慢整体的性能。InnoDB 引擎开创性的引入了Change Buffer 设计,Change Buffer 可以分为三种buffer,Insert Buffer、Delete Buffer 和Purge Buffer。主要思路如下:
- 对于非聚集索引的插入或更新操作,不是每一次都直接插入到索引页中,而是先判断插入或更新的索引页是否在缓冲池中。
- 如果存在于缓冲池,则直接进行插入或更新。
- 如果不存在则先放一个到Change Buffer 中,之后通过一定频率将 Change Buffer 和辅助索引节点合并,这时候通常能够将多个操作合并到一个操作中,大大提高非聚集索引的插入性能。
从上面我们可以看出,要使用Change Buffer 需要满足如下条件:
- 操作索引为辅助索引
- 索引列不唯一
对于删除操作而言,具体过程如下:
- 1、将记录标记为已删除,更新Delete Buffer。
- 2、从Purge Buffer中将真正的记录删除。
Double Write
InnoDB 通过两次写操作提升了数据的可靠性。double write 由两部分组成,一部分存在于内存中,大小为2MB。一部分存在于物理磁盘上的共享表空间中,共享表空间中开辟了连续128个页,分为两个区,每个区大小为1MB。具体过程如下:
- 将脏页复制到内存中的double write buffer
- 分两次每次1MB的顺序写到物理磁盘共享表空间的double write buffer上,每次写完后立即调用fsync函数,同步磁盘,避免缓冲写的问题。
- 写完物理盘上的double write buffer 后再将double write buffer 写入各个表空间文件中。
- 如果在后续写入过程中发生了宕机,则InnoDB引擎可以从共享表空间找到该页的一个副本,将其复制到表空间文件,然后再根据重做日志进行数据恢复。
整体过程如下:
Adaptive Hash Index
InnoDB 内部会对表上的所有索引页的查询使用情况进行监控统计,如果观察到某些数据访问非常频繁,InnoDB会对这些数据建立哈希索引来提升性能,这种技术叫做自适应哈希索引。这个过程外界是完全透明的并且不可干预。
Flush Neighbor Page
InnoDB 在刷新一个脏页的时候,会检测该页所在区的所有页,如果是脏页那么一并刷新回磁盘。这样做的好处是多个写入操作合并成一个写入操作,这个功能对于传统机械硬盘有着显著的好处,但是目前SSD 固态硬盘已经开始普及,当硬盘IOPS较高的时候,该特性效果并不明显,且效率不高——在如果该区所在的页的数据变化并不大的情况下,比如某个页只更新一个数据,这个时候进行了合并写入,之后很可能更新了该页的其他行,这个页又变成了脏页。这个时候该页需要不停的重复刷入磁盘。因此在 IOPS 较高的硬件下,可以考虑关闭该功能,以避免不必要的开销。