InnoDB关键特性

本篇博客是《Mysql技术内幕 InnoDB存储引擎(第二版)》的阅读总结.

工作方式

首先Mysql进程模型是单进程多线程的。所以我们通过ps查找mysqld进程是只有一个。

体系架构

InnoDB存储引擎的架构如下图所以,是由多个内存块组成的内存池,同时又多个后台线程进行工作,文件是存储磁盘上的数据。

后台线程

上面看到一共有四种后台线程,每种线程都在不停地做自己的工作,他们的分工如下:

  • Master Thread: 是最核心的线程,主要负责将缓冲池中的数据异步刷新的磁盘,保证数据的一致性,包括脏页的刷新、合并插入缓冲(INSERT BUFFER),UNDO页的回收等。下面几个线程其实是为了分担主线程的压力而在最新的版本中添加的。
  • IO Thread: InnoDB使用大量的异步IO来处理请求。IO Thread的主要工作就是负责IO请求的回调(call back)处理。异步IO可以分为4个,分别是:write, read, insert buffer 和 log IO thread。
  • Purge Thread: undo log是用来保证事务的,当一个事务正常提交后,这个undo log可能就不再使用了。purge thread就是用来清除这部分log已经分配的undo页的。
  • Page Cleaner Thread: 主要是把脏页的刷新从主线程中拿到单独的线程,减轻主线程的压力,减少用户查询线程的阻塞,提高整体性能。

内存

由于InnoDB是基于磁盘存储的,为了使CPU与磁盘能够快的交互,提升整体性能而采用了缓冲池技术。
读数据简单的说可以用下面的流程图

更新数据的流程则如下:

由缓冲池的作用可以看到,缓冲池越大所容纳的数据就越多,与磁盘的交互就会越少,性能也就越高。所以缓冲池的大小直接影响着数据库的整体性能。
InnoDB在内存中主要有以下几部分组成:

具体来看缓冲池中缓存的数据页类型有:

  • 索引页: 缓存数据表索引
  • 数据页: 缓存数据页,占缓冲池的绝大部分
  • undo页: undo页是保存事务,为回滚做准备的。
  • 插入缓冲(Insert buffer): 上面提到的插入数据时要先插入到缓存池中。
  • 自适应哈希索引(adaptive hash index): 除了B+ Tree索引外,在缓冲池还会维护一个哈希索引,以便在缓冲池中快速找到数据页。
  • InnoDB存储的锁信息(lock info):
  • 数据字典(data dictionary):
    内存中除了缓冲池外外还有:
  • 重做日志缓冲redo log: 为了避免数据丢失的问题,当前数据库系统普遍采用了write ahead log策略,既当事务提交时先写重做日志,再修改写页。当由于发生宕机而导致数据丢失时,可以通过重做日志进行恢复。InnoDB先将重做日志放到这个缓冲区,然后按照一定的频率更新到重做日志文件中。重做日志一般在下列情况下会刷新内容到文件:
    • Master Thread每一秒将重做日志缓冲刷新到重做日志文件
    • 每个事务提交时会将重做日志缓冲刷新到重做日志文件
    • 当重做日志缓冲池剩余空间小于1/2时,重做日志缓冲刷新到重做日志文件
  • 额外内存池: InnoDB存储引擎中,对内存的管理师通过一种称为内存堆的方式进行的,在对一些数据结构本身的内存进行分配时,需要从额外的内存池中进行申请,当该区域的内存不够时,会从缓冲池中进行申请。

缓冲池是一个很大的内存区域,InnoDB是如何对这些内存进行管理的呢。答案就使用LRU list。
LRU(Latest Recent Used, 最近最少使用)算法默认的是最近使用的放到表头,最早使用的放到表尾,依次排列。当有LRU填满时有新的进来就把最早的淘汰掉。InnoDB则是在这个基础上进行了修改:

  1. 最近使用的不放到表头,而是根据配置放到一定比例处,这个地方叫做midpoint, midpoint之前的成为new列表,之后的成为old列表。淘汰的同样是表尾的页。
  2. 为了保证new列表的不经常使用时能够淘汰,设置了一个超时时间:innodb_old_blocks_time,当数据在midpoint(我理解应该是在old列表中,不然这个点的页就一个,变化也比较频繁)的时间超过找个时间时就会被提升到表头,new列表的表尾页则被置换到old列表中。

这么做的原因主要是因为常见的索引或数据的扫描操作会连续读取大量的页,甚至是全表扫描。如果采用原来的LRU算法就会更新全部的缓冲池,其他查询需要的热点数据就会被冲走,导致更多的磁盘读取操作,降低数据库的性能。
LRU是用来管理已经读取的页,当数据库启动时LRU是空列表,既只有表头,没有内容。这时页都放在Free List中。当需要有数据读写时要进行需要获取分页,这时要从Free List中删除分页,然后添加到LRU list中。到一定时间Free List中的分页就会被分配完毕,这时候就正常使用上面的LRU策略。
LRU列表中的页被修改后,称该页为脏页(dirty page),既缓冲池中的数据和磁盘上的数据产生了不一致,这时脏页会被加入到一个Flush 列表中(注意,同时存在两个列表中)。然后根据刷新的机制定时的刷新到磁盘中。

Checkpoint技术

checkpoint其实就是一个刷新缓冲到磁盘的触发机制,当满足一定的条件时就会刷新缓冲到磁盘,这样做可以解决以下几个问题:

  • 缩短数据库的恢复时间: 数据库恢复可以使用redo log,但是如果要恢复的数据很多就会很慢。如果使用checkpoint刷新到磁盘,只需要从checkpoint开始恢复就可以了,所以速度会变快。
  • 缓冲池不够用时,将脏页刷新到磁盘。我们知道缓冲池的大小是由限制的,为了能够高效的使用缓冲池需要把一部分数据刷新到磁盘。
  • 重做日志不可用时,刷新脏页。重做日志并不是无限增大的,而是循环利用的。当有些已经不需要的页存在时可以覆盖写,当可用的页放不下时就会触发checkpoint,刷新到磁盘一部分脏页到磁盘,这样就能覆盖掉一些不再使用的重做日志。

checkpoint根据触发时间,刷新页的策略又可以分为:

  • sharp checkpoint:刷新所有的脏页到磁盘。一般发生在数据库关闭时,为了保证所有的数据能够正常持久化。
  • fuzzy checkpoint:只刷新部分脏页。运行时使用这种可以保证系统的性能。

    Master Thread的工作方式

关键特性

插入缓存

这里所说的插入缓存也是Insert Buffer, 区别是这个插入缓存不是缓冲池中的插入缓存,这里的插入缓存和数据页一样,业务物理页的组成部分。在介绍插入缓存之前先了解聚集索引和非聚集索引,他们之间最重要的区别就是:聚集索引的叶子节点存储的是数据,而且是按照物理顺序存储的;非聚集索引叶子节点是地址(也就是聚集索引键地址),是按照逻辑顺序存储的(以上言论是从网上了解到的,但是本书P194特别指出,聚集索引也不是按照物理地址连续的,而是逻辑上连续的)。
知道这个差别后就知道,当不停的插入数据时,如果是聚集索引的数据,按照物理顺序(这个应该是一般情况下,因为是一般聚集索引是主键,顺序递增的,所以这时候地址就是顺序的)连续插入,代价比较小。而如果是非聚集索引的插入则物理地址是离散的,会导致很大的系统开销,所以对于非聚集索引InnoDB开创性设计了Insert Buffer。使用InnoDB的Insert Buffer需要以下两个条件:

  • 索引是辅助索引(非聚集索引 secondary index);
  • 索引不是唯一(unique)的。

Insert Buffer的使用流程是:

要求索引不是唯一的是因为如果索引是唯一的,那么每次更新都要坚持是不是已经存在,每次还是要访问数据页,这就失去了使用Insert Buffer的优势。
后面还提到了Update buffer以及Merge的过程和Insert Buffer的实现,这里就不再一一说了。

两次写

上面提到的Insert Buffer是提高了数据库的性能,doublewrite则是提高了数据库的可靠性。一个场景是当一个16k的数据页只写了一部分,比如4k,这时候突然断电,就会导致这个页的数据不全。所以就会导致这个页的数据丢失。我们知道重做日志是用来恢复数据的,但是重做日志记录的是对页的物理操作,如果这个页已经发生了损坏在对其进行重做是没有意义的。

上面这段话,其实我并没有看懂,因为对页操作之前是先写重做日志的,当发生宕机时正在写数据页,证明这时候重做日志已经写完了。这时重做日志的记录的完整的,当用这个记录去恢复数据时,不管页是不是损坏,重做日志直接覆盖不就行了么?为什么不行呢?等到后面我更加深入的了解后再来补充。

doublewrite有两部分组成,一部分是内存中的doublewrite buffer, 大小为2MB,另一部分是物理磁盘上共享表空间中连续的128个页,既两个区,大小同样为2MB
。对缓冲池的脏页进行刷新时,比不直接写磁盘,而是会通过memcpy函数将脏页先复制到内存中的doublewrite buffer, 之后通过doublewrite buffer再分两次,每次1MB的写入共享表空间的物理磁盘上,然后马上调用fsync函数,同步磁盘,避免缓冲写带来的问题。完成doublewrite页的写入后,再将doublewrite buffer中的页写入各个表空间文件中。

如果磁盘写入时发生崩溃,可以从共享表空间的doublewrite中找到副本,将其复制到表空间文件,再应用重做日志。

这个地方也有一个疑问,当doublewrite写入的过程中发生了崩溃,这时候数据该怎么办呢?

自适应哈希索引

对于缓冲池中的页,为了能够快速的查找,InnoDB跟情况对其建立了一个hash index。这样对于等值查询就能够利用这个索引更加快速的查找,提高了查找的性能。

异步IO

为了提高磁盘的操作性能,当前的数据库系统都采用异步IO的方式处理磁盘操作。用户可以在发出一个IO请求胡立即再发出另一个IO请求,当全部IO请求发送完毕后,等待所有IO操作完成,这就是AIO。
AIO的另一个优势是可以进行IO Merge操作,也就是将多个IO合并为1个IO, 这样可以提高IOPS的性能。

刷新临近页

Flush Neighbor Page(刷新临近页)是当刷新一个脏页时,InnoDB会检测该页所在区的所有页,如果是脏页,那么一起进行刷新。