MySQL3_3缓存
MySQL中的Cache和Buffer负责记录数据缓存,以提供更高的吞吐率。
查询缓存
缓存失效的情况:
- 如果两个查询请求在任何字符上的不同(例如:空格、注释、大小写),都会导致缓存不会命中
- 如果查询请求中包含某些系统函数、用户自定义变量和函数、一些系统表,如 mysql 、information_schema、 performance_schema 数据库中的表,那这个请求就不会被缓存
- MySQL 的缓存系统会监测涉及到的每张表,只要该表的结构或者数据被修改,如对该表使用了 INSERT、 UPDATE、DELETE、TRUNCATE TABLE、ALTER TABLE、DROP TABLE 或 DROP DATABASE 语句,那使用该表的所有高速缓存查询都将变为无效并从高速缓存中删除。
查询缓存的维护开销大、且容易引起数据一致性问题,因此在 MySQL8.0 中已经被删除。
Buffer Pool
Buffer Pool 的作用原理
加载磁盘页面到 Buffer Pool
当我们需要访问某页中的数据时,需要将该页加载到 Buffer Pool 内。
InnoDB 中还存在两种特殊情况:
- 预读
预先将以后可能用到的页面加载到 Buffer Pool,根据加载方式还细分为线性预读和随机预读:
线性预读:如果顺序访问了某个区(extent)的页面超过innodb_read_ahead_threshold
这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到 Buffer Pool 的请求,注意异步读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。
随机预读:如果 Buffer Pool 中已经缓存了某个区的 13 个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到 Buffer Pool 的请求。
如果预读的页面很常用,则可以极大地提高语句的执行效率,而如果并不常用的话,反而会将 LRU 链表尾部的缓存页被迅速淘汰掉,导致缓存命中率降低。 - 全表扫描
全表扫描会导致大量页统统被加载到 Buffer Pool 内,这些页一般被使用到的频率也不高(全表扫描的执行频率不会太高),导致大大降低缓存命中率。
刷新脏页到磁盘
后台有专门的线程每隔一段时间负责把脏页刷新到磁盘,这样可以不影响用户线程处理正常的请求。
- 从LRU 链表的冷数据中刷新一部分页面到磁盘。
后台线程会定时从 LRU 链表尾部开始扫描一些页面,扫描的页面数量可以通过系统变量innodb_lru_scan_depth
来指定,如果从里边儿发现脏页,会把它们刷新到磁盘。这种刷新页面的方式被称之为BUF_FLUSH_LRU
。 - 从flush 链表中刷新一部分页面到磁盘。
后台线程也会定时从 flush 链表中刷新一部分页面到磁盘,刷新的速率取决于当时系统是不是很繁忙。这种刷新页面的方式被称之为BUF_FLUSH_LIST
。 - 同步刷新
有时候后台线程刷新脏页的进度比较慢,导致用户线程在准备加载一个磁盘页到 Buffer Pool 时没有可用的缓存页,这时就会尝试看看 LRU 链表尾部有没有可以直接释放掉的未修改页面,如果没有的话会不得不将 LRU 链表尾部的一个脏页同步刷新到磁盘(和磁盘交互是很慢的,这会降低处理用户请求的速度)。这种刷新单个页面到磁盘中的刷新方式被称之为BUF_FLUSH_SINGLE_PAGE
。
Buffer Pool 配置
innodb_buffer_pool_size
:Buffer Pool 占用的总内存空间大小,单位是字节。innodb_buffer_pool_instances
:访问 Buffer Pool 中的各种链表的时候都是需要加锁的,如果 Buffer Pool 特别大,且并发访问频率较高的情况下,会发生大量竞争,因此可以分配多个独立的小的 Buffer Pool。
每个实例占用的内存空间为:innodb_buffer_pool_size/innodb_buffer_pool_instances
。
注意当innodb_buffer_pool_size
的值小于 1G 的时候设置多个实例是无效的,InnoDB 会默认把innodb_buffer_pool_instances
的值修改为 1。innodb_buffer_pool_chunk_size
:出于对运行时可以方便调整 Buffer Pool 大小的考虑,每个 Buffer Pool 实例都是由若干 chunk 组成的。
注意:
innodb_buffer_pool_size
必须是innodb_buffer_pool_chunk_size
×innodb_buffer_pool_instances
的倍数- 如果在服务器启动时,innodb_buffer_pool_chunk_size × innodb_buffer_pool_instances 的值已经大于 innodb_buffer_pool_size 的值,那么 innodb_buffer_pool_chunk_size 的值会被服务器自动设置为 innodb_buffer_pool_size/innodb_buffer_pool_instances 的值。
查看 Buffer Pool 状态
1 | SHOW ENGINE INNODB STATUS |
- Total memory allocated:代表 Buffer Pool 向操作系统申请的连续内存空间大小,包括全部控制块、缓存页、以及碎片的大小。
- Dictionary memory allocated:为数据字典信息分配的内存空间大小,注意这个内存空间和 Buffer Pool 没啥关系,不包括在 Total memory allocated 中。
- Buffer pool size:代表该 Buffer Pool 可以容纳多少缓存页,注意,单位是页!
- Free buffers:代表当前 Buffer Pool 还有多少空闲缓存页,也就是 free 链表中还有多少个节点。
- Database pages:代表 LRU 链表中的页的数量,包含 young 和 old 两个区域的节点数量。
- Old database pages:代表 LRU 链表 old 区域的节点数量。
- Modified db pages:代表脏页数量,也就是 flush 链表中节点的数量。
- Pending reads:正在等待从磁盘上加载到 Buffer Pool 中的页面数量。
- 当准备从磁盘中加载某个页面时,会先为这个页面在 Buffer Pool 中分配一个缓存页以及它对应的控制块,然后把这个控制块添加到 LRU 的 old 区域的头部,但是这个时候真正的磁盘页并没有被加载进来,Pending reads 的值会跟着加 1。
- Pending writes LRU:即将从 LRU 链表中刷新到磁盘中的页面数量。
- Pending writes flush list:即将从 flush 链表中刷新到磁盘中的页面数量。
- Pending writes single page:即将以单个页面的形式刷新到磁盘中的页面数量。
- Pages made young:代表 LRU 链表中曾经从 old 区域移动到 young 区域头部的节点数量。
- 这里需要注意,一个节点每次只有从 old 区域移动到 young 区域头部时才会将 Pages made young 的值加 1,也就是说如果该节点本来就在 young 区域,由于它符合在 young 区域 1/4 后边的要求,下一次访问这个页面时也会将它移动到 young 区域头部,但这个过程并不会导致 Pages made young 的值加 1。
- Page made not young:在将 innodb_old_blocks_time 设置的值大于 0 时,首次访问或者后续访问某个处在 old 区域的节点时由于不符合时间间隔的限制而不能将其移动到 young 区域头部时,Page made not young 的值会加 1。
- 这里需要注意,对于处在 young 区域的节点,如果由于它在 young 区域的 1/4 处而导致它没有被移动到 young 区域头部,这样的访问并不会将 Page made not young 的值加 1。
- youngs/s:代表每秒从 old 区域被移动到 young 区域头部的节点数量。
- non-youngs/s:代表每秒由于不满足时间限制而不能从 old 区域移动到 young 区域头部的节点数量。
- Pages read、created、written:代表读取,创建,写入了多少页。后边跟着读取、创建、写入的速率。
- Buffer pool hit rate:表示在过去某段时间,平均访问 1000 次页面,有多少次该页面已经被缓存到 Buffer Pool 了。
- young-making rate:表示在过去某段时间,平均访问 1000 次页面,有多少次访问使页面移动到 young 区域的头部了。
- 需要大家注意的一点是,这里统计的将页面移动到 young 区域的头部次数不仅仅包含从 old 区域移动到 young 区域头部的次数,还包括从 young 区域移动到 young 区域头部的次数(访问某个 young 区域的节点,只要该节点在 young 区域的 1/4 处往后,就会把它移动到 young 区域的头部)。
- not (young-making rate):表示在过去某段时间,平均访问 1000 次页面,有多少次访问没有使页面移动到 young 区域的头部。
- 需要大家注意的一点是,这里统计的没有将页面移动到 young 区域的头部次数不仅仅包含因为设置了 innodb_old_blocks_time 系统变量而导致访问了 old 区域中的节点但没把它们移动到 young 区域的次数,还包含因为该节点在 young 区域的前 1/4 处而没有被移动到 young 区域头部的次数。
- LRU len:代表 LRU 链表中节点的数量。
- unzip_LRU:代表 unzip_LRU 链表中节点的数量(由于我们没有具体唠叨过这个链表,现在可以忽略它的值)。
- I/O sum:最近 50s 读取磁盘页的总数。
- I/O cur:现在正在读取的磁盘页数量。
- I/O unzip sum:最近 50s 解压的页面数量。
- I/O unzip cur:正在解压的页面数量。
Buffer Pool 内部组成
Buffer Pool 的基础组成部分是缓存页,大小和磁盘上的默认页大小一致都是 16KB,每一个缓存页都对应一个控制块。
这些控制信息包括该页所属的表空间编号、页号、缓存页在 Buffer Pool 中的地址、链表节点信息、一些锁信息以及 LSN 信息,当然还有一些别的控制信息。
每个页对应的控制信息占用的空间大小都是相同的,这块空间被称为控制块,控制块与缓存页之间是一一对应的关系,控制块被放到 Buffer Pool 的前面,而缓存页则被放到后面:
中间碎片空间的产生是由于 Buffer Pool 中剩余的空间已经不够容纳一对控制块和缓存页的大小了。
free 链表管理
Buffer Pool 初始化时会被划分成若干对控制块和缓存页,随着程序的运行,会不断的有磁盘页被加载到 Buffer Pool 中、或从 Buffer Pool 中被释放,为了知道哪些页是空闲的、哪些页已经被占用了,MySQL 会将所有空闲的缓存页对应的控制块作为一个节点放到一个链表中,称为free 链表。
注意 free 链表的结构中还有一个基节点,它是 Buffer Pool 外的一块空间,包含头节点(start)、尾节点(end)属性,主要用于定位这个 free 链表。
缓存页的哈希表
当我们需要访问某页中的数据时,需要将该页加载到 Buffer Pool 内,那么我们怎么知道该页是不是已经在 Buffer Pool 中了呢?其实是用一个哈希表来定位的,该哈希表的 key 为表空间号 + 页号
、value 为缓存页。
flush 链表管理
当 Buffer Pool 中某个缓存页的内容被修改,则它和磁盘上的对应页就不一致了,这样的缓存页就被称为脏页
,脏页并不会立刻被同步到磁盘上,而是会被加入到一个链表中,称为flush链表
。
LRU 链表管理
当 Buffer Pool 中不再有空闲的缓存页时,我们需要淘汰掉部分最近很少使用的缓存页,所以 InnoDB 会使用一个LRU 链表来组织缓存页,并按最近最少使用原则来淘汰旧的缓存页:
- 如果该页不在 Buffer Pool 中,在把该页从磁盘加载到 Buffer Pool 中的缓存页时,就把该缓存页对应的控制块作为节点塞到链表的头部。
- 如果该页已经缓存在 Buffer Pool 中,则直接把该页对应的控制块移动到 LRU 链表的头部。
只要我们使用到某个缓存页,就把该缓存页调整到 LRU 链表的头部,这样 LRU 链表尾部就是最近最少使用的缓存页,每次 Buffer Pool 中的空闲缓存页被用完时,就会释放 LRU 链表尾部的页面。
InnoDB 中还存在两种特殊情况:
- 预读
预先将以后可能用到的页面加载到 Buffer Pool,根据加载方式还细分为线性预读和随机预读:
线性预读:如果顺序访问了某个区(extent)的页面超过innodb_read_ahead_threshold
这个系统变量的值,就会触发一次异步读取下一个区中全部的页面到 Buffer Pool 的请求,注意异步读取意味着从磁盘中加载这些被预读的页面并不会影响到当前工作线程的正常执行。
随机预读:如果 Buffer Pool 中已经缓存了某个区的 13 个连续的页面,不论这些页面是不是顺序读取的,都会触发一次异步读取本区中所有其的页面到 Buffer Pool 的请求。
如果预读的页面很常用,则可以极大地提高语句的执行效率,而如果并不常用的话,反而会将 LRU 链表尾部的缓存页被迅速淘汰掉,导致缓存命中率降低。 - 全表扫描
全表扫描会导致大量页统统被加载到 Buffer Pool 内,这些页一般被使用到的频率也不高(全表扫描的执行频率不会太高),导致大大降低缓存命中率。
因此 InnoDB 中 LRU 链表被分成了两部分:
一部分存储使用频率非常高的缓存页,所以这一部分链表也叫做热数据,或者称 young 区域。
另一部分存储使用频率不是很高的缓存页,所以这一部分链表也叫做冷数据,或者称 old 区域。
针对预读的页面可能不进行后续访问情况的优化
当磁盘上的某个页面在初次加载到 Buffer Pool 中的某个缓存页时,该缓存页对应的控制块会被放到 old 区域的头部。这样针对预读到 Buffer Pool 却不进行后续访问的页面就会被逐渐从 old 区域逐出,而不会影响 young 区域中被使用比较频繁的缓存页。针对全表扫描时,短时间内访问大量使用频率非常低的页面情况的优化
全表扫描的页面首次会按上一条规则被加载到 old 区域的头部,之后一小段时间内这些页面由于访问频繁而被带入了 young 区,导致热数据被挤出了 young 区。因此 InnoDB 规定,在对某个处在 old 区域的缓存页进行第一次访问时就在它对应的控制块中记录下来这个访问时间,如果后续的访问时间与第一次访问的时间在某个时间间隔(innodb_old_blocks_time
)内,那么该页面就不会被从 old 区域移动到 young 区域的头部,否则将它移动到 young 区域的头部。
Change Buffer
当需要更新一个数据页时,如果数据页在内存中就直接更新,而如果这个数据页还没有在内存中的话,在不影响数据一致性的前提下,InnoDB 会将这些更新操作缓存在 change buffer 中,这样就不需要从磁盘中读入这个数据页了。在下次查询需要访问这个数据页的时候,将数据页读入内存,然后执行 change buffer 中与这个页有关的操作。通过这种方式就能保证这个数据逻辑的正确性。
访问数据页时,需要将 change buffer 中的操作应用到原始页中,这个操作被称为merge。另外,系统后台定时任务线程、数据库正常关闭过程也会执行 merge 操作。
change buffer 的好处包括:
- 更新操作先记录到 change buffer,减少了读磁盘操作;
- 数据的读入需要占用 buffer pool,因此 change buffer 也避免了占用内存。
使用 change buffer 的条件
使用 cache buffer 时需要注意以下两点:
- 唯一索引的更新不能使用 change buffer。
唯一索引的更新需要先判断这个操作是否违反唯一性约束,而这必须要将数据页读入内存才能判断,而已经读入内存的话,就没必要再使用 change buffer 了。
因此唯一索引不能使用 change buffer,只有普通索引可以使用。 - 大小有限
change buffer 使用的是 buffer pool 里的内存。 - 写多读少
读操作才会触发 merge 操作,在 merge 前记录的数据变更越多,change buffer 的收益也越大,因此 change buffer 适合写多读少的业务,页面在写完以后马上被访问到的概率比较小,此时 change buffer 的使用效果最好。比如账单、日志类的系统。
使用建议
- 更新性能普通索引比唯一索引更高,建议尽量选择普通索引。
- 如果所有的更新后面都伴随着对这个记录的查询,那么最好应该关闭 change buffer。
redo log 和 change buffer 之间的区别
- redo log 是任何写入操作都会记录的;change buffer 是在写入时页没有在内存中时使用的,将写入操作记录到 change buffer,而不是立刻读页面到 buffer pool 中;
- redo log 减少随机写磁盘开销,change buffer 减少随机读;
例子
1 | insert into a values(1, 2), (3, 4); |
插入两条数据,第一条数据所在的数据页在内存中,第二条不在,因此执行写入操作时,第一条不会使用到 change buffer,当然,最后二者都需要写入 redo log。
QA
为什么唯一索引上不能使用change buffer优化?
因为唯一索引需要将页读入内存来检查唯一性约束。
唯一索引会比普通索引更快吗?
不一定。
一种唯一索引比普通索引快的说法是唯一索引只要找到一行就直接返回了,而普通索引会继续匹配下一行,直到发现不匹配的情况,所以唯一索引比普通索引少查几行。
但是唯一索引不能利用change buffer优化。
某次写入change buffer后,系统重启了,change buffer会丢失吗?
不会丢失,因为事务提交的时候change buffer操作已经记录到redo log里了,因此崩溃恢复后change buffer能找回来。