innodb引擎总述
对应《MySQL技术内幕InnoDB存储引擎第二卷 》第二章
innodb引擎包含后台线程、内存模块以及硬盘中的文件,如下图所示:

1. 后台线程
后台线程分为IOThread、MasterThread、PurgeThread、PageCleanerThread。
1.1 IOThread
InnoDB大量使用了AIO,IOThread就用来处理这些请求的回调。 IOThread分为四种,Read、Write、InsertBuffer、logIO。Read、Write分别负责读取、写入操作,在InnoDB 1.0.X版本后数量为4个,之前分别为1个。
读线程的id都小于写线程的id。
1.2 MasterThread
MasterThread是一个非常重要的后台线程,其主要通过两个循环来进行刷新脏页、合并插入缓冲、清理undo log、写redo log等操作来保证数据的一致性。循环分为两种,分别是每秒循环以及每10秒循环,流程如下(因为掘金的md语法好像不支持流程图,我就直接用文字表示了,另外这个流程是根据innodb 1.0.X版本之前写的,后面做了优化,1.0.2之后,只有在innodb处于idle状态时才会进行每10s循环要做的处理,否则进行每秒循环的操作):
每秒循环: 写redo log->合并插入缓冲->刷新脏页->如果没有到时间,那么进行undo log的清理,合并插入缓冲。
合并插入缓冲并不是每秒都会进行的,而是要看前一秒内IO发生次数是否小于5次,小于5次则进行合并插入缓冲(因为IO小,所以认为磁盘压力不大),脏页的刷新也是根据脏页的比例是否大于innodb_max_dirty_pages_pct,大于则进行刷新脏页
每10秒循环: 脏页刷新->合并插入缓冲->写redo log-> 进行undo log的清理->刷新脏页
redo log:重做日志,事务在执行时,及时没有commit,也会不断写入,写入时,先写入内存中的重做日志缓冲中,再从缓冲中写入磁盘的重做日志。重做日志是以512Byte为单位进行存储的,单位为块,因为与磁盘扇区一样,所以可以保证写入的原子性,因此不需要两次写技术进行保护。
undo log: undo log记录了事务中数据的操作的“反操作”,事务在进行回滚操作时,通过undo log可以进行回滚,同时在innodb进行MVCC时,可以通过undo log保证数据的隔离性,实现一致性非锁定读。插入数据的回滚很简单,因为不会有其他事务引用当前插入的数据,所以直接删除插入的数据就可以了,但是更新和删除就不一样了,更新分为两种,如果更新的是主键的话,其实执行的是删除主键对应的记录,然后再插入一条新的记录,如果更新的不是主键的话,那么就需要看当前undo log中是否有其他事务在引用(在RR或者RC的事务隔离机制下,需要通过undo log找到数据的不同版本信息),有的话不能删除,没有的话才可以删除,这后面还涉及undo page的回收,后面几篇文章会有提及。
1.3 PurgeThread
该线程主要用来执行MasterThread中对undo log清理的任务,来分担MasterThread的工作量。
1.4 PageCleanerThread
该线程主要用来进行MasterThread中对脏页的刷新,分担MasterThread的工作量。
2. 内存划分
InnoDB内存结构如下:

额外提一下,innodb中有共享表空间,如果开启了独立表空间的话,那么每一个表都有自己的表空间,独立的表空间中存放了数据、索引以及Insert buffer Bitmap等信息。
2.1 LRU List&Flush List&Free List
和操作系统中用页进行内存管理一样,InnoDB也通过页的形式进行内存管理,所有的页全部挂载在LRU List中,这个链表,顾名思义,就是按照最近最少使用排序的链表,FreeList保存着所有未分配的空闲页,为新的页进行分配时,是先从FreeList中获取一个空闲页,然后进行分配,当FreeList无法为新的页分配内存时,就把LRU尾端的页分配给新的页。 为了防止频繁将热点页淘汰、读取(比如全表扫描),InnoDB采用了midpoint技术,即将LRU List中新挂载的页不放在链表的头部,而是放在midpoint处(由innodb_old_blocks_pct决定,比如该值为37,则代表新的页放在尾端的37%处,并且midpoint后面的部分为old列表,之前为new列表)。
这里还要介绍一个参数:innodb_old_block_time,新放入LRU List中的页,会放在old列表和new列表的交界处,那什么时候把这个页放到new列表呢?当下次读到这个页的时候,如果超过了innodb_old_block_time,那么就认为这个页是热点数据,就放到new列表中。举个例子:当预读了10个页,都放在了交界处,当时间过了2s后(innodb_old_block_time=1s),这个时候读取其中一个页,因为它达到要求,所以放到了new列表中,这时又大量预读了几百个页,刚刚预读剩下的9个页由于内存不够被淘汰了,也就意味着不需要的数据不占用了内存,节省了内存的开销,同时热点数据由于都在new列表中,保证了热点数据不用频繁的读取、淘汰,提升了效率。
innodb页的大小默认为16KB,当然也可以进行压缩,有2、4、8KB等大小,这三个分别对应三个unzip_LRU List,下面介绍分配不同规格的页时的步骤(偷点懒,上图片):

当LRU List的页被修改后,这个页就变为脏页了(内存数据与硬盘数据不一致),这个页会挂载到Flush List中(注意,这个页同时存在于LRU List和Flush List中)。Master Thread或者PageCleanerThread通过Flush List进行脏页的刷新。
2.2 重做日志
重做日志刚刚大致说过,主要是用于记录事务每秒发生的数据变化,注意重做日志是物理日志,记录的不是逻辑上的变化,与二进制日志是有区别的,后面几篇会提到的。 重做日志在内存中有重做日志缓冲,重做日志缓冲数据刷新到重做日志文件的条件有以下三个:
- MasterThread每秒和每10秒写入重做日志。
- 重做日志缓冲内存不足一半。
- 事务提交时。
checkpoint技术
当LRU List中页的数据被修改后,这个页就变为脏页了(挂载到Flush List)中,脏页也需要刷新到磁盘上(如果不刷新的话,如果数据库宕机,那么依靠重做日志恢复数据要花费很长时间)。
要注意,脏页同脏读、幻读是有区别的,脏页是内存中数据修改了,但是磁盘的数据没有修改,是符合数据一致性的(反正通过innodb引擎能够读出正确的数据),脏页指的是数据在RU隔离模式下,一个事务读到了另一个事务未commit的数据,幻读指的是一个事务读取了数据后,另一个事务commit了一些新的数据,这时候再读取就读取了刚提交的数据。
checkpoint用来解决如下几个问题:
- 缩短数据库的恢复时间
- 缓冲池不够用的时候,将脏页刷新到磁盘
- 重做日志不可用的时候,将脏页刷新到磁盘
checkpoint分为两种:
- sharp checkpoint:将所有脏页全部刷新。
- fuzzy checkpoint:只刷新部分脏页。
因为sharp checkpoint在运行时会导致数据库可用性非常差,下面主要介绍fuzzy checkpoint的刷新机制:
- master thread checkpoint: 就是master thread每秒和每10秒进行的脏页刷新。
- Flush LRU List checkpoint:innodb引擎需要保证LRU List有超过100个可用的空闲页,如果没有,那么就通过Flush List进行刷新(因为Flush List中的页也存在与LRU List)。
- Async/Sync Flush Checkpoint:因为redo log存储了用户对数据的操作,内存中的脏页由于各种原因(比如宕机)没有刷新到磁盘,可以用过redo log进行恢复,所以在脏页刷新到磁盘之前,没有刷新的redo log内容是不能够覆盖的(redo log是可以循环使用的,所以才会覆盖,已经刷新到磁盘的页对应的redo log可以认为基本上没有用了,所以可以覆盖写其他的redo log)。如果redo log不可用(redo log的不可覆盖的内容达到一定非常高的比例),那么内存中的脏页必须刷新到磁盘(因为再不刷新,redo log就没有空间写了,这个时候数据库宕机,数据就无法恢复了)
- Dirty Page too much checkpoint:缓冲池中(Flush List)脏页的比例(使用innodb_max_dirty_pages_pct进行控制)太高了,会刷新一部分到磁盘中。
数据库普遍采用write ahead log(WAL),也就是日志要先于数据落盘(操作系统的ext4文件系统也支持日志先于数据落盘)
3 InnoDB 关键特性
3.1 插入缓冲
insert buffer是innnodb非常重要的一个特性,Mysql是索引组织表,也就是说主键索引和记录放在一起,对于辅助索引呢,他就不和记录放在一起了,只存放了部分字段(索引字段)。由于辅助索引插入可能并不是连续的,这样在对索引页进行修改时,可能需要离散的读取不同的索引页然后修改,这样IO次数多,效率低。通过Insert Buffer可以很好的解决这个问题,当需要修改一个辅助索引时,如果这个索引页在内存中,那么直接进行修改,如果不在,那么就放到insert buffer中,这样就不需要获取索引页然后再修改了,可以将几个插入缓冲合并在一起,一起插入到一个页中(指的是对于能放到一个索引页中的插入缓冲进行合并)。
插入缓冲针对的是非唯一的辅助索引,试想一下,如果是唯一的辅助索引,那么innodb在放到insert buffer前还需要遍历所有的索引页判断唯一性,这样反而画蛇添足了,所以使用的是非唯一索引
插入缓冲是一个B+树,而且是位于共享表空间中(有的时候恢复独立表空间不成功就是因为辅助索引的插入缓冲在共享表空间),要注意的是所有表空间的插入缓冲都在一颗B+树上。为了追踪定位每一个索引页是否还有空间插入索引,这个时候就需要一个辅助的结构,Insert Buffer Bitmap(开启了独立表空间,这个bitmap结构就放到独立表空间了)。合并插入缓冲 的时机:
- MasterThread每秒和每10秒都需要进行合并插入缓冲。
- 当通过Insert Buffer Bitmap追踪到辅助索引页无多余空间时,进行合并插入缓冲。具体做法是,在插入索引时,计算插入后辅助索引页如果小于1/32页(InnoDB要求辅助索引页要至少有1/32的空闲空间),那么就强制执行一次合并插入缓冲,把合并后的结果和待插入的索引一起进行插入。
- 辅助索引页被读取到缓冲池后,会进行合并插入缓冲。
3.2 两次写
double write为innodb带来数据的可靠性,因为数据在从内存写入磁盘的时候可能会发生故障,这时写入的内容不完整,即使这时通过redo log恢复数据,因为redo log恢复时是直接操控物理页的,如果这个物理页本身出了故障,那么也无法恢复(我的理解是,写入不完整后,又因为其他原因,导致这个页损坏,所以原本redo log基于页的偏移量进行修改的操作无法进行了),因此,在恢复前需要一个页的副本,这样才可以通过两次写来恢复数据。

- 脏页被复制到两次写缓冲区中(doublewrite buffer)。
- 脏页分两次,每次1MB的写入共享表空间(在磁盘中)中的连续2MB的空间中。因为共享表空间中的2MB是连续的,因此连续写入开销不大。
- 写入共享表空间后,将2MB中的页分别写入各个表空间中。
3.3 自适应哈希索引
Adaptive Hash Index(AHI),因为索引使用B+树,一般高度为3~4层,一次查询需要在磁盘上查询3~4遍,所以innodb通过对内存中的热点页(热点数据)建立哈希索引,可以有效提升查询效率,其建立条件是:
- 查询条件一致。
- 连续查询超过100次。
- 不是范围查询,是等值查询。
- 页通过该模式访问了N次,N=页中记录*1/16。
哈希索引的key值是通过space_id<<20+space_id+offset得来的。对于哈希碰撞,使用的是链表法,对于每一个哈希值相同的页,都有一个指针指向哈希相同的页。
3.4 异步IO
参考AIO,例如:对于索引的扫描,可以分为多个IO请求,可以发出一个后,立即发出另一个,没有必要等一个结束了再开始下一个,这样提升了效率。
3.5 刷新邻近页
在innodb刷新一个脏页的时候,可以看看该页所在的区的所有页还有没有脏页,有的话一起刷新,这样就把多个IO请求合并为一个,提高了效率。
为什么是一个区呢,innodb的存储是按照段、区、页来划分的,区是连续的1MB空间,所以一个区的页可以认为他们相邻,这样相当于顺序读写,效率高。其实可以类比操作系统的刷新脏页,其也是把对于bio合并到一个request中,这样也有利于提升IO效率。