(三)MySQL全解----基于InnoDB存储引擎的内存缓冲区的结构和管理

前言

上一篇 MySQL运行时的缓存和缓冲 介绍了MySQL运行时占用系统的内存中所包含的缓存和缓冲

本篇则基于InnoDB存储引擎,详细剖析一下存储引擎的缓冲区

一 MySQL存储引擎的内存缓冲区

我们都知道:MySQL的数据都存储在磁盘

如下场景:
当查询数据:到磁盘读取(忽略查询缓存场景);当修改数据:先从磁盘读取,然后修改,修改完立马写入磁盘
显然每次操作都需检索磁盘,磁盘检索数据是很慢的,那么必然影响了MySQL的性能

试想一下:
如果磁盘数据先存放到内存中,读取/修改都直接操作内存中的数据,那么每次操作就不需要先到磁盘读取数据再进行操作,省略了检索磁盘的时间,提高了MySQL的响应速度

针对这一问题,MySQL的存储引擎引入了缓冲区:
MySQL几乎所有的存储引擎在启动时,都会向操作系统申请一块连续的内存空间,这块内存空间就是缓冲区。每个存储引擎的缓冲区不一定相同,如:InnoDB的缓冲区是innodb_buffer_pool,而MyISAM是key_buffer

MySQL支持的存储引擎中,应用最为广泛的是默认的InnoDB,所以接下来就以InnoDB的缓冲区为例,盘一盘MySQL内存缓冲区的结构和管理

二 InnoDB的内存缓冲区: Buffer Pool

InnoDB几乎将所有对数据库的操作都放在内存缓冲区执行,这块内存缓冲区在InnoDB中被称为Buffer Pool

MySQL 5.6及以后版本,默认配置下,Buffer Pool的大小为 128MB,由参数 innodb_buffer_pool_size 控制,一般建议为可用物理内存的 60%~80%,大小通过如下SQL命令查看:

show global variables like "%innodb_buffer_pool_size%";

InnoDB存储引擎的内存缓冲区结构图:
InnoDB的Buffer Pool结构图

其中:

  1. Data Page:数据页缓冲。主要存放磁盘的表数据页,提升MySQL查询速度
  2. Change Buffer:写入缓冲。主要存放要修改的数据,提升MySQL的写速度
  3. Index Page:索引页缓冲。主要存放索引的根节点,避免全盘查找索引根节点的操作
  4. Log Buffer:日志缓冲。存放MySQL的各种日志,提升日志的写入速度
  5. Dict Info:数据字典。存放MySQL所有库、表元信息
  6. Lock Space:锁空间。主要存放锁对象
  7. Adaptivity Hash:自适应hash索引。InnoDB会基于经常访问的索引页自动构建自适应hash索引
  8. LRU List:内存淘汰页列表。管理整个缓冲区的页
  9. Free List:空闲页列表。目前未被使用的内存页
  10. Flush List:脏页列表。未落盘的数据页,即有数据变动但是数据还没有写入到磁盘的数据页

一) Data Page(数据页)

1 什么是Data Page

InnoDB把磁盘中的数据划分为一个个的「页」,以页为单位对MySQL中的表数据进行管理,一个页大小默认16KB

当InnoDB拿到申请的内存空间后,把 缓冲区(Buffer Pool) 按照 页的大小 划分出一个个的页, 以页作为磁盘和内存交互的基本单位,这些页就叫做缓冲页

MySQL服务启动时,这些缓冲页都是空闲页,没有缓存任何数据,随着随着SQL语句(包括增删改查)的执行,将磁盘中的数据页加载到缓冲页,这些缓存了磁盘数据的缓冲页就是Data Page,以页为单位,每个页大小默认16KB,Data Page提升了MySQL的查询性能

2 Data Page的特点

  1. 数据从磁盘加载到Data Page中,使得InnoDB的缓冲区(Buffer Pool)也具备了查询缓存的功能,提升了查询速度
  2. SQL语句检索过程扫描到的所有数据页都会将加载到Data Page中,即使不包括所查询数据的数据页
  3. 在内存充足的情况下,InnoDB会尽可能的将磁盘中的所有表数据全部载入内存
  4. 当表数据过大,缓冲区(Buffer Pool)不足以载入全部表数据时,InnoDB使用内存管理与淘汰机制淘汰一些Data Page,防止内存溢出

3 一个读操作的加载过程

读操作的加载过程图

二) Change Buffer(写入缓冲)

1 什么是Change Buffer

缓存那些 不在缓冲区(Buffer Pool)的 变化了的辅助索引页 的缓冲页就是Change Buffer,以页为单位,每个页大小默认16KB

Change Buffer避免了对辅助索引页进行写操作时,从磁盘将辅助索引页读入缓冲区(Buffer Pool) 产生的大量随机磁盘I/O,提升了MySQL写的性能

Change Buffer在磁盘文件上也有对应的位置,属于系统表空间的一部分,当数据库服务器关闭时,索引变化的数据将被缓冲在系统表空间中

MySQL 5.6 及以后版本,Change Buffer占用缓冲区(Buffer Pool)的大小由参数 innodb_change_buffer_max_size 控制,表示占用缓冲区(Buffer Pool)的百分比。默认值25,最大值50

写入缓冲在一开始被称之为Insert Buffer(插入缓冲),只对insert操作生效
MySQL5.5之后的版本中,才正式改为写入缓冲,对于inser/update/delete语句都生效

2 Change Buffer的限制

虽然Change Buffer提升了MySQL写的性能,但是Change Buffer是有限制的:

1) Change Buffer只适用于缓存辅助索引页,即要变更的字段包含辅助索引字段,且辅助索引字段值对应的辅助索引页不在缓冲区(Buffer Pool)
因为唯一索引字段在操作数据前必须要进行唯一性检查,而同一张表的表数据不一定全部都加载到缓冲区(Buffer Pool),所以唯一性检查需要检索磁盘中的表数据,需要走磁盘
当insert/update/delete操作时,要变更的字段包含辅助索引字段,当辅助索引字段值对应的辅助索引页不在缓冲区(Buffer Pool),会把对辅助索引字段的修改写入到Change Buffer

2) 如果索引包含降序索引列或主键包含降序索引列,辅助索引也不适用Change Buffer

3 Change Buffer的merge时机

merge和刷盘是两个不同的操作:
merge是指把 Change Buffer 中对辅助索引页的变更合并到缓冲区(Buffer Pool)对应的辅助索引页中,之后这个辅助索引页变成脏页,等待刷盘
刷盘是指 缓冲区(Buffer Pool)中的脏页写到磁盘

merge时机:
1) 辅助索引页被加载到缓冲区(Buffer Pool)时触发:合并Change Buffer中对该辅助索引页的变更
2) 要变更数据对应的辅助索引页的空间不足时触发:一个辅助索引页的可用空间少于一定值(一般是1/32页)时,强制合并Change Buffer中对该辅助索引页的变更
3) Change Buffer空间不足时触发
4) 后台线程定时触发
5) MySQL服务关闭时触发

三) Index Page(索引页)

1 什么是Index Page

如下场景:
虽然InnoDB会将表数据加载到内存,但是表数据非常大时,显然无法把全部表数据载入内存。当需要使用索引去磁盘检索数据时,由于索引的根结点位于磁盘的任意位置,那么需要遍历磁盘才能找到索引的根结点

为了解决这一问题,InnoDB在缓冲区(Buffer Pool)中引入了Index Page:
缓存索引结点数据的缓冲页就是Index Page,以页为单位,每个页大小默认16KB

2 Index Page的特点

  1. 将索引的根节点载入Index Page后,命中索引的SQL语句,在Index Page找到对应索引的根节点,然后以根节点为起点使用索引检索数据,避免了全盘查找索引根节点的操作
  2. 随着服务的运行,也会将一些非根节点的索引页载入内存中,这是一种对于访问频率较高的索引页专门推出的优化机制
  3. 只存在表结构和索引,不存在任何数据情况下:每个索引的根节点只有一页大小,Index Page的大小=索引的个数*页大小

四) Log Buffer(日志缓冲)

InnoDB的缓冲区(Buffer Pool)中,缓存了日志的区域就是Log Buffer,单位字节,默认大小16MB

Log Buffer主要包括两部分:
undo_log_buffer:撤销日志buffer,最终写入undo log
redo_log_buffer:重做日志buffer,最终写入redo log

Log Buffer提升了InnoDB的日志写入速度:执行SQL时日志会先写入到Log Buffer,再由后台线程通过落盘策略写入到对应的log文件中

五) Dict Info(数据字典信息)

InnoDB的缓冲区(Buffer Pool)中,缓存了MySQL所有库、表信息的区域就是Dict Info

当查询某个库下面的表信息、某个表的字段、索引、约束等信息时,就是使用Dict Info中的数据

六) Lock Space(锁空间)

1 什么是Lock Space

InnoDB支持多种类型的锁,执行SQL时会涉及到锁(自动加锁/手动加锁),那么就会产生锁类型对应的锁对象,这些锁对象虽然是临时产生的,也需要空间存储

InnoDB的缓冲区(Buffer Pool)中,缓存了锁对象的区域就是Lock Space

Lock Space不只是存储了锁对象,还存储了并发事务的链表:锁的信息链表、死锁检测所需的事务等待链表

七) Adaptivity Hash(自适应hash索引)

1 什么是自适应hash索引

先说一下B+ Tree和Hash作为索引结构的优缺点
Hash:检索效率高,平均复杂度O(1);但数据无序,不适合排序查询
B+ Tree:数据有序;但检索效率取决于树的高度,平均复杂度O(logmn), m为B+ Tree的阶

为了在使用B+ Tree作为索引结构的同时也可以使用Hash索引,InnoDB引入了自适应hash索引:
MySQL运行过程中,InnoDB有一个监控索引搜索机制,如果发现自适应hash索引更好,InnoDB会自动建立自适应hash索引

2 自适应hash索引的特点

  1. 自适应hash索引是基于经常访问的索引页构建的
  2. 自适应hash索引减少了走B+ Tree索引的次数,提升了数据检索的效率
  3. MySQL更新自适应hash索引时涉及到并发问题,因此需要锁来保证: MySQL5.7之前只有一个分片,一把锁;MySQL5.7之后,默认8个分片,8个锁,最大512,提高了并行处理的能力
  4. 每个自适应hash索引绑定到一个特定的分区

三 Buffer Pool中内存的管理和淘汰

首先思考如下问题:

随着MySQL的运行,缓冲区(Buffer Pool)逐渐被使用,但是缓冲区(Buffer Pool)是有大小限制的。当超过缓冲区(Buffer Pool)大小时,InnoDB是如何淘汰已存在的数据?淘汰之后释放的内存不一定连续,导致整个缓冲区(Buffer Pool)变得零散,那么InnoDB又是如何管理整个缓冲区(Buffer Pool)的?

一) 缓冲页的控制块

为了管理Buffer Pool中的缓冲页,为每一个缓冲页都创建了一个控制块,控制块信息包括:缓存页的表空间、页号、缓存页地址、链表节点、锁信息、LSN 信息等
控制块也占用Buffer Pool的空间,Buffer Pool基于控制块来管理和淘汰缓冲页

Buffer Pool中控制块与缓冲页的关系是一对一,在Buffer Pool中的结构如下:
Buffer Pool中控制块与缓冲页的结构图

二) 空闲页链表(Free List)

当从磁盘加载数据页到Buffer Pool时,为了找到是否有合适的空闲页,需要遍历Buffer Pool中所有缓冲页 。虽然是在内存中遍历Buffer Pool,但是这样的操作依旧会耗时影响性能,所以为了更好的管理Buffer Pool中的空闲页,InnoDB使用了Free List

为了快速找到Buffer Pool中的空闲页,以空闲页的控制块作为结点,将所有空闲的缓冲页组成一个链表,这个链表就是Free List

当从磁盘加载数据页到Buffer Pool时,直接遍历Free List,然后将磁盘数据缓存页到空闲页中

Free List简化结构如下:
Free List结构图

其中:
head:指针,指向空闲页的控制块。表示Free List的第一个节点,
tail:指针,指向空闲页的控制块。表示Free List最后一个节点
count:Free List的节点数量
每个节点还包含一个指向下一个节点的指针

三) 脏页链表(Flush List)

当Buffer Pool中缓冲的数据页被修改后,会标记这个数据页。被标记过的数据页,称为标记页,或者脏页

当后台线程要刷盘时,为了找到脏页,需要遍历Buffer Pool中所有缓冲页,找到被标记的缓冲页然后刷盘。虽然是在内存中遍历Buffer Pool,但是这样的操作依旧会耗时影响性能,所以为了更好的管理Buffer Pool中的脏页,InnoDB使用了Flush List

为了快速找到Buffer Pool中的脏页,InnoDB以脏页的控制块作为节点,将所有脏页组成一个链表,这个链表就是Flush List

当后台线程开始刷盘,直接遍历Flush List,然后将脏页变更过的数据写到磁盘

Flush List简化结构如下:
Flush List结构图
其中:
head:指针,指向脏页的控制块。表示Flush List的第一个节点,
tail:指针,指向脏页的控制块。表示Flush List最后一个节点
count:Flush List的节点数量
每个节点还包含一个指向下一个节点的指针

四) 淘汰页链表(LRU List)

当从磁盘加载数据到Buffer Pool时,发现内存不足,为了找到可以淘汰的缓冲页,需要遍历Buffer Pool中所有缓冲页,然后淘汰。虽然是在内存中遍历Buffer Pool,但是这样的操作依旧会耗时影响性能,所以为了更好的管理Buffer Pool中需要淘汰的数据页,InnoDB使用了FRU List

为了快速找到Buffer Pool中可淘汰的缓冲页,InnoDB以可淘汰缓冲页的控制块作为节点,将所有可淘汰缓冲页组成一个链表,这个链表就是LRU List

淘汰是指清空已使用的缓冲页的所有数据,使其变成空闲页

五) 缓冲页与Free List, Flush List, LRU List三者的关系

LRU List是由 已使用但未曾变更过的缓冲页组成,空闲页和脏页是不会加入LRU List:空闲页本来就是未使用的页,淘汰空闲页没有意义;脏页存在没有落盘的数据,淘汰脏页导致数据丢失

Free List, Flush List, LRU List中包含的缓冲页是动态变化的,缓冲页可以在三个链表间动态转换

缓冲页在三者间的转换关系如下缓冲页在Free List, Flush List, LRU List间的转换

1) 当LRU List的缓冲页发生数据变更:缓冲页从LRU List转到Flush List
2) 当Flush List的缓冲页落盘:缓冲页从Flush List转到LRU List
3) 当从磁盘加载数据:缓冲页无变更从Free List转到LRU List;缓冲页有变更从Free List转到Flush List
4) 当从LRU List淘汰数据,淘汰的缓冲页不一定被使用:没有被使用的缓冲页从FRU Listz转到Free List;被使用的缓冲页清空数据后加载新数据,重新加入到LRU List

六) 内存淘汰机制

当Buffer Pool内存不足时,InnoDB会淘汰缓冲页,那么InnoDB的淘汰策略是随机淘汰?还是使用某种算法来淘汰?
如果随机淘汰数据,这个方法也是可行的,即使内存中的数据没了,磁盘中也会有数据,需要时再次加载到内存
如果希望:保留频繁被访问的数据页,淘汰很少被访问的数据页,显然随机淘汰的做法并不是一个较好的方案

InnoDB使用了基于LRU算法来淘汰数据

1 基础LRU算法淘汰过程

假设LRU List中有5个缓冲页,并且此时缓冲区空间已满,如下:
基础LRU算法淘汰过程----初始状态

如果此时一条查询SQL语句,命中了缓冲页5,则缓冲页5被移动到最前面,如下:
基础LRU算法淘汰过程----命中缓冲页5的状态

随后,又来一条查询SQL语句,查找的数据不在缓冲区,那么会从磁盘加载数据页,但缓冲区已经满了,需要淘汰一个缓冲页,此时就会将末尾的数据页淘汰,如下:
基础LRU算法淘汰过程----从磁盘加载数据页并淘汰尾节点状态

2 InnoDB中的LRU算法

使用基础LRU算法淘汰数据会带来两个问题:预读失效和Buffer Pool污染

2.1 预读失效

InnoDB存储数据时,会以64个数据页作为一个extent,当从磁盘加载数据时,一个extent中被加载的数据页达到阈值时,InnoDB会触发预读机制,提前将数据页载入Buffer Pool中。这些被提前载入Buffer Pool中的数据页,后面的操作并没有使用,这就是预读失效

InnoDB有两种预读机制策略:
1) 线性预读:当前extent中加载到Buffer Pool的数据页达到一定数量时,触发预读直接提前加载下一个extent到Buffer Pool
2) 随机预读:前extent中加载到Buffer Pool的数据页达到一定数量时,触发预读将extent剩下的数据页全部加载到Buffer Pool

2.2 Buffer Pool污染

当一条SQL语句扫描过程扫描到大量的数据页,由于Buffer Pool空间有限,扫描到的数据页都会加载到Buffer Pool,可能会将Buffer Pool中已有的缓冲页全都替换,只剩下这次操作载入的数据页。如果被替换的缓冲页是热数据页,当这些热数据再次被访问时,由于Buffer Pool中不存在,需要去磁盘检索数据,此时会产生大量的磁盘IO,导致MySQ 性能下降。这个过程就是Buffer Pool污染

Buffer Pool污染是扫描的数据页过多导致的,不一定是需要查询的结果太多导致的

2.3 InnoDB中的LRU List

InnoDB中LRU List的结构图如下:
图片来自MySQL官网,请参考 MySQL官网


LRU List结构图

其中:
New Sublist:新子链表,也称为young区。InnoDB将LRU List划分的两个区域之一,大致占LRU List长度的5/8
Old Sublist:旧子链表,也称为old区。InnoDB将LRU List划分的两个区域之一,大致占LRU List长度的3/8
Midpoint Insertion:LRU List的中点,从磁盘加载数据页的插入位置。young区的尾部和old区的头部相交的边界
head, tail:young/old区的头部和尾部

old 区占LRU List长度的比例通过 innodb_old_blocks_pc参数来设置,默认37,所以young区与old区比例默认是63:37

2.4 InnoDB中的LRU List的特点

1) LRU List分区:young区缓存真正的热点数据页,old区缓存有可能成为热点的数据页
2) 从磁盘加载数据页到Buffer Pool时,都只插入到old区的头部
3) 优先淘汰old区尾部的缓冲页
4) young区晋升机制:位于old区的缓存页,想要移动到young区,第一次访问后,必须在old区停留超过一定时间后,再次访问才可被移动到young区
5) InnoDB使用 LRU List分区 解决了预读失效,使用 young区晋升机制 解决了Buffer Pool污染

缓冲页晋升时必须要在old区停留一定时间,这个停留时间由参数 innodb_old_blocks_time决定,单位ms,默认1000,即1s

2.5 InnoDB对LRU List的再次优化

当缓冲页已经在young区,再次访问缓冲页时,真的有必要把它移动到young区的头部?

InnoDB针对young区再次做了优化:如果缓冲页处于young区的前1/4,再次访问这个数据页,不用把它移动到young区的头部;如果缓冲页处于young区的后3/4,再次访问这个数据页,把它移动到young区的头部

如果缓冲页在old区,且在old区的停留时间已经超过一定时间,再次访问这个数据页,那么不管这个数据页在old区的哪个位置,都会被移动到young区的头部

2.6 SQL执行时InnoDB中LRU List的变化

流程图如下:SQL执行时InnoDB中LRU List的变化流程图


过程详解:
1) 如果Buffer Pool中不存在,通过磁盘检索数据,扫描过程中扫描到的数据页,会依次插入到LRU List中old区的头部(即Midpoint Insertion),old区已存在的缓冲页往后移,如果Buffer Pool空间不足则淘汰old区尾部的缓冲页

2) 如果Buffer Pool中存在,且位于young区:如果缓冲页处于young区的前1/4,不用移动;如果缓冲页处于young区的后3/4 ,移到young区头部

3) 如果Buffer Pool中存在,位于old区:如果缓冲页在old区停留时间超过阈值,移到young区头部,LRU List中已存在的缓冲页往后移;如果在old区的停留时间没有超过阈值,保持不变

四 总结

经过前面的描述分析后,已经将InnoDB存储引擎的内存缓冲区的结构和管理 方面介绍的七七八八了,当然,其中一些用到的概念没有过多的深入分析与讲解,大家可以自行查找资料研究,错误的地方也欢迎大家指正

以下是对本文的总结:

  1. 虽然数据存储在磁盘上,但是InnoDB几乎所有的操作都是在Buffer Pool中完成,InnoDB对内存的使用已经到了极致,能在内存中完成的绝对不会产生磁盘IO
  2. InnoDB使用Free List, Flush List, LRU List 三个链表来管理所有缓冲的数据页
  3. Free List:管理所有未使用的缓冲页;Flush List:管理所有变更过的缓冲页(即脏页),LRU List:统一所有已使用但未变更过的缓冲页
  4. InnoDB的写入操作基本在Change Buffer中执行(除了主键索引需要走磁盘产生磁盘IO)
  5. InnoDB对产生日志的SQL操作,日志也会先写入到Log Buffer
  6. InndoDB使用了Index Page缓存了所有索引的根结点,可能包括非根结点
  7. InnoDB使用了改进后LRU算法:LRU List分区,young区晋升机制 解决了 预读失效 和 Buffer Pool污染 问题
  • 4
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值