Innodb为了加快对磁盘中数据的操作,在操作磁盘上的数据时,会先把数据存放到一块名为Buffer Pool的内存缓冲池中(缓冲池以页为单位进行缓存,页大小为16K)。
由于受到机器限制,内存的大小远小于磁盘的大小,因此需要一种机制来淘汰非热点数据,保证内存中存在的数据是较为频繁访问的数据。
其中LRU是这种管理场景下最常用的算法,LRU算法的思想为:
- 新数据插入到链表头部;
- 每当缓存命中(即缓存数据被访问),则将数据移到链表头部;
- 当链表满的时候,将链表尾部的数据丢弃。
这样就能够保证热点数据位于LRU的头部,而非热点数据随着其他数据的加入最终在链表尾部被丢弃。在实现该算法时一般会针对程序的特性对算法进行修改。
那么在实现LRU的过程中Innodb有以下需要注意的地方:
-
当数据库针对一张大表进行全表查询的时候,按照朴素的LRU算法,所有的数据都会被放入LRU链表中。从而将其他查询语句留下的高频数据挤到LRU尾部并丢弃。
-
LRU中为了避免对磁盘的多次读取而实现了预读机制,预读机制会额外读取一些可能会被读取的页。
- 线性预读:当一个区中有连续56个页面(56为默认值,一个区64页)被加载到BufferPool中,会将这个区中的所有页面都加载到BufferPool中。
- 随机预读:当一个区中随机13个页面(13为默认值)被加载到BufferPool中,会将这个区中所有页面都加载到BufferPool中。随机预读默认是关闭,由变量
innodb_random_read_ahead
控制。
按照朴素的LRU算法,这些数据都会被放入LRU链表中。从而将其他查询语句留下的高频数据挤到LRU尾部并丢弃。
如果以上产生的数据仅仅在这次查询中需要,并不是活跃的热点数据,那就会导致缓存命中率下降。
为了解决以上提到的缺点,InndoDB使用了以下两个策略:
- 将LRU链表划分为old区与young区,young区位于链表的头部,负责存放经常被访问的高频数据,而old区位于链表尾部,负责存放不经常访问的热点数据。而两个区的交汇点称为midpoint。Innodb中使用
innodb_old_blocks_pct
来划分两个区,该参数表示old区的比例。刚进来的数据存放在midpoint位置的old区域。 - 划分区域后需要一个策略来判断数据何时从old区进入young区。Innodb定义了参数
innodb_old_blocks_time
,单位为毫秒。当old区中某个页的最后查询时间跟第一次查询时间差超过该参数,该页就会从old区进入young区。这个策略避免了只查询一次或只在短暂的时间查询多次的数据进入young区,防止热数据被刷出内存。
有了以上策略,就能清楚知道Innodb如何使用LRU列表管理读取的页:
-
扫描过程中,若需要新插入数据页,首先从Free列表中查询是否有可用的空闲页。
- 有可用空闲页:将该页从Free列表中删除,放入到LRU列表中。
- 无可用空闲页:淘汰LRU列表末尾的页,将该内存空间分配给新的页。
将新的页插入到midpoint位置的old区域。
-
扫描过程中,若需要更新数据页,根据参数
innodb_old_blocks_time
判断是否将old区数据更新到young区。- 最后查询时间跟第一次查询时间差超过该参数,则说明多次访问该数据,则将其从old区加入young区,该操作称为page made young。
- 最后查询时间跟第一次查询时间差没有超过该参数,比如只查询了一次,或者一次查询该页的多个数据。这些操作的时间极短且频率低。不会将其加入young区,该操作称为page not made young。
通过命令SHOW ENGINE INNODB STATUS可以观察LRU列表的使用情况。
Databases pages:表示LRU列表中页的数量。
pages made young:表示LRU列表中页移动到young区的次数。
not young:表示LRU列表中页没有移动到young区的次数。
youngs/s 、non-youngs/s:表示了每秒这两类操作的次数。
Buffer pool hit rate:表示缓冲池的命中率。