面试系列-buffer pool缓冲池及相关链表

2022-10-27 15:54:04 浏览数 (2)

buffer pool

在对数据库执行增删改操作的时候,实际上主要都是针对内存里的Buffer Pool中的数据进行的,也

就是你实际上主要是对数据库的内存里的数据结构进行了增删改;Buffer Pool默认情况下是128MB,实际生产环境下完全可以对Buffer Pool进行调整;

代码语言:javascript复制
innodb_buffer_pool_size = 2147483648(分配2G)
数据页:MySQL中抽象出来的数据单位

MySQL对数据抽象出来了一个数据页的概念,数据页的大小是16KB,他是把很多行数据放在了一个数据页里,也就是说我们的磁盘文件中就是会有很多的数据页,每一页数据里放了很多行数据;设我们要更新一行数据,此时数据库会找到这行数据所在的数据页,然后从磁盘文件里把这行数据所在的数据页直接给加载到Buffer Pool里去(缓存页)也就是说,Buffer Pool中存放的是一个一个的数据页;

Buffer Pool中的描述数据大概相当于缓存页大小的5%左右,也就是每个描述数据大概是800个字节左右的大小,然后假设你设置的buffer pool大小是128MB,实际上Buffer Pool真正的最终大小会超出一些,可能有个130多MB的样子,因为他里面还要存放每个缓存页的描述数据。

数据库启动的时候初始化Buffer Pool

数据库只要一启动,就会按照你设置的Buffer Pool大小,稍微再加大一点,去找操作系统申请一块内存区域,作为Buffer Pool的内存区域。然后当内存区域申请完毕之后,数据库就会按照默认的缓存页的16KB的大小以及对应的800个字节左右的描述数据的大小,在Buffer Pool中划分出来一个一个的缓存页和一个一个的他们对应的描述数据。然后当数据库把Buffer Pool划分完毕。只不过这个时候,Buffer Pool中的一个一个的缓存页都是空的,里面什么都没有,要等数据库运行起来之后,当我们要对数据执行增删改查的操作的时候,才会把数据对应的页从磁盘文件里读取出来,放入Buffer Pool中的缓存页中。默认情况下磁盘上的数据页和缓存页是一 一对应起来的,都是16KB,一个数据页对应一个缓存页。

free链表

双向链表数据结构,这个free链表里,每个节点就是一个空闲的缓存页的描述数据块的地址,也就是说,只要你一个缓存页是空闲的,那么他的描述数据块就会被放入这个free链表中。刚开始数据库启动的时候,可能所有的缓存页都是空闲的,因为此时可能是一个空的数据库,一条数据都没有,所以此时所有缓存页的描述数据块,都会被放入这个free链表中;

free链表里面就是各个缓存页的描述数据块,只要缓存页是空闲的,那么他们对应的描述数据块就会加入到这个free链表中,每个节点都会双向链接自己的前后节点,组成一个双向链表。除此之外,这个free链表有一个基础节点,他会引用链表的头节点和尾节点,里面还存储了链表中有多少个描述数据块的节点,也就是有多少个空闲的缓存页。

free链表,他本身其实就是由Buffer Pool里的描述数据块组成的,你可以认为是每个描述数据块里都有两个指针,一个是free_pre,一个是free_next,分别指向自己的上一个free链表的节点,以及下一个free链表的节点。通过Buffer Pool中的描述数据块的free_pre和free_next两个指针,就可以把所有的描述数据块串成一个free链表,大家可以自己去思考一下这个问题。上面为了画图需要,所以把描述数据块单独画了一份出来,表示他们之间的指针引用关系。对于free链表而言,只有一个基础节点是不属于Buffer Pool的,他是40字节大小的一个节点,里面就存放了free链表的头节点的地址,尾节点的地址,还有free链表里当前有多少个节点。

数据加载的过程:首先,需要从free链表里获取一个描述数据块,然后就可以对应的获取到这个描述数据块对应的空闲缓存页,接着我们就可以把磁盘上的数据页读取到对应的缓存页里去,同时把相关的一些描述数据写入缓存页的描述数据块里去,比如这个数据页所属的表空间之类的信息,最后把那个描述数据块从free链表里去除就可以了;

怎么知道数据页有没有被缓存

数据库还会有一个哈希表数据结构,他会用表空间号 数据页号,作为一个key,然后缓存页的地址作为value,当要使用一个数据页的时候,通过“表空间号 数据页号”作为key去这个哈希表里查一下,如果没有就读取数据页,如果已经有了,就说明数据页已经被缓存了。也就是说,每次读取一个数据页到缓存之后,都会在这个哈希表中写入一个key-value对,key就是表空间号 数据页号,value就是缓存页的地址,那么下次如果你再使用这个数据页,就可以从哈希表里直接读取出来他已经被放入一个缓存页了。

flush链表

flush链表本质也是通过缓存页的描述数据块中的两个指针,让被修改过的缓存页的描述数据块,组成一个双向链表。凡是被修改过的缓存页,都会把他的描述数据块加入到flush链表中去,flush的意思就是这些都是脏页,后续都是要flush刷新到磁盘上去的;

Buffer Poo基于LRU算法淘汰部分缓存

淘汰缓存页,把一个缓存页里被修改过的数据,给他刷到磁盘上的数据页里去,然后这个缓存页就可以清空了,

让他重新变成一个空闲的缓存页。接着你再把磁盘上你需要的新的数据页加载到这个腾出来的空闲缓存页中去。

缓存命中率结合LRU链表,从磁盘加载一个数据页到缓存页的时候,就把这个缓存页的描述数据块放到LRU链表头部去,那么只要有数据的缓存页,他都会在LRU里了,而且最近被加载数据的缓存页,都会放到LRU链表的头部去;某个缓存页的描述数据块本来在LRU链表的尾部,后续你只要查询或者修改了这个缓存页的数据,也要把这个缓存页挪动到LRU链表的头部去,也就是说最近被访问过的缓存页,一定在LRU链表的头部,当缓存页没有一个空闲的时候,要找出来那个最近最少被访问的缓存页去刷入磁盘,此时就直接在LRU链表的尾部找到一个缓存页,他一定是最近最少被访问的那个缓存页,然后就把LRU链表尾部的那个缓存页刷入磁盘中,然后把需要的磁盘数据页加载到腾出来的空闲缓存页中就可以了;

LRU带来的隐患:

代码语言:javascript复制
# MYSQL预读机制
      MySQL 在从磁盘加载数据的的时候,会将数据页的相邻的其他的数据页也加载到缓存中。
# MySQL为什么要这么做  
      因为根据经验和习惯,一般查询数据的时候往往还会查询该数据相邻前后的一些数据,有人可能会反问:
      一个数据页上面不是就会存在该条数据相邻的数据吗?这可不一定,某条数据可能很大,
      也可能这条数据是在数据页在头部,也可能是在数据页的尾部,所以 MySQL 为了提高效率,
      会将某个数据页的相邻的数据页也加载到缓存池中。
触发:
1. innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个
数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制,
把下一个相邻区中的所有数据页都加载到缓
存里去
2. 全表扫描
基于冷热数据分离的LRU链表

第一次被加载了数据的缓存页,都会不停的移动到冷数据区域的链表头部;

innodb_old_blocks_time参数,默认值1000,也就是1000毫秒,必须是一个数据页被加载到缓存页之后,在1s之后,你访问这个缓存页,他才会被挪动到热数据区域的链表头部去。因为假设你加载了一个数据页到缓存去,然后过了1s之后你还访问了这个缓存页,说明你后续很可能会经常要访问它,这个时间限制就是1s,因此只有1s后你访问了这个缓存页,他才会给你把缓存页放到热数据区域的链表头部去;

请求需要访问的数据就在热数据区,不是直接把该数据所在的缓存页对应的描述数据转移到热数据区链表头部,因为热数据区的数据本身就是会被频繁访问的,这样子如果每次访问都去移动链表,势必造成性能的下降(影响再小极端情况下也可能会不可控),所以 MySQL针对热数据区的数据的转移也有相关的规则,如果被访问的数据所在的缓存页在热数据区的前25%,那么该缓存页对应的描述数据是不会被转移到热数据链表的头部的,只有当被访问的缓存页对应的描述数据在热数据区链表的后75%,该缓存页的描述数据才会被转移到热数据链表的头部;

缓存刷盘策略

一个后台线程,会运行一个定时任务,这个定时任务每隔一段时间就会把LRU链表的冷数据区域的尾部的一些缓存页,刷入磁盘里去,清空这几个缓存页,所以实际上在缓存页没用完的时候,可能就会清空一些缓存页了,被清除的缓存页必然会加入到free链表中,从flush链表中移除,从lru链表中移除,lru链表的冷数据区域的缓存页以及flush链表的缓存页,flush链表和lru链表中的缓存页在减少,free链表中的缓存页在增加;若实在没有空闲缓存页,要从磁盘加载数据页到一个空闲缓存页中,此时就会从LRU链表的冷数据区域的尾部找到一个缓存页,一定是最不经常使用的缓存页,然后刷入磁盘和清空,然后把数据页加载到这个腾出来的空闲缓存页里去;

数据库并发多Buffer Pool空间实例的优化

多线程并发访问一个Buffer Pool的时候必然会加锁,然后很多线程可能要串行着排队,一个一个的依次执行自己要执行的操作;

代码语言:javascript复制
innodb_buffer_pool_size = 8589934592  //buffer pool 总8G
innodb_buffer_pool_instances = 4  //切分四个
动态调整Buffer Pool的大小
代码语言:javascript复制
# 什么是chunk机制
chunk是 MySQL 设计的一种机制,这种机制的原理是将Buffer Pool拆分一个一个大小
相等的 chunk 块,每个 chunk 默认大小为 128M(可以通过参数
innodb_buffer_pool_chunk_size 来调整大小),也就是说Buffer Pool是由一个个
的chunk组成的

假设 Buffer Pool 大小是2GB,而一个chunk大小默认是128M,
也就是说一个2GB大小的 Buffer Pool 里面由16个 chunk 组成,每个chunk中有自己
的缓存页和描述数据,而 free 链表、flush 链表和 lru 链表是共享的

假设现在 Buffer Pool 有 2GB,里面有16个chunk,现在想要扩大到 4GB,那么这个时候只需要新申请一个个的 chunk 就可以了。这样不但不需要申请一块很大的连续的空间,更不需要将复制数据。

合理设置Buffer Pool

Buffer Pool 是不是越大越好,理论上是的。那如果一个机器内存是16GB那分配给 Buffer Pool 15GB,这样很显然是不行的,因为操作系统要占内存,你的机器上总会运行其他的进行的吧?那肯定也是需要占用内存的。根据很多实际生产经验得出得比较合理的大小是机器内存大小的(50%~60%)。

代码语言:javascript复制
show engine innodb status;
----------------------
 Buffer Pool  AND MEMORY
----------------------
--  Buffer Pool 的最终大小
Total memory allocated
--  Buffer Pool 一共有多少个缓存页
 Buffer Pool  size
-- free 链表中一共有多少个缓存也是可以使用的
Free buffers        
-- lru链表中一共有多少个缓存页
Database pages 
-- lru链表链表中的冷数据区一共有多少个缓存页
Old database pages  
-- flush链表中的缓存页的数量
Modified db pages     
-- 等待从磁盘上加载进来的缓存页的数量
Pending reads 
-- 即将从lru链表中刷入磁盘的数量,flush链表中即将刷入磁盘的缓存页的数量
Pending writes: LRU 0, flush list 0, single page 0
-- lru链表的冷数据区的缓存页被访问之后转移到热数据区的缓存页的数量,以及冷数据区里1s之内被访问但是没有进入到热数据区的缓存页的数量
Pages made young 260368814, not young 0
-- 每秒从冷数据转移到热数据区的缓存页的数量,以及每秒在冷数据区被访问但是没有进入热数据区的缓存页的数量
332.69 youngs/s, 0.00 non-youngs/s
-- 已经读取创建和写入的缓存页的数量,以及每秒读取、创建和写入的缓存页的数量
Pages read 249280313, created 1075315, written 32924991 359.96 reads/s, 0.02 creates/s, 0.23 writes/s
-- 表示1000次访问中,有多少次是命中了BufferPool缓存中的缓存页,以及每1000次访问有多少数据从冷数据区转移到热数据区,以及没有转移的缓存页的数量
 Buffer Pool  hit rate 867 / 1000, young-making rate 123 / 1000 not 0 / 1000
-- lru链表中缓存页的数量
LRU len: 8190
-- 最近50s读取磁盘页的总数,cur[0]表示现在正在读取的磁盘页的总数
I/O sum[5198]:cur[0]

0 人点赞