面试官爱问的 5 大InnoDB 关键特性,你知道吗?

129 阅读15分钟

大家好,本文基于《 MySQL技术内幕:InnoDB存储引擎 第二版 基于mysql 5.6》。文章是对本书中第一章和第二章内容的总结提炼,总结了读者认为比较重要的知识点,方便学习、阅读和回顾。

欢迎大家点赞收藏,后续会对其他章节也进行一些总结和提炼。

一、MySQL 体系结构和存储引擎

MySQL 被设计为一个单进程多线程架构的数据库。

MySQL区别于其他数据库的最重要的一个特点就是其插件式的表存储引擎

存储引擎是基于表的,而不是数据库。MySQL数据库的核心在于存储引擎。

1.1 InnoDB 存储引擎

  • 支持事务
  • 行锁设计
  • 支持外键
  • 通过多版本并发控制(MVCC)实现高并发性
  • 四种隔离级别
  • next-key locking 避免幻读
  • 提供插入缓冲(insert Buffer)、二次写(double write)、自适应哈希索引(adaptive hash index)、预读(read ahead)等高性能和高可用功能

表中数据的存储,采用了聚集的方式,因此每张表的存储都是按照主键的顺序进行存放。如果没有显式指定主键,会为每一行生成一个6字节的ROWID,并依次作为主键

1.2 MyISAM 存储引擎

不支持事务、表锁设计、支持全文索引,它的缓冲池只缓存索引文件,而不缓存数据文件。

二、InnoDB 存储引擎

第一个完整支持ACID事务的MySQL存储引擎。

2.1 特点

  • 行锁设计
  • 支持MVCC
  • 支持外键
  • 提供一致性非锁定读
  • 支持事务

2.2 后台线程

  • Master Thread:主要负责将缓冲池中的数据异步刷新到磁盘。保证数据的一致性,包括脏页的刷新、合并插入缓冲、UNDO 页的回收等
  • IO Thread:InnoDB 中大量使用了 AIO(Async IO)来处理 IO 请求。IO Thread 主要是负责这些IO请求的回调(call back)处理
  • Purge Thread:Purge Thread 来回收已经使用并分配的 undo 页。在 InnoDB1.1 之前,purge 操作在 Master Thread 中完成,之后到独立的线程中进行,提高性能。 InnoDB1.2 之后,支持多个 Purge Thread,进一步加快 undo 页的回收
  • Page Cleaner Thread:InnoDB1.2.x版本中引入,将之前版本中脏页的刷新操作都放入单独的线程中来完成。目的是减轻 Master Thread 的工作及对于用户查询线程的阻塞。

2.3 内存

InnoDB 存储引擎是基于磁盘存储的,并将其中的记录按照页的方式进行管理。

由于 CPU 和硬盘速度之间的鸿沟,使用缓冲池技术来提高数据库的性能。读写都优先在缓冲之中进行。

  • 读:将从硬盘中读取到的页存放到缓冲中,下次读相同的页先判断缓冲中是否有,有则读,没有则读取硬盘
  • 改:先修改缓冲中的页,然后再以一定频率刷新到磁盘上(Checkpoint机制)

缓冲池中缓存的数据页类型有:索引页、数据页、undo页、插入缓冲、自适应哈希索引、InnoDB存储的所信息、数据字典信息等。InnoDB 1.0.x 版本开始, 允许有多个缓冲池实例,好处是减少数据库内部的资源竞争,增加并发处理能力。

预读失败: 由 IO 操作的特性将未实际使用的数据加载到了 Buffer Pool 中缓存了,但后续却未使用。

缓冲池污染: 可能 SQL 的执行需要扫描大量页数据,为了缓存这些页数据,导致缓冲池中大量热点数据替换出去

缓冲池中页的大小默认为 16KB。

数据库中的缓冲池是通过 LRU(最近最少使用)算法来进行管理的。最频繁使用的在列表前端,最少使用的在列表尾端。 InnoDB 对 LRU 算法进行了一些改进,加入了 midpoint 位置。新读取的页放到 midpoint 位置上,默认配置下该值为 5/8。 midpoint 之前的列表叫做 new 列表,之后的列表叫做 old 列表。new 列表即热点数据。

这样做的目的是为了防止热点数据被刷出。

LRU 列表用来管理已经读取的页。

InnoDB 从 1.0.x 开始支持压缩页功能。将原本16K的页压缩为1K、2K、4K、8K,对于非 16K 的页,通过 unzip LRU 列表管理,LRU 列表的页包含 unzip LRU 列表中的页。对于 unzip LRU 的管理详见 P29。

脏页: LRU 列表中的页被修改了,但是磁盘还没有。

Flush 列表中的页为脏页列表。 脏页既存在于 LRU 列表中,页存在于 Flush 列表中。LRU 列表用来管理缓冲池中页的可用性(用于查询、更新?),Flush 列表用来管理 将页刷新回磁盘,二者互不影响。

InnoDB首先将重做日志信息先放入到重做日志缓冲中,然后以一定频率刷新到重做日志文件。

重做日志缓冲刷新到重做日志文件的时机:

  1. Master Thread 每秒刷新
  2. 每个事务提交时
  3. 重做日志缓冲池剩余空间小于1/2时

2.4 Checkpoint 技术

Checkpoint 所做的事情就是将缓冲池中的脏页刷回磁盘。

为了避免数据丢失,当前事务型数据库系统都普遍采用了 Write Ahead Log 策略。事务提交时,先写重做日志,再写修改页。

主要是为了解决下述问题:

  1. 缩短数据库恢复时间
  2. 缓冲池不够用时,将脏页刷新到磁盘(缓冲池大小有限,数据刷不到磁盘,查询命中率低)
  3. 重做日志不可用时,刷新脏页(Redo 日志大小有限,通过刷新策略可以有效的重复使用文件,不用开辟新空间)

InnoDB 通过 LSN(Log Sequence Number)来标记版。LSN 是 8 字节的数字,其单位是字节,每个页有 LSN,重做日志中也有 LSN, Checkpoint 也有 LSN。

分为两种 Checkpoint

  • Sharp Checkpoint:发生在数据库关闭时将所有的脏页刷新回磁盘。
  • Fuzzy Checkpoint:只刷新一部分脏页,而不是刷新所有的脏页回磁盘。

InnoDB 中可讷讷个发生如下几种情况的Fuzzy Checkpoint:

  • Master Thread Checkpoint:每秒或每十秒的速度刷新一定比例,异步,用户查询线程不会阻塞
  • FLUSH_LRU_LIST Checkpoint:需要保证 LRU 有空闲页可以使用,没有则清除尾端的页,如果尾端的页是脏页则触发。MySQL5.6 版本也就是 InnoDB1.2.x 之后这个检查被放在了一个单独的 Page Cleaner 线程中进行,并且用户可以控制 LRU 列表中可用页的数量
  • Async/Sync Flush Checkpoint:指重做日志文件不可用的情况(重做日志文件空余空间太少)。此时脏页是从脏页列表中选取的。详细情况见P35。
    • 这是为了保证重做日志的循环使用的可用性。要保证空闲空间在 0.3 。
    • 在 InnoDB 1.2.x 版本之前,Async 会阻塞发现问题的用户查询线程;Sync 会阻塞所有用户查询线程。之后,也就是 MySQL 5.6 版本之后,这部分的刷新操作也放入单独的 Page Cleaner Thread 中,故不会阻塞用户查询线程。
  • Dirty Page too much Checkpoint:脏页太多时进行,InnoDB 1.0.x 版本之前,该参数默认为 90 ,之后版本为 75。
    • 目的是为了保证缓冲池中有足够可用的页

2.5 Master Thread 工作方式

2.5.1 InnoDB 1.0.x 版本之前

Master Thread 具有最高的线程优先级别。内部由多个循环组成,主循环(loop)、后台循环(backgroup loop)、刷新循环(flush loop)、暂停循环(suspend loop)。 Master Thread 会根据数据库运行状态在以上循环中切换。

2.5.1.1 Master Thread

大多数的操作在主循环(loop)中。其中由两大部分,每秒钟的操作和每十秒中的操作。

每秒一次的操作如下:

  • 日志缓冲刷新到磁盘,即使这个事务还没有提交(总是)(这就是为什么再大的事务提交的时间也是有限的)
  • 合并插入缓冲(可能,前一秒内发生的IO次数小于5次,即IO压力小,则执行)
  • 至多刷新100个InnoDB的缓冲池中的脏页到磁盘(可能,如果脏页太多,超过配置的阈值,执行)
  • 如果当前没有用户活动,则切换到 background loop(可能)

每十秒一次的操作:

  • 刷新 100个脏页到磁盘(可能,过去 10秒之内磁盘的 IO 小于200次,则执行)
  • 合并至多 5个插入缓冲(总是)
  • 将日志缓冲刷新到磁盘(总是)
  • 删除无用的 Undo 页(总是)(full purge 操作,每次最多尝试收回 20 个 undo 页)
  • 刷新 100个或者 10个脏页到磁盘(总是,脏页比例小于 70,刷新 10脏页,大于 70,刷新 100脏页)
2.5.1.2 background loop

若当前没有用户活动,或者数据库关闭,就会切换到这个循环。

会执行如下操作:

  • 删除无用的 Undo 页(总是)
  • 合并 20 个插入缓冲(总是)
  • 跳回主循环(总是)
  • 不断刷新 100 个页知道符合条件(可讷讷个。跳转到 flush loop 中完成)

若 flush loop 也没有什么可以做了,就切换到 suspend loop,将 Master Thread 挂起。

2.5.2 InnoDB 1.2.x 版本之前

之前的版本,InnoDB 对 IO 是有限制的,在缓冲池向磁盘刷新时做了一定的硬编码(hard coding),对于今天的硬件来说,对磁盘 IO 性能有一定的限制。

改进如下:

  1. InnoDB Plugin(从 InnoDB 1.1.x 版本开始)提供了参数 innodb_io_capacity ,用来表示磁盘 IO 的吞吐量,默认值为 200。
    • 在合并插入缓冲时,合并插入缓冲的数量为 innodb_io_capacity 的 5%
    • 在从缓冲区刷新脏页时,刷新脏页的数量为 innodb_io_capacity。
  2. 从 InnDB 1.0.x 版本开始 innodb_max_dirty_pages_pct 默认值从 90 调到 75。脏页占据总体的 90 太大了。75 既加快了刷新脏页的频率,又能保证磁盘 IO 的负载。
  3. 引入 innodb_adaptive_flushing(自适应地刷新),原来只有脏页的比例大于阈值之后才会刷新,加入这个参数之后,会根据产生重做日志的速度来决定最合适的刷新脏页数量,在小于阈值时,也会刷新一定的脏页。
  4. 之前每次进行 full Purge 时,最多回收 20 个 Undo 页,InnoDB 1.0.x 引入了一个参数,可以动态修改,默认 20。

2.5.2 InnoDB 1.2.x 版本

该版本将刷新脏页的操作,从 Master Thread 分离到一个单独的 Page Cleaner Thread,减轻了 Master Thread 的工作,进一步提高了系统的并发性。

2.6 InnoDB 关键特性

  • 插入缓冲(insert Buffer)
  • 两次写(Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步IO(Async IO)
  • 刷新邻接页(Flush Neighbor Page)

2.6.1 插入缓冲

2.6.1.1 Insert Buffer

B+ 树的特性决定了非聚集索引插入的离散性,需要离散的访问非聚集索引页,由于随机读取的存在导致插入操作性能下降。

Insert Buffer 的设计中,对于非聚集索引的插入和更新操作,不是每次都直接插入到索引页中,而是先判断是否在缓存池中,若在,则直接插入; 不在则先放到一个 Insert Buffer 对象中,再以一定的频率和情况进行 Insert Buffer 和辅助索引页子节点的 merge(合并)操作。 这将多个插入合并为一个操作中,大大提高了对于非聚集索引插入的性能。

Insert Buffer 的使用需要同时满足下面两个条件:

  • 索引是辅助索引
  • 索引不是唯一(如果是,则插入需判断唯一性,会造成随机读,则失去意义)

存在一个问题:当写密集时,插入缓冲会占用过多的缓冲池内存,默认最大为 1/2,可以修改配置解决。

2.6.1.2 Change Buffer

在 InnoDB 1.0.x 版本开始引入 Change Buffer,可将其视为 Insert Buffer 的升级版。在这个版本中,对 DML 操作(插入、删除、更新) 都进行缓冲,分别是:Insert Buffer、Delete Buffer、Purge buffer。

跟 Insert Buffer 一样,Change Buffer的适用对象依然是非唯一辅助索引。

从 InnoDB 1.2.x 版本开始,可以通过参数 innodb_change_buffer_max_size 来控制 Change Buffer 最大使用内存的数量,默认值为 25 ,即 1/4 的缓冲池内存空间, 该参数最大有效值是 50。

2.6.1.3 Insert Buffer 的内部实现

Insert Buffer 是一棵 B+ 树。B+ 树就分叶子结点和非叶子结点。

非叶子结点存放的是查询的 search key(键值),构造如图:

非叶子节点

一共占用 9 字节,space 表示待插入记录所在表的表空间id,占 4 字节,marker 占 1 字节,用来兼容老版本的 Insert Buffer。 offset 表示页所在的偏移量,占 4 字节。

叶子结点结构:

叶子结点

其中 Space、marker、offset 与非叶子结点含义相同,一共占用 9 字节,第四个字段 metadata 占据 4 字节,第五个字段是实际记录。 所以需要额外的 13 字节的开销。

还有一个页类型为 Insert Buffer Bitmap 的页用来标记每个辅助索引页的可用空间。

2.6.1.4 Merge Insert Buffer

何时将 Insert Buffer 合并到真正的辅助索引中呢?

  • 辅助索引页被读取到缓冲池时,此时若Insert Buffer中对应的记录则合并到该辅助索引中。
  • Insert Buffer Bitmap 页追踪到该辅助索引页已无可用空间时(可用空间少于 1/32 时,进行强制合并)
  • Master Thread

2.6.2 两次写

double write 可以提高数据页的可靠性。

部分写失效: 当 InnoDB 正在进行数据写入时,某个页只写了前 4KB,之后就发生了宕机,导致页损坏。

double write 由两部分组成,一部分是内存中的 double write buffer,大小为 2MB,另一部分是屋里磁盘上共享表空间中连续的 128 个页, 大小同样为 2MB。

在对缓冲池的脏页进行刷新时,并不直接写磁盘,而是先通过memcpy函数将脏页先复制到内存中的doublewrite buffer, 之后通过 doublewrite buffer 再分两次,每次 1MB 顺序地写入共享表空间的物理磁盘上,然后马上 调用 fsync 函数,同步磁盘。 doublewrite 是连续的。是顺序写,开销小。完成doublewrite的写入之后,再将doublewrite buffer 中的页也写入各个表空间文件中。 此时的写入是离散的。

注:doublewrite 中存储的是完整的页。

doublewrite

为什么不怕在数据写到共享表空间的 doublewrite 时,发生宕机导致页副本缺失?

doublewrite 是为了解决数据文件本身页损坏导致的数据丢失问题,这种情况单纯靠 redo log 是无法完成修复的,因为本身数据文件在中的页就有问题。 而如果在数据写到共享表空间 doublewrite 时发生宕机,导致 doublewrite 的页损伤,其实是可以通过 redo log 来进行修复的,因为我们 本身的数据文件中的页是完好的,那么直接执行 redo log 进行重写就好了。

2.6.3 自适应哈希索引

哈希的查找时间复杂度是 O(1),B+ 树的查找次数,取决于 B+ 树的高度,在生产环境中,一般是 3-4 层,故需要查询 3-4 次。

InnoDB 会监控对表上各索引页的查询,如果建立哈希索引会带来性能提升,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index,AHI)。

AHI 通过缓冲池的 B+ 树页结构而来,建立速度很快,而且不需要对整张表结构建哈希索引。

InnoDB 会自动根据访问频率和模式来自动为某些热点页建立哈希索引。

  • 对这个页的连续访问模式必须一样,访问模式一样,也就是查询条件一样。
  • 以该模式访问了 100 次
  • 页通过该模式访问了 N 次,其中 N= 页中记录 * 1/16

哈希索引只能用来搜索等值的查询,其他查找类型,如范围查询是不能使用哈希索引的。

2.6.4 异步 IO

与 AIO 对应的是 Sync IO,每进行一次 IO 操作,都需要等待此次操作结束才能继续接下来的操作。

当全部 IO 请求发送完毕后,等待所有 IO 操作的完成,这就是 AIO。

AIO 的另一个优势是可以进行 IO Merge 操作,也就是将多个 IO 合并为一个 IO,这样可以提高 IOPS 性能。

假如用户需要访问三个连续的页,同步 IO 需要运行 3 次 IO 操作,而 AIO 会判断这三个页是连续的,而只读取一次,全部读取。

InnoDB 1.1.x 之前,AIO 通过 InnoDB 中的代码模拟实现,之后提供了内核级别 AIO 的支持,称为 Native AIO。

2.6.5 刷新邻接页(Flush Neighbor Page)

工作原理:当刷新一个脏页时,如果该页所在区的页是脏页,则会一起进行刷新,好处就是可以通过 AIO 将多个 IO 操作合并为一个,提高性能。

该工作机制在传统机械硬盘下有着显著的优势,但是对于固态硬盘有着超高 IOPS 性能的磁盘,则建议将该功能关闭。

持续更新go语言及K8S等go相关生态的文章、后端组件、前沿技术趋势及概念,欢迎大家点赞收藏,关注后续文章。