侧边栏壁纸
  • 累计撰写 247 篇文章
  • 累计创建 16 个标签
  • 累计收到 0 条评论

目 录CONTENT

文章目录

Buffer Pool

kaixindeken
2021-06-29 / 0 评论 / 0 点赞 / 109 阅读 / 3,375 字

Buffer Pool 的引入

InnoDB 存储引擎会将所有索引和数据信息持久化到磁盘文件中,然后基于表空间和数据页来管理表记录,数据页是 MySQL 管理维护表记录的基本单位,对于设置索引的列,还会为其维护 B+ 索引树。

不过,当 MySQL 服务端执行 SQL 查询并返回结果集给客户端时,是不可能直接和磁盘文件交互的,这样性能太差了!InnoDB 的优化方案是在 MySQL 服务端启动时,向操作系统申请一片连续的内存空间,当数据库查询需要访问某个数据页的数据时(如果命中索引的话,可以通过 B+ 树快速定位索引值所在的数据页,否则的话需要全表扫描定位),则将整个数据页的数据加载到这片内存空间,即便只是操作一条记录,也会这么做,因为数据页是 MySQL 维护表记录的基本单位,然后我们就可以在内存中对数据进行读写操作了,读写操作完成后,也不会立即释放对应的内存空间,而是缓存起来,方便下次使用,避免磁盘 IO。对于这片内存空间,有一个专有的名词 —— Buffer Pool,中文译作缓冲池。

注:到这里,就可以将 MySQL 数据库查询的完整流程串起来了,对于命中索引的全值匹配查询,可以通过对应列所在的 B+ 树快速定位其所在的数据页,如果该数据页已经加载到 Buffer Pool 了,则直接在内存中基于二分查找快速定位索引位置,否则将该数据页从磁盘加载到内存再进行二分查找,如果该索引是主键索引,可以直接返回数据,否则还要再进行回表操作(更复杂的查询语句请在此基础上推导)。关于写入操作,也会在 Buffer Pool 中操作,然后 MySQL 会在系统空闲时异步将其持久化到磁盘,否则频繁的磁盘 IO 会影响系统性能。

Buffer Pool 简介

配置缓冲池大小

在 MySQL 服务端可以通过 innodb_buffer_pool_size 配置项全局配置 Buffer Pool 的大小,其默认值是 128M(配置):

1.jpeg

你可以通过配置文件、启动命令配置其大小,从 MySQL 5.7.5 版本开始,还可以在运行时通过全局设置动态配置它的值:

SET GLOBAL innodb_buffer_pool_size=1073741824; # 1GB

innodb_buffer_pool_size 配置值一般可以按照如下公式设置:

系统可用内存 - 系统正常运行内存 - (峰值时的连接数 * 每个连接需要的内存)

在可用的内存区间内,自然是越大越好,这样,MySQL 交互可以尽可能在内存中完成,减少了不必要磁盘 IO,整体性能会更好。

查看 Buffer Pool 信息

你可以在命令行运行如下命令查看 InnoDB 存储引擎的状态信息:

SHOW ENGINE INNODB STATUS\G

其中也包含了 Buffer Pool 信息:

1.jpeg

内存结构

Buffer Pool 申请的内存空间是连续的,这样操作起来更高效,并且这些内存空间由控制块和缓存页组成,每个缓存页存放的都是从磁盘加载的数据页,其大小和数据页一样都是固定的 16 KB,控制块存放的是该数据页所属的表空间、页号、对应缓存页在 Buffer Pool 中的地址等信息,所以控制块和缓存页是一一对应的,这些控制块的大小也是固定的 808B,在 Buffer Pool 中,所有控制块都存放在前面,缓存页存放在后面,所以对应的内存结构如下所示:

1.jpeg

注:按照 innodb_buffer_pool_size 申请的内存空间仅仅是用于存放缓存页的,不包括控制块,所以 Buffer Pool 的大小总是会大于 innodb_buffer_pool_size 配置值。

MySQL 服务端在启动时申请完 Buffer Pool 之后就按照固定大小分配好了所有的控制块和缓存页的内存空间,只不过这些缓存页开始都是空闲的,只有从磁盘加载数据页时才会将其填充到某个空闲的缓存页,那么这个填充又是怎么实现的呢?系统怎么知道哪些缓存页是空闲的呢?

Buffer Pool 底层操作

缓存页装载原理

这个时候,缓存页对应的控制块就发挥作用了。

原来,MySQL 会将所有的空闲缓存页对应控制块存放到一个称之为空闲(free)链表的地方,在 MySQL 刚启动的时候,所有的缓存页都是空闲的,因此所有的控制块都会添加到这个空闲链表,以后每次从磁盘加载数据页到 Buffer Pool,就从这个空闲链表中取一个空闲的缓存页装载,并且把缓存页对应的控制块信息填上(表空间、页号等),然后从空闲链接移除这个缓存页对应的控制块节点即可。

那 MySQL 底层又是如何得知某个数据页已经被加载到 Buffer Pool,而不必从磁盘文件加载呢?原来,当 MySQL 从磁盘文件加载数据页到缓存页之后,就会以表空间 + 页号为 key,缓存页地址为 value 维护一个哈希表,下次需要加载数据页时,先根据这个 key 从哈希表查询对应 value 是否存在,如果存在,则从 Buffer Pool 读取数据,否则从磁盘空间加载,重复上述步骤。

缓存页刷新算法

由于 Buffer Pool 大小是有限的,因此可以装载的缓存页数量也是有限的,但数据库存放的数据表记录是一直在增长的,总有一天会超过 Buffer Pool 的大小,这个时候,和 Redis 之类的缓存系统一样,就面临着旧的缓存页被新加载的数据页覆盖的问题,MySQL 底层是如何实现这个刷新策略的呢?

和缓存系统类似,MySQL 底层通过 LRU(Least Recently Used,最近最少使用)算法淘汰老的缓存页 —— 在底层维护了一个 LRU 链表:将新加载的缓存页放到链表头部,每次某个缓存页被读取,也将其移动到链表头部,这样一来,最近不常使用的缓存页就自然下沉到链表尾部了。当 Buffer Pool 没有剩余空间存放新加载的数据页时,覆盖链表尾部的缓存页即可。

不过由于 MySQL 为了优化性能,提供了预读机制,所以导致这个 LRU 链表实现起来更复杂一些:将热数据放到了一个名为 young 的区域,将冷数据放到了一个名为 old 的区域,把 LRU 链表一分为二,young 区域位于链表头部,old 区域位于链表尾部,预读的数据页会先放到 old 区域的头部,这样,就不会影响 LRU 算法淘汰缓存命中率低的缓存页的机制。

缓存页更新同步

为了提升系统性能,对 MySQL 表记录的更新操作(包括插入、修改、删除)也是在已加载到 Buffer Pool 中的缓存页中完成的(不然直接写入磁盘性能也太差了),更新后的缓存页就和磁盘上的数据页数据不一致了,这样的缓存页被称之为脏页。每次更新缓存页后,MySQL 底层并不会立即把修改同步到磁盘上,而是在系统空闲时进行异步更新,从而提升系统性能,具体细节我们后面介绍 MySQL 数据写入原理的时候再详细介绍,这里先了解即可。

那 MySQL 又是如何知道哪些缓存页被更新过,哪些没有被更新,从而异步同步到磁盘文件呢?原来,MySQL 底层会将所有修改过的缓存页存放到刷新(flush)链表,刷新链表和空闲链表的结构一样,MySQL 会读取这个链表的节点进行磁盘同步操作。

0

评论区