我们都知道 MySQL 有一个特性就是持久化储存到磁盘中 我们存进去就要取出来这也是MySQL的速度为什么比不上Redis。但是MySQL 并不是完全就摆烂每次都进行 Select update 的时候都重新去磁盘IO 这样MySQL肯定会更慢 速度快 和 持久化 MySQL 表示我都要 但是两者都要肯定没有单方的极限速度 持久就放磁盘 速度就放内存 鱼和熊掌兼得 MySQL是怎么去做的 MySQL配合InnoDB 是怎么通过磁盘和内存的配合达到持久化 但是也能保证一定的查询速度 那就是今天的主角 InnoDB 良心之作 -----> Buffer pool 缓存池
Buffer pool 是 InnerDB 存储引擎的一个重要组件,MySQL 的所有 CRUD 操作都是围绕 Buffer pool 进行的。现在只知道 Buffer pool 是一个缓冲池,里面存放了磁盘数据的缓存,那么 Buffer pool 是一个什么样的结构,是如何在 SQL 执行过程中起作用的呢?
如果对于MySQL 有一定了解的朋友应该明白 我们所添加的数据 是放在磁盘里面的 ,我们查询数据 或者修改数据的时候 修改数据的手也需要先将数据从磁盘中将数据取出 读写磁盘速度是很慢的 因为要进行IO操作,尤其和内存比起来更是没的说。但是,我们平时在执行 SQL 时,无论写操作还是读操作都能很快得到结果,并没有预想中的那么慢。
有些朋友会说 因为我加了索引 索引查询速度会比较快,有索引当然快了。但是有一点,索引文件也是存储在磁盘上的,查找过程会产生磁盘 I/O。如果同时对某行数据进行多次操作,那岂不是要重复产生很多次磁盘 IO 吗?
可能你想到了,那我把数据存在内存里不就可以了吗?内存速度比磁盘快,这准没毛病。没错,那该怎么存呢?我们接着往下看
如果没有过多的去关注数据库相关知识的同学可能不是很了解这个东西 但是 看到缓冲池有感觉有点熟悉 接下来就让我们一起学习 OR 复习一下 什么是缓冲池 作用是什么
首先这个 缓冲池 是属于Innodb的 不是属于MySQL的 这个不要搞混了 我们先大概了解一下 Buffer pool 的作用
缓冲池(buffer pool)是一种降低磁盘访问的机制;
在专用服务器上面 通常会将百分之八十的内存分配给缓冲池
缓冲池通常以 ·页(page)· 为单位缓存数据;
缓冲池的常见管理算法是LRU,memcache,OS,InnoDB都使用了这种算法;
InnoDB对普通LRU进行了优化:
根据自己的理解画的图 如果有不对的地方请大家指正 这只是拆分的一部分图 一次放出来我是看不懂

通过将页数据从磁盘中复制到buffer pool 里面 利用 buffer pool 的高吞吐进行数据处理 最终返回数据
数据库启动时,会根据配置的 Buffer pool 大小申请一块合适的内存空间,作为 Buffer pool 的内存区域,之后会按照缓存页大小和元数据大小把 Buffer pool 内存区域划分为缓存页和元数据。只是初始化时缓存页都是空的,在执行 CRUD 时会把数据页加载到缓存页中。

buffer pool 用到页次数比较少的话会被淘汰掉 但是有一个问题就是 我下图这种情况

如果我空白页 和 占用页不是连续的怎么办 我是乱插入吗 这个肯定不是的 buffer pool 除啦有数组之外还有三个链表 其中一个是free链表就是去解决这个问题

实际上元数据中保存了一对双向指针,指针 free_pre 指向当前元数据的前一个元数据地址,指针 free_next 指向当前元数据的后一个元数据地址。

有了 free 链表结构之后,怎么把数据页读取到缓存页?这时就可以从 free 链表中获取一个元数据找到对应的缓存页,然后把数据读到缓存页就可以了,随后把 free 中的这个元数据移除。可是元数据是 MySQL 初始化时创建的,直接删掉?那对应的缓存页岂不是没有元数据了?实际上所谓的删除就是让当前元数据的前后元数据不要再引用自己了,那么这个元数据也就从链表中移除了。
以三个元数据为例,free 链表初始状态每相邻的两个元数据都相互形成了“环”,并且链表的头尾地址保存在基础节点中。此时链表中存在三个节点两个环。

当链表尾部的元数据被使用,那么被使用元数据的前一个元数据不再引用它的地址,而且基础节点保存的尾部节点地址也向前移动到前一个元数据地址。此时链表中只存在两个节点一个“环”。

当链表头部的元数据被使用,那么被使用元数据的后一个元数据不再引用它的地址,而且基础节点保存的头部节点地址也向后移动到后一个元数据地址,此时链表中只存在一个节点零个“环。

当链表的所有元数据都被使用后,链表将不存在了?不,所谓的链表只是元数据中的前后指针形成的。空闲的元素据用完后,元数据并不会实际删除,只是从链表移除而已。当使用完元数据后,元数据将重新添加到链表,只不过把它的指针交给它的前后节点就行了,这相当于删除的逆向操作。

数据库会存在一个哈希表的结构,会用表空间号+数据页号,作为一个 key,然后缓存页地址作为 value。当使用这个数据页时,就可以通过 key 去查找数据页是否已经缓存,防止数据页被重复加载。

基于 free 链表找到一块空闲的缓存页写入数据后,然后更新了这个缓存页,此时缓存页中的数据就与磁盘中的数据页不一致了,那么这个缓存页就是脏数据或者说脏页。最终在内存里更新的这些脏页是会被刷入磁盘的,但是不可能所有的缓存页都刷入磁盘,因为有些缓存页根本没有更新过。所以需要一个数据结构来保存这些被修改过的数据页
首先明白几个问题
内存碎片
BP划分完全部缓存页和描述信息块后,还剩点内存,但却再也放不下新的缓存页。
DB在BP中划分缓存页时,会让所有缓存页和描述信息块都紧密挨一起,尽可能减少内存碎片。
脏数据页
增删改时,若发现数据页没缓存,就会从free链表找空闲缓存页,读取到BP的缓存页,但若已缓存,则下次直接使用缓存页。
所以你要更新的数据页都会在BP缓存页,让你能在内存中直接执行增删改。所以肯定会更新BP缓存页数据,一旦更新了,则缓存页数据和磁盘的数据页数据,就不一致了,这时的BP缓存页就是脏数据,即为脏页。
最终这些在内存里更新的脏页数据,都是要被刷回磁盘文件的。
但不可能所有缓存页都刷盘,因为有的缓存页可能因查询而被读取到BP,可能根本没修改过!
于是DB引入flush链表,类似free链表,通过缓存页的描述信息块的两个指针,让被修改过的缓存页的描述信息块组成双向链表。
被修改过的缓存页,都会将其描述信息块加入到flush链表中去,flush就是这些都是脏页,后续都是要flush刷新到磁盘

现在我知道数据页要加载到缓存,需要通过 free 链表找到一个空闲的缓存页,然后把数据写入缓存页。但是缓存页的数量是有限的,当缓存页用尽了该咋办呢?应该通过一定的机制把一些缓存页刷回磁盘,空闲一些缓存页出来。那么哪些缓存页需要被刷入磁盘呢?当然是那些不经常使用的缓存页给刷入磁盘啦,这时就需要引入 LRU 链表。
与 free 链表类似,LRU 链表也是一个双向链表。元数据从 free 链表中取出,然后对应的缓存页被写入数据,同时元数据加入 LRU 链表。而且只要缓存页被访问(包括查询和修改)就会被移动到链表头部,这样当缓存页不足时,把链表尾部的缓存页刷入磁盘就可以空闲出缓存页了。

缓存预读
当数据页加载到缓存时会连带着把相邻的数据页一同加载到缓存,而不用每次读数据时,都从磁盘加载到缓存,以减少 IO 次数。但是这样一来,很可能预读的缓存根本没有用到,却又占用了缓存页并且处于 LRU 链表头部,而 LRU 尾部的位置可能又使用的频繁。那么恰巧这时缓存页不足需要淘汰时,就会把尾部经常使用的淘汰掉,而预加载的从未使用的却保留了下来。这显然是不合理的。触发预读机制的参数:
innodb_read_ahead_threshold: 默认为 56,如果顺序访问一个数据区的数据页超过这个阈值就会触发预读机制,把下一个数据区的所有数据页都加载到缓存。
innodb_random_read_ahead: 默认为 OFF 关闭的,如果一个数据区连续 13 个数据页被访问,此时会触发预读机制,把这个数据区的所有数据页加载到缓存。

全表扫描
全表扫描会把表中的数据全部加载到缓存,加入到 LRU 链表的头部,而全表扫描的这些数据可能只会使用一次,但是链表尾部的那些缓存页可能经常使用。淘汰缓存页时就会把那些经常使用的缓存页淘汰掉,而全表扫描的缓存页却保留了下来。

数据冷热分离
为了解决预加载和全表扫描带来的问题,MySQL 设计了数据冷热分离的 LRU。也就是说实际上 LRU 链表并不是一个单纯的链表,它分为了热数据区和冷数据区,使用 innodb_old_blocks_pct 参数来控制冷数据区占整个链表的比例,默认为 37 。那么数据页首次加载到缓存时,实际上处于冷数据区的缓存页头部。
MySQL 有个参数 innodb_old_blocks_time 默认值为 1000,在 1000 ms 后再次访问冷数据区头部的缓存页时就会被移动到热数据区的头部了,并不是立即访问就会移动。
所以这时预加载和全表扫描加载的缓存页会被放在冷数据区,而热数据区的缓存页只要被访问就会一直在热数据区,也就不会导致频繁访问的缓存页被淘汰了。
基于这样 LUR 冷热分离机制,MySQL 就会优先淘汰冷数据区尾部的缓存页。

感觉上有点像JVM模型 新生代 老年代的感觉

再就是 MySQL 对这套机制进行了优化。按照现有规则会把热数据区访问的数据页移动到头部,如果说热数据区的缓存页使用的极为频繁,那么这种移动将是极为损耗性能的,而且也没有必要。所以对这个规则优化后,只有在热数据区的后面 3/4 的缓存页被访问后会移动到热数据区头部,前面的 1/4 部分不会发生移动,这样就尽可能的减少了链表中的节点移动了。
LRU 尾部的缓存页是如何淘汰刷入磁盘的?首先并不是缓存页耗尽时才会把 LRU 冷数据区尾部的缓存页淘汰刷入磁盘的。MySQL 会有一个定时线程去扫描,每隔一段时间把一些缓存页写入磁盘,留出空闲的缓存页,从 flush 链表移除,加入 free 链表。
而热数据区也有可能存在被修改的缓存页,这个定时线程会在适当的时机把 flush 中的缓存页都刷入磁盘。被刷入磁盘的缓存页会从 flush 链表和 LRU 链表移除。
如果实在没有空闲的缓存页,此时又有新的数据页要加载到缓存,那么会从 LRU 链表的冷数据区尾部淘汰刷盘,空闲出缓存页。


默认情况如下:
缓冲池的 3/8 专用于 旧子列表
列表的分割是 新列表的尾部 与旧列表的头部边界
当Innodb 复制页到缓存池的时候,它会初始化的将它插入到中间点 也就是旧列表的头部 一页可以被读取是因为这是用户启动的操作 如上面说到的 SQL 查询:select * from student where id = 7 或者是InnodDB 预读操作的一部分
访问旧子列表中的页会使这个页变得年轻,该页会被移动到新子列表的头部。移动的触发时机取决于页被读取的原因(用户启动的操作或者1noDB的预读操作)。如果页读取是由于用户启动的操作而引起的,第一次访问页后会立即将页移动到新子列表的头部。如果页读取是InnoDB预读操作引起的,第一次访问页后不会立即发生移动页的操作,或者直到该页被驱逐都不会发生,
随着数据库的运行,缓冲池中未被访问的页会通过向列表尾部移动来来“老化”(即被驱逐的概率加大)。新旧子列表的页会随着其他页的更新而被老化。旧子列表的页也会随着插到中间点的页而老化。最终,一个未使用的页面到达旧子列表的尾部并被驱逐。

理想地,你可以根据实际情况将缓冲池的大小设置得尽可能大,从而留出足够多的内存给服务器上的其他进程运行,而不会出现过多的分页。缓冲池越大,nnoDB就越像一个内存数据库,从磁盘读取数据一次,后续的读取期间从内存访问数据
在具有足够内存的64位系统上,您可以将缓冲池拆分成多个部分,以减少并发操作之间对内存结构的争用。详情见配置多个缓冲池实例。
你可以将经常访问的数据保留在内存中,而不管操作的活动突然激增,这些操作会将大量不常用的数据带入缓冲池。详情见使缓冲池扫描具有抵抗性
你可以控制如何以及何时执行预读取请求来将分页异步地预取到缓冲池中,以应对即将到来的需求。详情见配置 InnoDB缓冲池预取(预读) 你可以控制后台何时刷新以及是否根据工作负载动态调整刷新速率。详情见配置缓冲池刷新 你可以配置1noDB如何保存当前缓冲池的状态来避免服务器重启后长时间的预热。详情见保存和恢复缓冲池的状态
缓冲池很大程度减少了磁盘 I/O 带来的开销,通过将操作的数据行所在的数据页加载到缓冲池可以提高 SQL 的执行速度。
为了减少磁盘 I/O,Innodb 通过在缓冲池中提前读取多个数据页来进行优化,这种方式叫作预读。
上一篇:黑马学Docker(二)
下一篇:手把手带你搭建个人博客系统(二)