MySQL InnoDB 概述(三) 关键特性

852 阅读6分钟

前言

记录一下关于 InnoDB 的关键特性。

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

插入缓冲 Insert Buffer

背景

我们都希望数据的插入都是顺序存放的,这也是为什么 MySQL 会建议我们使用 自增主键。这样会让磁盘进行顺序写,减少随机写,性能会因此大大提高。

当一张表上有多个索引时,MySQL 需要为表的索引建立一个索引数据,这个时候,索引数据就一般不会和主键那样自增了,对索引的插入,就会引起随机读写。如何避免这个问题呢? Insert Buffer 为这个问题提出解决方案。也是 InnoDB 非常关键的特性。

使用 Insert Buffer 需要满足以下两个条件

  • 索引是辅助索引
  • 索引不是唯一的

解决方案

既然更新索引是会引起随机读写,那可以考虑先记在Insert Buffer中,修改就算完成了,性能是很好的。这样需要依次解决几个问题(以下索引页均指辅助索引页):

  • 1、Insert Buffer 会不会存在掉电丢数据问题?Insert Buffer 其实也是需要写进磁盘的,在内存里,Insert Buffer 是 Buffer Pool 的一部分,在磁盘中个,它属于 系统表空间 System Tablespace。
  • 2、Insert Buffer 在设计上就是一个B+Tree,其叶子节点记录的数据是:索引页+偏移量,因此在使用 Insert Buffer 时,必须要保证索引页时可用才能保证 Merge Insert Buffer 成功。为此,需要维护一个 Insert Buffer Bitmap 页来追踪索引页的使用情况。详细的可以参考《MySQL 技术内幕 InnoDB存储引擎》。
  • 3、在解决怎么保存 Insert Buffer 的问题后,最后的问题是如何合并 Insert Buffer。这里列举三个合并的时机:
    • 辅助索引页被读取到缓冲池时
    • Insert Buffer Bitmap 判定索引页无使用空间
    • Master Thread 每 10s 会进行一次 Merge Insert Buffer

二次写 Double Write

背景

二次写是提升 InnoDB 可靠性的机制。redo log 是用于 cash safe 恢复的,但 redo log 的记录是A页x偏移量修改为n,这时候考虑一个问题,A页16K的数据,向磁盘同步了4K的时候断电了,那A页的数据就无法被 redo log 恢复了,因为A页被部分修改,偏移量已经不准确。为了解决这个问题,就需要在同步脏页前,增设一个副本以保证可靠性。这个机制就是 二次写

解决方案

  • 在内存中定义一片空间:doublewrite buffer,对应的磁盘物理文件:系统表空间中连续128个页。

  • 在对缓冲池的脏页进行刷新的时候,不直接写磁盘,先会把脏页复制一份到 doublewrite buffer,然后马上调用 fsync 函数写到磁盘中,doublewrite buffer是连续的,所以IO开销不大。完成后,再把 doublewrite 中的脏页数据写到表的文件中(这个过程是随机写的)。

  • 恢复过程

    • MySQL 重启时,会先进行 crash recovery。
    • 读取表文件 .ibd files,发现同步文件出现异常
    • 从二次写buffer中读取数据,进行恢复
  • 当.ibd 正确写入后,磁盘文件中的 doublewrite buffer 可以被擦除或重写。

自适应哈希索引 Adaptive Hash Index

背景

这是 InnoDB 的一个性能优化方案。Hash 查找要比B+Tree查找速度更快,InnoDB 存储引擎会监控对表上各个索引页的查询,假如观察到热点数据建立哈希索引可以带来速度提升,则建立哈希索引,这个过程是自动的,所以成为 自适应哈希索引 AHI Adaptive Hash Index。

解决方案

这里的机制大体是自动化,具体实现暂时不展开。

异步IO Async IO

背景

对于磁盘的操作性能优化,大部分的数据库系统都采用异步IO (Asynchronous IO AIO) 来处理磁盘操作。

设想 Sync IO (同步IO) 的操作,即每进行一次IO操作,都需要等待操作结束后才能继续接下来的操作。设想一个查询需要从磁盘提取多个页,如果一个个页地拉取,则会导致严重的性能问题。

另外一个好处是,当大量的读盘请求数据提交时,程序可以更具数据的特点,进行 IO Merge ,三个读请求的数据是连续的,则会自动合并为一个读请求。 Linux iostat 命令可以观察到 io 的工作状况。开启后 AIO 后,官方的测试数据显示性能会提高 75%。

具体实现

在同步IO中,查询线程会将 IO 请求先提交到队列中,并由后台线程获取请求,并发起同步IO调用,调用完成后再通知查询线程。这里的限制在于 后台 IO 线程数的配置,假如配置过少,可能会造成太忙。

使用AIO,查询线程会直接把 IO 请求提交到操作系统中,后台线程只需要等待操作系统的IO事件信号,收到信号后,提交数据给查询线程。这里后台线程只是负责交付,就不会有阻塞了。

刷新邻近页 Flush Neighbor Page

AIO 中提及到 IO Merge 的好处,在 InnoDB 中,不用 AIO 也会考虑是否可以使用这种便利之处。假设页A1、A2、A3是连续的页,A2 需要被刷到磁盘去,InnoDB 会检查 A1、A3 是否也是脏页,假如是,就可以进行 IO Merge,通过一次磁盘寻址完成写入。这个想法是好的,不过得考虑两个情况:

  • A1、A3 的修改可能很少,而且他们修改可能很频繁,这个时候刷新邻近页作用并不大
  • 固态硬盘寻址速度很快,这个特性是否还有必要?

因此,对于机械硬盘,这个特性是比较有用,固态就并不十分有用。

参考

后记

这是MySQL InnoDB 概述最后一篇学习笔记。后面有时间会围绕 InnoDB 的锁和事务做进一步的学习记录。

以前 MySQL 对我来说就只是一个可靠的存储中间件,提供了 ACID 等特性。通过了解InnoDB 的关键特性,以及其为了保证数据库的可靠和性能做的设计,原理也不是想象中那么神秘,但在工程实现上就非常有考究,有非常多的参数要根据IO的特点,硬件的特点去定制,以兼顾可靠性的同时,取得最好的性能。跟随《MySQL 技术内幕 InnoDB 存储引擎》可以比较容易地了解其脉络,而要考究一些机制的细节,则需要阅读官方文档,文档会对每个机制的优劣都做了分析。在存储的问题,确实是没有银弹的。

MySQL 作为 SQL 的一个典型代表,其在存储可用性和性能采取的取舍和其他存储是不太一样的,但他们大多都是面临相同或类似的问题,认真学习 MySQL 的底层原理,可以类比快速学习 Redis 、各种 MQ 的持久话特性。