把学习当成一种习惯
选择往往大于努力,越努力越幸运

InnoDB的Buffer Pool(二)

  通过Buffer Pool(一)这篇文章,我们可以知道,MySQL有一个核心内存组件 : Buffer Pool,并且该组件里面还存在一个Insert Buffer;我们对MySQL做DML操作的时候,其实是先走Buffer Pool,缓冲池找不到数据页了再去磁盘找,减少了磁盘IO的次数,提高了性能.可以说缓冲池犹如MySQL的心脏,对提高性能起到了非常重要的作用,所以我们有必要深入学习缓冲池的一些比较细节的东西,本篇文章就是基于第一篇文章没有讲到的细节来继续了解缓冲池.

Buffer Pool的核心作用

  要明白Buffer Pool的核心作用,需要理解以下几个词: 磁盘顺序IO、磁盘随机IO、内存顺序IO、内存随机IO
  磁盘顺序IO:看名字就知道,以顺序的方式写入文件,所以磁盘顺序IO肯定比磁盘随机IO快;但是在某种程度下,磁盘顺序I/O访问(特别是写操作)是能够匹敌内存的随机I/O访问性能的.【PS:MySQL的redolog、binlog都是磁盘上顺序IO,所以写入的时候可以很快,某种程度上来说,几乎可以跟内存随机读写的性能差不多.】
  磁盘随机IO:看名字也可以知道,以随机的方式写入文件;
  内存顺序IO:大家都知道内存访问很快,其实内存中也存在顺序IO,并且比内存随机IO快.
  内存随机IO:内存中也存在随机IO.
  MySQL对表空间的磁盘文件进行查询数据页的时候就需要再磁盘上随机读,因为你要读取的数据页可能在磁盘的任意一个位置,所以你在读取磁盘里的数据页的时候只能是用随机读这种方式.
  Buffer Pool就是MySQL向操作系统申请的一块内存,如果没有Buffer Pool,我们每次的DML操作都要进行磁盘的IO操作,如果是磁盘上的随机IO,那性能肯定比较差,所以Buffer Pool出现了,减少磁盘上的随机IO,这就是它的核心作用!!!也就是说,增加内存(Buffer Pool)是来解决磁盘随机I/O读取问题的最好的办法.

磁盘随机读写的相关性能指标

  磁盘随机读写来说,主要关注的性能指标是,IOPS 、 响应延迟 、吞吐量.
  IOPS : 表示磁盘每秒可以执行多少次磁盘读写操作;IOPS的指标对数据库的CRUD操作的QPS(每秒执行多少条SQL语句)影响非常大,为什么 : 因为你执行一条更新的SQL语句的时候,可能要涉及到多个磁盘上数据页的操作,并且也会涉及到一个redo log日志的顺序写操作,这些都是跟IOPS指标息息相关的.
  响应延迟 : 表示磁盘随机读写操作的响应延迟;假设你的底层磁盘支持你每秒执行200个随机读写操作(IOPS指标),每个操作耗费1ms还是5ms完成呢,这个就有很大的影响了.
  吞吐量 : 表示每秒磁盘的读写可以达到多少字节;假设你的磁盘支持你每秒执行200个随机读写操作(IOPS指标),同时每个操作的响应延迟也不耗时,但此时吞吐量低了,这个也有很大的影响了.

Buffer Pool里都存了什么

  缓冲池存的大部分是数据页(data page)和索引页(index page,也是数据页),但是不要只认为缓冲池中只有数据页,只是它们占缓冲池很大的一部分而已,其中缓冲池还包括undo页、Insert buffer(写缓冲)、自适应哈希索引、InnoDB存储的锁信息、数据字典信息等
  默认情况下,磁盘上的数据页和缓存页(载入Buffer Pool的数据页)是 一 一 对应起来的,都是16KB一个数据页对应一个缓存页.
  数据页(16KB) : MySQL在磁盘管理的最小单位,默认一个页大小是16KB,每个页存储的是多行数据,行数据由单向链表连接着,页与页之间是双向链表连接着,并且每个页对应着一个描述信息;
  描述信息(800字节左右) : 描述对应的缓存页的信息,比如表空间、数据页的编号、这个缓存页在Buffer Pool中的地址以及其他东西.在Buffer pool中,每个缓存页的描述数据放在最前面,然后各个缓存页放在后面.
  总的来说,当你到磁盘查询数据的时候,是以页的单位载入到内存中的,即使只查询一行数据,也是载入一页的数据到内存中.

Buffer Pool占用最多的就是数据页,并且对这些数据页进行了分类、管理.

如何分类 : 空闲页、干净页、脏页 :

  1. 空闲页 : 没有数据的页就是空闲页,当磁盘上的一个数据页载入到缓存中时,就需要使用到内存的一个空闲页;
  2. 脏页 : 被更新过且还没刷盘的数据页就是脏页,也就是说内存中的数据页跟磁盘上的数据页是不一样的;
  3. 干净页 : 没有被更新过的数据页就是干净页,也就是说内存中的数据页跟磁盘上的数据页是一样的;

如何管理:三条链表 : Free List(空闲列表) 、 LRU List(LRU淘汰列表) 、 Flush List(脏页列表);

  1. Free List : 当数据库刚启动时(redo log不用重做),LRU List是空的,即没有任何页,此时缓冲池中都是空闲页,放在Free List中;
  2. LRU List : 当需要把数据页从磁盘载入到缓冲池时,首先从 Free List 中查找是否有可用的空闲页,若有则将该页从 Free List 中删除,并且把该空闲页分配给载入内存的那个数据页使用,然后放入到 LRU List 中;如果 Free List 没有可用的空闲页呢,这个时候 LRU List 就来发挥他的作用了,根据 LRU 算法,淘汰 LRU List 尾端的页,将该内存空间(空闲页)分配给载入内存的那个数据页使用.
  3. Flush List : 只要是被修改过的数据页,都会把他对应的描述数据块加入到Flush List 中去,即缓冲池中的页的数据和磁盘上的页的数据产生了不一致了.Flush的意思就是这些都是脏页,后续都是要刷新到磁盘上去的;通过Flush List,就可以记录下来哪些数据页是脏页了.

【PS : 脏页既可以同时存在于LRU List 和 Flush List中】

Free List(空闲双向列表)

  数据库刚启动的时候(初始化Buffer Pool),此时16KB大小的缓存页 和 800字节左右的描述数据的大小 都是空闲页,里面什么都没有;
  每个节点都是一个空闲的缓存页的描述数据块的地址,也就是说,只要你一个数据页是空闲的,那么他的描述数据块就会被放入这个Free List中.
  随着你不停的把磁盘上的数据页载入到空闲的缓存页里去,Free List 中的空闲缓存页是不是会越来越少?因为只要你把一个数据页载入到一个空闲缓存页里去,Free List 中就会减少一个空闲数据页.当空闲数据页不够的时候,LRU List的作用就出来了.

LRU List(LRU淘汰双向列表)

  LRU List,它可以知道哪些数据页是最近最少被使用的(冷数据).
  LRU List 也是一条双向链表,总体分为热点数据、冷数据,链表的头部是热点数据,尾部是冷数据,InnoDB存储引擎对LRU算法有做了优化,具体的看下面小节的知识点.
  假设我们从磁盘载入一个数据页到缓存页的时候,就把这个数据页的描述数据块放到LRU List里去,那么只要有数据的数据页,他都会在LRU List里了,而且最近被加载数据的数据页,都会放到LRU List 的头部(热点数据).

Flush List(脏页双向列表)

  只要是被修改过的数据页,都会把他对应的描述数据块加入到Flush List 中去,并且Flush List中的脏页最终要是刷到磁盘上的.

传统LRU淘汰算法存在的问题

  对于传统的LRU算法的思想,对磁盘载入到内存中的数据页,就会加入到LRU List的头部,也就是加入到热点数据中,但是带来了问题 :
  全表扫描 : 假如你执行一条分页条件查询语句,并且没有使用到索引查询,所以此时会走全表扫描,这个时候会把所有的数据页一页一页的载入到Buffer Pool中,并且把这些数据页依次加入到 LRU List 头部中,此时有的数据页只是当前查询操作的时候需要,并不是活跃数据,也成为热点数据了.
  预读机制 : 预读机制是MySQL的一种查询优化的算法,说的就是当你从磁盘上载入一个数据页的时候,他可能会连带着把这个数据页相邻的其他数据页,也载入到缓存里去.参数innodb_random_read_ahead来控制的是否开启预读机制,他默认是OFF.参数innodb_read_ahead_threshold,他的默认值是56,意思就是如果顺序的访问了一个区里的多个数据页,访问的数据页的数量超过了这个阈值,此时就会触发预读机制.假如你开启预读机制,从磁盘上载入数据页到缓存中(相邻也载入),但相邻的数据页不是我们要的数据页,此时也跟着加入到了热点数据去了.
  传统的LRU算法保证了被访问的数据放在热点区域中,不怎么被访问的数据就会被淘汰,但是以上两个场景导致即使不是热点数据也会被加入到LRU List 头部中,所以InnoDB存储引擎在传统的LRU算法基础上做了优化.
  InnoDB存储引擎在LRU List 中加入了 midpoint 位置,新读取到的数据页,并不是加入到头部中去了,而是放入到LRU List的midpoint位置,在默认的情况下,该位置是在LRU List长度的5/8处,midpoint位置可由参数:innodb_old_blocks_pct控制,默认值为37,也就是说新读取的数据页会被加入到LRU List尾端的37%位置(差不多3/8位置).在InnoDB中,在midpoint位置前面的称为new列表(热点数据),之后的称为old列表(冷数据).
  查询的数据页直接加入到LRU List的头部,是不可取的,此时改为加入到midpoint位置,那加入到midpoint位置后,这些数据页中只是加入到midpoint位置,InnoDB存储引擎是怎么样去区分哪些数据页才是真正的热点数据?InnoDB存储引擎引入了一个参数innodb_old_blocks_time,用于表示页读取到 midpoint 位置后,需要等待多久才会被加入到LRU List的头部,默认是1秒,也就是说过了这1s,还能存活下去,就调到热数据区了.
  LRU List 小结: LRU LIst 37%的空间是用来刷的,通过innodb_old_blocks_pct来控制,建议innodb_old_blocks_pct 调成20%,80%给热点数据.

LRU List中冷热数据区的监控

// 看看几个比较重要的值:
SHOW ENGINE INNODB STATUS\G;
// 缓冲池共有 8192 个数据页 8192 * 16KB = 131072 KB / 1024 KB = 128MB的缓冲池
Buffer pool size   8192
// 空闲页的数量
Free buffers       4019
// LRU中数据页的数量
Database pages     4045
// Flush中脏页的数量
Modified db pages  0
// LRU List中页移动到头部的次数 , LRU List中页移动到尾部的次数
Pages made young 2418, not young 9326
// 每秒进入LRU的热数据区域的数量 , 每秒进入LRU的冷数据区域的数量
0.00 youngs/s, 0.00 non-youngs/s
// Buffer pool hit rate 重点关注的一个值,表示缓冲池命中率 100% 表示良好 该值不能低于95%
Buffer pool hit rate 1000 / 1000, young-making rate 0 / 1000 not 0 / 1000

non-youngs/s的值过大原因 :

  • 进行全表扫描;
  • innodb_old_blocks_pct设置过小,冷数据区域空间太小,导致载入到冷数据区域的时候,会频繁淘汰冷数据;
  • innodb_old_blocks_time设置太大,也会导致频繁淘汰冷数据(没坚持到10s,被刷出去了)

youngs/s的值过大的原因 :

  • innodb_old_blocks_time设置太小,很快就可以刷入热数据区域了.

写缓存 --- Insert Buffer

  我们已经知道了对于磁盘上的随机IO,性能的很差的,也就是每次执行一次更新SQL语句的时候,都要进行一次磁盘随机IO,而InnoDB存储引擎引入了写缓存(Insert Buffer)来避免这个问题,也就是每次的更新操作是在内存中操作的,避免了每次执行都要进行磁盘随机IO操作.
  对于主键索引,插入数据是按照主键递增的顺序进行写入,所以一般是磁盘顺序写入(不需要磁盘随机写入).【PS:UUID的插入和辅助索引一样,是随机的,即使主键ID是自增类型】
  对于辅助索引(非唯一索引) : 插入的数据并不像主键索引那样顺序写入,而是需要离散地访问非聚集索引页,由于随机读取磁盘的操作导致插入操作的性能下降了.
  所以Insert Buffer的作用体现出来了 : 对于非聚集索引(非唯一索引)的插入操作,并不是每次都要去插入磁盘IO上的索引页,而是先去Buffer Pool找一下有没有这个索引页,如果有,直接插入,如果没有,则先放入Insert Buffer对象中,然后再以一定的频率和情况进行Insert Buffer和辅助索引页子节点进行merge操作(合并),每次的操作都要进行离散地访问非聚集索引页和多次操作后再进行一次离散地访问非聚集索引页的性能可想而知.【PS : 什么是merge操作:就是你修改的数据页记录在Insert buffer中,最终还是要跟数据页合并起来的】

对于Insert Buffer的使用需要同时满足两大条件 :

  • 索引是辅助索引
  • 索引不是唯一索引 : 唯一索引,InnoDB存储引擎需要保证唯一性,所以会进行一次离散地访问非聚集索引页.

  对于插入场景比较密集的情况下,Insert Buffer会占用Buffer Pool过多的内存空间,默认最大占用1/2的Buffer Pool内存,可以通过参数IBUF_POOL_PER_MAX_SIZE来控制Insert Buffer大小进行控制,例如IBUF_POOL_PER_MAX_SIZE = 3 ,则表示只能使用1/3的缓冲池内存.

Change Buffer --- Insert Buffer的升级

  Change Buffer是Insert Buffer的升级,和Insert Buffer一样,Change Buffer也是只适合于非唯一辅助索引;
  InnoDB存储引擎对于更新操作(UPDATE),总是先Delete-mark,再Insert新记录,因此Update会产生两条ibuf entry.
  InnoDB存储引擎通过参数 innodb_change_buffering 来控制使用各种Buffer选项: inserts \ deletes \ purges \ changes \ all \ none ;

  • inserts : 只是开启buffer insert操作;
  • deletes : 只是开delete-marking操作;
  • changes : 开启buffer insert操作和delete-marking操作
  • purges : 对只是在后台执行的物理删除操作开启buffer功能
  • none : 不开启Change Buffer
  • all : 默认值,开启buffer inserts、delete-marking operations、purges

变量 innodb_change_buffer_max_size 来控制Change Buffer最大的使用内存,默认为25,该参数最大是50.

Insert Buffer的内部实现

Insert Buffer的数据结构是一棵B+Tree

  Insert Buffer的使用场景,即非唯一辅助索引的插入操作(更新操作是先Delete-mark,再Insert新记录),接下来看看Insert Buffer的内部实现 :
  Insert Buffer的数据结构其实是一颗B+Tree,全局维护着一棵B+Tree,也就是说所有表的插入辅助索引共同维护一棵Insert Buffer的B+Tree,这颗B+Tree就存储在共享表空间中,默认是ibdata1.
  既然是一棵B+Tree,那么就需要知道非叶子节点和叶子节点存储的是什么信息:非叶子节点存储的是查询的search key(键值,一共占用9个字节),如下:
其构造包括三个字段:space(4 byte) + marker(1byte) + offset(4byte) = search key(9 byte)
space : 表示表空间的Id,在InnoDB存储引擎中,每个表都有一个唯一的表空间Id,space占用4个字节.
marker : 占用1个字节,用来兼容老版本的Insert Buffer.
offset : 页所在的偏移量,占用4个字节.
  当一个非唯一辅助索引插入到页的时候,如果这个页刚好不在Buffer Pool中,那么此时就需要插入到Insert Buffer中,那么InnoDB存储引擎就会根据插入的辅助索引页的信息构造一个search key(9个字节),并且把这条记录插入到Insert Buffer的叶子节点中,叶子节点的前三个字段同非叶子节点含义相同,占用9个字节,第4个字段是metadata,占用4个字节,其中metadata有一个IBUF_REC_OFFSET_COUNT 占用2个字节,来排序每个记录进入Insert Buffer的顺序,从Insert Buffer的叶子节点的第5个字段开始,就是实际插入记录的各个字段,如下:
space(4 byte) | marker(1 byte) | offset(4 byte) | metadata(byte) | secondary index record

Insert Buffer Bitmap记录辅助索引页

  其实存在一个问题:页的大小是16KB,若此时对同一个非唯一辅助索引页有多次插入操作(Insert Buffer),而磁盘上的数据页的大小不够跟Insert Buffer的数据页merge了,怎么办呢:InnoDB存储引擎引入了一种特殊页来标记每个辅助索引页的可用空间(space,page_no),这个页的类型就是Insert Buffer Bitmap.
  每个Insert Buffer Bitmap页用来追踪16384个辅助索引页,也就是256个区(Extent,一个区64个数据页,16KB * 64 = 1024KB = 1MB ),每个 Insert Buffer Bitmap页都在16384个页的第二个页中.
  每个辅助索引页在Insert Buffer Bitmap页中占4 bit,如下:

  • IBUF_BITMAP_FREE (2 bit): 表示该辅助索引页中的可用空间数量,0表示无可用空间,1表示剩余空间大于1/32页(512字节),2表示剩余空间大于1/16页,3表示剩余空间大于1/8.
  • IBUF_BITMAP_BUFFERED (1 bit): 1表示该辅助索引页有记录被缓存在Insert Buffer B+Tree中
  • IBUF_BITMAP_IBUF (1 bit) : 表示该页为Insert Buffer B+Tree索引页

Merge Insert Buffer

  既然Insert/Change Buffer是一棵B+Tree,插入非唯一辅助索引的时候,若Buffer Pool找不到数据页,则直接插入到Insert Buffer中,但Insert Buffer最终还是要merge(合并)磁盘上的数据页的,那么什么时候合并呢?(三种情况)

  1. 辅助索引页被读取到缓冲池时;
  • 当辅助索引页在磁盘被载入到Buffer Pool的时候(例如正常的SELECT查询操作),这个时候就会查询Insert Buffer Bitmap,确认一下该数据页是否在Insert Buffer中存在记录,如果有,则会进行Merge操作,也就是将Insert Buffer B+Tree中的该页的记录插入到该辅助索引页中.PS:可以看到该页多次的记录操作一次合并到了原有的辅助索引页中了,性能会有大幅提高.
  1. Insert Buffer Bitmap 页追踪到该辅助索引页已无可用空间;
  • Insert Buffer Bitmap 页用来追踪每个辅助索引页的可用空间,并至少1/32页的空间.如果插入辅助索引页的记录时检测到该数据页插入记录后可用空间会小于1/32页,则会强制进行一次Merge操作,即强制读取磁盘上的辅助索引页载入到Buffer Pool中,将Insert Buffer B+Tree中该页的记录及待插入的记录插入到辅助索引页中.
  1. Master Thread;
  • Master Thread 线程中每秒或每10秒进行一次Merge Insert Buffer操作,只是每次Merge的不是一个页,而是多个页.根据参数srv_innodb_io_capactiy的百分比来决定真正合并多少个辅助索引页,InnoDB存储引擎是如何得知需要合并的辅助索引页的:我们知道,在Insert Buffer B+Tree 中,辅助索引页根据(space,offset) 排序好了,而InnoDB是随机选择一个页,读取到的页中的space及之后所需要数据量的页去进行Merge.若进行Merge时,要进行Merge的表已经被删除了,此时直接丢弃已经被Insert/Change Buffer的数据记录.

重做日志缓存,同样是一块内存----但不是存在缓冲池中的,自己独立跟操作系统申请的内存

  InnoDB的内存区域除了有缓冲池外,还有重做日志缓存(redo log buffer);
  MySQL启动的时候,会跟操作系统申请的一块连续内存空间,重做日志缓存专门用来缓冲redo log写入的.
  重做日志缓存一般不用设置很大,因为一般的情况下每一秒钟都会将重做日志缓存刷新到日志文件中,所以只需保证每秒产生的事务量在这个缓存大小之内即可,该值由变量innodb_log_buffer_size控制.

redo log

  为什么有 redo log 日志文件 : 为了避免数据丢失问题,事务数据库系统普遍采用了Write Ahead Log(WAL)策略,即当事务提交时,先写重做日志,再修改Buffer Pool中的页,所以即使MySQL宕机了,也可以依靠重做日志来完成数据的恢复,这也是事务ACID中的D(持久性).
  redo log对于InnoDB存储引擎来说至关重要的,也是InnoDB存储引擎自己特有的,它记录了对于InnoDB存储引擎的事务日志.

  redo log :

  • 每个InnoDB存储引擎至少一个重做日志文件组(group)
  • 每个文件组至少有2个重做日志文件,默认是ib_logfile0 和 ib_logfile1
  • 在重做日志组文件中每个文件的大小都是一致的,并且是以循环写的方式写入到文件中
  • InnoDB存储引擎会先写重做日志文件1(ib_logfile0),当第一个文件写满后,会切换到重做日志文件2(ib_logfile1)写入
  • 当重做日志文件2也被写满后,会切换到重做日志文件1中

  redo log 相关参数 :

  • innodb_log_file_size : 指定每个重做日志文件的大小
  • innodb_log_files_in_group : 指定日志文件组中重做日志文件的数量,默认为2,即2个重做日志文件
  • innodb_log_buffer_size : 指定了重做日志缓存的大小
  • innodb_log_group_home_dir : 指定了重做日志文件的存放路径,默认为 ./ ,即在MySQL数据库的数据目录下
SHOW VARIABLES LIKE 'innodb%log%'\G;
// 只截图了相关参数
+------------------------------------+------------+
| Variable_name                      | Value      |
+------------------------------------+------------+
| innodb_log_buffer_size             | 1048576    |
| innodb_log_file_size               | 50331648   |
| innodb_log_files_in_group          | 2          |
| innodb_log_group_home_dir          | .\         |
+------------------------------------+------------+

  重做日志文件的大小设置对于InnoDB存储引擎的性能有着非常大的影响,重做日志文件不能设置太大,如果设置太大,则MySQL重启的时候恢复数据的时间会很长;同样的也不能设置太小,不然的话会导致不断的切换重做日志文件的写入,频繁的重做日志文件切换就需要对覆盖的内容进行Checkpoint机制(下个小节讲),会导致性能抖动.

redo log 基本格式

redo log的基本格式 : redo_log_type | space | page_no | redo_log_body

  • redo_log_type : 占用1个字节,表示重做日志的类型
  • space : 可能小于4个字节,表示表空间的ID
  • page_no : 表示页的偏移量
  • redo_log_body : 表示重做日志的数据部分,恢复的时候需要调用相应的函数进行解析来恢复数据页

  重做日志文件的操作不是直接写入,也是先写入一个重做日志缓存(redo log buffer),然后按照一定的顺序写入到重做日志文件中.

redo log buffer flush

  既然是在内存操作,那么MySQL宕机了,redo log buffer中的一些缓存日志数据没有刷新到磁盘导致丢失了,所以InnoDB存储引擎必须保证redo log buffer的缓存日志数据刷新到磁盘 :
1.Master Thread 每一秒会将重做日志缓存刷新到redo log中
2.每个事务提交时会将重做日志缓存刷新到redo log中
3.当重做日志缓存空间大小小于1/2时,重做缓存就会刷新到redo log中
4.参数innodb_flush_log_at_trx_commit 控制

  • 表示在每次事务提交(commit)操作的时候,处理重做日志的方式
  • 该参数有效值分别为 : 0 \ 1 \ 2
  • 0 : 表示事务提交的时候,并不将事务的重做日志写入磁盘上的文件,而是等待Master Thread每秒的刷新操作.[PS:可能会丢失1秒的重做日志文件,MySQL数据库宕机了或操作系统服务器宕机了,此时在redo log buffer中的重做日志文件丢失了]
  • 1 : 表示每次事务执行提交操作的时候,都会将重做日志缓存同步写到磁盘.[PS:不会丢失重做日志文件,默认且最安全]
  • 2 : 表示将重做日志文件异步写到磁盘,即写到操作系统的缓存中.(并不能保证在事务执行提交操作的时候会写入重做日志文件,MySQL数据库宕机了且操作系统服务器并没有宕机时,此时写入磁盘的重做日志文件保存在文件系统缓存中,当恢复时同样可以保证数据库不丢失,反之操作系统服务器宕机了,就会丢失重做日志文件了)[PS:可能会丢失1秒的重做日志文件,操作系统服务器宕机了,保留在文件系统缓存的重做日志文件丢失了]

Checkpoint机制 --- 刷脏页

  通过前面对缓冲池的认识,已经可以知道对页的操作首先是在缓冲池中完成的,并且 :

  • 如果进行一条DML语句,例如Update或者DELETE改变了页中的记录,那么此时的数据页就是脏页了,即缓冲池中的这个脏页跟磁盘上的页不一致了,此时脏页也必须刷新到磁盘,但是如果每次一更新数据页就进行一次脏页刷新到磁盘(进行一次磁盘随机IO操作),那么这个开销是非常大的.
  • InnoDB存储引擎采用了Write Ahead Log策略,即当事务提交时,先写重做日志,再修改页,来避免MySQL数据库宕机了,也可以通过重做日志文件来恢复数据.

  假设重做日志文件可以无限增大,且缓冲池也足够大,能够缓存数据库中所有的数据,那么缓冲池中的脏页是不需要刷新到磁盘的,当发生MySQL数据库宕机时,完成可以依靠redo log来恢复整个数据库系统中的数据,但是这必须同时满足两个条件 :

  • 缓冲池可以缓存数据库中所有的数据 (很难做到)
  • redo log可以无限增大 (可以满足,但是对于成本要求太高了)

  即使两个条件满足了,那么一种情况出现了:MySQL数据库宕机了,重做日志文件非常的大,此时恢复数据是不是要花很长的时间?所以此时恢复的代价也会非常大.

因此,Checkpoint机制出现了,解决了这几个问题 :

  • 缩短MySQL数据库宕机后恢复数据的时间【PS:即使MySQL宕机了,恢复的数据也不用重做所有的日志,以为Checkpoint之前的的页已经刷盘了】
  • 缓冲池不够用了,将脏页刷新到磁盘中【PS:当缓冲池不够用时,根据LRU算法会淘汰最近最少使用的页(若此页是脏页,需要触发Checkpoint机制)】
  • 重做日志不够用了,将脏页刷新到磁盘中(覆盖掉的重做日志内容需要刷脏页)【PS:我们通过上面知识点已经知道,重做日志文件不是无限增大的,是循环写入的,循环写入被覆盖的内容就必须强制Checkpoint机制】

  对于InnoDB存储引擎来说,是通过LSN(Log Sequence Number)来标记版本的,LSN占用8个字节的数字,每个页、重做日志、Checkpoint都有LSN.【PS:本文不重点深入理解Checkpoint机制】

两种Checkpoint机制

1.Sharp Checkpoint : 即发生MySQL数据库正常关闭时,会将所有的脏页刷盘,这是默认的工作方式,参数innodb_fast_shutdown = 1.【PS:这时候logfile的日志就没用了,脏页已经写到磁盘上了】

  • 数据库在运行时不会使用Sharp Checkpoint,在引擎内部使用Fuzzy Checkpoint,即只刷新一部分脏页,而不是刷新所有的脏页回磁盘.

2.Fuzzy Checkpoint : 只刷新一部分脏页,而不是刷新所有的脏页回磁盘,Fuzzy Checkpoint,并且分为多种情况的Checkpoint :

  • Master Thread Checkpoint : Master Thread(主线程)差不多会以每秒或每10秒的速度从缓冲池中刷新一定比例的页回磁盘,这个过程的异步的,此时InnoDB存储引擎可以进行其他操作,用户查询线程不会阻塞
  • FLUSH_LRU_LIST Checkpoint : InnoDB存储引擎需要保证LRU List中有差不多100个空闲页,如果没有100个空闲页,则InnoDB存储引擎会根据LRU算法淘汰尾端的数据页,如果这些页存在脏页,则需要触发Checkpoint机制,参数innodb_lru_scan_depth 控制LRU List中空闲页的数量,默认为1024.
  • Async/Sync Checkpoint : 重做日志文件不可用的时候,这时候就需要触发Checkpoint机制来把被覆盖的重做日志文件内容的这些页刷盘.【不会阻塞用户查询操作】
  • Dirty Page too much : 即脏页太多了,导致InnoDB存储引擎触发Checkpoint,其实就是说保证缓冲池中空闲页的数量,参数innodb_max_dirty_pages_pct控制,该值75表示缓冲池中脏页占据75%时,会强制Checkpoint.

Buffer Pool支持同时多个实例

  Buffer Pool缓冲池负责管理着Free List,Flush List,LRU List,如果过大达到几十G,如果某个线程更新资源池,可以造成其它线程等待的瓶颈.
  InnoDB存储引擎可以增加Buffer Pool实例的个数,通过innodb_buffer_pool_instances 参数来增加Buffer Pool实例的参数,并且使用哈希函数将读取缓存的数据页随机分配到一个缓冲池里面,这样缓冲区实例就自己管理自己的了,就不会造成等待的瓶颈了.
  innodb_buffer_pool_size必须大于1GB,生成Buffer Pool多实例才有效,最多支持64个Buffer Pool实例,通过在my.cnf添加innodb_buffer_pool_instances=3来添加.

总结

Buffer Pool 通过三条双向链表来管理数据页(空闲页、脏页、干净页),其中对传统的LRU算法进行了优化.

LRU算法相关参数 :

  • innodb_old_blocks_pct : 默认37,表示新读取的数据页会被加入到LRU List尾端的37%位置(差不多3/8位置)
  • innodb_old_blocks_time : 表示页读取到mid位置后,需要等待多久才会被加入到LRU List的头部,默认是1秒,过了这1s,还能存活下去,就调到热数据区了

Buffer Pool 引入了Insert/Change Buffer,对于使用Insert/Change Buffer,需要同时满足以下两个条件 :

  • 辅助索引;
  • 非唯一索引;

Insert Buffer 相关参数 :

  • innodb_buffer_poor_size : Buffer Pool的大小;
  • IBUF_POOL_PER_MAX_SIZE : 控制Insert/Change Buffer大小进行控制,例如IBUF_POOL_PER_MAX_SIZE = 3 ,则表示只能使用1/3的缓冲池内存
  • innodb_change_buffering : 来控制使用各种Buffer选项: inserts \ deletes \ purges \ changes \ all(默认选项) \ none
  • innodb_change_buffer_max_size : 控制Insert/Change Buffer最大的使用内存,默认为25,该参数最大是50

重做日志文件相关参数 :

  • innodb_log_buffer_size : 指定了重做日志缓存的大小
  • innodb_log_file_size : 指定每个重做日志文件的大小
  • innodb_log_files_in_group : 指定日志文件组中重做日志文件的数量,默认为2,即2个重做日志文件
  • innodb_log_group_home_dir : 指定了重做日志文件的存放路径,默认为 ./ ,即在MySQL数据库的数据目录下

Checkpoint机制 : 采用两种Checkpoint机制 : Sharp Checkpoint 和 Fuzzy Checkpoint

  • Sharp Checkpoint : 即发生MySQL数据库正常关闭时,会将所有的脏页刷盘,数据库在运行时不会使用Sharp Checkpoint
  • Fuzzy Checkpoint : 只刷新一部分脏页,而不是刷新所有的脏页回磁盘,数据库在运行时会使用Fuzzy Checkpoint

结束语

  • MySQL的内存还有很多值得深入学习的地方,尤其是Checkpoint机制,后期会独立一篇文章来深入总结Checkpoint机制.
  • 原创不易
  • 希望看完这篇文章的你有所收获!

相关参考资料


目录