本篇已收录在 MySQL 是怎样运行的 学习笔记系列html
缓存的重要性
InnoDB存储引擎在处理客户端的请求时,当须要访问某个页的数据时,就会把完整的页的数据所有加载到内存中,也就是说即便咱们只须要访问一个页的一条记录,那也须要先把整个页的数据加载到内存中。将整个页加载到内存中后就能够进行读写访问了,在进行完读写访问以后并不着急把该页对应的内存空间释放掉,而是将其缓存起来,这样未来有请求再次访问该页面时,就能够省去磁盘IO的开销了。mysql
Buffer Pool
为了缓存磁盘中的页,在MySQL服务器启动的时候就向操做系统申请了一片连续的内存,他们给这片内存起了个名,叫作Buffer Pool(中文名是缓冲池)。sql
默认状况下Buffer Pool只有128M大小。缓存
能够在启动服务器的时候配置innodb_buffer_pool_size参数的值,它表示Buffer Pool的大小,就像这样:服务器
[server]
innodb_buffer_pool_size = 268435456
其中,268435456的单位是字节,也就是我指定Buffer Pool的大小为256M。须要注意的是,Buffer Pool也不能过小,最小值为5M(当小于该值时会自动设置成5M)。
dom
Buffer Pool内部组成
Buffer Pool中默认的缓存页大小和在磁盘上默认的页大小是同样的,都是16KB。学习
为了更好的管理这些在Buffer Pool中的缓存页,mysql 为每个缓存页都建立了一些所谓的控制信息, 包括该页所属的表空间编号、页号、缓存页在Buffer Pool中的地址、链表节点信息、一些锁信息以及LSN信息spa
控制块和缓存页是一一对应的,它们都被存放到 Buffer Pool 中,其中控制块被存放到 Buffer Pool 的前边,缓存页被存放到 Buffer Pool 后边,因此整个Buffer Pool对应的内存空间看起来就是这样的:操作系统
free 链表
当咱们最初启动MySQL服务器的时候,须要完成对Buffer Pool的初始化过程,就是先向操做系统申请Buffer Pool的内存空间,而后把它划分红若干对控制块和缓存页。可是此时并无真实的磁盘页被缓存到Buffer Pool中(由于尚未用到),以后随着程序的运行,会不断的有磁盘上的页被缓存到Buffer Pool中。code
从磁盘上读取一个页到Buffer Pool中的时候该放到哪一个缓存页的位置呢?或者说怎么区分Buffer Pool中哪些缓存页是空闲的,哪些已经被使用了呢?咱们最好在某个地方记录一下Buffer Pool中哪些缓存页是可用的,这个时候缓存页对应的控制块就派上大用场了,咱们能够把全部空闲的缓存页对应的控制块做为一个节点放到一个链表中,这个链表也能够被称做free链表(或者说空闲链表)。
从图中能够看出,咱们为了管理好这个free链表,特地为这个链表定义了一个基节点,里边儿包含着链表的头节点地址,尾节点地址,以及当前链表中节点的数量等信息。这里须要注意的是,链表的基节点占用的内存空间并不包含在为Buffer Pool申请的一大片连续内存空间以内,而是单独申请的一块内存空间。
缓存页的哈希处理
前边说过,当咱们须要访问某个页中的数据时,就会把该页从磁盘加载到Buffer Pool中,若是该页已经在Buffer Pool中的话直接使用就能够了。那么问题也就来了,咱们怎么知道该页在不在Buffer Pool中呢?难不成须要依次遍历Buffer Pool中各个缓存页么?一个Buffer Pool中的缓存页这么多都遍历完岂不是要累死?
咱们能够用表空间号 + 页号做为key,缓存页做为value建立一个哈希表,在须要访问某个页的数据时,先从哈希表中根据表空间号 + 页号看看有没有对应的缓存页,若是有,直接使用该缓存页就好,若是没有,那就从free链表中选一个空闲的缓存页,而后把磁盘中对应的页加载到该缓存页的位置。
flush链表的管理
若是咱们修改了Buffer Pool中某个缓存页的数据,那它就和磁盘上的页不一致了,这样的缓存页也被称为脏页(英文名:dirty page)。
每次修改缓存页后,咱们并不着急当即把修改同步到磁盘上,而是在将来的某个时间点进行同步, 再建立一个存储脏页的链表,凡是修改过的缓存页对应的控制块都会做为一个节点加入到一个链表中,由于这个链表节点对应的缓存页都是须要被刷新到磁盘上的,因此也叫flush链表。链表的构造和free链表差很少,假设某个时间点Buffer Pool中的脏页数量为n,那么对应的flush链表就长这样:
LRU链表的管理
Buffer Pool对应的内存大小毕竟是有限的,若是须要缓存的页占用的内存大小超过了Buffer Pool大小, 就须要把某些旧的缓存页从Buffer Pool中移除,而后再把新的页放进来. 当Buffer Pool中再也不有空闲的缓存页时,就须要淘汰掉部分最近不多使用的缓存页。
咱们能够再建立一个链表,因为这个链表是为了按照最近最少使用的原则去淘汰缓存页的,因此这个链表能够被称为LRU链表(LRU的英文全称:Least Recently Used)。当咱们须要访问某个页时,能够这样处理LRU链表:
若是该页不在Buffer Pool中,在把该页从磁盘加载到Buffer Pool中的缓存页时,就把该缓存页对应的控制块做为节点塞到链表的头部。 若是该页已经缓存在Buffer Pool中,则直接把该页对应的控制块移动到LRU链表的头部。
只要咱们使用到某个缓存页,就把该缓存页调整到LRU链表的头部,这样LRU链表尾部就是最近最少使用的缓存页
划分区域的LRU链表
上边的这个简单的LRU链表用了没多长时间就发现问题了,由于存在这两种比较尴尬的状况:
状况一:InnoDB 提供了一个预读功能(英文名:read ahead)。所谓预读,就是InnoDB认为执行当前的请求可能以后会读取某些页面,就预先把它们加载到Buffer Pool中。预读原本是个好事儿,若是预读到Buffer Pool中的页成功的被使用到,那就能够极大的提升语句执行的效率。但是若是用不到呢?这些预读的页都会放到LRU链表的头部,可是若是此时Buffer Pool的容量不太大并且不少预读的页面都没有用到的话,这就会致使处在LRU链表尾部的一些缓存页会很快的被淘汰掉,也就是所谓的劣币驱逐良币,会大大下降缓存命中率。
状况二:有的小伙伴可能会写一些须要扫描全表的查询语句(好比没有创建合适的索引或者压根儿没有WHERE子句的查询)。意味着将访问到该表所在的全部页!假设这个表中记录很是多的话,那该表会占用特别多的页,当须要访问这些页时,会把它们通通都加载到Buffer Pool中, 这严重的影响到其余查询对 Buffer Pool的使用,从而大大下降了缓存命中率。
由于有这两种状况的存在,因此 InnoDB 把这个LRU链表按照必定比例分红两截,分别是:
一部分存储使用频率很是高的缓存页,因此这一部分链表也叫作热数据,或者称young区域。 另外一部分存储使用频率不是很高的缓存页,因此这一部分链表也叫作冷数据,或者称old区域。
查看Buffer Pool的状态信息
SHOW ENGINE INNODB STATUS
mysql> SHOW ENGINE INNODB STATUS\G (...省略前边的许多状态) ---------------------- BUFFER POOL AND MEMORY ---------------------- Total memory allocated 13218349056; Dictionary memory allocated 4014231 Buffer pool size 786432 Free buffers 8174 Database pages 710576 Old database pages 262143 Modified db pages 124941 Pending reads 0 Pending writes: LRU 0, flush list 0, single page 0 Pages made young 6195930012, not young 78247510485 108.18 youngs/s, 226.15 non-youngs/s Pages read 2748866728, created 29217873, written 4845680877 160.77 reads/s, 3.80 creates/s, 190.16 writes/s Buffer pool hit rate 956 / 1000, young-making rate 30 / 1000 not 605 / 1000 Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s LRU len: 710576, unzip_LRU len: 118 I/O sum[134264]:cur[144], unzip sum[16]:cur[0] -------------- (...省略后边的许多状态)