【MySQL自身的性能优化】InnoDB 的 Buffer Pool

这里写目录标题

  • 一、引入缓存的重要性
  • 二、InnoDB 的 Buffer Pool
    • 1. Buffer Pool 内部组成
    • 2. free 链表管理空闲页
    • 3. flush 链表管理脏页
    • 4. LRU 链表提高缓存命中
      • 那咱需要咋地解决预读问题呢?
      • 那咱需要咋地解决 Buffer Pool 污染问题呢?
    • 5. 脏页什么时候被刷入磁盘?
  • 三、总结

有近俩个来月没有写博客了,也不知道自己在瞎忙活什么。之前我有写过一篇关于 MySQL 的内存池一篇博客 InnoDB体系架构之内存池(buffer pool),那是看楠哥视频后总结出来的。然后现是看了《MySQL 是怎样运行的》一书,也可能是基础相比之前要好了些,理解的要好点了吧,就再总结一篇,现进入正文。

一、引入缓存的重要性

无论是系统数据还是用于存储用户数据的索引(包括聚簇索引和二级索引),都是以页为基本单位存放在表空间中。所谓的表空间,只不过是 InnoDB 对一个或几个实际文件的抽象(ibd)。说到底数据是存放在磁盘上的,那磁盘的速度是非常慢的,而且在磁盘缓冲区和内存之间进行交互的时候也是需要霸占着CPU的,不管是在性能上还是在资源上都是一种不好的选择。所以 InnoDB 存储引擎在处理客户端的请求时,如果需要访问某个页的数据,就会把完整的页中的数据全部加载到内存中。也就是说,即使你是需要访问某索引页的一条记录,也需要先把整个页的数据加载到内存中(这一方面是和MySQL存储数据是在索引中,另一方面就是为了更好的缓存数据)。将整个页加载到内存中后就可以进行读写访问了,而且在读写访问之后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样将来有请求再次访问该页面时,就可以省下磁盘 I/O 的开销了。

二、InnoDB 的 Buffer Pool

为缓存上面提到的磁盘中的页,在 MySQL 服务器启动时就向操作系统申请了一片连续的内存,这块内存的名字就叫 Buffer Pool(缓冲池)
查看 innodb_buffer_pool_size 可以查看到其申请的内存大小,我这分配的是 128mb;
在这里插入图片描述

1. Buffer Pool 内部组成

整个Buffer Pool 由控制块、缓冲页和碎片组成。

  • 控制块:它记录着缓冲页的描述信息,比如该页所属的表空间编号、页号、缓冲页在 Buffer Pool 中的地址等等。设置的 innodb_buffer_pool_size 的大小不包含这个
  • 缓冲页:buffer pool 中存放的【数据页】称之为【缓冲页】,和磁盘上的数据页是一一对应的,都是16KB,缓冲页的数据,是从磁盘上加载到 buffer pool 当中的一个完整页。
  • 碎片:若设置的Buffer Pool 放满了缓冲页,剩余的内存则称之为碎片。

在这里插入图片描述

2. free 链表管理空闲页

在最初启动 MySQL 服务器的时候,需要完成 Buffer Pool 的初始化工作。先向系统中申请 Buffer Pool 的内存空间,然后把它划分成若干对控制块和缓冲页。此时是没有磁盘页被存到 Buffer Pool 中的。
而有个 free 链表就是用来管理这些空闲页的,以控制块作为节点,进行连接组成的一个链表
在这里插入图片描述当有磁盘页需要被缓存到 Buffer Pool 中时,就可以进行下面操作

  1. 可以取出一个空闲的缓冲页;
  2. 然后将其描述信息填入控制块;
  3. 最后将 free 链表中的对应节点删除,表示已经不再空闲,而哪个磁盘页就放到对应的缓冲页的位置上。
  4. 然后将 表空间号 + 页号 作为 key,控制块地址作为 value,存入到一个哈希表中,方便后续读取或写对应的缓冲页。

若读某页的逻辑就大概如下:
在这里插入图片描述

3. flush 链表管理脏页

设计 Buffer Pool 除了可以提高读性能,也可提高写性能。当需要更新数据的时,若可以根据 表空间号 + 页号 可以找到对应的控制块地址,说明 Buffer Pool 存在对应的缓冲页,那么进行写操作可以选择直接对 缓冲页 进行写操作,那么它就和对应磁盘上的数据不一致了,这样的缓冲页也被称为脏页

这些脏页的控制块最后也会被拼接成一个链表——flush 链表
在这里插入图片描述和 Free 链表是一样的,只是区别是 Free 链表的结点是空闲缓冲页的控制块,而 Flush 链表的结点是脏页的控制块。
有了 Flush 链表之后,后台线程就可以通过遍历这个链表,然后将脏页写入到磁盘

在这里插入图片描述从上图的InnoDB存储引擎的体系架构中看出其后台线程所担任的角色:
负责刷新内存池中的数据,保证缓存池中的内存缓存是最近的数据。此外将已修改的数据文件刷新到磁盘文件中,同时保证在数据库中发生异常的情况下 InnoDB 能恢复到正常运行状态。(如:Master Thread:负责将缓存池中的数据异步刷新到磁盘;IO Thread:异步处理IO请求,提高数据库的性能

4. LRU 链表提高缓存命中

看见 LRU(Least Recently Used) 第一反应就是最近最少使用淘汰策略
这里使用它的目的就是因为内存的针对,Buffer Pool 的内存资源也是有限的,当无法缓存新的数据的时候,希望把一些不用的缓冲页给淘汰掉,空闲出缓冲页供新的数据页使用。
针对上面的需求,Buffer Pool 使用了 LRU 淘汰策略去实现。

简单的 LRU 算法的实现思路是这样:

  • 当访问的页在 Buffer Pool 里,就直接把该页对应的 LRU 链表节点移动到链表的头部;
  • 当访问的页不在 Buffer Pool 里,除了要把页放入到 LRU 链表的头部,还要淘汰 LRU 链表末尾的节点。

阐述了这三种链表,咱就知道 Buffer Pool 是怎样管理数据的了:
在这里插入图片描述解释图:

  • Free Page(空闲页),表示此页未被使用,位于 Free 链表;
  • Clean Page(干净页),表示此页已经被使用,但是页面未发生修改,位于 LRU 链表;
  • Dirty Page(脏页),表示此页【已被使用】且【已经被修改】,其数据和磁盘上的数据已经不一致了。当脏页上的数据写入磁盘后,内存数据和磁盘数据一致,那么该页就变成了干净页。脏页同时存在于 LRU 链表和 Flush 链表。

简单的 LRU 算法并没有被 MySQL 使用,因为简单的 LRU 算法无法避免下面这俩个问题:

  • 预读失效;
  • Buffer Pool 污染;

何为预读?
Innodb 提供了一个看起来比较贴心的服务——预读。所谓预读,就是 InnoDB 认为执行当前的请求时,可能会在后面读取某些页面,于是就预先把这些页面加载到 Buffer Pool 中。默认是线性预读,而不是随机预读。

咱就是说预读本来是个好事,但是如果预读的页用不到呢?插入到 LRU 链表的头部,然后满了的话把尾部的页都淘汰掉,从而大大降低 Buffer Pool 的命中率。

大部分情况下局部性原理还是可靠的。

那咱需要咋地解决预读问题呢?

想要避免这种预读效果带来的影响,最好就是让预读页停留在 Buffer Pool 里的时间尽可能的短,让真正被访问的页才移动到 LRU 链表的头部,从而保证真正被读取的热数据留在 Buffer Pool 里的时间尽可能长

MySQL 改进了 LRU 算法,将 LRU 划分了俩个区域:Old 区域 和 Young 区域

在这里插入图片描述Young区域和Old区域在 LRU 链表的占比是由 innodb_old_blocks_pct 参数设置的。

划分这俩个区域后,预读的页就只需要加入到 Old 区域的头部,当页被真正访问的时候,才将页插入 Young 区域的头部。如果预读的页一致没有被访问,就会从 Old 区域移除,这样就不会影响 Young 区域的热点数据了。

假设有一个长度为 10 的 LRU 链表,其中 young 区域占比 70 %,old 区域占比 30 %。
在这里插入图片描述

现在有个编号为 20 的页被预读了,这个页只会被插入到 old 区域头部,而 old 区域末尾的页(10号)会被淘汰掉。

在这里插入图片描述

如果 20 号页一直不会被访问,它也没有占用到 young 区域的位置,而且还会比 young 区域的数据更早被淘汰出去。

如果 20 号页被预读后,立刻被访问了,那么就会将它插入到 young 区域的头部,young 区域末尾的页(7号),会被挤到 old 区域,作为 old 区域的头部,这个过程并不会有页被淘汰。

在这里插入图片描述

虽然通过划分 old 区域 和 young 区域避免了预读失效带来的影响,但是还有个问题无法解决,那就是 Buffer Pool 污染的问题。

何为 Buffer Pool 污染?
当某个 SQL 语句扫描了大量的数据时,在 Buffer Pool 空间比较有限的情况下,可能会将 Buffer Pool 里的所有页都替换出去,导致大量数据被淘汰了,等这些热数据又被再次访问的时候,由于缓存未能命中,就会产生大量的磁盘 IO,MySQL 性能就会急剧下降,这个过程被称为 Buffer Pool 污染
只要咱说无用查询,这种LRU策略就会引起 Buffer Pool 污染。

比如数据量很大的表 t_user,执行了下面这个查询:

select * from t_user where name like "%xiaolin%";

即时这查询的结果就几条,但是这也会引起索引失效,这样就得进行全表扫描(就是说从聚簇索引的叶子节点记录、节点遍历),接下来会发生下面这些过程:

  • 从磁盘读到的页加入到 LRU 链表的 Old 区域头部;
  • 当从页里读取行记录时,也就是页被访问的时候,就要将该页放到 Young 区域头部;
  • 接下来拿行记录的 name 字段和字符串 xiaolin 进行模糊匹配,如果符合条件,就加入到结果集里;
  • 如此往复,直到扫描完表中的所有记录。

经过这一番折腾,原本 Young 区域的热点数据都会被替换掉。

举个例子,假设需要批量扫描:21,22,23,24,25 这五个页,这些页都会被逐一访问(读取页里的记录)。
在这里插入图片描述

在批量访问这些数据的时候,会被逐一插入到 young 区域头部。

在这里插入图片描述

可以看到,原本在 young 区域的热点数据 6 和 7 号页都被淘汰了,这就是 Buffer Pool 污染的问题。

那咱需要咋地解决 Buffer Pool 污染问题呢?

很多缓存页只是被访问了一次,但是却只因为被访问了一次而进入到 Young 区域,从而导致热点数据被替换了。现在我们只要提高 Young 区域的门槛,这样就可以有效地保证 Young 区域里的热点数据不会被替换掉。

MySQL 是这样做的,进入到 Young 区域条件增加了一个停留在 Old 区域的时间判断。

具体是这样做的,在对某个处在 Old 区域的缓存页进行第一次访问时,就在它对应的控制块中记录下来这个访问时间:

  • 如果后续的访问时间与第一次访问的时间在某个时间间隔内,那么该缓存页就不会被从 old 区域移动到 Young 区域的头部;
  • 如果后续的访问时间与第一次访问的时间不在某个时间间隔内,那么该缓存页移动到 young 区域的头部;

这个间隔时间是由 innodb_old_blocks_time 控制的,默认是 1000 ms。

就是说,只有同时满足【被访问】与【在 Old 区域停留时间超过 1 秒】俩个条件,才会被插入到 Young 区域头部,这样就解决了 Buffer Pool 污染问题。

另外,MySQL 针对 young 区域其实做了一个优化,为了防止 young 区域节点频繁移动到头部。young 区域前面 1/4 被访问不会移动到链表头部,只有后面的 3/4被访问了才会。

5. 脏页什么时候被刷入磁盘?

若修改数据时 Buffer Pool 有这个数据,那么自然会直接修改 BufferPool 控制块映射的缓存页,这样一来这个缓存页就被称为脏页,其控制块也会被放入到 Flush 链表中进行管理。

但是脏页终究还是要被刷入磁盘的,保证缓存和磁盘数据的一致,一般情况下为了不影响性能,都会在一定时机进行批量刷盘。

可能大伙担心,如果脏页还没有来得及刷入到磁盘内,MySQL 宕机了,不就丢失数据了吗?
这个不用担心,InnoDB 考虑到这一点,更新操作采用的是 Write Ahead Log 策略,就是说先写入日志,再写入磁盘,通过 redo log 日志让 MySQL 拥有了崩溃恢复的能力

下面几种情况会触发脏页的刷新:

  • 当 redo log 日志满了的情况下,会主动触发脏页刷新到磁盘;
  • Buffer Pool 空间不足时,需要将一部分数据页淘汰掉,如果淘汰的是脏页,需要先将脏页同步到磁盘;
  • MySQL 认为空闲时,后台线程会定期将适量的脏页刷入到磁盘;
  • MySQL 正常关闭之前,会把所有的脏页刷入到磁盘;

在开启慢 SQL 监控后,如果你发现 【偶尔】会出现用时稍长的 SQL ,这可能是因为脏页在刷新到磁盘时可能会给数据库带来性能开销,导致数据库操作抖动。

如果间断地出现这种现象,就需要调大 Buffer Pool 空间或 Redo Log 日志的大小。

三、总结

为尽可能地降低磁盘 IO,来提高数据库的读写性能,InnoDB 存储引擎设计了一个 缓冲池(Buffer Pool)

Buffer Pool 以页为单位缓冲数据,可以通过 innodb_buffer_pool_size 参数调整缓冲池的大小,默认是 128 M。

Buffer Pool 的内部组成有三种角色,控制块、缓冲页、碎片

InnoDB 通过三种链表来管理缓冲页:

  • Free List(空闲页链表),管理空闲页;
  • Flush List(脏页链表),管理脏页;
  • LRU List(淘汰链表),管理脏页+干净页,将最近且经常查询的数据缓存在其中,而不常查询的数据就淘汰出去。

InnoDB 对 LRU 做了一些优化,我们熟悉的 LRU 算法通常是将最近查询的数据放到 LRU 链表的头部,而 InnoDB 做了以下俩点优化(当然还有很多优化点,这里阐述的是重要的俩个):

  • 将 LRU 链表分为了 Young 和 Old 区域俩个部分加入缓冲池的页优先被放到 Old 区域,页被访问时,才进入到 Young 区域,目的是为了解决预读失效问题
  • 当 【页被访问】 和 【Old 区域停留时间超过 innodb_old_blocks_time 阈值(默认为1秒)】 时,才会将页插入到 Young 区域,否则还是插入到 Old 区域,目的是为了解决批量数据访问(如全表扫描),大量热数据淘汰的问题。

可以通过调整 innodb_old_blocks_pct 参数,设置 young 区域和 old 区域比例。

在开启了慢 SQL 监控后,如果你发现 【偶尔】 会出现一些用时稍长的 SQL,这是因为脏页在刷新到磁盘时导致数据库性能抖动。如果在很短的时间出现这种现象,就需要调大 Buffer Pool 空间或 redo log 日志的大小。

参考文献:

  1. 《MySQL是怎样运行的》第十七章 InnoDB 的 Buffer Pool。
  2. 小林的解开Buffer Pool 的面纱

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:http://www.mzph.cn/news/634431.shtml

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈email:809451989@qq.com,一经查实,立即删除!

相关文章

94.乐理基础-记号篇-装饰音记号(二)助音的四种类型

内容参考于:三分钟音乐社 上一个内容:93.乐理基础-记号篇-装饰音记号(一)级进、跳进、经过音、辅助音-CSDN博客 当前写的助音是扩展的辅助音的内容,所以要先看上一个内容 助音的四种类型:下方的三个辅助…

自己是如何使用单元测试

前言 自己是如何使用单元测试 进行单元测试能够让我们在编写方法的具体实现代码后,能清晰地看到其是否能实现预期的功能,有助于我们及时修正自己方法中存在的bug,以免在后续使用到某方法时出现意想不到的错误。 一、引入单元测试所使用的依赖…

【ARMv8M Cortex-M33 系列 7.2 -- HardFault 问题定位 1】

文章目录 问题背景堆栈对齐要求Cortex-M33 的 FPU 功能 问题背景 rt-thread 在PendSV_Handler退出的时候发生了HardFault_Handler是什么原因?且 LR 的值为0xfffffffd 堆栈对齐要求 在 ARM Cortex-M 架构中,堆栈指针 (SP) 必须始终保持 8 字节对齐。这…

米贸搜|Facebook新手请查收!如何在FB上定位到B类受众?

一、确定目标受众和营销目标 在利用Facebook进行获客之前,B2B企业需要首先明确目标受众和营销目标。目标受众是指潜在的客户或合作伙伴,而营销目标可能是增加销量、提高品牌知名度、获取客户线索等。 Facebook的受众定位可以分成三大类:人口…

对象存储, 开源MinIO docker-compose.yml 文件

文章目录 python SDK 文档地址:docker-compose.yml 文件控制台使用:应用服务中使用样例: python SDK 文档地址: https://min.io/docs/minio/linux/developers/python/API.html docker-compose.yml 文件 version: 3services:min…

C语言中的浮点数存储

首先明确一个概念:C语言中整形是按照二进制存储在内存中,浮点型是按科学计数法存储在内存中(本质上存储的还是二进制数据0和1)。 如果没看懂这句话,没关系!看完以下正文,你就会豁然开朗&#x…

Qt简单使用与初识

🌇个人主页:平凡的小苏 📚学习格言:命运给你一个低的起点,是想看你精彩的翻盘,而不是让你自甘堕落,脚下的路虽然难走,但我还能走,比起向阳而生,我更想尝试逆风…

【汇编】pushf popf

两个指令的作用分别是将16位标志寄存器压入栈、从栈中取出,但是貌似没人做实验验证。 这里做个实验加深理解,顺便总结下几个标志寄存器的作用。 一、结构 8086CPU的flag寄存器的结构如下: OF:判断运算结果是否溢出 CF&#xff1…

VUE--组件的生命周期及其基本应用

VUE的生命周期 上图是实例生命周期的图表,需要注意以下几个重要时期: 创建期:beforeCreated、created 挂载期:beforeMount、mounted 更新期:beforeUpdate、updated 销毁期:beforeUnmount、unmounted 生命周…

蓝桥杯真题(Python)每日练Day2

题目 题目分析 对于本题首先确定其数据结构为优先队列,即邮费最小的衣服优先寄,算法符合贪心算法。可以直接使用queue库的PriorityQueue方法实现优先队列。关于PriorityQueue的使用方法主要有: import queue q queue.Queue()# 队列 pq qu…

JAVA实现向Word模板中插入Base64图片和数据信息

目录 需求一、准备模板文件二、引入Poi-tl、Apache POI依赖三、创建实体类(用于保存向Word中写入的数据)四、实现Service接口五、Controller层实现 需求 在服务端提前准备好Word模板文件,并在用户请求接口时服务端动态获取图片。数据等信息插…

代理IP是什么,代理IP的工作原理是怎么样的?

数字时代,随着数据采集的应用,代理IP也受到越来越多人的关注。但是,很多人对代理IP的具体概念及其工作原理并不是很了解。今天我们就来针对代理IP的相关知识做一个简单的探讨,说说代理IP是什么,它的工作原理又是什么&a…

redis未授权访问全漏洞复现

redis未授权访问全漏洞复现 Redis 有关的漏洞具有明显的时间分段性,在15年11月之前,主要是未授权导致的数据泄露,获得一些账号密码。另外还可以 DoS(参考:Sangfor VMP redis unauthorized access vulnerability&#…

VUE--组件通信(非父子)

一、非父子通信 --- event bus 事件总线 作用:非父子组件之间进行简易的消息传递 步骤: 1、创建一个都能访问到的事件总线(空vue实例)--- utils/EventBus.js import Vue from vue export default new Vue({}) 2、 接收方&…

mysql数据库:迁移数据目录至另一台服务器步骤

一、概述 最近由于项目需要,我们需要进行数据库服务器的更换和迁移工作。迁移计划和步骤如下: 1、首先,在新的数据库服务器上进行环境的搭建和配置,确保数据库版本、配置等一致。 2、然后,将备份的数据库数据导入到…

SpringBoot整合SSE

目录 1.SseController2. SseServiceSseServiceSseServiceImpl 3.SendMessageTask4.将定时任务加入启动类5.参考资料 1.SseController Slf4j RestController RequestMapping("sse") public class SseController {Autowiredprivate SseService sseService;RequestMappi…

【算法练习】leetcode算法题合集之二分查找篇

二分查找 LeetCode69.x的平方根 LeetCode69.x的平方根 只要小于等于就可以满足条件了。 class Solution {public int mySqrt(int x) {int left 0, right x;int ans -1;while (left < right) {int mid (right - left) / 2 left;if ((long) mid * mid < x) {ans mi…

为 OpenCV 编写文档(二)

常用命令 这里通过简短的示例描述了最常用的 doxygen 命令。有关可用命令的完整列表和详细说明&#xff0c;请访问命令参考。 基本命令 brief - 带有简要实体描述的段落 param - 函数参数的描述。 多个相邻语句合并到一个列表中。如果在实际函数签名中找不到具有此名称的参数…

2024潮乎新年盲盒H5版本可易支付对接

前端三十行和三十一行改成你域名 后台.env文件修改数据库 下载地址&#xff1a;YISHEN源码网

驾驭车联网的力量:深入车联网网络架构

车联网&#xff0c;作为移动互联网之后的新风口&#xff0c;以网联思想重新定义汽车&#xff0c;将其从简单的出行工具演化为个人的第二空间。车联网涵盖智能座舱和自动驾驶两大方向&#xff0c;构建在网联基础上&#xff0c;犀思云多年深度赋能汽车行业&#xff0c;本文将从车…