innodb-buffer pool
buffer pool概念
里面缓存着大量数据(数据页),使 CPU 读取或写入数据时,不直接和低速的磁盘打交道,直接和缓冲区进行交互,从而解决了因为磁盘性能慢导致的数据库性能差的问题。
内存结构
数据页
InnoDB 中,数据管理的最小单位为页,默认是 16KB。
Free List
启动MySQL时需要完成Buffer Pool的初始化, 刚刚初始完化的Buffer Pool所有的页都是空闲页, 都会被加入到 Free List 中,需要从数据库中重新分配新的页,可以直接从Free List中获取. 如果没有足够的空闲页可以使用则需要从 FLU List 或者 LRU List 淘汰一定的节点.
LRU List
buffer pool 的目的是加速写和加速读,因此必须想办法提高内存数据页的缓存命中率。InnoDB 基于经典的 LRU 算法管理 buffer pool 中的数据页。一般情况下 list 头部存放的是热数据,就是所谓的 New Sublist(最近经常访问的数据),list 尾部存放的就是 Old Sublist(最近不被访问的数据)。
buffer pool采用的是lru算法
- buffer pool 中会分为两个区域,一个NewSublist 一个OldSublist,默认是buffer pool 5/8的位置开始进行分割.
- 当一个页从磁盘读取到缓存池中时会将新的页插入到OldSublist的最开始处.
- 如果是 OldSublist 的数据被访问到了, 这个页信息就会被移动到的头部变成NewSublist 的第一页
- 如果数据一直不被访问,最后都会通过移除最近最久未使用的页的模式进行移除buffer pool.
"中点插入"的意义: 这样的机制主要是为了保证当出现有一次性需要读取大量数据时, 即会有大量的新的页数据需要被插入buffer pool中,如果全部全部从头对buffer pool进行移除则会导致当前查询后原有的热点数据需要重新到磁盘中进行读取. 而"中点插入"的策略就是为了保证在读取大量新的数据页的同时,仍然保留最近常读取的数据,减少下次对磁盘数据io.
FLU List
内存数据页和磁盘数据页内容不一致的时候,这个数据页被称为“脏页”。内存数据写入磁盘后,内存和磁盘的数据页内容就一致了,称为“干净页”。不论脏页还是干净页,都存在内存里。因为当产生脏页时并不会立即刷新到磁盘,而是一段时间后统一由后台线程进行刷新到磁盘,所有就需要将脏页的信息进行标记存储到FLU List,这里注意不是存储页数据, 而 FLU List 中的脏页记录只是通过指针指向 LRU List 中的脏页.
当需要从 FLU List 中淘汰页的时候,从链表尾部开始淘汰。加入 FLU List,需要使用 flush_list_mutex 保护,所以能保证 FLU List 中节点的顺序。虽然脏页既存在于 LRU List 中,也存在与 FLU List 中,但 LRU List 用来管理缓冲池中页的可用性,FLU List 用来管理将脏页刷新回磁盘,二者互不影响。
写入数据原理
double write 背景
InnoDB buffer pool 一页脏页大小为 16 KB,如果只写了前 4KB 时发生宕机,那这个脏页就发生了写失败,会造成数据丢失。为了避免这一问题,InnoDB 使用了 double write 机制(InnoDB 将 double write 的数据存于共享表空间中)。在写入数据文件之前,先将脏页写入 double write 中,当然这里的写入都是需要刷盘的。有人会问 redo log 不是也能恢复数据页吗?为什么还需要 double write?这是因为 redo log 中记录的是页的偏移量,比如在页偏移量为 800 的地方写入数据 xxx,而如果页本身已经发生损坏,应用 redo log 也无济于事。
insert buffer 背景
InnoDB 的数据是根据聚集索引排列的,通常业务在插入数据时是按照主键递增的,所以插入聚集索引一般是顺序磁盘写入。但是不可能每张表都只有聚集索引,当存在非聚集索引时,对于非聚集索引的变更就可能不是顺序的,会拖慢整体的插入性能。为了解决这一问题,InnoDB 使用了 insert buffer 机制,将对于非聚集索引的变更先放入 insert buffer ,尽量合并一些数据页后再写入实际的非聚集索引中去。
首先在MySQLServer层会对执行的SQL语句进行进行一个校验,校验成功后才会进行事务的正式提交:
- InnoDB 会将数据页缓存至内存中的 buffer pool,所以 insert 语句到了这里并不需要立刻将数据写入磁盘文件中,只需要修改 buffer pool 当中对应的数据页就可以了。
- 为了保证即使MySQL宕机,insert数据不会丢失,还需要写入redolog。innodb_flush_log_at_trx_commit=1 时,每次事务提交都会触发一次 redo log 刷盘。(redo log 是顺序写入,相比直接修改数据文件,redo 的磁盘写入效率更加高效)
- 如果开启了 binlog 日志,我们还需将事务逻辑数据写入 binlog 文件,且为了保证复制安全,建议使用 sync_binlog=1 ,也就是每次事务提交时,都要将 binlog 日志的变更刷入磁盘。
- 当 buffer pool 中的数据页达到一定量的脏页或 InnoDB 的 IO 压力较小 时,都会触发脏页的刷盘操作。开启 double write 时,InnoDB 刷脏页时首先会复制一份刷入 double write,在这个过程中,由于double write的页是连续的,对磁盘的写入也是顺序操作,性能消耗不大。
- 无论是否经过 double write,脏页最终还是需要刷入表空间的数据文件。刷入完成后才能释放 buffer pool 当中的空间。
- insert buffer 也是 buffer pool 中的一部分,当 buffer pool 空间不足需要交换出部分脏页时,有可能将 insert buffer 的数据页换出,刷入共享表空间中的 insert buffer 数据文件中。
- 当 innodb_stats_persistent=ON 时,SQL 语句所涉及到的 InnoDB 统计信息也会被刷盘到 innodb_table_stats 和 innodb_index_stats 这两张系统表中,这样就不用每次再实时计算了。
- 步骤7的另一种情况: 有一些情况下可以不经过 double write 直接刷盘
a. 关闭 double write
b. 不需要 double write 保障,如 drop table 等操作
总结: 一条 insert 语句的所有涉及到的数据在磁盘上会依次写入 redo log,binlog,(double write,insert buffer) 共享表空间,最后在自己的用户表空间落定为安。
访问数据页原理
脏页(FLU List)刷新策略
刷新时机
以下四种情况,InnoDB 会进行脏页刷新:
- MySQL 认为系统空闲;
- MySQL 正常关闭过程;
- Free List 不足;
- redo log 写满。
需要注意: redo log写满时系统将不再接受数据的更新操作,这时所有更新语句会被阻塞.此时需要将 redo log 中 write pos 向前推进,推进范围内 Redo Log 涉及的所有脏页都需要 flush 到磁盘中. 所以redo log 不宜设置过小或写太慢,否则可能造成 redo log 频繁写满,导致频繁触发 flush 脏页.
刷新邻接页
innodb_flush_neighbors:值为 1 为连坐刷脏页,0 为只刷自己的.MySQL 8.0 中,innodb_flush_neighbors 参数的默认值已经是 0 了.
原理: 在准备刷一个脏页的时候,如果这个数据页旁边的数据页刚好也是脏页,就会把这个“邻居”也带着一起刷掉;而且这个把“邻居”拖下水的逻辑还可以继续蔓延,也就是对于每个邻接数据页,如果跟它相邻的数据页还是脏页,同样会被 flush 掉. 顺序IO的效率远比随机IO的效率高.