文章目录
- [一、插入缓冲(Insert Buffer)](#一、插入缓冲(Insert Buffer))
-
- [1. Insert Buffer](#1. Insert Buffer)
- [2. Change Buffer](#2. Change Buffer)
- [3. Insert Buffer的内部实现](#3. Insert Buffer的内部实现)
- [4. Merge Insert Buffer](#4. Merge Insert Buffer)
- 二、两次写(Doublewrite)
- [三、自适应哈希索引(Adaptive Hash Index)](#三、自适应哈希索引(Adaptive Hash Index))
- [四、异步IO(Asynchronous IO)](#四、异步IO(Asynchronous IO))
- [五、刷新邻接页(Flush Neighbor Page)](#五、刷新邻接页(Flush Neighbor Page))
InnoDB是MySQL的默认存储引擎,自MySQL 5.5版本起成为默认存储引擎,为MySQL提供了强大的事务处理能力和高性能的数据管理功能,本来主要围绕他的关键核心特性的详细介绍。
一、插入缓冲(Insert Buffer)
1. Insert Buffer
我们知道,InnoDB存储引擎默认使用B+树结构存储数据,当数据是连续有顺序的写入,则插入速度会比较快,但是当数据插入是无序的,则需要离散读取后再插入,那么插入的速度就会变慢。
在实际开发中我们常常会创建如下索引,来看看他们的插入性能:
- 聚集索引:当主键为自增且有顺序的插入,则插入很快;当主键无序的插入,则插入性能较低。
- 非聚集索引:非主键索引的插入往往是无序的,需要离散读取,所以插入性能较低。
- 非聚集唯一索引:非聚集唯一索引每次插入都需要检查是否唯一,需要离散读取,所以插入性能也较低。
为了解决非聚集索引的插入性能问题,InnoDB存储引擎引入了Insert Buffer,对于非聚集索引的插入或更新操作,不是每一次真接插入到索引页中,而是先判断插入的非聚集索页是否在缓冲池中,若在,则直接插入;若不在,则先放入到一个Insert Buffer对象中,然后再以一定的频率和情况进行Insert Buffer和铺助索引页子节点的merge(合并)操作,这时通常能将多个插入合并到一个操作中(因为在一个索引页中),大大提高了对于非聚集索引插入的性能。
同时,InnoDB存储引擎在使用Insert Buffer时必须满足以下两个条件:
- 索引是辅助索引:大部分非聚集索引的插入性能太差,为了尽可能避免离散读取,通过使用Insert Buffer提高插入性能。
- 索引不是唯一的:非聚集唯一索引每次插入都需要检查是否唯一,无法避免离散读取,所以不在使用范围。
目前Insert Buffer使用存在一个问题,就是在写请求并发过高的情况下,会占用过多的缓冲池大小,默认最大可以占用到50%的大小。为了控制Insert Buffer的内存大小,可以通过配置参数 innodb_ibuf_pool_size_per_max_size
来调整。比如设置成innodb_ibuf_pool_size_per_max_size=30
,则最大占用缓冲池的30%大小。
2. Change Buffer
InnoDB存储引擎从1.0.x版本开始引入了 Change Buffer,实际上就是 Insert Buffer 的升级版,也只能支持非唯一的辅助索引,但能提供 INSERT(插入)、DELETE(删除标记->物理删除)、UPDATE(删除标记旧->插入新->物理删除旧)操作的缓存。
Change Buffer 内部实际上是由以下几个不同的缓存类型组成:
- Insert Buffer:插入(insert),缓存非唯一二级索引的插入操作。
- Delete Buffer:删除标记(delete mark),将记录标记为删除。
- Purge Buffer:物理删除(purge),将记录真正删除。
InnoDB存储引擎提供了参数 innodb_change_buffering
来配置 Change Buffer 开启缓存Buffer类型,具体参数如下:
值 | 含义 | 说明 |
---|---|---|
none |
不缓存任何操作 | 禁用 Change Buffer |
inserts |
只缓存插入操作(Insert Buffer) | 对插入性能有优化 |
deletes |
只缓存删除标记操作(Delete Buffer) | 对逻辑删除有优化 |
changes |
缓存插入和删除标记(inserts + deletes) | 对插入和删除都有优化 |
purges |
缓存物理删除操作(Purge Buffer) | 由后台 Purge 线程执行的物理删除 |
all |
缓存所有操作(inserts + deletes + purges) | 默认值,适用于大多数场景 |
同时也提供了参数 innodb_change_buffer_max_size
来控制 Change Buffer 占用缓冲池内存空间的大小。比如默认配置 innodb_change_buffer_max_size=25
表示最大占用缓冲池25%内存大小。
3. Insert Buffer的内部实现
Insert Buffer的数据结构其实是一棵B+树,在MySQL4.1之前版本每个表都有一棵Insert Buffer B+树,而之后的版本则改为全局所有表使用同一棵Insert Buffer B+树。这棵B+树存储在系统表空间中,也就是默认的ibdata1文件中。
当我们试图通过独立表空间ibd文件恢复表中数据时,往往会导致CHECK TABLE失败。这是因为表的辅助索引中的数据可能还在Insert Buffer中,也就是系统表空间中,所以通过ibd文件进行恢复后,还需要进行REPAIR TABLE操作来重建表上所有的铺助索引。
因为Insert Buffer本质是一棵B+树,所以也是由叶子节点和非叶子节点组成。我们就来看看他的底层结构如何?
非叶子节点
Insert Buffer的非叶子节点存放的是查询的search key(键值),一共占9字节,具体结构如下图所示:
- space:占4字节,表示待插入记录所在表的表空间ID,每个表都有唯一的的space id用于查找表。
- marker:占1字节,为了兼容老版本Insert Buffer。
- offset:占4字节,表示页所在的偏移量大小。
当一个辅助索引要插入到页(space,offset)时,如果这个页不在缓冲池中,那么会先构造出一个search key结构,再查询Insert Buffer B+树,最后将这条记录插入到B+树对应的叶子节点中。
叶子节点
对于插入到Insert Buffer B+树叶子节点的记录,需要按照如下结构进行构造:
- space:占4字节,表示待插入记录所在表的表空间ID,每个表都有唯一的的space id用于查找表。
- marker:占1字节,为了兼容老版本Insert Buffer。
- offset:占4字节,表示页所在的偏移量大小。
- metadata :占4字节。
- IBUF_REC_OFFSET_COUNT: 占2字节,整数。用来标记每个记录进入Insert Buffer的顺序,保证按这个顺序回放(replay)时记录能正确。
- IBUF_REC_OFFSET_TYPE: 占1字节,记录了Insert Buffer的操作类型(INSERT/UPDATE/DELETE)。
- IBUF_REC_OFFSET_FLAGS: 占1字节,标志位(压缩页、反向索引等)。
- secondary index record:待插入的索引数据,实际插入记录的各个字段值。
Insert Buffer Bitmap
在启用Insert Buffer后,辅助索引页(space,page_no)中的记录可能被插入到Insert Buffer B+树中,为了保证每次Merge Insert Buffer页必须成功,还需要有一个特殊的页用来标记每个辅助索引页(space,page_no)的可用空间。这个页的类型就是Insert Buffer Bitmap。
每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent)。每个Insert Buffer Bitmap页都在16384个页的第二个页中。每个辅助索引页在Insert Buffer Bitmap页中占用4位(bit),主要由以下三个部分组成:
名称 | 大小(bit) | 说明 |
---|---|---|
IBUF_BITMAP_FREE | 2位 | 记录辅助索引页的剩余空间状态: - 0 :剩余空间 < 512字节(1/32页) - 1 :512字节 ≤ 剩余空间 < 1KB(1/16页) - 2 :1KB ≤ 剩余空间 < 2KB(1/8页) - 3 :剩余空间 ≥ 2KB 当剩余空间 < 512字节时,触发合并操作。 |
IBUF_BITMAP_BUFFERED | 1位 | 标识该辅助索引页是否有记录被缓存在Insert Buffer的B+树中: - 0 :无缓存记录 - 1:存在缓存记录(需合并)。 |
IBUF_BITMAP_IBUF | 1位 | 标识该页是否为Insert Buffer B+树自身的索引页: - 0 :用户表的辅助索引页 - 1:Insert Buffer B+树的内部页(非用户数据页)。 |
4. Merge Insert Buffer
我们知道,Insert/Change Buffer的底层是一棵B+树,如果要实现插入记录的辅助索引页不在缓冲池中,那就需要将辅助索引记录先插入到这棵B+树中。但是Insert Buffer中的记录是什么时候合并到真正的辅助索引中的?这就是我们接下来要讨论的Merge Insert Buffer的触发场景,主要有以下三种情况:
- 辅助索引页被读取到缓冲池时:当辅助索引页被读取到缓冲池中时,例如当客户端执行正常的SELECT查询操作,这时需要检查Insert Buffer Bitmap页,然后确认该辅助索引页是否有记录存放于Insert Buffer B+树中。若有,则将Insert Buffer B+树中该页的记录插入到该辅助索引页中。因为是一次性对该页多次的记录操作合并到了原有的辅助索引页中,因此性能会有大幅提高。
- Insert Buffer Bitmap页追踪到该辅助索引页空间不足时:Insert Buffer Bitmap页用来追踪每个辅助索引页的可用空间,并至少有1/32页的空间。若插入辅助索引记录时检测到插入记录后可用空间会小于1/32页,则会强制进行一个合并操作,即强制读取辅助索引页,将Insert Buffer B+树中该页的记录及待插入的记录插入到辅助索引页中。
- Master Thread:在Master Thread线程中每秒或每10秒可能触发一次Merge Insert Buffer操作,而每次合并页的数量多少由配置参数决定,每次合并哪些页由随机算法决定。
合并辅助索引页的随机算法
在Insert Buffer B+树中,辅助索引页根据(space,offset)都已排序好,故可以根据(space,offset)的排序顺序进行页的选择。然而,对于Insert Buffer页的选择, InnoDB存储引擎并非采用这个方式,它随机地选择Insert Buffer B+树的一个页,读取该页中的space及之后所需要数量的页。该算法在复杂情况下应有更好的公平性。同时,若进行merge时,要进行merge的表已经被删除,此时可以直接丢弃已经被Insert/Change Buffer的数据记录。
二、两次写(Doublewrite)
如果说Insert Buffer带给InnoDB存储引擎的是性能上的提升,那么Doublewrite(两次写)带给InnoDB存储引擎的是数据页的可靠性。
我们先想象这样一个场景:当InnoDB存储引擎正在写入某个页到表中,而这个页只写了一部分,比如16KB的页,只写了前4KB,还没有全部写完,刚好在这个时候数据库宕机了,那么还未写完的数据就会丢失,这种情况被称为部分写失效(partial page write)。
为了解决写失效的问题,InnoDB存储引擎提出了Doublewrite技术,解决了当写失效的情况发生时,先找到系统表空间中的Doublewrite备份副本,将副本复制到表空间文件,再进行重做日志恢复。下图是Doublewrite技术架构实现:
Doublewrite内部组成
- Doublewrite Buffer:内存部分,大小为2MB。
- Doublewrite页备份:磁盘备份,系统表空间连续的128个页,即2个区,大小也为2MB。
Doublewrite执行过程
当进行缓冲池脏页刷新时,并不会直接写入磁盘,而是会通过memcpy函数将脏页先复制到内存中的Doublewrite Buffer,再通过Doublewrite Buffer分两次,每次1MB顺序的写入系统表空间,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。
Doublewrite Buffer写入系统表空间因为Doublewrite页是连续的内存,性能开销较小。完成Doublewrite页写入后,再将Doublewrite Buffer写入各个表空间文件中,此时的写入是离散的。
Doublewrite状态查询
可以通过命令 SHOW GLOBAL STATUS LIKE "innodb_dblwr%";
查询Doublewrite运行情况。
Innodb_dblwr_pages_written :Doublewrite写入页的数量。
Innodb_dblwr_writes:实际写入次数。
因为每次写入1MB即1024KB,每个页大小是16KB,那么1024/16=64就是每次写入的最大页数量为64。如果高峰期发现Innodb_dblwr_pages_written:Innodb_dblwr_writes
远小于64:1
,说明系统的写入压力不大。
三、自适应哈希索引(Adaptive Hash Index)
哈希(Hash)是一种非常快的查找方法,时间复杂度为O(1),仅需要一次就能找到结果。而B+树的二分查找的次数取决于B+树本身的高度,时间复杂度可能为O(logN)。
InnoDB存储引擎为了提高索引的查询速度,通过监控对所有表上索引页的查询,根据访问频率和模式自动为某些热点查询建立哈希索引,只要能带来速度提升,就去建立哈希索引,这就是自适应哈希索引(AHI)。
创建哈希索引的条件
- 对这个页的访问模式必须连续且一样:访问模式一样指的是对同一个联合索引的查询条件必须一样,而且必须是连续的访问条件都一样,如果出现条件不一样的交替查询,也不会去创建哈希索引。
- 相同访问模式访问了100次
- 页通过该访问模式访问了N次,其中 N=页中记录*1/16
自适应哈希索引是内部自动优化的,无需人为调整,默认为开启状态。当开启AHI后,读取和写入速度可以提高2倍,辅助索引的连接操作性能可以提高5倍。可以使用命令 SHOW ENGINE INNODB STATUS;
实时查看自适应哈希索引的使用情况:
四、异步IO(Asynchronous IO)
为了提高磁盘操作性能,InnoDB存储引擎采用异步IO(Asynchronous IO,AIO)的方式来处理磁盘操作。所谓的AIO,就是发生IO请求后无需等待IO的响应就可以处理下一个IO请求。在InnoDB存储引擎中,read ahead方式的读取都是通过AIO完成,脏页的刷新即磁盘的写入操作则全部由AIO完成。
AIO的主要优势
- 连续并发处理IO请求,无需等待上一个IO请求结果。
- 可以进行IO merge操作,将多个IO操作合并为1个IO操作。
Native AIO
在InnoDB1.1.x之前,AIO的实现通过InnoDB存储引擎中的代码来模拟实现。而从InnoDB1.1.x开始(InnoDB Plugin不支持),提供了内核级别AIO的支持,称为Native AIO。因此在编译或者运行该版本MySQL时,需要libaio库的支持,否则启动时报错。
Native AIO功能目前只有windows和Linux系统支持,Mac OSX暂不支持。可以通过参数 innodb_use_native_aio
来控制是否开启,默认打开,官方测试反馈启用后可以提升75%的数据库恢复速度。
五、刷新邻接页(Flush Neighbor Page)
InnoDB存储引擎还提供了刷新邻接页(Flush Neighbor Page)的特性。其工作原理为:当刷新一个脏页时,InnoDB存储引擎会检测该页所在区(extent)的所有页,如果是脏页,那么一起进行刷新。
好处 :通过AIO可以将多个IO写入操作合并为一个IO操作,减少刷盘频率。适用于机械硬盘,固态硬盘的IOPS很高,建议关闭。可以通过参数 innodb_flush_neighbors
控制,默认开启。
缺点:可能将不怎么脏的页刷盘,之后立刻又脏了,相当于IO变相增多。