Innodb存储引擎

  • PS:部分图片暂缺,后续画完补上。

一、概述

  1. Innodb 存储引擎是日常开发中,使用最多的存储引擎;它主要的设计目标是为了支持那些面向在线事务处理(OLTP)的应用,主要的特点是行锁设计,支持外键。

  2. 使用MySQL的前提下,甚至可以说需要完整ACID支持事务的系统,底层的存储引擎都是使用的Innodb,往下看对比各类引擎的特性,你就会知道我所言非虚。你可能会说:MyISAM存储引擎可以用于全文检索啊?因为有更好的替代方案啊,完全可以使用Elasticsearch替代它。

  3. Innodb存储引擎将数据存放于逻辑表空间中,这个表空间由Innodb存储引擎自身进行管理,对上层用户透明,它还支持将每个表单独存放到独立的 .idb 文件中。<共享表空间><每个表可以有独立的表空间>
    对于逻辑表空间,Innodb 可以把所有数据都存放到共享表空间,也可以选择为每个表建立独立的表空间

  4. Innodb支持行多版本技术,以提高并发,后续章节将细说。除此之外Innodb还提供了插入缓冲(insert buffer)、二次写、自适应哈索引、预读等提高性能和可用性的功能。

  5. 对于表中数据行的存储,MySQL使用聚集索引,就是说每行数据根据主键按序存储;值得一提的是,MySQL的辅助索引虽然也是按辅助索引的键值排序存放,但是辅助索引不存储完整的数据行,只是存储了到主键的映射,使用辅助索引不可避免的最后会进行一次离散读,获取到实际的行数据。

二、Innodb体系架构

可以简单的认为Innodb由一个内存池和一些后台线程组成。

后台线程

  1. Master Thread
    它是非常核心的后台线程,主要负责将缓冲池中的数据异步刷新到磁盘,保证数据一致性(将数据的更新持久化到磁盘中),包括:脏页刷新、合并插入缓冲、UNDO页回收等。

  2. IO Thread
    Innodb 中大量使用AIO处理IO请求,IO Thread 主要负责这些IO请求的回调。

  3. Purge Thread
    事务提交后,其所有的undo log(事务执行过程中,可能被用于回滚,事务提交后可以复用)可能不再需要,Purge Thread 主要负责回收 undo 页。

  4. Page Cleaner Thread
    它的作用是将之前版本的Innodb中的脏页刷新操作放入单独的线程中,它的目的是为了减轻原Master Thread 的工作,以及对用户查询线程阻塞,进一步提高Innodb引擎的性能。

内存池

Innodb 存储引擎的内存体系结构包括:缓冲池(innodb_buffer_pool)、重做日志缓冲(rodo_log_buffer)、额外内存池(innodb_additional_mem_pool_size),我们先重点关注,缓冲池。
photo_buffer

1. 缓冲池

Innodb引擎是基于磁盘对数据页进行存储的,然而CPU速度与磁盘速度之间有着巨大的鸿沟,所以数据库系统通常使用缓冲池技术来提高性能。缓冲池的本质是一块内存区域,把CPU与磁盘的交互变更为与内存进行交互。

  • 当需要读取数据时先判断缓冲池中是否存在,否则从磁盘中读取;只要被读取过的页都会被缓冲池管理起来(可能被LRU算法淘汰)。

  • 当需要对数据进行修改时,判断页是否在缓冲池中,如果不在则从磁盘读取到缓冲池中;对数据的修改都是先修改缓冲池中的数据页,然后以一定的频率刷新到磁盘(参考Master Thread 刷新脏页的动作)。所谓一定频率,指的是脏页刷新并不是每次修改页之后都进行,而是通过checkpoint技术进行控制。

  • 由于需要将大量的数据页加载到缓冲池中,所以缓冲池的大小对Innodb引擎非常重要;

  • 其中缓冲池中缓存的数据页类型有:索引页、数据页、undo 页、插入缓冲、自适应哈希索引、Innodb存储的锁信息、数据字典信息等。

2. LRU List、Free List 、Flush List

  • LRU:Latest Recent Used 最近之最少使用算法。<末尾淘汰>
  • Free List:空闲列表。
  • Flush List:LRU List中的页被修改后称为脏页,Flush List 中的数据页即为脏页。

前边讲了缓冲池是什么,而LRU则是数据库管理缓冲池中数据页的算法。即频繁使用的页会往LRU前端移动,而较少使用的则会逐渐移动到尾部,最终淘汰。(Innodb默认页的大小为16K)。

Innodb对LRU进行了调整,在其中加入了midpoint,最新读取的页不会直接放到LRU头部,而是放到 midpoint锁代表的位置处,其一般取值为LRU列表长度的5/8 处。midpoint之前称为new列表,之后称为old列表。引入midpoint的目的是避免某些扫描操作读取了大量的页,将真正的热点数据挤出LRU列表。

脏页数据会同时存在于:LRU List 和 Flush List 中。


3. 重做日志缓冲 redo log buffer

Innodb 会先将重做日志写到重做日志缓冲,在事务提交时或者由Master Thread 定时每秒刷新的磁盘上的重做日志文件中。
重做日志缓冲可能的刷新磁盘时机(配置项控制,参考"双一原则"):

  • Master Thread 每秒调用fsync 批量刷新。
  • 每次事务提交后立即调用 fsync 刷新。
  • 当 redo log buffer 空间使用量到达一半时。(默认为:8M)

4. 额外的内存池

数据库数据相关的内存从缓冲池中分配,然而需要考虑到程序本身运行也是需要分配内存的,对Innodb本身数据结构(例如:缓冲控制对象)进行内存分配时,需要从额外的内存空间分配,当该区域不足时,会从缓冲池中申请。

三、CheckPoint技术

提及缓冲池时,说到了,对数据的修改都是在内存中进行的,而被修改的脏页由checkpoint机制,控制刷新到磁盘。

  1. 当满足如下条件:
  • 缓冲池足够大,以至于可以容下所有的数据页;
  • 重做日志可以无限膨胀。

此时数据就不需要刷回磁盘了,即便发生了宕机也可以通过重做日志进行恢复。

  1. 需要考虑的问题是:
  • 缓冲池页不可能无限扩大,当数据量到达TB 甚至更高级别时,通过内存来存放全部数据已经变得不现实。
  • 重做日志过大,恢复过程将非常耗时;
  1. 针对上述问题,CheckPoint技术的目的是解决如下几个问题:
  • 缩短数据库恢复时间;
  • 缓冲池不够用时将数据刷回磁盘;<参考LRU溢出末尾数据页>
  • 当重做日志不可用时,刷新脏页到磁盘。<重做日志设计为重复使用,不能无限增大>

checkpoint 执行后会标记已经被刷新到磁盘了的重做日志位置,当发生宕机时,只需要恢复 checkpoint 标记位置之后的重做日志即可。当重做日志被刷新到磁盘后,该部分空间即可重复利用。
Innodb 通过LSN (log Sequence Number)来标记版本,它是一个8位的数字单位是字节。每个页、重做日志、checkpoint 都有 自己的 LSN。

  1. checkpoint 执行时机:
  • 每次关闭系统时,会执行 Sharp Checkpoint将所有脏页刷新到磁盘。
  • Master Thread 定时将一定数量的脏页刷新到磁盘。
  • LRU List 末尾数据页溢出时,如果是脏页也需要执行Checkpoint
  • 当重做日志文件不可以时,也需要执行checkpoint 强制将一些脏页刷新到磁盘,以保证重做日志文件的可用。

当数据库宕机后恢复时,如果 checkpoint 的 lsn 小于 redo log 的 lsn 那么需要重做,重做执行时,判断页的lsn 如果页的lsn 小于checkpoint 的lsn,则需要执行重做。

四、Innodb关键特性

下列特性为Innodb带来了更高的可靠性:

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

Insert buffer (插入缓冲)

insert buffer 定义

  1. 插入缓冲(Insert buffer) 是Innodb 中一项非常重要的功能,虽然 缓冲池(innodb_buffer_pool)中有插入缓冲的信息,他跟数据页一样,也是物理页的一个组成部分。

  2. 我们知道Innodb 中,完整的数据行通过聚集索引维护(以主键顺序保存);当没有显式定义主键时Innodb会自己从表定义的列中选择一个主键;当不存在能够作为主键的列时,会自动生成一个6字节的主键。根据这个特性,我么可以得知数据在聚集索引上的插入是非常快的,因为它不需要频繁的从磁盘读取另一个页。

  3. 有主键上的聚集索引,同样也有其它字段上的辅助索引

     a. 辅助索引即:非聚集索引,只保存关联主键,不保存完整行数据,由于非聚集索引不能保证跟聚
     集索引插入顺序一致,当批量插入操作时对辅助索引执行插入,就会出现一些对磁盘上非聚集索引
     页的离散访问。
     
     b.例如:当主键是一个自增整数,而辅助索引是一个时间字段,当我们对主键序的字段进行插入
     时,这是最极端的情况是,每一条被插入的数据所在非聚集索引页,可能都不同,这时每对辅助索
     引插入一次就需要读一次磁盘(假定数据库未被预热,缓冲池无相关数据页)。
     
     c. 由于每一条插入辅助索引都需要离散的访问磁盘数据,所以会拖累整体的插入速度。若改为使用
     insert Buffer 后:插入时聚集索引有序,不会发生离散读,而对辅助索引的操作只在内存中进行,
     因而可以大幅度提高插入效率。
    
  4. 为此,Innodb 设计了 insert buffer,对于非聚集索引的插入,并不会直接插入非聚集的辅助索引中,而是先判断数据页是否在内存中。如果不在,则先存放到 Insert buffer 对象中,这时会设定一个假象,假定数据已经插入聚集索引中,实际上并没有。运行过程中,再以一定频率对Insert Buffer中的辅助索引叶子节点进行合并(merge),这时通常能将多个插入合并为一个插入。

当然,使用Insert Buffer 也是有条件的:

  • 索引是辅助索引
  • 该索引不是唯一的,<因为数据保存至insert buffer 时不会查索引判断数据是否存在>
  1. insert buffer 存在的问题是,在写密集情况下,插入缓冲会占用非常多的缓冲池内存。

  2. 后续版本还出现了 Change Buffer 它被视为 Insert Buffer 的升级,它不仅是插入,他对所有DML 操作都进行缓冲:INSERT、UPDATE、DELETE,对应于:insert buffer、Purge buffer、delete buffer。

Insert buffer 的实现

  1. Innodb 维护的 insert buffer 是一个B+ 树,新版本的 Insert buffer 是一颗b+ 树,存放于共享表空间中,负责所有表辅助索引上插入的insert buffer 功能支持。
    photo_insert_buffer

  2. Insert Buffer B+ 树种维护插入的行数据时,叶子节点和非叶节点,都需要记录数据所在表、以及所在页的偏移量、数据进入时的顺序(Change Buffer 回放时需要保证正确顺序),故此,叶节点上相较于聚集索引上实际存放的数据,会多消耗9个字节。

  3. 启用Insert Buffer 后,需要使用:Insert Buffer Bitmap来记录每个辅助索引页的剩余可用空间,因为需要保证每次merge 操作必须成功;每个Insert Buffer Bitmap页用来追踪16384个辅助索引页(256个区),每个Insert Buffer Bitmap页都在这 16384个页中的第二个页。例如:merge 操作合并了30 条该辅助索引页上的插入操作,但是辅助索引页上并没有足够空间保存这30条数据,这时可能就会出现问题。

Merge Insert Buffer

合并 insert buffer 操作可能发生的情况:

  • 辅助索引页被读取到缓冲池中时。
  • Insert Buffer Bitmap 页最终到该辅助索引页已无可用空间,当监测到该辅助索引页中剩余空间小于1/32 时会强制执行merge ,即将该辅助索引页强制读取到缓冲池中。
  • Master Thread 定时执行。<没10秒执行>

可以大胆的理解为:insert buffer 将多次可能需要操作多个磁盘上非聚集索引页数据的操作,在内存中合并为一个操作,从而减少的对非聚集索引页的频繁离散读取,从而提高效率。

两次写 doublewrite

photo_double

  1. doublewrite 带来了数据页可靠性的提升;

  2. 前边描述Innodb的后台线程时提到了Master Thread,它需要完成的任务之一是:将脏页刷新到磁盘。

  3. 对于单个脏页刷新磁盘的过程是离散的, 由于是离散的,如果发生宕机,会带来部分写失效问题。 例如,正在写某个页到磁盘只写入了一部分就发生了宕机,导致磁盘中保存的页数据本身损坏了,那么由于它本身损坏了,那么再对它进行重做是没有意义的。
    简单的说:离散写磁盘,宕机时物理磁盘中数据被损坏,缓冲池中脏页数据丢失,导致无法恢复。

  4. 针对这个问题设计了 doublewrite 机制,它由两个部分组成:一个是位于内从中的 doublewrite buffer 大小为 2M (两次写缓冲,位于内存中,一旦宕机则丢失);另一个位于共享表空间中,它是 一块连续的磁盘空间,包含连续的128个页(两个区,后续详述),同样的大小为:128 * 16K = 2M。

  5. 当写脏页时,并不会直接写磁盘,而是通过memcpy 函数将内存中的脏页先复制到内存中的doublewrite buffer中(容量2M)。然后分两次,每次1M将这些脏页写入到,共享表空间中的doublewrite 区域并立即调用fsync 同步磁盘。因为前面提到,写共享表空间是连续的,所以非常快开销很小。

  6. 当写入共享表空间完成后,则开始进行从:内存中的 doublewrite buffer 将所有脏页离散的写入Innodb维护的数表空间中。
    就算来自同一张表的脏页,也无法保证向数据表空间写脏页连续,因为他们必定位于 各自表的B+ 树上的不同位置。

当发生宕机行为,导致各表独立表空间中数据页因部分写失效被损坏,会发生如下行为:

  • 重启数据库,进行重做时,发现表的 独立表空间中 的数据页损坏。
  • 共享表空间中 的doublewrite 区,拿到完好的数据页,将其刷新到 表的 独立表空间中 ,然后对其应用重做日志恢复数据。

你可能会问:要是宕机时,写 double write 也失败了呢?

  • 这个问题不难回答,观察上述流程第五、六步,不难发现:不论是内存中的 doublewrite buffer 还是 共享表空间中的 doublewrite 区,他们都必须在离散写脏页前完成。
  • 如果,写doublewrite 都失败了,是不会发生脏页写磁盘空间行为的,那么就更不会有各个表的,表空间文件中数据页,因部分写导致的失效问题。
  • 这时只需要对没有损坏的页数据,应用redo log进行重做即可恢复数据。

这个机制其实很好理解:

自适应哈希索引

散列算法可以得到常数级的访问时间,Innodb会监控各索引上的查询,若观察到建立哈希索引可以带来提升,会自动建立哈希索引--自适应哈希索引 ( Adaptive Hash Index:AHI ) 。它通过缓冲池中的B+树页来构建,且不需要对整棵树进行散列,所以构建速度非常快。具体使用场景可以结合散列算法来了解,所以它不能查询范围,只能根据等值查询(需要根据key进行散列查找对应的桶)。它有Innodb 自动维护,用户无法干预。
构建要求:

  • 该模式访问了100次;
  • 页通过该模式访问了:页中记录数 / 16 次。

异步IO

  1. 为提高磁盘性能,当前数据库系统多采用异步IO (Asynchronous IO) 来处理磁盘操作。与之相对应的是:Sync IO,每个IO操作都必须等别的操作完成后才能进行。

  2. 当一次查询过程中需要扫描索引上的多个页时,如果不使用异步IO,那么会消耗无谓的等待时间,而使用异步IO可以并发的扫描,并最终合并扫描结果,执行速度将得到大大的提升。

刷新邻接页

它的工作原理是:当刷新一个脏页时,Innodb存储引擎会检测页所在 “区” (后续章节会提到)的所有页,如果是脏页,那么就会一起刷新,这样做的目的是通过AIO将多个IO写入操作合并为一个。该特性在传统机械硬盘上有良好表现。

本文来自我对 《MySQL技术内幕:InnoDB存储引擎》一书阅读过后的二次创作,文件颇多截图引用书中插图,此外本文主要用作个人学习后的思考感悟的记录,或许不如原书讲得深入且全面,强烈建议购买原书深入了解更多的细节。

-- 文章搬迁自个人CSDN博客

原文地址:https://www.cnblogs.com/bokers/p/15375967.html