当前位置: 首页 > news >正文

【InnoDB内存结构】缓冲池,变更缓冲区,自适应哈希索引,日志缓冲区

目录

一.缓冲池(Buffer Pool)

1.1.缓冲池的作用?

1.2.缓冲池是如何组织数据的?

1.2.1.缓冲池的结构是怎样的?

1.2.2 缓冲池中页与页之间是如何建立连接的?——控制块

1.2.3 内存中的数据页与磁盘上的数据页是什么关系?

1.2.4 Buffer Pool的大小可以设置吗?

1.2.5 Buffer Pool中Instances的数量如何确定?

1.2.6 Chunk的作用是什么?

1.2.7 Instances中Chunk的数量如何确定?

1.2.8 控制块与Page是如何初始化的?

1.2.9 可以通过缓冲池配置来提升性能吗?

1.3.缓冲池中的页是如何进行管理的?

1.4 缓冲池采用哪种淘汰策略?是如何实现的?

1.5 怎么查看当前缓冲池的信息?

1.6.缓冲池小结

二. 变更缓冲区(Chang Buffer)

2.1.变更缓冲区的作用

2.1.1 变更缓冲区只适用于二级索引?

2.1.2 Merge的触发时机有哪些?

2.2 变更缓冲区的主要配置项都有哪些?

三. 自适应哈希索引(Adaptive hash index)

3.1. 自适应哈希索引的作用?

3.2.为什么要创建自适应哈希索引?

3.3.关于自适应哈希索引有哪些配置项?

3.4.怎么查看自适应哈希索引的信息?

四.日志缓冲区

4.1.日志缓冲区的作用

4.2.日志缓冲区和变更缓冲区的区别


InnoDB存储引擎中内存结构的主要组成部分有哪些?

 我们可以去官网看看:MySQL :: MySQL 8.0 Reference Manual :: 17.4 InnoDB Architecture

我们本文的主角是左边的内存结构。

内存结构

  1. 缓冲池 (Buffer Pool)——内存中的主要工作区域,优化查询的性能

  2. 变更缓冲区 (Change Buffer)——优化修改操作的性能

  3. 日志缓冲区 (Log Buffer)

  4. 自适应哈希索引 (Adaptive Hash Index)——进一步提升查询的性能

为什么需要内存结构?

这个问题在InnoDB架构那篇文章已经做了解释,再来回顾一下:

从MySQL实现的角度来思考这个问题,数据库的作用就是保存数据,用户的真实数据最终都会保存在磁盘上,在查询数据的过程中,如果每次都从磁盘上读取会严重影响效率,为了提高数据的访问效率,InnoDB会把查询到的数据缓存到内存中,当再次查询时,如果目标数据已经存在于内存中,就可以从内存中直接读取,从而大幅提升效率。

也就是说磁盘结构中的文件是用来保存数据实现数据持久化的,内存结构是用来缓存数据提升效率的。

一.缓冲池(Buffer Pool)

先来讲解缓冲池(Buffer Pool),这个可是MySQL内存结构里面最重要的一部分

1.1.缓冲池的作用?

  • 缓冲池在内存结构中的位置,如下图所示:

  • 缓冲池主要用来缓存被访问的InnoDB表和索引数据页,是主内存中的一片区域,允许直接从内存访问频繁使用的数据从而提高效率。在专用数据库服务器上,通常会将多达80%的物理内存分配给缓冲池。

  • 其次缓冲池不仅缓存了磁盘的数据页,也存储了锁信息、Change Buffer信息、Adaptive hash index、Double write buffer等信息。

其实我们也可以去官网看看:MySQL :: MySQL 8.0 Reference Manual :: 17.5.1 Buffer Pool

1.2.缓冲池是如何组织数据的?

  • 缓冲池组织数据的方式也可以说是缓冲池用到的数据结构,在这之前回顾一下InnoDB表空间的存储结构。

  • 每个InnoDB表空间在磁盘上对应一个 .ibd 文件,其中包含了叶子节点段和非叶子节点段等逻辑段,段中包含了区组,区组中管理着区,区别包含数据页,数据页中包含数据行,每分别对着不同的数据结构目的就是便于数据的管理与高效访问

1.2.1.缓冲池的结构是怎样的?

  • 从缓冲池的概念了解到它是主内存中的一片区域,在专用服务器上会将多达80%的物理内存分配给缓冲池,在这么大的内存空间中如何保证效率就是要解决的问题

  • 缓冲池也采用与表空间类似的方式对数据进行组织,如下图所示:

    • 缓冲池中包含至少一个 Instances 实例Instances 是真正的缓冲池的实例对象,内存操作都是在 Instances 中进行的;

    • 每个 Instances 中包含至少一个 Chunk 块,Chunk 是在服务器运行状态下动态调缓冲池进行大小时操作的块大小;

    • 每个Chunk块中包含和管理若干个从磁盘加载到内存的 Page 数据页


  • 可以看出缓冲池通过定义不同的数据结构,但最终管理的是每个数据页,这些数据页是从磁盘中加载到内存的 ,也就是说内存的数据页其实是磁盘的数据页的副本磁盘中的数据页加载到内存中之后,对应的就是内存中的数据页,并且页与页之间用链表连接。

  • 那么这时就有一个问题,我们知道磁盘中的数据页大小默认是16KB,并且通过头信息中的 next_record 记录下一行地址偏移量,在页的结构定义中并没有一个字段用来表示内存中下一页的地址,那么在内存中如何为每个页建立连接呢?

1.2.2 缓冲池中页与页之间是如何建立连接的?——控制块

首先我们要明白:磁盘上的数据页位置是固定的 系统需要某个数据页时,直接根据它固定的“门牌号”(表空间 + 页号)就能找到它。磁盘并不关心这个页是不是常用。

但是,缓冲池(内存)就大不一样了! 这里寸土寸金,MySQL 只会缓存那些最常用、最活跃的数据页。不常用或暂时不用的页会被及时清理出去(淘汰),给新来的热点数据腾地方。这就导致了一个关键问题:

  • 数据页在内存中的位置是动态变化的,今天在这儿,明天可能就被挪走了!

  • 我们无法再用磁盘那套固定的“门牌号”来快速定位内存中的页了。

那么,缓冲池里这么多变动的数据页,怎么知道谁挨着谁?怎么快速找到它们?

为了解决这个难题,InnoDB 引入了“控制块”这个关键角色。 它就是专门用来在内存中为这些“流动”的数据页建立秩序、指明方向的!


  • 由于数据页中没有一个字段用来表示内存中下一页的地址,为了每个数据页在内存中实现链表连接,InnoDB定义了一个叫“控制块”的数据结构,“控制块”中有三个重要的信息分别是:

    • 指向数据页的内存地址

    • 前一个控制块的内存地址

    • 后一下控制块的内存地址

  • 之后再用一个双向链表管理每个控制块,如下图所示:

  • 为了确定控制块链表的超始位置,专门定义了一个头节点,头节点中包含了三个主要的信息,如图中所示:

    • 第一个控制块的内存地址

    • 最后一个控制块的内存地址

    • 链表中控制块的数量

  • 通过遍历控制块链表就可以遍历内存中的数据页


我知道大家有点懵,我们可以用图书馆的比喻来理解缓冲池中的“控制块”机制,这样会更直观:

📚 通俗版解释:图书馆 vs 缓冲池

  1. 书 = 数据页
    图书馆的书(磁盘上的数据页)本身只记录内容,不会标注下一本书的位置。比如《MySQL技术内幕》这本书里不会写“下一本是《高性能MySQL》在3号书架”。

  2. 图书索引卡 = 控制块
    管理员制作了索引卡(控制块),每张卡片记录:

    • ✅ 书的位置:例如“《MySQL技术内幕》在2号书架3层”

    • ✅ 前后卡片:例如“前一张是《数据库原理》的卡片,后一张是《高性能MySQL》的卡片”

  3. 卡片目录盒 = 双向链表
    所有索引卡用绳子串成双向链表(头节点是目录盒的封面):

    • 📌 封面(头节点)标注:第一张卡片位置、最后一张卡片位置、总卡片数

    • 🔗 每张卡片都连着前一张和后一张(双向指针)

🔍 工作流程

  1. 找书时
    管理员直接翻卡片目录盒 → 找到目标书的卡片 → 按卡片位置拿书(无需挨个书架找)。

  2. 新书入库
    新书《Redis设计》来了:

    • 先做一张新卡片(创建控制块)

    • 把卡片插入目录盒链表(更新前后卡片的指针)

    • 把书放到空闲书架(缓冲页)

  3. 淘汰旧书
    书架满了时:

    • 按卡片顺序找到最久未看的书(LRU算法)

    • 把这本书移出书架(释放缓冲页)

    • 撕掉对应的卡片(从链表删除控制块)

✔ 解答问题

        缓冲池中主要缓存的是磁盘中的数据页,由于数据页中没有一个字段用来表示内存中下一页的地址,InnoDB定义了“控制块”的数据结构,控制块中有一个指向数据页内存地址的指针,实现“控制块”与数据页的一一对应,并且把每个控制块连接成一个双向链表,用一个单独的头节点记录链表上的第一个和最后一个节点,这样通过遍历控制块链表就可以遍历内存中的数据页。

1.2.3 内存中的数据页与磁盘上的数据页是什么关系?

它们本质上是一对“镜像双胞胎”:

  1. 磁盘数据页是“本体”:它持久地存储在硬盘的某个固定位置(由表空间和页号唯一确定),是数据的最终归宿。

  2. 内存数据页是“活跃副本”:当需要访问磁盘上的某个数据页时,InnoDB 会将其完整加载到缓冲池(内存)中一个预先分配好的位置,形成一个内容完全相同的副本。

关键点在于内存中的管理方式:

  • 并非直接管理数据页本身: 缓冲池不会用一个大数组简单粗暴地存放这些内存数据页。

  • 控制块是“管家”和“桥梁”对于每一个加载到内存的磁盘数据页,缓冲池都会创建一个对应的控制块 (Control Block)。这个控制块是内存管理的核心单元。

  • 指针维系“形影不离”控制块中最重要的信息之一,就是一个指向其对应的、在内存中真实存在的那个数据页副本的内存地址的指针。就像管家手里拿着指向特定房间的钥匙。

  • 链表组织控制块: 这些控制块本身会被组织成各种链表(如 LRU List, Flush List, Free List)。我们通过遍历或操作这些控制块链表,间接地管理和访问它们所指向的真实内存数据页。

总结来说:

  • 磁盘数据页是唯一、持久的实体。

  • 内存数据页是磁盘页在内存中的临时、动态的副本。

  • 一个磁盘数据页被加载到内存后,在缓冲池中有且仅有一个内存数据页副本与之对应。

  • 控制块是连接磁盘页标识符(表空间+页号)和内存中该页副本实际位置的关键桥梁,并且是缓冲池进行高效管理(查找、淘汰、刷脏)的基础数据结构。

1.2.4 Buffer Pool的大小可以设置吗?

  • 可以通过系统变量 innode_buffer_pool_size 进行设置,设置时以字节为单位:默认值为134217728 字节,即 128MB;最大值取决于CPU架构和操作系统,在 32 位系统上最大值为4294967295(2^{32}-1),在 64 位系统上最大值为 18446744073799551615(2^{64}-1

这里需要注意的是,InnoDB为"控制块"分配额外的内存空间,也就是说"控制块"并不会占用Buffer Pool的内存空间,所以实际分配的内存总空间比指定的缓冲池大小大10%左右。

  • 缓冲池设置的值越大,在多次访问相同表数据时,磁盘I/O就会越少,因为数据都已经缓存在内存中,所以效率也就越高,但是服务器启动时初始化时间会比较长。

查看缓冲池大小可以使用下面的SQL语句

SHOW VARIABLES LIKE 'innodb_buffer_pool_size';

1.2.5 Buffer Pool中Instances的数量如何确定?

  • 通过系统变量innodb_buffer_pool_instances可以设置缓冲池实例的个数,默认是1,最大值64;

  • 当缓冲池Buffer Pool的大小小于1GB时,无论指定innodb_buffer_pool_instances数是多少都会自动调整为1;

  • 当缓冲池Buffer Pool的大小大于1GB时,innodb_buffer_pool_instances默认值为8,也可以指定大于1的值来设置Instances的数量,多个Instances可以提升服务器的并发性;

  • 为了获得最佳的效率,通过指定innodb_buffer_pool_instances和innodb_buffer_pool_size为每个缓冲池实例设置至少为1GB的空间;

查看Instances的数量可以使用下面的SQL语句

SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';

 


1.2.6 Chunk的作用是什么?

 Chunk 是在服务器运行状态下动态调缓冲池进行大小时操作的块大小,为避免在调整大小操作期
间复制所有缓冲池中的数据页,调整操作以 "块"为基本单位执行;

我知道大家还是不太明白,我现在就举个例子说明一下

        想象一下数据库的缓冲池就像一个巨大的仓库(内存),里面存放着经常要访问的货物(数据页)。管理员(DBA)有时候需要根据业务变化在线(服务器正在运行、数据库正在处理请求)地扩大或缩小这个仓库的大小。

  • 最笨的方法: 直接把旧仓库拆了,建一个新尺寸的仓库,然后把所有货物一件一件搬过去。这在数据库里意味着:锁定整个缓冲池 -> 分配一块新大小的内存 -> 把旧缓冲池里每一个有效的数据页都复制到新内存 -> 释放旧内存。

    • 问题: 复制海量数据页极其耗时!数据库在这期间性能会严重下降甚至卡死(阻塞),这对于需要高可用的在线系统是无法接受的。

Chunk 的妙用:把大麻烦拆成小步骤

Chunk 就是为了解决这个“在线调整仓库大小太慢”的问题而引入的概念。它的核心思想是:

把缓冲池这块大内存,划分成一系列大小固定、连续的小块内存(即 Chunk)。调整缓冲池大小不再以单个数据页为单位复制,而是以 Chunk 为单位进行添加或移除。

通俗解释:

  1. 仓库分区化: 把整个大仓库(缓冲池)预先划分成许多大小相同的标准集装箱(Chunk)。比如每个集装箱(Chunk)是 128MB。

  2. 仓库扩容:

    • 管理员说:“仓库需要扩大 1GB!”

    • 系统不需要拆掉整个旧仓库。它只需要在旧仓库旁边找一块空地,放上 8 个新的标准集装箱(8 Chunks * 128MB = 1GB)

    • 旧仓库里的货物(数据页)原封不动地待在原来的集装箱里。

    • 新集装箱是空的,等待新货物(新读取的数据页)进来。

    • 结果: 仓库瞬间变大了!几乎不需要等待(只是分配新内存块的时间),旧货物完全不受影响。

  3. 仓库缩容:

    • 管理员说:“仓库需要缩小 512MB!”

    • 系统不需要一件一件搬走所有货物。它只需要挑选出 4 个相对空闲或者比较容易清空的集装箱(4 Chunks * 128MB = 512MB)

    • 如果集装箱里还有重要货物(脏页),系统会先把它们安全地搬回永久存储(磁盘刷脏),然后把这个集装箱整体移除(释放其内存)。

    • 其他集装箱里的货物完全不受影响。

    • 结果: 仓库缩小了!影响范围仅限于被移除的那几个集装箱里的数据页,而不是整个仓库。


比如在服务器运行时想要调整缓冲池的大小可以通过以下SQL语句:

--把缓冲池大小设置为1GB
SET GLOBAL innode_buffer_pool_size=1073741824;
  • 注意:启动调整大小操作只有在所有活动事务完成后才会开始。一旦调整大小操作开始,新的事务和操作必须等到调整大小操作完成才可以访问缓冲池。

1.2.7 Instances中Chunk的数量如何确定?

  • Chunk 大小可以通过系统变量 innode_buffer_pool_chunk_size 进行设置,默认为 134217728 字节,即 128MB;在设置大小时可以以 1048576 字节即 1MB 为单位增加或减少;块中包含的数据页数取决于 innode_page_size;

  • 更改 innode_buffer_pool_chunk_size 的值时注意以下条件:

    • 如果 innode_buffer_pool_chunk_size * innode_buffer_pool_instances 大于当前缓冲池大小,innode_buffer_pool_chunk_size 将被截断为 innode_buffer_pool_size / innode_buffer_pool_instances。

    • 缓冲池大小必须始终等于或倍数于 innode_buffer_pool_chunk_size *
      innode_buffer_pool_instances。
      如果修改了 innode_buffer_pool_chunk_size 的值,导致不符合这个规则,那么在缓冲池初始化时 innode_buffer_pool_size 会自动四舍五入为等于或者倍数字 innode_buffer_pool_chunk_size * innode_buffer_pool_instances 的值。

注意:innodb_buffer_pool_chunk_size 的设置可能会影响到缓冲池⼤⼩ innodb_buffer_pool_size 的值,所以要调整 innodb_buffer_pool_chunk_size 的值之前⼀定要计算好

1.2.8 控制块与Page是如何初始化的?

前面介绍了 Chunk 中管理的是具体的数据页,当缓冲池初始化完成时会把每个数据页所占用的内存空间和对应的控制块分配好,只不过在没有从磁盘加载数据时,内存中的数据页是空的而已。(提前分配空间但是没填数据进去)

控制块和缓冲页是一一对应的关系。

        当缓冲池初始化的过程中,会为 Chunk 分配置内存空间,此时“控制块”会从 Chunk 的内存空间从左向右进行初始化,数据页所占的内存会从 Chunk 的内存空间从右向左进行初始化,当所剩的内存空间不够一组“控制块”+数据页所占的空间时,就会产生碎片空间,如果适好够用则不会出现碎片空间,如下图所示:

内存初始化完成之后,建立控制块与内存中缓冲数据页之间的关系,从左开始第一个控制块指向第一个缓冲数据页的内存地址

我们来深入理解一下,想象一个 Chunk 就是一块连续的、未划分的大蛋糕。

  1. 规划分配: 数据库系统知道:

    • 一个控制块需要多大内存(假设为 C 字节)。

    • 一个缓冲页需要多大内存(等于磁盘页大小,假设为 P 字节,如 16KB)。

    • Chunk 的总大小是固定的(S 字节)。

  2. 双向分配开始: 系统需要在这块 S 字节的内存上,尽可能多地分配 (控制块 + 缓冲页) 对。为了高效管理和避免内部碎片,它采用了一个巧妙的双向分配策略

    • 控制块队列(从左向右): 系统从 Chunk 的起始地址(最左边) 开始,预留空间用于放置控制块。它一个接一个地分配控制块的内存空间。这些空间是连续的,形成了一个控制块数组。此时,这些控制块结构体本身的内存被分配好了,但它们内部还是“空”的(比如指向缓冲页的指针还是 NULL),等待初始化。

    • 缓冲页队列(从右向左): 同时,系统从 Chunk 的结束地址(最右边) 开始,预留空间用于放置缓冲页。它一个接一个地分配缓冲页的内存空间。这些空间也是连续的(每个大小 P 字节)。此时,这些缓冲页的内存区域被划出来了,但里面还没有任何有效数据(是空的或随机值)。

    • 向中间靠拢: 想象两拨工人,一拨从 Chunk 最左边开始向右砌小房子(控制块),另一拨从 Chunk 最右边开始向左砌大房子(缓冲页)。他们同时开工,各自向 Chunk 的中间推进。

  3. 相遇与碎片产生: 随着左右两边同时分配:

    • 理想情况(无碎片): 当左右两边分配的内存总和恰好等于 Chunk 总大小 S,并且最后分配的一个控制块和一个缓冲页完美衔接,中间没有剩余空间时,就没有碎片。这要求 S 必须是 (C + P) 的整数倍(S = N * (C + P))。

    • 实际情况(有碎片): 大多数情况下,S 不是 (C + P) 的精确整数倍。当左右两边分配的内存总和接近 S 时,中间会剩余一小块内存。这块剩余内存的大小 F 满足 F < (C + P)。也就是说,这块剩余空间既不够再放一个控制块,也不够再放一个缓冲页,更不够放一组 (控制块 + 缓冲页)。这块空间就是 “碎片”

    • 分配停止: 一旦系统检测到剩余空间 F 小于 (C + P),分配就停止了。此时,Chunk 中包含:

      • 左边:N 个已分配的控制块空间。

      • 右边:N 个已分配的缓冲页空间。

      • 中间:一块大小为 F 的碎片空间(如果 F > 0)。

  4. 建立关系(关键步骤): 内存空间划分好后,系统开始初始化控制块建立控制块与缓冲页的关联

    • 顺序对应: 系统按照它们分配的顺序进行配对。第一个(最左边的)控制块对应第一个(最右边分配的,也就是物理地址上最靠 Chunk 末尾的)缓冲页。第二个控制块对应第二个缓冲页(紧挨着第一个缓冲页左边),依此类推,直到第 N 个控制块对应第 N 个缓冲页(物理地址上最靠中间的那个缓冲页)。

    • 指针赋值: 对于每个控制块 i (i=1 到 N),系统会执行一个关键操作:将该控制块内部的一个指针成员(例如 frame)的值,设置为对应缓冲页 i 的起始内存地址。 这就好比给控制块 i 发了一把钥匙,这把钥匙能唯一打开缓冲页 i 的门(访问那块内存)。

    • 初始化状态: 同时,控制块的其他字段被初始化。例如,将“页号”标记为无效(表示缓冲页为空),状态标记为“空闲”,访问计数清零等。对应的缓冲页内存区域内容仍然是无效的。

    • 碎片处理: 中间那块碎片内存 F 字节,不会被用于存放任何控制块或缓冲页数据。它会被记录在案,可能用于未来存放一些非常小的内部管理数据结构,或者就简单地被“浪费”掉了(内部碎片)。系统知道这块区域的存在,但在管理缓冲页时不会考虑它。

 举个例子

假设一个简化场景(数字仅为示意):

  • Chunk 大小 S = 1000 字节

  • 控制块大小 C = 50 字节

  • 缓冲页大小 P = 150 字节

  • 一组 (C+P) = 200 字节

  1. 分配:

    • 从左向右分配控制块:位置 0-49 (控制块1), 50-99 (控制块2), 100-149 (控制块3), 150-199 (控制块4)。 -> 已用 200 字节。

    • 从右向左分配缓冲页:位置 850-999 (缓冲页1)。位置 700-849 (缓冲页2)。位置 550-699 (缓冲页3 )。位置 400-549 (缓冲页4)。 -> 已用 4 * 150 = 600 字节。

    • 问题: 左右分配总和 200 + 600 = 800 字节。中间剩余空间 = 1000 - 800 = 200 字节。

    • 发现剩余 200 >= (50+150)=200! 还可以再分一组! 修正分配:

      • 左边继续:200-249 (控制块5)

      • 右边继续:250-399 (缓冲页5)

      • 总和:左边 5*50=250字节,右边 5*150=750字节,总计1000字节。完美,无碎片!

  2. 建立关系:

    • 控制块1 (0-49) -----> 指向 缓冲页1 (850-999) 的地址 (850)

    • 控制块2 (50-99) ----> 指向 缓冲页2 (700-849) 的地址 (700)

    • 控制块3 (100-149) --> 指向 缓冲页3 (550-699) 的地址 (550)

    • 控制块4 (150-199) --> 指向 缓冲页4 (400-549) 的地址 (400)

    • 控制块5 (200-249) --> 指向 缓冲页5 (250-399) 的地址 (250)

如果 S=1100 字节:

  • 左边分配 5 个控制块 (0-249) -> 250 字节

  • 右边分配 5 个缓冲页 (250-399, 400-549, 550-699, 700-849, 850-999) -> 750 字节

  • 总和 1000 字节,剩余 100 字节 (F=100 < (50+150)=200) -> 碎片产生在位置 1000-1099 (或按顺序在中间)

  • 关系同上,只建立 5 对。


  • 当前从磁盘中加载数据页时,就可以在把数据缓存在内存中的空闲数据页中

1.2.9 可以通过缓冲池配置来提升性能吗?

当然可以,通过配置以下关于缓冲池的系统变量来提高性能,其中包括:

  • 配置缓冲池大小

  • 配置多个缓冲池实例

  • 防止缓冲池扫描

  • 配置缓冲池预取(预读)

  • 配置缓冲池刷新策略

  • 保存和恢复缓冲池状态

  • 从核心文件中排除缓冲池页

这些变量可以通过以下语句查看

SHOW VARIABLES LIKE 'innodb_buffer_pool%';

至于具体该怎么配置,我们后面慢慢说。 

1.3.缓冲池中的页是如何进行管理的?

这里管理的是内存中数据页的状态。

那么内存中数据页的状态有下面3种

  1. 当缓冲池初始化完成后,缓冲池中的数据页只是被分配了内存空间,并没有真实的数据。
  2. 当用户进行数据查询时真实的数据从磁盘加载到内存中并分配一个内存中的数据页,这时内存中数据页的状态从空间变成了有实际的数据;
  3. 当用户修改数据时,并不是直接修改磁盘中的数据页,而是修改内存中数据页中的数据页,这时内存中数据页的状态从有实际数据变成了被修改。

在缓冲池中采用三个链表维护内存页,这三个链表也对应着内存中页的三种状态,分别是:

  • Free :未使用的页,也可以称做空闲页;

  • Clean :已使用但未修改的页,也可以称做干净页;

  • Dirty :已修改的页,也可以称做脏页。

对应的三个链表分别是 Free List、LRU List 和 Flush List:

  • Free List:只管理Free 页

  • LRU List:管理Clean页和Dirty页

  • Flush List:只管理Dirty页

如下图所示

  • Free List:管理者空闲的也就是没有被使用的内存页,当执行查询操作时,如果对应的页已经在 buffer pool 中则直接返回数据,如果没有且 Free List 不为空,则从磁盘中查询对应的数据并存到 Free List 的某一页中,然后把这个页从 Free List 中移除并放入 LRU List 中。

  • LRU List:管理所有从磁盘中读取的数据页,包括未被修改的和已被修改的数据页,并根据 LRU 算法对链表中的页节点进行维护与淘汰。当数据库刚启动时 LRU List 是空的,这时从内存中申请到的页都存放在 Free List 中,当数据从磁盘读取到缓冲池时,首先从 Free List 中查找是否有可用的空闲页,如果有则把该页从 Free List 中删除并加入到 LRU List;如果没有,则根据 LRU 算法淘汰 LRU List 末尾的页,并将该内存空间分配给新数据页;

  • Flush List:当 LRU List 中的页被修改后会被标识为 脏页(Dirty page),并把脏页加入到 Flush List 中,在这种情况下,数据库会通过刷盘机制把 Flush List 中的脏页刷回磁盘;Flush List 是一个专门用来管理脏页的列表。脏页既存在于 LRU List 中,也存在于 Flush List 中,LRU List 用来管理缓冲池中页的可用性,Flush List 用来管理要被刷回磁盘的页,二者互不影响。Flush List 中的脏页在执行了刷盘操作后会将空间还给 Free List。


我们串起来看看啊!!

        缓冲池本质上是一块巨大的内存区域,用于缓存从磁盘读取的数据页,以加速数据库的读写操作。这些内存中的数据页并非静态存在,它们根据使用和修改情况,动态地在三种状态间转换:空闲页(Free)干净页(Clean) 和 脏页(Dirty)。为了高效管理处于不同状态的页,InnoDB 精心设计了三个链表:Free ListLRU List 和 Flush List,它们各司其职,紧密配合。

        当数据库启动,缓冲池初始化完成后,所有预先分配好的内存页都处于 空闲页(Free) 状态。此时,这些页只占用了内存空间,里面没有任何有效的磁盘数据内容。它们全部由 Free List(空闲链表) 管理。Free List 就像一个仓库管理员,手里掌握着所有可用的、空白的“笔记本”(内存页)。

        当用户执行查询需要读取数据时,数据库首先会在缓冲池中查找对应的数据页是否已在内存中(即是否在 LRU List 里)。如果没有找到(缓存未命中),就需要从磁盘读取数据。这时,数据库会向 Free List “申请”一个空白页。如果 Free List 不为空(有可用空闲页),它会从其管理的页中取出一个,将磁盘数据加载到这个页里。一旦数据加载完成,这个页的状态就从 Free 转变为了 干净页(Clean) —— 因为此刻内存中的数据与磁盘上的原始数据完全一致。紧接着,这个新加载的页会被从 Free List 中移除,并加入到 LRU List(最近最少使用链表) 中。LRU List 是缓冲池的核心管理链,它负责管理所有正在被使用的数据页,无论它们是 Clean 还是 Dirty 状态。

        LRU List 并非一个简单的先进先出或后进先出的队列,它内部采用了优化的 LRU(Least Recently Used)算法。具体来说,它被划分成了“热数据区”和“冷数据区”。新加入的数据页(刚从磁盘加载的 Clean 页)通常会被放入冷数据区的头部。如果一个页在冷数据区停留了足够长的时间(超过配置的 innodb_old_blocks_time,默认约1秒)并且被再次访问,它才有机会晋升到热数据区。热数据区存放的是最常被访问的热点数据。LRU List 的核心职责之一是决定当缓冲池空间不足(即 Free List 为空)时,哪些页应该被淘汰以腾出空间给新数据。淘汰过程主要发生在冷数据区的尾部。系统会优先淘汰那些处于 Clean 状态的页,因为它们的数据与磁盘一致,直接释放其空间即可。如果遇到的是 脏页(Dirty),处理就会复杂一些。脏页是指那些已经被用户修改过的页,内存中的数据与磁盘上的原始数据已经不一致。淘汰脏页不能简单地释放空间,必须先将修改写回磁盘以保证数据持久性。

        当用户执行修改操作(如 UPDATE、DELETE)时,数据库并不会直接修改磁盘文件,而是先找到 LRU List 中对应的数据页(可能是 Clean 或已经是 Dirty 的),在内存中修改其内容。一旦修改发生,这个页的状态就变成了 脏页(Dirty)。此时,除了继续存在于 LRU List 中(因为它仍然是一个活跃的、被使用的页),它还需要被加入到 Flush List(刷脏链表)。Flush List 是一个专门负责追踪所有需要写回磁盘的脏页的链表。它的排序依据是页的修改时间(确切地说是页第一次被修改时对应的日志序列号 LSN),最早修改的脏页排在前面。这样设计是为了确保在需要刷盘时,能优先将修改时间最久(理论上也最有可能需要被覆盖或恢复)的脏页写回磁盘。脏页因此同时存在于两个链表:LRU List 管理其缓存生命周期(何时被访问、何时可能被淘汰),Flush List 管理其持久化顺序(何时以及按什么顺序写回磁盘)。

        刷盘操作(将脏页数据写回磁盘)主要由后台线程周期性地执行,也可以在某些条件下被主动触发,例如当 Free List 空间严重不足需要快速回收脏页时,或者为了满足事务持久性要求(如提交时根据配置可能需要同步刷写相关日志和脏页)。当后台线程工作或主动刷盘被触发时,它会从 Flush List 的头部(即最早修改的页)开始,将脏页的内容写回其对应的磁盘位置。一旦一个脏页成功写回磁盘,它在内存中的数据就再次与磁盘一致,状态也就从 Dirty 变回了 Clean。这个页会从 Flush List 中移除(因为它不再是脏页了),但它仍然保留在 LRU List 中。只有当这个 Clean 页后来在 LRU 淘汰过程中被选中淘汰时,它才会从 LRU List 中移除,其占用的内存空间被释放回 Free List,等待被重新分配给新的数据页。这就是脏页被刷盘后空间最终归还给 Free List 的路径。


每个缓冲池都采用三个链表维护内存页,这三个链表也对应着内存中页的三种状态,分别是:

  • Free 未使用的页,也可以称做空闲页;

  • Clean 已使用但未修改的页,也可以称做干净页;

  • Dirty 已修改的页,也可以称做脏页。

内存中有这么多数据页如何快速找到目标页?

  • 首先第一种办法是通过遍历,这种做法显示不能满足性能要求

  • InnoDB采用的是 Page Hash 的方式,也就是每当把磁盘中数据页加载到内存时,用数据页的表空间id和页号做为Key,当前页在内存中的地址做为Value保存起来,每次查询时就可以通过Key快速定位到目标页,如果内存中没有目标页,则从磁盘中获取。

缓冲池中的数据放不下了怎么办?

  • InnoDB根据根据自身的实际场景,使用淘汰策略来淘汰相应的数据页,从而释放出内存空间,以便新的数据页加载到内存中

1.4 缓冲池采用哪种淘汰策略?是如何实现的?

  • 缓冲池淘汰策略采用变形的最近最少使用(LRU)算法(在原来LRU算法的基础上做了修改),以下出现的LRU算法指的是LRU变形算法

缓冲池使用 LRU算法管理链表,当有新页面添加到缓冲池时,最近最少使用的页将被淘汰,并将新页添加到列表的中间,这种中点插入策略将列表视为两个子列表:

  • 链表头部,是存放最近访问的新页(年轻页)子列表;

  • 链表尾部,是存放最近较少访问的旧页子列表。

如下图所示:

经常使用的页保存在新子列表中,较少使用的页保存在旧子列表中,随着时间的推移,旧子列表中的页将会逐渐被淘汰。默认情况下,算法的执行过程如下:

  • 缓冲池总容量的5/8用于新子列表,3/8用于旧子列表;

  • 列表的中间插入点是新子列表的尾部与旧子列表头部的交界

  • 当一个页被读入缓冲池时,首先插入到中点做为子列表的头节点;

  • 当前的页在旧子列表中时,把被访问的页移动到新子列表的头部,使其成为“新”页;

  • 数据库运行的过程中,缓冲池中被访问页面的位置不断更新,未访问的页面向列表的尾部移动,从而逐渐“变老”,最终超出缓冲池容量的页从旧子列表的尾部被淘汰。

如果大家感兴趣,也可以去官网看看:MySQL :: MySQL 8.0 Reference Manual :: 17.5.1 Buffer Pool

为什么要把页插入到中间而不是直接插入到新子列表的头部?

  • 因为InnoDB在读取页时,可能会发生“预读”,预读的意思是InnoDB根据当前访问的记录自动推断后面可能会访问哪个页,并把他们提前加载到内存中,从而提高以后查询的效率,预读的页以并不一定会被真正的读取,从中间点插入可以使其尽快被淘汰。

1.5 怎么查看当前缓冲池的信息?

通过使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 BUFFER POOL AND MEMORY 部分查看有关缓冲池的指标。

缓冲池指标位于InnoDB标准监视器输出的缓冲池和内存部分:

SHOW ENGINE INNODB STATUS\G

 我们截取了下面这个

那么至于怎么解读这个呢?我们可以去官网看看:MySQL :: MySQL 8.0 Reference Manual :: 17.5.1 Buffer Pool

官方文档翻译过来就是下面这样子 

InnoDB 缓冲池指标

指标名称 (Name)描述 (Description)
总分配内存 (Total memory allocated)为缓冲池分配的总内存(字节)。
字典分配内存 (Dictionary memory allocated)为 InnoDB 数据字典分配的总内存(字节)。
缓冲池大小 (Buffer pool size)分配给缓冲池的总页数。
空闲缓冲区 (Free buffers)缓冲池空闲列表的总页数。
数据库页 (Database pages)缓冲池 LRU 列表的总页数。
旧数据库页 (Old database pages)缓冲池旧 LRU 子链表的总页数。
已修改数据库页 (Modified db pages)缓冲池中当前已修改的页数。
挂起读取 (Pending reads)等待读入缓冲池的缓冲池页数。
挂起写入 LRU (Pending writes LRU)需要从 LRU 列表底部写入的缓冲池中旧脏页数量。
挂起写入刷新列表 (Pending writes flush list)检查点期间需要刷新的缓冲池页数。
挂起写入单页 (Pending writes single page)缓冲池中挂起的独立页写入数量。
年轻化页数 (Pages made young)在缓冲池 LRU 列表中变为年轻状态(移动到“新”页子链表头部)的总页数。
非年轻化页数 (Pages made not young)在缓冲池 LRU 列表中未变为年轻状态(保持在“旧”子链表中未变年轻)的总页数。
年轻化速率 (youngs/s)访问缓冲池 LRU 列表中旧页并导致其年轻化的每秒平均次数(仅适用于旧页,每次访问均计数)。详见下方备注说明。
非年轻化速率 (non-youngs/s)访问缓冲池 LRU 列表中旧页但未导致其年轻化的每秒平均次数(仅适用于旧页,每次访问均计数)。详见下方备注说明。
读取页数 (Pages read)从缓冲池读取的总页数。
创建页数 (Pages created)在缓冲池内创建的总页数。
写入页数 (Pages written)从缓冲池写入的总页数。
读取速率 (reads/s)每秒平均缓冲池页读取次数。
创建速率 (creates/s)每秒平均缓冲池页创建次数。
写入速率 (writes/s)每秒平均缓冲池页写入次数。
缓冲池命中率 (Buffer pool hit rate)从缓冲池(而非磁盘存储)读取页的命中率。
年轻化产生率 (young-making rate)导致页年轻化的平均访问命中率(适用于所有缓冲池页访问,不仅限于旧子链表)。详见下方备注说明。
非年轻化产生率 (not (young-making rate))未导致页年轻化的平均访问命中率(可能因未满足 innodb_old_blocks_time 延迟,或新子链表中的命中未触发移动到头部)。适用于所有缓冲池页访问。详见下方备注说明。
预读页数 (Pages read ahead)每秒平均预读操作次数。
未访问即逐出页数 (Pages evicted without access)每秒平均从缓冲池逐出但未被访问的页数。
随机预读 (Random read ahead)每秒平均随机预读操作次数。
LRU 长度 (LRU len)缓冲池 LRU 列表的总页数。
解压 LRU 长度 (unzip_LRU len)缓冲池 unzip_LRU 列表的长度(页数)。
I/O 总量 (I/O sum)访问的缓冲池 LRU 列表页总数。
当前 I/O (I/O cur)当前时间间隔内访问的缓冲池 LRU 列表页总数。
解压 I/O 总量 (I/O unzip sum)解压缩的缓冲池 unzip_LRU 列表页总数。
当前解压 I/O (I/O unzip cur)当前时间间隔内解压缩的缓冲池 unzip_LRU 列表页总数。

备注说明 (Notes):

  1. youngs/s (年轻化速率):

    • 仅适用于旧页。该指标基于页访问次数(同一页的多次访问均被计数)。

    • 优化建议: 若在没有大型扫描时该值很低,可考虑减少 innodb_old_blocks_time 延迟时间,或增加旧子链表占缓冲池的百分比(innodb_old_blocks_pct)。增大旧子链表比例可延长旧页移动到 LRU 尾部的时间,增加其被再次访问并年轻化的机会。

  2. non-youngs/s (非年轻化速率):

    • 仅适用于旧页。该指标基于页访问次数(同一页的多次访问均被计数)。

    • 优化建议: 若在执行大型表扫描时未观察到较高的 non-youngs/s 值(以及较高的 youngs/s 值),可尝试增加 innodb_old_blocks_time 延迟值。

  3. young-making rate (年轻化产生率) 与 not (young-making rate) (非年轻化产生率):

    • 这两个指标统计所有缓冲池页访问,不仅限于旧子链表。

    • 它们通常不会相加等于总的缓冲池命中率。

    • 旧子链表中的页命中会将其移动到新子链表。

    • 新子链表中的页命中,仅当该页距离头部一定距离时,才会将其移动到链表头部(并非每次命中都导致移动)。

    • not (young-making rate) 指未导致页年轻化的命中率,原因包括:

      • 访问旧页但未满足 innodb_old_blocks_time 延迟要求。

      • 访问新子链表的页但未触发移动到头部(因距离头部不够远)。

1.6.缓冲池小结

  • 缓冲池是用来缓存各种数据,最主要的就是缓存从磁盘中加载的数据页,从而提升效率

  • 缓冲池为了方便数据组织定义了不同的数据结构,包括 Instances 实例、Chunk 块、其中 Buffer Pool 中包含至少一个 Instances 、Instances 包含至少一个 Chunk 块、Chunk 管理若干个从磁盘加载到内存的 Page 数据页

  • 缓冲池能过三个链表管理内存中的数据页,分别是 Free List 、LRU List 和 Flush List

  • 缓冲池淘汰策略采用变形的最近最少使用(LRU)算法

二. 变更缓冲区(Chang Buffer)

首先我们看看作为内存结构的另外一部分,变更缓冲区在哪里呢?

很显然啊,变更缓冲区是占用了分配给缓冲池的内存。

我们之前说缓冲池里面管理的是数据页(磁盘里面数据页的副本)

但是我们变更缓冲池里面管理的可不是数据页,而是Change Buffer页。

大家要是感兴趣也可以去官网看看:MySQL :: MySQL 8.0 Reference Manual :: 17.5.2 Change Buffer 

2.1.变更缓冲区的作用

一句话说就是提升修改数据时的效率

一般来说,如果要修改一条数据,内存与磁盘之间的交互过程

  1. 在磁盘中找到对应的数据行
  2. 将数据行所在数据页加载到内存中
  3. 在内存中完成对数据行的修改
  4. 把修改过后的数据行所在的数据页写回磁盘

这里面发生了两次磁盘IO才能完成一次数据修改操作,我们之前说影响数据库性能的因素很大程度是因为磁盘IO次数太多了,如果我们能显著减少磁盘IO次数,那MySQL的性能会得到显著提升。

那么变更缓冲池是怎么做的呢?

  • 变更缓冲区用来缓存对二级索引数据(后面会说)修改(注意Chang Buffer存储的是修改),是一个特殊的数据结构,当使用 INSERT、UPDATE 或 DELETE 语句修改二级索引对应的数据时,如果对应的数据页在缓冲池中则直接更新,如果不在缓冲池中,那么就把修改操作缓存到变更缓冲区,这样就不用立即从磁盘读取对应的数据页了,当之后的读操作将对应的数据页从磁盘加载到缓冲池中时,变更缓冲区中缓存的修改操作再批量合并到缓冲池,从而达到减少磁盘I/O的目的。执行流程如图所示:

我来讲解一下这个图啊!! 

步骤1:事务1 修改二级索引数据 (DML操作)

  • 场景: 事务1 执行 INSERTUPDATE 或 DELETE 语句,这些操作涉及到对二级索引的修改(例如,更新了一个有索引的字段)。

  • 关键决策点:目标索引页是否在缓冲池(Buffer Pool)中?

    • 是 (在 Buffer Pool 中):

      • 直接操作:直接在内存中的 Buffer Pool 里找到对应的二级索引数据页进行修改。

      • 结果: 修改立即体现在内存中。之后由后台线程在适当时候将脏页刷回磁盘(写IO)。

    • 否 (不在 Buffer Pool 中):

      • 缓存到变更缓冲区:不立即去磁盘读取目标索引页! 而是将 “需要做什么修改”(例如,“在索引页X插入值Y”,“在索引页Z删除值W”)这个操作指令本身记录到内存中的 Change Buffer

      • 结果: 事务1 的操作快速完成(因为它只操作了内存中的Change Buffer),避免了等待磁盘读取索引页的随机IO。被修改的二级索引页仍然在磁盘上保持旧状态

步骤2:事务2 读取事务1修改过的数据

  • 场景: 稍后,事务2 执行一个查询(SELECT),这个查询需要访问事务1 修改过的那个二级索引(或其关联的数据)。

  • 关键动作:触发磁盘读取与合并

    • 从磁盘读取: 系统发现事务2需要访问的二级索引页不在 Buffer Pool 中(或者虽然可能在,但Change Buffer里有关于它的待处理修改)。

    • 加入 Buffer Pool: 将该二级索引页从磁盘加载到 Buffer Pool(发生一次读IO)。

    • 合并修改: 就在加载该页到 Buffer Pool 的瞬间! 系统会立即查询 Change Buffer:

      • 查找所有缓存着的、针对这个特定二级索引页的修改操作(这些操作来自之前类似事务1的操作)。

      • 将这些积压的修改操作(INSERT/UPDATE/DELETE指令)一次性、批量地应用到刚加载进内存的这个索引页上。

    • 结果:

      • 此时,Buffer Pool 中的这个二级索引页已经包含了事务1的修改 + 可能其他事务缓存的修改,是最新的状态。

      • 事务2 看到的就是包含了事务1修改结果的最新数据(满足事务隔离性,如可重复读RR下通过MVCC机制保证)。

      • 这个被合并修改过的索引页变成了脏页,等待后台线程刷回磁盘(一次写IO)。


 变更缓冲区用来缓存对二级索引数据的修改,当数据页没有被回载到内存中时先把修改缓存起来,等到其他查询操作发生时数据页被加载到内存后,再直接修改内存中的数据页,从而达到减少磁盘I/O的目的。

2.1.1 变更缓冲区只适用于二级索引?

关于索引在数据库初阶已经做了介绍,我们知道索引分为聚集索引(主键)和二级索引(自定义).我们先来弄明白主键索引和二级索引到底是啥吧

主键索引和二级索引是什么

想象一本厚厚的电话簿(这就是数据库表):

  1. 主键索引 = 按身份证号排序的电话簿:

    • 每个人的信息(姓名、电话、地址)就是一条数据行

    • 这本电话簿的唯一排序方式是按每个人的身份证号(ID) 从小到大排列。

    • 想通过身份证号找人?非常快!因为顺序排好了,直接翻到大概位置就行。这就是主键索引(聚簇索引)。它决定了数据行在磁盘上物理存储的顺序。这本电话簿本身就是数据的“原件”。

  2. 二级索引 = 另外制作的“快捷查找小册子”:

    • 现在你想通过姓名快速找人,而不是身份证号。怎么办?

    • 你会单独做一本小册子:这本小册子只记录姓名和对应的身份证号(ID),并按姓名的拼音顺序排列好。

    • 想找“张三”的电话?

      • 先查“姓名小册子”(二级索引),按拼音顺序快速找到“张三”,同时看到他的身份证号是 ID123

      • 然后拿着这个 ID123,再去主电话簿(主键索引)里按身份证号顺序快速找到 ID123 那一行,就能看到张三的完整信息(电话、地址等)。

    • 这本“姓名小册子”就是一个二级索引

    • 二级索引数据指的就是这本小册子里记录的内容:索引字段的值(比如“张三”) + 对应的主键值(ID123)。它不存储完整的用户信息(电话、地址),只存储建立索引的那个字段(姓名)和指向主键的“指针”(ID)。

我再多提一嘴,二级索引的核心特点:

  • 基于非主键列: 它是建立在像姓名邮箱城市订单状态商品类别文章关键词这些不是主键的列上的。

  • “快捷目录”而非“原件”: 它不是数据的“原件”(原件在主键索引里)。它只是一个独立的、额外的查找目录

  • 存储内容精简: 它的“目录条目”只包含:

    • 索引列本身的值(比如 姓名=‘张三’)。

    • 该行数据对应的主键值(比如 ID=123)。

  • 物理独立排序: 这本“目录小册子”在磁盘上按照索引列的值(比如姓名拼音)重新排序存放。张三李四在索引里可能挨着,但他们在主电话簿(主键索引)里的物理位置可能隔得很远。

  • 需要“回表”: 使用二级索引找到数据通常需要两步:

    1. 在二级索引中查找:找到索引列值(如‘张三’)和对应的主键值(如ID123)。

    2. 回主键索引查找:拿着主键值(ID123)回到主键索引(聚集索引)中查找,才能定位到该行的完整数据(电话、地址等)。这一步叫做“回表(Bookmark Lookup)”。

  • 可存在多个: 一本电话簿(表)可以制作多个不同的“快捷目录”(二级索引),比如按姓名、按城市、按职业各做一本。

  • 可唯一或不唯一: “姓名目录”可能允许重名(不唯一索引),“邮箱目录”通常要求唯一(唯一索引)。

  • 组合索引: 你还可以做“省份+城市”目录(组合索引),先按省排序,同一个省里再按市排序,方便找某个省某个市的人。

为什么变更缓冲区只适用于二级索引,而不能用于聚集索引(主键)?

为什么聚集索引(主键)不能放入变更缓冲区?

我们现在加上一下变更缓冲区存储的是主键索引数据的修改,那么它就有下面2个核心问题

  1. 核心矛盾:唯一性约束的即时性要求

    • 聚集索引就是数据本身在磁盘上的物理排序依据,它必须强制保证主键的唯一性

    • 想象场景:两条 INSERT 语句试图插入相同主键值 (id=1)。如果将它们都缓存到变更缓冲区:

      • 在缓存阶段,数据库无法验证这个 id=1 是否已经存在于磁盘上的真实数据页中(因为数据页还没加载)。

      • 当未来某个时刻,包含 id=1 的数据页被加载到缓冲池,变更缓冲区尝试将这两个插入操作合并进去。

    • 灾难性后果: 合并操作会尝试在同一个位置(或逻辑上相同的主键位置)插入两条 id=1 的记录,这直接违反了主键的唯一性约束,导致数据不一致或操作失败。

  2. 无法承受的代价: 为了保证唯一性,数据库必须在执行 INSERT 或涉及主键更新的 UPDATE 操作时立刻检查目标主键值是否已存在。这要求:

    • 要么目标数据页已在缓冲池(直接检查)。

    • 要么必须立刻从磁盘读取对应的数据页进行检查和修改。

    • 变更缓冲区的“延迟加载和合并”机制与此根本冲突。 它无法在缓存阶段解决唯一性验证问题。

总结聚集索引: 主键的唯一性要求对数据的修改(尤其是插入和主键更新)必须是实时、同步的,无法容忍变更缓冲区带来的延迟和批量合并操作,否则会破坏数据的最基本约束。


为什么二级索引可以放入变更缓冲区?

  1. 核心优势:非唯一性与操作可延迟性

    • 二级索引通常不唯一(如姓名索引、城市索引)。允许存在重复值。

    • 继续上面的插入场景:两条 INSERT 语句插入 id=100 (name='Alice') 和 id=101 (name='Alice')。它们都需要在 name 索引上添加一个 'Alice' 条目。

      • 变更缓冲区可以安全地缓存这两个操作:在name索引页P插入('Alice', 100) 和 在name索引页Q插入('Alice', 101)(P和Q可能不同)。

      • 即使这两个 'Alice' 最终指向了同一个索引页(比如P),缓存的是两个独立的插入指令。当索引页P被加载时,合并操作会顺序执行这两个插入。由于索引允许重复值,插入两个相同的索引值是完全合法的,不会造成冲突或违反约束。

  2. 变更缓冲区的妙用:

    • 对于不在缓冲池中的二级索引页的修改(插入、删除标记、更新对应的删除+插入),变更缓冲区不要求立即加载磁盘页

    • 它只是将“要做什么修改”(操作指令)记录在内存中的变更缓冲区里。

    • 延迟加载,批量合并: 只有当后续某个读操作(或系统刷脏等后台进程)真正需要访问那个特定的二级索引页时,才会发生:

      • 从磁盘读取该索引页到缓冲池。

      • 在加载完成的瞬间,立刻查找变更缓冲区。

      • 将变更缓冲区中所有积压的、针对这个特定索引页的修改操作(可能来自之前多次写操作),一次性、批量地应用到刚加载到内存的索引页上。

      • 此时,这个索引页在内存中就是最新的状态了。

      • 之后在合适的时机(如刷脏),将这个修改好的页写回磁盘(一次写IO)

  3. 核心收益:I/O次数锐减

    • 省掉读IO: 避免了为执行单次修改而立刻读取不在内存中的索引页的随机读IO。

    • 合并写IO: 变更缓冲区可能积攒了多个针对同一个索引页的修改。当这个页最终被加载并合并修改后,只需一次写IO就可以将所有这些修改持久化到磁盘。而原始方法可能需要多次写IO(每次修改如果页不在内存,读后改完立刻写也算一次写IO)。同时,后台线程合并刷脏也能进一步优化写。

2.1.2 Merge的触发时机有哪些?

  • 读取对应的数据页时;

  • 当系统空闲或者 Slow Shutdown 时,主线程发起 merge;

  • Change buffer 的内存空间即将耗尽时;

  • Redo Log 写满时。

2.2 变更缓冲区的主要配置项都有哪些?

  • 主要的配置项有缓冲类型更改缓冲区的最大大小

缓冲类型

在修改二级索引数据时变更缓冲区可以减少磁盘I/O从而提高效率,但是变更缓冲区占用了缓冲池的一部分空间,从而减少了可用于缓存数据页的内存,如果业务场景读多写少,或者表中的二级索引相对较少,那么可以考虑禁用更改缓冲从而提高缓冲池空间。

可以通过选项文件或 SET GLOBAL 语句对系统变量 innode_change_buffering 进行设置,来控制变更缓冲区对于插入、删除操作(索引记录被标记为删除)和清除操作(当索引记录被物理删除时的开启或禁用:

  1. 删除操作:索引记录被标记为删除

  2. 清除操作:索引记录被物理删除时

  3. 更新操作:是插入和删除操作的组合

  • all :默认值,缓存插入、删除标记操作和清除

  • none :不缓存任何操作

  • inserts :只缓存插入操作

  • deletes :只缓存删除标记操作

  • changes :缓存插入和删除标记操作

  • purges :缓存发生在后台的物理删除操作

更改缓冲区的最大大小

  • 通过 innodb_change_buffer_max_size 系统变量可以设置变更缓冲区的最大大小,默认为25,最大为50,表示更改缓冲区占缓冲池内存总大小的百分比。

  • 在有大量插入、更新和删除的业务场景中,可以考虑增加 innodb_change_buffer_max_size 的值,在大部分是读多写少,比如用于报表的静态数据场景中考虑减少 innodb_change_buffer_max_size 的值

  • 需要注意的是,如果变更缓冲区占了缓冲池太多的内存空间,会导致缓冲池中的数据页更快地淘汰。

怎么查看当前变更缓冲区的信息?

  • 通过使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分查看有关更改缓冲区状态的信息。

SHOW ENGINE INNODB STATUS\G

 

三. 自适应哈希索引(Adaptive hash index)

首先,这个自适应哈希索引在哪里呢?

 看到了吧,自适应哈希索引占的空间还是缓冲池的一部分啊。

3.1. 自适应哈希索引的作用?

自适应哈希索引的主要作用就是提升查询效率

  • 自适应哈希索引可以使InnoDB存储引擎在不牺牲事务特性和可靠性以及缓冲池空间足够的前提下提升效率,使用起来更像是一个内存数据库,哈希索引根据经常访问的索引页自动构建;

  • 根据InnoDB内部的监控机制,如果监控到某些查询通过建立哈希索引可以提高性能,则自动对这个页创建哈希索引,这个过程称为自适应,所以叫自适应哈希索引;

  • 如果表完全放在内存中,则哈希索引可以通过直接查找任何元素来加快查询速度

3.2.为什么要创建自适应哈希索引?

  • InnoDB存储引擎的数据存储于B+树中,B+树通常只有3到5层,叶子节点才会存储真实数据,非叶子节点存储的是路径信息但从根节点到叶节点的寻路涉及到多层页面内记录的比较,即使所有路径上的页面都在内存中,也非常消耗CPU的资源

  • InnoDB对寻路的开销进行了优化,比如寻路结束后将cursor缓存起来方便下次查询复用;尽可能的避免单词寻路开销,Adaptive hash index(AHI)便是为此而设计,可以理解为B+树的索引

  • 本质上是通过缩短寻路路径(Search Path)从而提升MySQL查询性能的一种方式


我们可以举一个生活中的例子来说明一下

想象一个巨型图书馆(数据库):

  1. 常规查找(遍历B+树):

    • 图书馆的书架按书的编号(主键索引)或作者姓氏(二级索引)严格排序(B+树结构)。

    • 你想找一本特定编号的书(如 编号=10086):

      • 你需要从图书馆入口(B+树根节点)开始,根据编号大小,一层层下到不同的分区(非叶子节点),最终到达正确的书架(叶子节点),找到那本书(数据行)。

    • 或者你想找某位作者(如 作者='金庸')的所有书:

      • 你需要先到“作者索引”区(二级索引B+树),根据作者姓氏找到对应的索引卡片(上面写着作者名+书编号),拿到所有书的编号。

      • 然后拿着这些编号,一个个回到“主书架”(主键索引B+树)按编号查找每一本书。

    • 这种查找方式(B+树)非常高效(O(log n)复杂度),尤其对于范围查询,但它仍然是路径查找,需要走几步才能到达目标。

  2. 痛点:热点数据的反复路径查找

    • 想象图书馆里有一本《哈利波特》特别火,每天有成千上万的人来找这本书。

    • 虽然B+树路径已经很高效,但每个读者都要重复走一遍:入口 -> A区指示牌 -> B区走廊 -> C排书架 -> 最终找到。

    • 即使书就在那里,路径是固定的,但重复走这段固定的路径本身也消耗时间和精力(CPU计算资源)。

  3. 自适应哈希索引的妙招:给热点书开“直达专线”

    • 图书馆管理员(InnoDB存储引擎)非常聪明,它有个监控系统(内部监控机制)。

    • 它发现《哈利波特》这本书(更精确地说,是查找编号=10086作者='J.K.罗琳'的查询)被请求的次数异常频繁

    • 管理员决定: 给这本超级热门的书建立一个“直达专线”

      • 它在图书馆最显眼的位置(内存中,在缓冲池之上)放了一个超级简单的“直达电话簿”(哈希表)。

      • 这个电话簿只记录最常被问的书名或编号和它的精确物理位置

        • 例如:键=10086 -> 指向《哈利波特》物理位置的指针

        • 或 键='J.K.罗琳' -> 指向该作者所有书索引卡片位置的指针

    • 现在,再有读者来找《哈利波特》:

      • 他不用再去走B+树那套复杂的路径了。

      • 他直接跑到“直达电话簿”前,说出编号=10086

      • 电话簿瞬间(O(1) 时间复杂度)告诉他书的精确物理位置

      • 读者一步直达书架拿到书。

    • 效果: 对于这本超级热门的书,查找速度从“走几步路”变成了“瞬间传送”,快得飞起!

自适应哈希索引的核心原理与特点:

  1. “自适应”的含义:自动识别热点,按需创建

    • 它不是对整个数据库表或索引都建哈希索引(那会占用巨大内存且效率不高)。

    • 监控机制: InnoDB 内部持续监控查询模式。如果发现某个特定的值(如 user_id=123)或者某个索引页上的查询频率非常高,并且通过建立哈希索引能显著提升这些查询的速度。

    • 自动创建: 当监控到这种“热点”模式时,InnoDB 会自动地、动态地在内存中为这个热点数据所在的索引页构建一个哈希索引条目。这个过程对用户和应用完全透明,无需任何配置。

    • 自动维护: 当热点转移(比如 user_id=123 不再活跃,而 user_id=456 变得热门),旧的哈希条目可能会被淘汰,新的会被创建。哈希索引也会随着底层B+树索引页的更新(如分裂)而自动更新。

  2. 作用域:内存中的索引页

    • AHI 只在内存(Buffer Pool)中起作用。它缓存的是已经被加载到内存的、频繁被访问的索引页中的特定键值的位置信息。

    • 它的目标不是减少磁盘IO(那是缓冲池Buffer Pool和变更缓冲区Change Buffer的主要工作),而是优化已经在内存中的数据的查询速度。它让内存中的数据访问快得像内存数据库(如Redis)一样。

  3. 工作原理:瞬间直达

    • 哈希表的核心思想是:根据键值(Key,如 10086 或 'J.K.罗琳'),通过一个哈希函数计算出一个地址(或槽位),这个地址直接指向数据(或数据指针)所在的位置。

    • 对于等值查询(WHERE id = 10086WHERE name = 'Alice'),使用AHI,数据库引擎无需遍历B+树的层级结构,直接通过计算哈希值,一步就能定位到内存中该记录的确切位置(或二级索引中该键值对应的主键位置),效率是O(1)。

  4. 优势:极致优化热点查询

    • 显著降低CPU开销: 避免了高频查询反复遍历B+树节点的计算成本。

    • 大幅提升热点查询速度: 对于点查(Point Query),速度可以提升数倍甚至更高,响应时间更稳定。

    • 零配置,自管理: DBA 通常不需要干预,由引擎智能管理。

    • 利用富余内存: 在缓冲池(Buffer Pool)空间充足的前提下工作,不牺牲核心的事务特性(ACID)和可靠性。

  5. 理想场景:内存数据库般的体验

    • 全表在内存: 如果整个表的数据和索引都能常驻内存(Buffer Pool足够大),那么AHI可以使得对该表的等值查询速度无限接近纯粹的内存哈希数据库。

    • 热点数据在内存: 更普遍的情况是,只有频繁访问的“热点”索引页和数据页会长期留在内存中。AHI 正是针对这些内存中的热点数据,将其访问路径优化到极致。

总结一下自适应哈希索引:

它就像是数据库引擎内置的一个智能热点加速器。通过持续监控,它自动识别出哪些数据(特定的键值或索引页)被疯狂访问。对于这些“明星数据”,它在内存中悄悄构建了一个直达通道(哈希索引)。当查询再次命中这些热点时,引擎就能绕过常规的B+树导航路径,瞬间直达目标,大大降低了CPU消耗,将响应速度推向极致。这一切都是在后台自适应完成的,充分利用了富余的内存资源,让InnoDB在处理高频点查时,能获得堪比内存数据库的流畅体验。它的核心价值在于优化内存中热点数据的访问效率

自适应哈希索引的Key - Value如何设置?

  • 以查询条件为key,B+树页的地址为value的Hash Index

自适应哈希索引在保存在哪里?

        自适应哈希索引会占用缓冲池一部分内存区域,在缓冲池初始化后被初始化为了避免AHI的锁竞争压力,AHI支持分区,可以使用 innode_adaptive_hash_index_parts 参数配置分区个数,默认为8。

注意:自适应哈希索引是InnoDB内部的优化方式,外部不能干预

3.3.关于自适应哈希索引有哪些配置项?

  • 通过设置系统变量 innode_adaptive_hash_indexinnode_adaptive_hash_index 开启或关闭自适应哈希索引

    • 在选项文件中设置系统变量 innode_adaptive_hash_index=[1∣0]innode_adaptive_hash_index=[1∣0] 实现开启与关闭

    • 通过命令行选项 --skip-innode-adaptive-hash-index 也可以关闭自适应哈希索引

  • 每个自适应散列索引被绑定到不同的分区中,每个分区有不同的锁保护,分区数量由系统变量 innode_adaptive_hash_index_partsinnode_adaptive_hash_index_parts 控制,默认置为 8,最大值为 512。

3.4.怎么查看自适应哈希索引的信息?

通过使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分查看自适应哈希索引使用信息,如果锁争抢过多,可以考虑增加自适应哈希索引分区数量或禁用自适应哈希索引。

SHOW ENGINE INNODB STATUS\G

 

四.日志缓冲区

首先我们得知道日志缓冲区在内存结构的哪里?

 日志缓冲区可以与磁盘进行交互啊。

日志缓冲区管理的可不是数据页,也不是Change Buffer页,而是日志页。

关于Log Buffer和Redo log是怎么工作,怎么配合的,日志页的结构,在这里的话我不想详细介绍。我要等到讲解innoDB存储引擎的磁盘结构里面的Redo Log一起去讲。

我们这里就只粗略谈谈Log Buffer即可。

4.1.日志缓冲区的作用

  • 日志缓冲区是服务器启动时向操作系统申请的一片连续的内存区域,存储即将要写入磁盘日志文件的数据。

  • 在对数据库进行DML(增删改)操作时,InnoDB会记录对应操作的日志,比如为保证数据完整性实现数据库崩溃恢复的Redo Log,这些日志会首先写入Log Buffer中,从而解决同步写磁盘导致的性能问题,然后根据不同落盘策略最终写入磁盘

日志不通过Log Buffer直接写入磁盘不行吗?

  • 如果日志不通过Log Buffer直接写入磁盘,那么每次进行DML操作都会进行一次磁盘I/O,这样会严重影响效率,所以把日志统一写入内存中的Log Buffer,根据刷盘策略统一进行落盘操作,可以实现一次磁盘I/O写入多条日志,从而提升效率。

Log Buffer与日志文件是如何配合工作的?

关于Log Buffer与日志文件之间的交互过程、RedoLog的结构和RedoLog的写入时机,本专题最后在介绍磁盘上的Redo Log时会详细讲解它的工作原理

4.2.日志缓冲区和变更缓冲区的区别

我们来深入浅出地解释 日志缓冲区 (Log Buffer) 和 变更缓冲区 (Change Buffer) 的核心区别,它们虽然名字里都带“缓冲区”,但职责和目标截然不同:

想象场景: 你经营一家大型仓库(数据库),记录着所有货物的进出(数据修改)。

1. 变更缓冲区 (Change Buffer) - “仓库修改代办处”

  • 核心职责: 优化对“货物目录卡片”(二级索引)的修改效率。

  • 具体工作:

    • 当仓库管理员(事务)需要更新、插入或删除某个货物的位置信息时,这个信息记录在分散的“货物分类目录卡片柜”(二级索引)里。

    • 如果管理员发现要修改的那张特定目录卡片所在的抽屉(索引页) 此刻不在他手边的办公桌(内存/Buffer Pool)上,他不会立刻跑去仓库深处翻找那个抽屉。

    • 他只是在办公桌的 “修改待办事项本”(Change Buffer) 上快速记一笔:“在卡片柜A区第5抽屉,插入卡片:商品Y,货架Z” 或 “在卡片柜B区第3抽屉,删除卡片:商品X”。

    • 这个“待办事项本”只记录操作指令(增、删、改什么),不记录修改的完整细节(比如商品Y的图片、重量等具体数据)。

  • 目标: 避免即时的随机磁盘读取(读IO)! 通过暂存操作指令,等以后真正需要访问那个卡片抽屉时(比如有人要查商品Y在哪),再一次性把抽屉搬出来(读磁盘),同时把积压的待办事项(Change Buffer记录)批量合并进去。这大大减少了为了修改目录卡片而满仓库跑腿找抽屉(随机读IO) 的次数,显著提升修改目录的效率(特别是频繁修改不常访问的目录时)。

  • 关键特点:

    • 服务对象: 仅针对二级索引的修改。

    • 内容: 缓存的是一系列修改操作指令(如 Insert (Key, PK)Delete Marker)。

    • 持久性: 它本身不是持久化的。如果数据库崩溃,丢失的只是这些“待办事项”,但数据本身在主索引和日志的保护下是安全的(待办事项可以根据日志恢复或重建)。它是纯性能优化组件。

    • 触发合并: 当对应的二级索引页被读取到内存(Buffer Pool)时触发合并。


2. 日志缓冲区 (Log Buffer) - “仓库操作流水账速记本”

  • 核心职责: 确保仓库所有货物变动(数据修改)的绝对安全和可恢复性。

  • 具体工作:

    • 无论管理员是修改了主货物清单(主键索引/聚集索引)还是某个分类目录卡片(二级索引),只要对仓库的货物状态做了任何实质性的改动(增删改数据行),他必须立刻、完整、按顺序地把“谁、在什么时候、对什么货物、做了什么改动、改动后的样子”这些关键操作细节,记录到“操作流水账”(重做日志 - Redo Log)里。

    • 为了不耽误管理员干活(事务提交要快),不会每次都立刻把这条流水账写到永久的账本仓库(磁盘)里。管理员手边有个 “速记本”(Log Buffer)

    • 每当发生修改,管理员就先在速记本上快速记下这笔流水账(例如:“2023-10-27 10:00:00,事务ID123,在货架Z插入商品Y,详情:...”)。

    • 这个“速记本”记录的是修改的物理细节(具体哪些字节被改了,改成什么样)。

  • 目标: 保证持久性 (Durability) 和崩溃恢复能力 (Crash Recovery)! 核心是:

    • 快速提交: 事务提交时,只要确保它的流水账记录写到了速记本(Log Buffer)或者更好的,写到永久的账本仓库(磁盘Log File),就可以认为事务安全完成(即使数据还没真正写到仓库货架上)。

    • 崩溃恢复: 万一仓库突然断电(数据库崩溃),重启后只需要查看这个“操作流水账”,就能精确重现崩溃前一刻所有已提交事务所做的修改,把仓库恢复到一致状态。没有它,数据修改可能在崩溃中永久丢失。

    • 组提交优化: 数据库会把短时间内多个事务的流水账记录在“速记本”上攒一小批,然后一次性、顺序地写到永久的账本仓库(磁盘)。这比每条记录都写一次磁盘快得多(顺序写IO vs 随机写IO)。

  • 关键特点:

    • 服务对象: 所有对数据的修改(包括主键索引、二级索引、Undo Log 的修改等)。

    • 内容: 缓存的是修改的物理内容(物理日志),描述数据页的字节级变化。记录的是“做了什么”的原始证据。

    • 持久性: 是保证持久性 (ACID 的 D) 的核心!事务提交时,其相关的 Redo Log 必须以某种策略(如 innodb_flush_log_at_trx_commit=1 时)保证写入磁盘的日志文件才算真正提交成功。这是数据安全的生命线

    • 触发写入: 事务提交、Log Buffer 满、后台线程定时刷、Checkpoint 机制等。


核心区别总结表

特性变更缓冲区 (Change Buffer)日志缓冲区 (Log Buffer)
核心目标提升性能: 减少修改二级索引时的随机读磁盘IO保证安全: 确保数据持久性崩溃恢复能力
服务对象仅二级索引的修改操作 (Insert/Update/Delete)所有数据修改 (主键索引、二级索引、Undo等)
缓存内容操作指令: Insert (Key, PK)Delete Marker 等物理修改细节: 数据页具体的字节变化信息
关键作用延迟加载 + 批量合并,优化写效率快速提交 + 顺序写盘,保证持久性和崩溃恢复
持久性非持久化: 崩溃后丢失可重建,纯性能优化强持久化: 事务提交需确保日志落盘(策略依赖),是安全基石
触发时机对应二级索引页被读取到内存时触发合并事务提交、缓冲区满、后台线程刷盘等触发写入磁盘
类比修改目录卡片的待办事项本” (只记要做什么操作)仓库所有操作的流水账速记本” (记做了什么及细节)

简单一句话区分:

  • 变更缓冲区 (Change Buffer) 是数据库为了少跑腿(减少随机读IO),暂时把修改目录卡片(二级索引)的“待办事项” 记在便签本上,等以后顺路时再处理的 性能优化工具

  • 日志缓冲区 (Log Buffer) 是数据库为了保证绝对安全(不丢数据),先把所有货物变动(数据修改)的“原始证据” 快速记在速记本上,并尽快誊写到永久账本(磁盘)的 安全卫士。它记录了修改的“铁证”,是崩溃恢复的唯一依据。

http://www.dtcms.com/a/276623.html

相关文章:

  • 【项目】GraphRAG基于知识图谱的检索增强技术-实战入门
  • 代码随想录算法训练营65期第17天
  • 余电快速泄放电路
  • 【InnoDB磁盘结构1】系统表空间,独立表空间,双写缓冲区
  • C语言基础知识--动态内存管理
  • 贪心算法题解——划分字母区间【LeetCode】
  • 操作系统—第三章 内存管理
  • 169. 多数元素
  • 二分搜索 (左程云)
  • 【Docker基础】Dockerfile核心概念解析:什么是Dockerfile?与镜像、容器的关系
  • shiro550反序列化漏洞复现(附带docker源)
  • AV1比特流结构
  • zynq-PS篇——bperez77中DMA驱动注意事项
  • 车载以太网-旁路配置
  • MyBatis基于XML配置详细使用指南
  • IMU姿态传感器
  • 栈题解——最小栈【LeetCode】
  • 学历一般,基础一般还有必要刷算法题吗
  • 一种Φ325海底管道机械三通结构设计cad【1张】三维图+设计说明书
  • python学习笔记【1】对字符串的处理
  • 网络安全day1-2笔记
  • kettle从入门到精通 第101课 ETL之kettle DolphinScheduler调度kettle
  • RAG进阶之术:用“父子Chunk”策略破解复杂查询的“上下文迷局”
  • Win11怎样进入WinRE恢复环境
  • 并发--Callable vs Runnable
  • 深入理解 Boost.Asio 中的异步核心 boost::asio::io_context
  • AI智能体|扣子(Coze)搭建【裸眼3D著名故事动画视频】工作流
  • NOIP普及组|2005T1淘淘摘苹果
  • 常用控件QWidget
  • 部署Harbor私有仓库