MySQL InnoDB 的 Buffer Pool
虽然 InnoDB 是基于磁盘的存储引擎,但是磁盘交互的速度还是太慢了,不可能每次都去读取磁盘。因此我们需要有缓存,在 MySQL 中,Buffer Pool 就是一片连续的内存空间,用来缓存页的数据。默认 128M,也可以自己设置。
Buffer Pool 的空间主要可以分为两部分,一部分是缓存的页的数据,每个缓存页的大小都是16KB,一部分是控制信息,如图:
当 MySQL 从磁盘中读取了一个页的时候,这个页就会被放入 Buffer Pool 中缓存起来。MySQL 为 Buffer Pool 维护了一个 free 链表(链表的基节点是单独申请的内存空间),用于存放空闲的缓存页的控制块的信息。这样当需要将磁盘上的页放入缓存页时,就可以从 free 链表中找到空闲的缓存页,缓存数据,然后把该缓存页对应的节点从free链表中移除。
那么当需要读取一个页时,MySQL 如何知道这个页是否已经在 Buffer Pool 中被缓存了呢?定位一个页,用表空间号+页号就可以了,因此,可以用一个哈希表来进行定位。
由于现实情况下往往都是多线程环境,对 Buffer Pool 的一些操作需要加锁,单一的 Buffer Pool 可能会影响速度,因此在 Buffer Pool 特别大的时候(至少超过 1G),我们可以把它们拆分成若干个小的Buffer Pool,每个 Buffer Pool 都称为一个实例,它们都是独立的,独立的去申请内存空间、管理各种链表,所以在多线程并发访问时并不会相互影响,从而提高并发处理能力。
LRU 链表
Buffer Pool 的大小终究是有限的,如果已经没有空闲的缓存页了,而这时又从磁盘加载了新的页,必然需要将旧的缓存页移除,那么如何决定移除哪些缓存页呢?Buffer Pool的最终目的还是提高缓存命中率,因此这里采用的算法就是LRU(选择最近最少使用的缓存页进行移除),LRU的算法就不介绍了。
虽然旧的缓存页的淘汰策略有了,但是如果仅仅使用最简单的LRU,还是会遇到一些问题,比如预读机制导致缓存的页不一定会用到但又淘汰了已经缓存的页;全表扫描时会导致淘汰掉访问频率非常高的页。因此,MySQL 的 Buffer Pool 其实分成了两部分,一部分叫做 young 区域,用于存放热点数据,一部分叫做 old 区域,存储访问频率不是很高的页;两者的比例取决于系统变量innodb_old_blocks_pct
。
从磁盘中加载的页首先会放到 old 区域中(放到 old 区域的头部),如果超过了一定的时间间隔(系统变量 innodb_old_blocks_time
),又再次对这个页进行访问,那么这个页就会被移到young区域的头部(这样能有效避免全表扫描时访问频率不高的页被加入 young 区域)。
当然,对于这个LRU链表,还有很多优化策略,比如为了不频繁的移动 young 区域的节点,只有在访问的页面位于young区域的1/4的后边时,才将其移动到young区域头部,从而提升性能。还有一些其他的优化策略就不一一介绍了。
Flush 链表
如果修改了某个缓存页的数据,那么它就和磁盘上的数据不一致了,这样的缓存页叫做 dirty page,我们需要在特定的时机将 dirty page 的改动同步到磁盘上(频繁的立即同步会严重影响性能因为磁盘很慢)。MySQL 会通过一个 Flush 链表来维护 dirty page 的信息,来记录哪些页是需要将来同步到磁盘的。
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。主要有两种刷新路径:
- 扫描 LRU 链表,发现 dirty page 就刷新到磁盘
- 从 flush 链表中刷新一部分页面到磁盘
如果后台刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到 Buffer Pool 时没有可用的缓存页,这时就会尝试看看 LRU 链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将LRU链表尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。
如果你对这篇内容有疑问,欢迎到本站社区发帖提问 参与讨论,获取更多帮助,或者扫码二维码加入 Web 技术交流群。
上一篇: MySQL 索引
绑定邮箱获取回复消息
由于您还没有绑定你的真实邮箱,如果其他用户或者作者回复了您的评论,将不能在第一时间通知您!
发布评论