干货来啦!InnoDB的Buffer Pool和其它特性

242 阅读10分钟

内容大纲

  • InnoDB的自适应哈希索引
  • InnoDB的双写缓冲区
  • Change Buffer
  • Buffer Pool的内部组成
  • Free空闲链表管理
  • 缓冲页的hash处理
  • Flush脏页链表管理
  • LRU链表的管理
  • 刷新脏页到磁盘方式

1.InnoDB三大特性

1.1.Buffer Pool(见下文)

1.2.自适应Hash索引

InnoDB的索引一般使用B+Tree的数据结构,但是InnoDB还支持一种索引的方式:哈希索引。InnoDB底层会实时监控索引,如果某个索引使用十分频繁,就会被认定为这个索引是热点数据,然后在InnoDB的Buffer Pool内存中创建一个Hash索引,称之为自适应Hash索引。创建以后,如果下次的查询又命中索引,那么就可以利用这个Hash索引直接O(1)的时间复杂度就能查询结果。 但是,自适应hash索引只能提供等值匹配的查询,做范围、模糊查询是不支持的。而且InnoDB的自适应Hash索引使用的是拉链法解决Hash冲突,并且外部无法干预,但是可以选择开启和关闭。

1.3.双写缓冲区 DoubleWrite Buffer

如果出现了页数据的丢失,或页刷盘的不完整,就有可能导致InnoDB丢失数据的情况,而RedoLog的恢复是建立在完整的Page页的,所以InnoDB为了提高可靠性,提出了双写缓冲区的概念。 一个Page是16KB,OS的pageCache是4KB大小,这种不一致的情况就有可能导致刷盘的故障。DoubleWrite Buffer分为内存和磁盘两个模块的架构。

     1. Page页数据先从Buffer Pool内存中拷贝到双写缓冲区中。
     1. 双写缓冲区将Page页数据刷盘到磁盘的双写空间中。
     1. 双写缓冲区将Page页数据正常刷盘到.ibd数据文件中。

InnoDB的双写机制是默认开启的。

2.Change Buffer

MySQL提出了对DML语句(增删改)做了进一步的效率优化,提出了一个ChangeBuffer的区域,这部分区域在Buffer Pool中,目的是提高InnoDB对非唯一性索引的增删改操作的效率。 如果一个写操作修改了非唯一性索引的数据,而且这个索引又不在Buffer Pool中,将这些写操作记录到Change Buffer中,等待后续这些非唯一性索引的页缓存到Buffer Pool时,将Page数据和DML操作数据进行Merge合并。这样就能减少了磁盘IO动作,同时提高了增删改的效率。 之所以Change Buffer不能针对主键or唯一索引,是因为修改唯一性索引需要检查数据的唯一性,无可避免的需要读取Page业数据进行唯一性的校验,那么这种场景直接就在Buffer Pool中进行了。

2.1 Change Buffer适用场景

     - 适合大部分是非唯一索引的场景。
     - 适合写多、读少的场景,或者是写后不会立刻读取的场景。

3.缓存引入的重要性

InnoDB是通过页来管理数据的,页是存放在表空间中,表空间说白了就是对数据文件的抽象,数据始终是存储在磁盘上的,如果每次CRUD的操作都要和磁盘进行IO交互,那MySQL的性能将会大打折扣,吞吐量页没法保证。此时InnoDB就需要引入一个Buffer缓冲区来优化这个问题,当客户端要访问某个页面或者数据时,可以直接从Buffer缓冲区中快速读取,这个缓冲区是在内存中的,速度不言而喻。而且这个Buffer会将页数据缓存一段时间,以便于再次访问页数据时能够快速定位,提高效率。 总之,缓存的引入就是为了减少磁盘的IO操作。但是缓存的引入也带来了许多问题,例如:内存资源问题、Buffer管理维护问题、数据一致性问题等。

4.InnoDB的Buffer Pool是什么

从名字上看就能明白这是一个基于内存的一个缓冲池子,这个pool池中缓存了一个一个的页。MySQL服务器启动的时候就会向操作系统申请一块连续的内存空间,这个连续内存空间叫Buffer Pool。 这个Buffer Pool可以通过系统参数调节innodb_buffer_pool_size,默认只有128MB,最小5MB。

5.Buffer Pool的组成结构和原理

5.1 Buffer Pool的基本组成

image.png 碎片:每一个缓冲页都有对应的控制块,这些控制块和缓冲页的大小和Buffer Pool大小如果不是很对应倍数,那么就存在碎片区域。每个控制块的大小占用很小,基本不到页大小的5%,连1KB都不到。

5.2 free空闲链表管理

MySQL服务启动的时候,向操作系统申请了一块连续的内存空间Buffer Pool,但是这个缓冲池里没有任何数据页面和控制数据,此时控制块和缓冲页里的内容是空的,当有读取操作任务时,从磁盘读取页数据到Buffer Pool中,但是具体将页放在那儿呢?这时候就需要一个和JVM分配内存类似的结构空闲链表来维护和提供空闲地址的信息。 image.png 其中,这个链表中的控制块就是Buffer Pool中的控制块,只不过被free基几点组成了一个双向链表。其中这个基结点/头结点不属于Buffer Pool缓冲池内,它占用的空间很小,只有40字节

5.3 缓冲页的哈希表快速定位

当我们要访问Buffer Pool查询某个页面是否在缓冲池中时,不可能遍历整个缓冲页。InnoDB基于哈希表的思想,将表空间号 + 页号作为key,控制块地址就是value。通过哈希表的O1时间复杂度,就可以快速定位某个页是否在其中了,找到了控制块,那么缓冲页同样也就找到了。

5.4 Flush脏页链表管理

脏页:如果SQL语句修改了某个页面的数据,此时Buffer Pool中的缓冲页数据就和磁盘上的数据不一致了,这样的页称之为脏页。 脏页毫无疑问肯定是要刷新回磁盘的,但是如果每修改一次就刷新一次,这未免太频繁了,对性能有很大影响。所以InnoDB设计了将脏页组成一个链表来管理何时刷盘等操作。 image.png 很显然,一个控制块不可能即是空闲链表的节点又是脏页链表的节点。

5.5 LRU缓冲管理链表

5.5.1 Buffer Pool空间不够了怎么办?

Buffer Pool毕竟是内存资源,也有用完的时候。作为MySQL提高性能的利器,如果出现了缓冲池大小不够用的情况,也不可能无限的去操作系统申请内存;肯定是有自己的一套管理内存淘汰相关的策略。当发现Buffer Pool不够用的时候,肯定是要淘汰掉一些不常用的页面。

5.5.2 使用普通的LRU链表管理淘汰策略有什么问题?

LRU又派上了用场,如果使用LRU链表的形式来管理。当访问一个页面时将该页面控制块挪到链表头部,后续如果发生了内存不够用的情况,就淘汰掉链表末端的节点即可。 但是这种简单的LRU方式有两个大问题:

        - InnoDB有预读的功能,结合空间局部性原理,一次性从磁盘读取多个页面到Buffer Pool中,如果许多页面压根用不上怎么办?
        - 如果有全表扫描这样的场景,整颗B+Tree的页面都放到Buffer Pool中,将其它缓冲页挤出去,影响到了其它热点的缓冲页。

5.5.3 预读

        - `**线性预读**`**:如果连续访问某个区的页面超过了阈值(默认56,可调),那么innoDB就会触发一次异步任务,将下一个区中的全部页面加载到Buffer Pool中。**
        - `**随机预读**`**:如果某个区中连续13个页面都被加载到了Buffer Pool中,那么innoDB就会触发一次异步任务,将当前区中的剩下页面都加载到Buffer Pool中。随机预读功能默认是关闭的,也可以通过参数开启。(这13个页面必须是热页面,要在LRU的yong区前1/4范围)**

5.5.4 分区域的LRU链表管理淘汰策略

热点转移的思想就可以用在这里,innoDB将LRU链表进行了分区域的管理,按照一定的比例分成两截:

        - `热数据区、young区`:存储使用频率很高的缓冲页的控制块。
        - `冷数据区、old区`:存储使用率不是那么高的缓冲页的控制块。

image.png

5.5.5 分区域的LRU优点

        - 当有新的缓冲页加载到Buffer Pool来,会被放到old区域的头结点上。
        - old区的控制块被读取的时候不会立即放入yong区的头,而是会判断上一次访问该控制块的时间间隔,如果大于某个间隔(默认1s,可调)就不会被放入yong区。
        - 只有访问yong区的1/4之后的区域的缓冲时,才会挪动这些控制块到yong的头结点,因为没必要每次访问yong区的缓冲都要挪动,毕竟yong区本来就是热点缓冲,没太大必要操作这么频繁。

6.脏页的刷盘

MySQL服务后台线程有专门的的线程定时周期性的将脏页刷新到磁盘,有两种方式:

6.1 从LRU遍历寻找脏页刷盘

后台线程会定时从LRU链表的尾部开始扫描控制块,找到有脏页的控制块,然后刷盘这个缓冲页面。

6.2 从Flush脏页链表取一部分刷盘

后台线程定时从flush脏页链表中刷新一部分的缓冲页面,刷盘的速率等取决于MySQL服务的繁忙程度。

7.多Buffer Pool实例

Buffer Pool本质上是一段连续的内存空间,在多线程访问的情况下对Buffer Pool的各个链表操作都要加锁处理。那么整个MySQL只有一份儿缓冲池的设计肯定是不行的,MySQL就提供了一个参数:innodb_buffer_pool_instances来控制申请Buffer Pool实例的个数。每个Buffer Pool的链表相互独立、内存也独立。这样多线程访问的时候通过热点分散思想就提高了整个MySQL服务效率。 image.png

7.1 chunk

早期Buffer Pool的实例、大小等参数在MySQL启动时就要指定,不能在运行时修改。在MySQL5.7.5版本后,推出了chunk的概念,一个Buffer Pool中包含多个chunk,每一个chunk就是一个连续的物理空间,需要向操作系统申请。 image.png