本文参考《MySQL技术内幕》

InnoDB存储引擎的关键特性包括:

  • 插入缓冲(Insert Buffer)
  • 两次写(Double Write)
  • 自适应哈希索引(Adaptive Hash Index)
  • 异步I/O(Async IO)
  • 刷新邻接页(Fluch Neighbor Page)
    上述这些特性为InnoDB引擎带来了更好的ing能以及更高的可靠性。

1. 插入缓冲

1. Insert Buffer

Insert Buffer是InnoDB存储引擎关键特性中最令人激动与兴奋的一个功能。不过这个名字可能会让人认为插入缓冲是缓冲池中的一个组成部分。其实不然,InnoDB缓冲池中有Insert Buffer信息固然不错,但是Insert Buffer和数据页一样,也是物理页的一个组成部分。

在InnoDB存储引擎中,一般情况下,主键是行唯一的标识符。通常应用程序中行记录的插入顺序是按照主键递增的顺序进行插入的。因此,插入聚集索引一般是顺序的,不需要磁盘的随机读取。因为,对于此类情况下的插入,速度还是非常快的。(但并非所有的主键插入都是顺序的,如果主键类是UUID这样的类,那么插入和辅助索引一样,也是随机的。)

但是不可能每张表上只有一个聚集索引,更多情况下,一张表上还有多个非聚集索引的辅助索引。在这样的情况下产生了一个非聚集且不是唯一的索引。在进行插入操作时,数据的存放对于非聚集索引叶子节点的插入不是顺序的,这时需要离散地访问非聚集索引页,由于随机读取的存在而导致了插入操作性能下降。这是因为B+树的特性决定了非聚集索引插入的离散性。

Insert Buffer的设计,对于非聚集索引的插入和更新操作,不是每一次直接插入到索引页中,而是先判断插入非聚集索引页是否在缓冲池中,若存在,则直接插入,不存在,则先放入一个Insert Buffer对象中。数据库这个非聚集的索引已经插到叶子节点,而实际并没有,只是存放在另一个位置。然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),这就大大提高了对于非聚集索引插入的性能。

需要满足的两个条件:

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

当满足上面两个条件,InnoDB就会使用Insert Buffer,这样就能提高插入性能了。但是考虑这样一种情况:应用程序进行大量插入操作,这些都涉及了不唯一的非聚集索引也就是使用了Insert Buffer。此时若发生了MySQL宕机,势必会有大量Insert Buffer并没有合并到实际的非聚集索引中。此时恢复可能要很长的时间。

辅助索引不能是唯一的,因为在插入缓冲时,数据库并不去查找索引页来判断插入的记录的唯一性。如果去查找肯定又会有离散读取的情况发生,从而导致Insert Buffer失去了意义。

2. Change Buffer

InnoDB从1.0.x版本开始引入了Change Buffer,可将其视为Insert Buffer的升级。从这个版本开始,InnoDB存储引擎可以对DML操作——INSERT、DELETE、UPDATE都进行缓冲,他们分别是:Insert Buffer、Delete Buffer、Purge buffer。

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

对一条记录进行UPDATE操作可能分为两个过程:

  1. 将记录标记为已删除(Delete Buffer);
  2. 真正将记录删除(Purge buffer)。

因此Delete Buffer对应UPDATE操作的第一个过程,即将记录标记为删除。Purge Buffer对应UPDATE操作的第二个过程,即将记录真正的删除。同时,InnoDB存储引擎提供了参数innodb_change_buffering,用来开启各种Buffer的选项。该参数可选的值为:inserts、deletes、purges、changes、all、none。inserts、deletes、purges就是前面讨论过的三种情况。changes表示启用inserts和deletes,all表示启用所有,none表示都不启用。该参数默认值为all。

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

在MySQL 5.5版本中通过命令SHOW ENGINE INNODB STATUS,可以观察到类似如下的内容:

mysql> SHOW ENGINE INNODB STATUS\G;
……
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 34397, seg size 34399, 10875 merges

merged operations:

insert 20462, delete mark 20158, delete 4215

discarded operations:

insert 0, delete mark 0, delete 0
……

可以看到这里显示了merged operations和discarded operation,并且下面具体显示Change Buffer中每个操作的次数。insert表示Insert Buffer;delete mark表示Delete Buffer;delete表示Purge Buffer;discarded operations表示当Change Buffer发生merge时,表已经被删除,此时就无需再将记录合并(merge)到辅助索引中了。

3. Merge Insert Buffer

通过前面的小节读者应该已经知道了Insert/Change Buffer是一棵B+树。若需要实现插入记录的辅助索引页不在缓冲池中,那么需要将辅助索引记录插入到这棵实际B+树中。但是Insert Buffer中的记录何时合并(merge)到真正的辅助索引中呢?

概括地说,Merge Insert Buffer的操作可能发生在以下几种情况下:

  1. 辅助索引页被读取到缓冲池时;
  2. Insert Buffer Bitmap页追踪到该辅助索引页已无可用空间时;
  3. Master Thread。

第一种情况为当辅助索引页被读取到缓冲池中时,例如这在执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。可以看到对该页多次的记录操作通过一次操作合并到了原有的辅助索引页中,因此性能会有大幅提高。

Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中。这就是上述所说的第二种情况。

还有一种情况,之前在分析Master Thread时曾讲到,在Master Thread线程中每秒或每10秒会进行一次Merge Insert Buffer的操作,不同之处在于每次进行merge操作的页的数量不同。

2. 两次写

如果说Insert Buffer给InnoDB带来了性能上的提升,那么两次写带给InnoDB的时数据页的可靠性。

当数据库发生宕机时,可能InnoDB存储引擎正在将某个页写入到表中,而这个页只写了一部分,比如16KB的页,只写了4KB,之后就发生了宕机,这种情况被称为部分写失效。虽然重做日志可以进行数据恢复,但必须意识到,重做日志中记录的是对页的物理操作(例如偏移量800处写‘aaaa’记录),当页本身损坏时,重做是没有意义的。这就是说,在应用重做日志前,用户需要一个页的副本,当部分写失效发生时,必须先通过页的副本来还原该页,再进行重做,这就是Double Write。

InnoDB的doublewrite体系架构如图:
图片说明

doublewrite由两部分组成,一部分是内存中的doublewrite buffer,大小为2M;另一部分是物理磁盘上共享表空间中连续的128个页,即2个区,大小同样为2M。

对缓存池中的脏页进行刷新时,并不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer中,之后通过doublewrite buffer再分两次,每次1M顺序的写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。

上述过程中,因为doublewrite页是连续的,因此这个过程是顺序写的,开销并不大。

在完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中,此时的写入则是离散的。可通过如下命令观察doublewrite运行的情况:

SHOW GLOBAL STATUS LIKE 'innodb_dblwr%'\G

当OS将页写入磁盘时宕机,恢复时,InnoDB可从共享表空间的doublewrite中找到该页的一个副本,将其复制到表空间文件,再应用重做日志恢复
有些文件系统(如ZFS)本身就提供了部分写失效的防范机制,此时用户就不需要启用doublewrite了,可通过skip_innodb_doublewrite参数禁用doublewrite。

3. 自适应哈希索引

哈希是一种非常快的查找方法,在一般情况时间复杂度为O(1),即一般只需要一次查找就能定位数据。而B+树的查找次数,取决于B+树的高度,在生产环境中,B+树的高度一般为3-4层,故需要查询3-4次。

InnoDB存储引擎会监控对表上各索引页的查询。如果观察到建立哈希索引可以提升速度,则建立哈希索引,称之为自适应哈希索引(Adaptive Hash Index, AHI)。AHI是通过缓冲池的B+树页构造而来的。因此建立的速度非常快,且不需要对整张表构建哈希索引。InnoDB存储引擎会自动根据访问的频率和模式来自动地为某些热点页建立哈希索引。

对于缓冲池页地哈希表来说,在缓冲池中的Page页都有一个chain指针,它指向相同哈希值的页。那么InnoDB存储引擎的缓冲池对于其中的页是怎么进行查找的呢?
InnoDB存储引擎的表空间都有一个space_id,用户所要查询的应该是某个表空间的某个连续16KB的页,及偏移量offset。InnoDB将space_id左移20位,然后加上这个space_id和offset,即关键字K = space_id << 20 + space_id + offset,然后用除法散列到各个槽中。

AHI有一个要求,对这个页的连续访问模式(查询条件)必须一样的。例如联合索引(a,b)其访问模式可以有以下情况:

  1. WHERE a=XXX;
  2. WHERE a=xxx AND b=xxx。

访问模式一样指的是查询的条件一样,若交替进行上述两张查询,InnoDB存储引擎不会对该页构造AHI。此外AHI还有如下要求:

  1. 以该模式访问了100次;
  2. 页通过该模式访问了N次,其中N=页中记录数量/16。

根据官方文档显示,启用AHI后,读取和写入的速度可以提高2倍,负责索引的链接操作性能可以提高5倍。其设计思想是数据库自由化的,无需DBA对数据库进行人为调整。

哈希索引有以下限制:

  1. 哈希索引只包含哈希值和行指针,而不存在字段值,所以不能使用索引中的值来避免读取行。不过,访问内存中的行的速度很快,所以大部分情况下这一点对性能的影响并不明显。
  2. 哈希索引数据并不是按照索引值顺序存储的,所以也就无法用于排序。
  3. 哈希索引也不支持部分索引匹配查找,因为哈希索引始终是使用索引的全部内容来计算哈希值的。例如,在数据列(A,B)上建立哈希索引,如果查询只有数据列A,则无法使用该哈希索引。
  4. 哈希索引只支持等值比较查询,包括=、IN()、<=>(注意<>和<=>是不同的操作,<=>是说两个操作码均为NULL时,其所得值为1;而当一个操作码为NULL时,其所得值为0)。也不支持任何范围查询,例如WHERE price > 100。
  5. 访问哈希索引的数据非常快,除非有很多哈希冲突(不同的索引列却有相同的哈希值)。当出现哈希冲突的时候,存储引擎必须遍历链表中的所有的行指针,逐行进行比较,直到找到所有符合条件的行。
  6. 如果哈希冲突很多的话,一些索引维护操作的代价也会很高。例如,如果在某个选择性很低(哈希冲突很多)的列上建立哈希索引,那么当从表中删除一行时,存储引擎需要遍历对应哈希值的链表中的每一行,找到并删除对应行的引用,冲突越多,代价越大。

4. 异步IO

为了提高磁盘操作性能,当前的数据库系统都采用异步IO的方式来处理磁盘操作。InnoDB也是如此。

与AIO对应的是Sync IO,即每进行一次IO操作,需要等待此次操作结束才能继续接下来的操作。但是如果用户发出的是一条索引扫描的查询,那么这条SQL语句可能需要扫描多个索引页,也就是需要进行多次IO操作。在每扫描一个页并等待其完成再进行下一次扫描,这是没有必要的。用户可以在发出一个IO请求后立即再发出另外一个IO请求,当全部IO请求发送完毕后,等待所有IO操作完成,这就是AIO。

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

在InnoDB 1.1.x之前,AIO的实现是通过InnoDB存储引擎中的代码来模拟的。但是从这之后,提供了内核级别的AIO的支持,称为Native AIO。Native AIO需要操作系统提供支持。Windows和Linux都支持,而Mac则未提供。在选择MySQL数据库服务器的操作系统时,需要考虑这方面的因素。

MySQL可以通过参数innodb_use_native_aio来决定是否启用Native AIO。在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新,也是通过AIO完成。

5. 刷新邻接页

InnoDB存储引擎在刷新一个脏页时,会检测该页所在区(extent)的所有页,如果是脏页,那么一起刷新。这样做的好处是通过AIO可以将多个IO写操作合并为一个IO操作。该工作机制在传统机械磁盘下有显著优势。但是需要考虑下面两个问题:

  1. 是不是将不怎么脏的页进行写入,而该页之后又会很快变成脏页?
  2. 固态硬盘有很高IOPS,是否还需要这个特性?

为此InnoDB存储引擎1.2.x版本开始提供参数innodb_flush_neighbors来决定是否启用。对于传统机械硬盘建议使用,而对于固态硬盘可以关闭。