MySQL Buffer Pool
1. 介绍
虽然MySQL的数据存储在磁盘中,但磁盘相比内存的读写性能还是太差了,所以MySQL设计了 缓冲池(Buffer Pool) 来优化这一部分:
- 读取数据时,如果数据存在Buffer Pool中则直接返回,否则再去磁盘中读取
- 修改数据时,首先修改Buffer Poll所在的页,并将其设为脏页,最后由后台线程将脏页写入磁盘
2. Buffer Pool缓存
2.1 Buffer Pool
InnoDB会把存储的数据划分为若干个页,以页为磁盘和内存交互的基本单位,默认大小为16KB。因此Buffer Pool同样需要按页来划分。MySQL启动后InnoDB会为Buffer Pool分配一块连续的内存空间(默认是128MB,可以通过innodb_buffer_pool_size
进行调整),一般设置为物理内存的60%-80% 并按照16KB的大小划分成缓存页。
Buffer Pool缓存的内容如下:
- 数据页
- 索引页
- 插入缓存页
- undo页
- 自适应哈希索引
- 锁信息
2.2 控制块
InnoDB为每一个缓存页都创建了一个控制块,并防止在Buffer Pool最前面,包含了缓存页的表空间、页号、地址、链表节点等信息
2.3 Free链表 & Flash链表
- Free链表
为了能快速的找到空闲的缓存页,InnoDB还使用了Free链表,其节点维护了空闲缓存页的控制块,头节点维护了头、尾和数量信息。每次从磁盘中加载一个页到Buffer Pool的时都会从Free链表中获取并移除空闲缓存页。
- Flash链表
InnoDB的后台会定时遍历Flush链表寻找脏页并写入磁盘,为了能快速的找到脏页,InnoDB也设计了Flash链表,和Free链表的结构相同,只不过节点维护的是脏页的控制块。
2.4 LRU链表
缓存的最终目的是减少磁盘IO的次数,所以缓存应该尽量保留访问率高的数据,淘汰访问率低的。
常见的算法是LRU(Least Recently Used),即维护LRU链表,将最近访问的节点放在链表头部,这样如果有新的节点需要加入的时候,先删除末尾的节点再将新节点放在头部即可,但简单的LRU并不能解决预读失败和Buffer Pool污染问题。
1. 预读机制
因为MySQL的数据是落在连续空间的页上的,所以靠近一块空间的数据在接下来也有很大概率被访问到,于是在加载数据页时,也会将临近的数据页一并加载进来。当提前加载的数据没有被访问时,则称为预读失败。 在LRU算法中,显然会把预读失败的页也放在链表前面,这样就大大降低了缓存的命中率
为了解决这个问题,MySQL将LRU划分了old和young两个区域,其比例默认是young:old = 63:37
,将预读的页先放在old头部,等到实际读的时候再放到young的头部,解决这个问题
2. Buffer Pool污染问题
当一个SQL语句扫描大量数据的时候,很可能将Buffer Pool所有的数据都替换了,导致热数据的缓存失效,这就是Buffer Pool污染。
注意,只要是扫描的范围大就会导致这个问题,如下面的语句会导致索引失效,进而进行全表扫描,将所有的记录都放到young头部。
select * from staff where name like "%xxx%";
为了解决这个问题,MySQL为进入到 young 区域条件增加了一个停留在 old 区域的时间判断,只有后续访问超过第一次进入old一定时间间隔(默认1000ms)才会进入young
此外,为了降低数据移动到头部的次数,young区域前1/4的数据在被访问的时候并不会发生移动。
2.5 脏页刷盘机制
为了降低磁盘的io次数,脏页按照以下规则写入磁盘:
- redo log日志已满
- Buffer Pool空间不足,如果淘汰的是脏页则先写入
- MySQL认为空闲时,会定期写入适当的脏页
- MySQL正常关闭前,写入全部脏页
当脏页刷盘时,会给数据库带来一定的性能开销,导致部分SQL的耗时较长,如果过于频繁可以调大Buffer Pool空间或减少redo log日志的大小,减少1、2条触发的概率
MySQL异常退出时,因为MySQL的机制时Write Ahead Log,所以会先写入日志,因此不需要担心数据消失
3. Change Buffer
从Buffer Pool可以知道,数据在写入时如果在Buffer Pool中有缓存则会先写成脏页,再找机会进行刷盘。对于非缓存的数据,MySQL也设计类Change Buffer这块区域,优化了其中非唯一索引的写入效率。
- 当数据不在缓冲池中时,对于唯一索引页数据
- 因为需要比较当前的索引是否已经存在,所以必须进行磁盘IO,将数据加载至缓存
- 更新缓存数据并设置为脏页
- 写入redo Log
- 对于非唯一索引数据:
- 操作直接写入Change Buffer中
- 等待合适时机写入Buffer Pool(如修改的数据被查询,加载到了缓存池)
- 写入redo Log