前言用户对数据库的最基本要求就是能高效的读取和存储数据但是读写数据都涉及到与低速的设备交互为了弥补两者之间的速度差异所有数据库都有缓存池用来管理相应的数据页提高数据库的效率当然也因为引入了这一中间层数据库对内存的管理变得相对比较复杂。本文主要分析MySQL Buffer Pool的相关技术以及实现原理源码基于阿里云RDS MySQL 5.6分支其中部分特性已经开源到AliSQL。Buffer Pool相关的源代码在buf目录下主要包括LRU ListFlu ListDouble write buffer, 预读预写Buffer Pool预热压缩页内存管理等模块包括头文件和IC文件一共两万行代码。基础知识***Buffer Pool Instance: *** 大小等于innodb_buffer_pool_size/innodb_buffer_pool_instances每个instance都有自己的锁信号量物理块(Buffer chunks)以及逻辑链表(下面的各种List)即各个instance之间没有竞争关系可以并发读取与写入。所有instance的物理块(Buffer chunks)在数据库启动的时候被分配直到数据库关闭内存才予以释放。当innodb_buffer_pool_size小于1GB时候innodb_buffer_pool_instances被重置为1主要是防止有太多小的instance从而导致性能问题。每个Buffer Pool Instance有一个page hash链表通过它使用space_id和page_no就能快速找到已经被读入内存的数据页而不用线性遍历LRU List去查找。注意这个hash表不是InnoDB的自适应哈希自适应哈希是为了减少Btree的扫描而page hash是为了避免扫描LRU List。数据页InnoDB中数据管理的最小单位为页默认是16KB页中除了存储用户数据还可以存储控制信息的数据。InnoDB IO子系统的读写最小单位也是页。如果对表进行了压缩则对应的数据页称为压缩页如果需要从压缩页中读取数据则压缩页需要先解压形成解压页解压页为16KB。压缩页的大小是在建表的时候指定目前支持16K8K4K2K1K。即使压缩页大小设为16K在blob/varchar/text的类型中也有一定好处。假设指定的压缩页大小为4K如果有个数据页无法被压缩到4K以下则需要做B-tree分裂操作这是一个比较耗时的操作。正常情况下Buffer Pool中会把压缩和解压页都缓存起来当Free List不够时按照系统当前的实际负载来决定淘汰策略。如果系统瓶颈在IO上则只驱逐解压页压缩页依然在Buffer Pool中否则解压页和压缩页都被驱逐。***Buffer Chunks: *** 包括两部分数据页和数据页对应的控制体控制体中有指针指向数据页。Buffer Chunks是最低层的物理块在启动阶段从操作系统申请直到数据库关闭才释放。通过遍历chunks可以访问几乎所有的数据页有两种状态的数据页除外没有被解压的压缩页(BUF_BLOCK_ZIP_PAGE)以及被修改过且解压页已经被驱逐的压缩页(BUF_BLOCK_ZIP_DIRTY)。此外数据页里面不一定都存的是用户数据开始是控制信息比如行锁自适应哈希等。***逻辑链表: *** 链表节点是数据页的控制体(控制体中有指针指向真正的数据页)链表中的所有节点都有同一的属性引入其的目的是方便管理。下面其中链表都是逻辑链表。***Free List: *** 其上的节点都是未被使用的节点如果需要从数据库中分配新的数据页直接从上获取即可。InnoDB需要保证Free List有足够的节点提供给用户线程用否则需要从FLU List或者LRU List淘汰一定的节点。InnoDB初始化后Buffer Chunks中的所有数据页都被加入到Free List表示所有节点都可用。***LRU List: *** 这个是InnoDB中最重要的链表。所有新读取进来的数据页都被放在上面。链表按照最近最少使用算法排序最近最少使用的节点被放在链表末尾如果Free List里面没有节点了就会从中淘汰末尾的节点。LRU List还包含没有被解压的压缩页这些压缩页刚从磁盘读取出来还没来的及被解压。LRU List被分为两部分默认前5/8为young list存储经常被使用的热点page后3/8为old list。新读入的page默认被加在old list头只有满足一定条件后才被移到young list上主要是为了预读的数据页和全表扫描污染buffer pool。***FLU List: *** 这个链表中的所有节点都是脏页也就是说这些数据页都被修改过但是还没来得及被刷新到磁盘上。在FLU List上的页面一定在LRU List上但是反之则不成立。一个数据页可能会在不同的时刻被修改多次在数据页上记录了最老(也就是第一次)的一次修改的lsn即oldest_modification。不同数据页有不同的oldest_modificationFLU List中的节点按照oldest_modification排序链表尾是最小的也就是最早被修改的数据页当需要从FLU List中淘汰页面时候从链表尾部开始淘汰。加入FLU List需要使用flush_list_mutex保护所以能保证FLU List中节点的顺序。***Quick List: *** 这个链表是阿里云RDS MySQL 5.6加入的使用带Hint的SQL查询语句可以把所有这个查询的用到的数据页加入到Quick List中一旦这个语句结束就把这个数据页淘汰主要作用是避免LRU List被全表扫描污染。***Unzip LRU List: *** 这个链表中存储的数据页都是解压页也就是说这个数据页是从一个压缩页通过解压而来的。***Zip Clean List: *** 这个链表只在Debug模式下有主要是存储没有被解压的压缩页。这些压缩页刚刚从磁盘读取出来还没来的及被解压一旦被解压后就从此链表中删除然后加入到Unzip LRU List中。***Zip Free: *** 压缩页有不同的大小比如8K4KInnoDB使用了类似内存管理的伙伴系统来管理压缩页。Zip Free可以理解为由5个链表构成的一个二维数组每个链表分别存储了对应大小的内存碎片例如8K的链表里存储的都是8K的碎片如果新读入一个8K的页面首先从这个链表中查找如果有则直接返回如果没有则从16K的链表中分裂出两个8K的块一个被使用另外一个放入8K链表中。核心数据结构InnoDB Buffer Pool有三种核心的数据结构buf_pool_tbuf_block_tbuf_page_t。***but_pool_t: *** 存储Buffer Pool Instance级别的控制信息例如整个Buffer Pool Instance的mutexinstance_no, page_hashold_list_pointer等。还存储了各种逻辑链表的链表根节点。Zip Free这个二维数组也在其中。***buf_block_t: *** 这个就是数据页的控制体用来描述数据页部分的信息(大部分信息在buf_page_t中)。buf_block_t中第一字段就是buf_page_t这个不是随意放的是必须放在第一字段因为只有这样buf_block_t和buf_page_t两种类型的指针可以相互转换。第二个字段是frame字段指向真正存数据的数据页。buf_block_t还存储了Unzip LRU List链表的根节点。另外一个比较重要的字段就是block级别的mutex。***buf_page_t: *** 这个可以理解为另外一个数据页的控制体大部分的数据页信息存在其中例如space_id, page_no, page state, newest_modificationoldest_modificationaccess_time以及压缩页的所有信息等。压缩页的信息包括压缩页的大小压缩页的数据指针(真正的压缩页数据是存储在由伙伴系统分配的数据页上)。这里需要注意一点如果某个压缩页被解压了解压页的数据指针是存储在buf_block_t的frame字段里。这里介绍一下buf_page_t中的state字段这个字段主要用来表示当前页的状态。一共有八种状态。这八种状态对初学者可能比较难理解尤其是前三种如果看不懂可以先跳过。***BUF_BLOCK_POOL_WATCH: *** 这种类型的page是提供给purge线程用的。InnoDB为了实现多版本需要把之前的数据记录在undo log中如果没有读请求再需要它就可以通过purge线程删除。换句话说purge线程需要知道某些数据页是否被读取现在解法就是首先查看page hash看看这个数据页是否已经被读入如果没有读入则获取(启动时候通过malloc分配不在Buffer Chunks中)一个BUF_BLOCK_POOL_WATCH类型的哨兵数据页控制体同时加入page_hash但是没有真正的数据(buf_blokc_t::frame为空)并把其类型置为BUF_BLOCK_ZIP_PAGE(表示已经被使用了其他purge线程就不会用到这个控制体了)相关函数buf_pool_watch_set如果查看page hash后发现有这个数据页只需要判断控制体在内存中的地址是否属于Buffer Chunks即可如果是表示对应数据页已经被其他线程读入了相关函数buf_pool_watch_occurred。另一方面如果用户线程需要这个数据页先查看page hash看看是否是BUF_BLOCK_POOL_WATCH类型的数据页如果是则回收这个BUF_BLOCK_POOL_WATCH类型的数据页从Free List中(即在Buffer Chunks中)分配一个空闲的控制体填入数据。这里的核心思想就是通过控制体在内存中的地址来确定数据页是否还在被使用。***BUF_BLOCK_ZIP_PAGE: ***当压缩页从磁盘读取出来的时候先通过malloc分配一个临时的buf_page_t然后从伙伴系统中分配出压缩页存储的空间把磁盘中读取的压缩数据存入然后把这个临时的buf_page_t标记为BUF_BLOCK_ZIP_PAGE状态(buf_page_init_for_read)只有当这个压缩页被解压了state字段才会被修改为BUF_BLOCK_FILE_PAGE并加入LRU List和Unzip LRU List(buf_page_get_gen)。如果一个压缩页对应的解压页被驱逐了但是需要保留这个压缩页且压缩页不是脏页则这个压缩页被标记为BUF_BLOCK_ZIP_PAGE(buf_LRU_free_page)。所以正常情况下处于BUF_BLOCK_ZIP_PAGE状态的不会很多。前述两种被标记为BUF_BLOCK_ZIP_PAGE的压缩页都在LRU List中。另外一个用法是从BUF_BLOCK_POOL_WATCH类型节点中如果被某个purge线程使用了也会被标记为BUF_BLOCK_ZIP_PAGE。***BUF_BLOCK_ZIP_DIRTY: *** 如果一个压缩页对应的解压页被驱逐了但是需要保留这个压缩页且压缩页是脏页则被标记为BUF_BLOCK_ZIP_DIRTY(buf_LRU_free_page)如果该压缩页又被解压了则状态会变为BUF_BLOCK_FILE_PAGE。因此BUF_BLOCK_ZIP_DIRTY也是一个比较短暂的状态。这种类型的数据页都在Flush List中。***BUF_BLOCK_NOT_USED: *** 当链表处于Free List中状态就为此状态。是一个能长期存在的状态。***BUF_BLOCK_READY_FOR_USE: *** 当从Free List中获取一个空闲的数据页时状态会从BUF_BLOCK_NOT_USED变为BUF_BLOCK_READY_FOR_USE(buf_LRU_get_free_block)也是一个比较短暂的状态。处于这个状态的数据页不处于任何逻辑链表中。***BUF_BLOCK_FILE_PAGE: *** 正常被使用的数据页都是这种状态。LRU List中大部分数据页都是这种状态。压缩页被解压后状态也会变成BUF_BLOCK_FILE_PAGE。***BUF_BLOCK_MEMORY: *** Buffer Pool中的数据页不仅可以存储用户数据也可以存储一些系统信息例如InnoDB行锁自适应哈希索引以及压缩页的数据等这些数据页被标记为BUF_BLOCK_MEMORY。处于这个状态的数据页不处于任何逻辑链表中***BUF_BLOCK_REMOVE_HASH: *** 当加入Free List之前需要先把page hash移除。因此这种状态就表示此页面page hash已经被移除但是还没被加入到Free List中是一个比较短暂的状态。总体来说大部分数据页都处于BUF_BLOCK_NOT_USED(全部在Free List中)和BUF_BLOCK_FILE_PAGE(大部分处于LRU List中LRU List中还包含除被purge线程标记的BUF_BLOCK_ZIP_PAGE状态的数据页)状态少部分处于BUF_BLOCK_MEMORY状态极少处于其他状态。前三种状态的数据页都不在Buffer Chunks上对应的控制体都是临时分配的InnoDB把他们列为invalid state(buf_block_state_valid)。如果理解了这八种状态以及其之间的转换关系那么阅读Buffer pool的代码细节就会更加游刃有余。接下来简单介绍一下buf_page_t中buf_fix_count和io_fix两个变量这两个变量主要用来做并发控制减少mutex加锁的范围。当从buffer pool读取一个数据页时候会其加读锁然后递增buf_page_t::buf_fix_count同时设置buf_page_t::io_fix为BUF_IO_READ然后即可以释放读锁。后续如果其他线程在驱逐数据页(或者刷脏)的时候需要先检查一下这两个变量如果buf_page_t::buf_fix_count不为零且buf_page_t::io_fix不为BUF_IO_NONE则不允许驱逐(buf_page_can_relocate)。这里的技巧主要是为了减少数据页控制体上mutex的争抢而对数据页的内容读取的时候依然要加读锁修改时加写锁。Buffer Pool内存初始化Buffer Pool的内存初始化主要是Buffer Chunks的内存初始化buffer pool instance一个一个轮流初始化。核心函数为buf_chunk_init和os_mem_alloc_large。阅读代码可以发现目前从操作系统分配内存有两种方式一种是通过HugeTLB的方式来分配另外一种使用传统的mmap来分配。*HugeTLB: *** 这是一种大内存块的分配管理技术。类似数据库对数据的管理内存也按照页来管理默认的页大小为4KBHugeTLB就是把页大小提高到2M或者更加多。程序传送给cpu都是虚拟内存地址cpu必须通过快表来映射到真正的物理内存地址。快表的全集放在内存中部分热点内存页可以放在cpu cache中从而提高内存访问效率。假设cpu cache为100KB每条快表占用1KB页大小为4KB则热点内存页为100KB/1KB100条覆盖1004KB400KB的内存数据但是如果也默认页大小为2M则同样大小的cpu cache可以覆盖1002M200MB的内存数据也就是说访问200MB的数据只需要一次读取内存即可(如果映射关系没有在cache中找到则需要先把映射关系从内存中读到cache然后查找最后再去读内存中需要的数据会造成两次访问物理内存)。也就是说使用HugeTLB这种大内存技术可以提高快表的命中率从而提高访问内存的性能。当然这个技术也不是银弹内存页变大了也必定会导致更多的页内的碎片。如果需要从swap分区中加载虚拟内存也会变慢。当然最终要的理由是4KB大小的内存页已经被业界稳定使用很多年了如果没有特殊的需求不需要冒这个风险。在InnoDB中如果需要用到这项技术可以使用super-large-pages参数启动MySQL。mmap分配在Linux下多个进程需要共享一片内存可以使用mmap来分配和绑定所以只提供给一个MySQL进程使用也是可以的。用mmap分配的内存都是虚存在top命令中占用VIRT这一列而不是RES这一列只有相应的内存被真正使用到了才会被统计到RES中提高内存使用率。这样是为什么常常看到MySQL一启动就被分配了很多的VIRT而RES却是慢慢涨上来的原因。这里大家可能有个疑问为啥不用malloc。其实查阅malloc文档可以发现当请求的内存数量大于MMAP_THRESHOLD(默认为128KB)时候malloc底层就是调用了mmap。在InnoDB中默认使用mmap来分配。分配完了内存buf_chunk_init函数中把这片内存划分为两个部分前一部分是数据页控制体(buf_block_t)在阿里云RDS MySQL 5.6 release版本中每个buf_block_t是424字节一共有innodb_buffer_pool_size/UNIV_PAGE_SIZE个。后一部分是真正的数据页按照UNIV_PAGE_SIZE分隔。假设page大小为16KB则数据页控制体占的内存:数据页约等于1:38.6也就是说如果innodb_buffer_pool_size被配置为40G则需要额外的1G多空间来存数据页的控制体。划分完空间后遍历数据页控制体设置buf_block_t::frame指针指向真正的数据页然后把这些数据页加入到Free List中即可。初始化完Buffer Chunks的内存还需要初始化BUF_BLOCK_POOL_WATCH类型的数据页控制块page hash的结构体zip hash的结构体(所有被压缩页的伙伴系统分配走的数据页面会加入到这个哈希表中)。注意这些内存是额外分配的不包含在Buffer Chunks中。除了buf_pool_init外建议读者参考一下but_pool_free这个内存释放函数加深对Buffer Pool相关内存的理解。Buf_page_get函数解析这个函数极其重要是其他模块获取数据页的外部接口函数。如果请求的数据页已经在Buffer Pool中了修改相应信息后就直接返回对应数据页指针如果Buffer Pool中没有相关数据页则从磁盘中读取。Buf_page_get是一个宏定义真正的函数为buf_page_get_gen参数主要为space_id, page_no, lock_type, mode以及mtr。这里主要介绍一个mode这个参数其表示读取的方式目前支持六种前三种用的比较多。***BUF_GET: *** 默认获取数据页的方式如果数据页不在Buffer Pool中则从磁盘读取如果已经在Buffer Pool中需要判断是否要把他加入到young list中以及判断是否需要进行线性预读。如果是读取则加读锁修改则加写锁。***BUF_GET_IF_IN_POOL: *** 只在Buffer Pool中查找这个数据页如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则直接返回空。加锁方式与BUF_GET类似。***BUF_PEEK_IF_IN_POOL: *** 与BUF_GET_IF_IN_POOL类似只是即使条件满足也不把它加入到young list中也不进行线性预读。加锁方式与BUF_GET类似。***BUF_GET_NO_LATCH: *** 不管对数据页是读取还是修改都不加锁。其他方面与BUF_GET类似。***BUF_GET_IF_IN_POOL_OR_WATCH: *** 只在Buffer Pool中查找这个数据页如果在则判断是否要把它加入到young list中以及判断是否需要进行线性预读。如果不在则设置watch。加锁方式与BUF_GET类似。这个是要是给purge线程用。***BUF_GET_POSSIBLY_FREED: *** 这个mode与BUF_GET类似只是允许相应的数据页在函数执行过程中被释放主要用在估算Btree两个slot之前的数据行数。接下来我们简要分析一下这个函数的主要逻辑。首先通过buf_pool_get函数依据space_id和page_no查找指定的数据页在那个Buffer Pool Instance里面。算法很简单instance_no (space_id 20 space_id page_no 6) % instance_num也就是说先通过space_id和page_no算出一个fold value然后按照instance的个数取余数即可。这里有个小细节page_no的第六位被砍掉这是为了保证一个extent的数据能被缓存到同一个Buffer Pool Instance中便于后面的预读操作。接着调用buf_page_hash_get_low函数在page hash中查找这个数据页是否已经被加载到对应的Buffer Pool Instance中如果没有找到这个数据页且mode为BUF_GET_IF_IN_POOL_OR_WATCH则设置watch数据页(buf_pool_watch_set)接下来如果没有找到数据页且mode为BUF_GET_IF_IN_POOL、BUF_PEEK_IF_IN_POOL或者BUF_GET_IF_IN_POOL_OR_WATCH函数直接返回空表示没有找到数据页。如果没有找到数据但是mode为其他就从磁盘中同步读取(buf_read_page)。在读取磁盘数据之前我们如果发现需要读取的是非压缩页则先从Free List中获取空闲的数据页如果Free List中已经没有了则需要通过刷脏来释放数据页这里的一些细节我们后续在LRU模块再分析获取到空闲的数据页后加入到LRU List中(buf_page_init_for_read)。在读取磁盘数据之前我们如果发现需要读取的是压缩页则临时分配一个buf_page_t用来做控制体通过伙伴系统分配到压缩页存数据的空间最后同样加入到LRU List中(buf_page_init_for_read)。做完这些后我们就调用IO子系统的接口同步读取页面数据如果读取数据失败我们重试100次(BUF_PAGE_READ_MAX_RETRIES)然后触发断言如果成功则判断是否要进行随机预读(随机预读相关的细节我们也在预读预写模块分析)。接着读取数据成功后我们需要判断读取的数据页是不是压缩页如果是的话因为从磁盘中读取的压缩页的控制体是临时分配的所以需要重新分配block(buf_LRU_get_free_block)把临时分配的buf_page_t给释放掉用buf_relocate函数替换掉接着进行解压解压成功后设置state为BUF_BLOCK_FILE_PAGE最后加入Unzip LRU List中。接着我们判断这个页是否是第一次访问如果是则设置buf_page_t::access_time如果不是我们则判断其是不是在Quick List中如果在Quick List中且当前事务不是加过Hint语句的事务则需要把这个数据页从Quick List删除因为这个页面被其他的语句访问到了不应该在Quick List中了。接着如果mode不为BUF_PEEK_IF_IN_POOL我们需要判断是否把这个数据页移到young list中具体细节在后面LRU模块中分析。接着如果mode不为BUF_GET_NO_LATCH我们给数据页加上读写锁。最后如果mode不为BUF_PEEK_IF_IN_POOL且这个数据页是第一次访问则判断是否需要进行线性预读(线性预读相关的细节我们也在预读预写模块分析)。LRU List中young list和old list的维护当LRU List链表大于512(BUF_LRU_OLD_MIN_LEN)时在逻辑上被分为两部分前面部分存储最热的数据页这部分链表称作young list后面部分则存储冷数据页这部分称作old list一旦Free List中没有页面了就会从冷页面中驱逐。两部分的长度由参数innodb_old_blocks_pct控制。每次加入或者驱逐一个数据页后都要调整young list和old list的长度(buf_LRU_old_adjust_len)同时引入BUF_LRU_OLD_TOLERANCE来防止链表调整过频繁。当LRU List链表小于512则只有old list。新读取进来的页面默认被放在old list头在经过innodb_old_blocks_time后如果再次被访问了就挪到young list头上。一个数据页被读入Buffer Pool后在小于innodb_old_blocks_time的时间内被访问了很多次之后就不再被访问了这样的数据页也很快被驱逐。这个设计认为这种数据页是不健康的应该被驱逐。此外如果一个数据页已经处于young list当它再次被访问的时候不会无条件的移动到young list头上只有当其处于young list长度的1/4(大约值)之后才会被移动到young list头部这样做的目的是减少对LRU List的修改否则每访问一个数据页就要修改链表一次效率会很低因为LRU List的根本目的是保证经常被访问的数据页不会被驱逐出去因此只需要保证这些热点数据页在头部一个可控的范围内即可。相关逻辑可以参考函数buf_page_peek_if_too_old。buf_LRU_get_free_block函数解析这个函数以及其调用的函数可以说是整个LRU模块最重要的函数在整个Buffer Pool模块中也有举足轻重的作用。如果能把这几个函数吃透相信其他函数很容易就能读懂。首先如果是使用ENGINE_NO_CACHE发送过来的SQL需要读取数据则优先从Quick List中获取(buf_quick_lru_get_free)。接着统计Free List和LRU List的长度如果发现他们再Buffer Chunks占用太少的空间则表示太多的空间被行锁自使用哈希等内部结构给占用了一般这些都是大事务导致的。这时候会给出报警。接着查看Free List中是否还有空闲的数据页(buf_LRU_get_free_only)如果有则直接返回否则进入下一步。大多数情况下这一步都能找到空闲的数据页。如果Free List中已经没有空闲的数据页了则会尝试驱逐LRU List末尾的数据页。如果系统有压缩页情况就有点复杂InnoDB会调用buf_LRU_evict_from_unzip_LRU来决定是否驱逐压缩页如果Unzip LRU List大于LRU List的十分之一或者当前InnoDB IO压力比较大则会优先从Unzip LRU List中把解压页给驱逐否则会从LRU List中把解压页和压缩页同时驱逐。不管走哪条路径最后都调用了函数buf_LRU_free_page来执行驱逐操作这个函数由于要处理压缩页解压页各种情况极其复杂。大致的流程首先判断是否是脏页如果是则不驱逐否则从LRU List中把链表删除必要的话还从Unzip LRU List移走这个数据页(buf_LRU_block_remove_hashed)接着如果我们选择保留压缩页则需要重新创建一个压缩页控制体插入LRU List中如果是脏的压缩页还要插入到Flush List中最后才把删除的数据页插入到Free List中(buf_LRU_block_free_hashed_page)。如果在上一步中没有找到空闲的数据页则需要刷脏了(buf_flush_single_page_from_LRU)由于buf_LRU_get_free_block这个函数是在用户线程中调用的所以即使要刷脏这里也是刷一个脏页防止刷过多的脏页阻塞用户线程。如果上一步的刷脏因为数据页被其他线程读取而不能刷脏则重新跳转到上述第二步。进行第二轮迭代与第一轮迭代的区别是第一轮迭代在扫描LRU List时最多只扫描innodb_lru_scan_depth个而在第二轮迭代开始扫描整个LRU List。如果很不幸这一轮还是没有找到空闲的数据页从三轮迭代开始在刷脏前等待10ms。最终找到一个空闲页后page的state为BUF_BLOCK_READY_FOR_USE。控制全表扫描不增加cache数据到Buffer Pool全表扫描对Buffer Pool的影响比较大即使有old list作用但是old list默认也占Buffer Pool的3/8。因此阿里云RDS引入新的语法ENGINE_NO_CACHE(例如SELECT ENGINE_NO_CACHE count(*) FROM t1)。如果一个SQL语句中带了ENGINE_NO_CACHE这个关键字则由它读入内存的数取据页都放入Quick List中当这个语句结束时会删除它独占的数据页。同时引入两个参数。innodb_rds_trx_own_block_max这个参数控制使用Hint的每个事物最多能拥有多少个数据页如果超过这个数据就开始驱逐自己已有的数据页防止大事务占用过多的数据页。innodb_rds_quick_lru_limit_per_instance这个参数控制每个Buffer Pool Instance中Quick List的长度如果超过这个长度后续的请求都从Quick List中驱逐数据页进而获取空闲数据页。删除指定表空间所有的数据页函数(buf_LRU_remove_pages)提供了三种模式第一种(BUF_REMOVE_ALL_NO_WRITE)删除Buffer Pool中所有这个类型的数据页(LRU List和Flush List)同时Flush List中的数据页也不写回数据文件这种适合rename table和5.6表空间传输新特性因为space_id可能会被复用所以需要清除内存中的一切防止后续读取到错误的数据。第二种(BUF_REMOVE_FLUSH_NO_WRITE)仅仅删除Flush List中的数据页同时Flush List中的数据页也不写回数据文件这种适合drop table即使LRU List中还有数据页但由于不会被访问到所以会随着时间的推移而被驱逐出去。第三种(BUF_REMOVE_FLUSH_WRITE)不删除任何链表中的数据仅仅把Flush List中的脏页都刷回磁盘这种适合表空间关闭例如数据库正常关闭的时候调用。这里还有一点值得一提的是由于对逻辑链表的变动需要加锁且删除指定表空间数据页这个操作是一个大操作容易造成其他请求被饿死所以InnoDB做了一个小小的优化每删除BUF_LRU_DROP_SEARCH_SIZE个数据页(默认为1024)就会释放一下Buffer Pool Instance的mutex便于其他线程执行。LRU_Manager_Thread这是一个系统线程随着InnoDB启动而启动作用是定期清理出空闲的数据页(数量为innodb_LRU_scan_depth)并加入到Free List中防止用户线程去做同步刷脏影响效率。线程每隔一定时间去做BUF_FLUSH_LRU即首先尝试从LRU中驱逐部分数据页如果不够则进行刷脏从Flush List中驱逐(buf_flush_LRU_tail)。线程执行的频率通过以下策略计算我们设定max_free_len innodb_LRU_scan_depth * innodb_buf_pool_instances如果Free List中的数量小于max_free_len的1%则sleep time为零表示这个时候空闲页太少了需要一直执行buf_flush_LRU_tail从而腾出空闲的数据页。如果Free List中的数量介于max_free_len的1%-5%则sleep time减少50ms(默认为1000ms)如果Free List中的数量介于max_free_len的5%-20%则sleep time不变如果Free List中的数量大于max_free_len的20%则sleep time增加50ms但是最大值不超过rds_cleaner_max_lru_time。这是一个自适应的算法保证在大压力下有足够用的空闲数据页(lru_manager_adapt_sleep_time)。Hazard Pointer