MySQL的InnoDB存储引擎内存结构

InnoDB内存结构分为三部分,分别是BufferPool缓冲池,ChangeBuffer写缓冲区,LogBuffer日志缓冲区。

InnoDB存储引擎内存结构图
BufferPool缓冲池

作用:主要是用来缓存表数据和索引数据的。MySQL服务正常启动的时候,会向操作系统申请一块内存区域,用来缓存表数据和索引数据,这块内存区域就是BufferPool。

客户端发送SQL查询数据的时候呢,首先是先到BufferPool查看有没有缓存对应的数据,如果有则直接返回。如果没有,会从磁盘加载,但是加载的并不只是查询的数据。而是把查询的数据所在的数据页从磁盘加载到BufferPool当中

BufferPool

为什么要去加载数据页而不是数据?

主要是为了减少磁盘IO次数。数据页存储多条数据,如果下一次查询的数据也是在之前查询返回的数据页当中,就可以直接从BufferPool返回数据,而不用再次从磁盘加载数据。减少磁盘IO的次数,降低性能损耗。

BufferPool除了存储从磁盘加载的数据页,还存储控制块,控制块和数据页是一一对应的关系。每个控制块记录着与它对应的数据页的表空间号,数据页编号和缓存页在BufferPool的内存地址

BufferPool默认大小是128MB,主要是用来存储缓存页的。控制块则是另外向操作系统申请一块内存空间。缓存页大小和数据页大小一样,都是16KB。而控制块大概占缓存页的20%,也就是800B左右。

BufferPool内容

如何得知数据页被缓存到BufferPool中?

MySQL底层会维护一个Hash表,key是表空间号+数据页号,value是对应的控制块

=> 如果通过表空间号和页编号,能够从这个哈希表找到对应的控制块,就能找到控制块对应的缓存页。

=> 但是如果找不到控制块,就意味着这个控制块对应的缓存页是没有被缓存到BufferPool中,需要从磁盘加载

MySQL底层维护的Hash表
Page的分类

对于Page的使用情况,分为未使用和已使用的Page

对于未被使用的Page,也叫FreePage,没有缓存任何从磁盘加载过来的数据。

对于被使用的Page,分为两种,分别是CleanPage干净页,还有DirtyPage脏页

=> cleanPage : 干净页,就是数据没有被修改过,和磁盘上数据页的数据是一致的

=> dirtyPage:脏页,就是数据已经被修改过,和磁盘上数据页的数据是不一致的

Page的分类
Page的管理

FreeList

双向链表,用于管理BufferPool中所有空闲页。BP中所有空闲页对应的控制块都会作为FreeList的节点,由FreeList负责管理这些节点

当需要从磁盘加载数据页到BufferPool的时候

=> 首先,先从FreeList选出一个节点,记录从磁盘加载到BufferPool的数据页的表空间号和数据页编号

=> 由控制块找到对应缓存页的位置,把数据页从磁盘加载到被选出的控制块对应的缓存页的位置上

=> 此时空闲页变成干净页,需要把之前选出的控制块从FreeList中移除

=> 把表空间号+数据页编号作为key,把控制块作为value记录到MySQL底层维护的那个哈希表中

什么时候会有新的节点加入到FreeList中?

当BufferPool满了或者FreePage不足的前提下,会触发LRU链表的淘汰算法,被淘汰的节点会重新作为FreePage对应的控制块重新加入到FreeList当中

FreeList

FlushList

双向链表。主要是负责对DirtyPage的刷盘操作。BufferPool的缓存页被修改之后,并不是立即写入到磁盘数据页中,而是先交给FlushList管理,后续会有一个后台线程定期的到FlushList中找到脏页,统一执行刷盘操作

有后台线程在未来的某个时间点统一刷盘,可以减少大量的磁盘IO次数,降低MySQL性能损耗

链表结构基本和FreeList无异,也是有一个基节点,负责记录头节点和尾节点信息,以及链表节点个数

什么时候会有节点从FlushList中移除?

=> 后台线程定期的到FlushList中做刷盘操作,DirtyPage → CleanPage,从FlushList移除

=> MySQL服务正常关闭之前,都会统一做刷盘操作

=> BufferPool满了需要新的空闲页的时候触发LRU淘汰算法,被淘汰的节点对应的缓存页刚好是脏页的时候

FlushList

LRUList

双向链表。由BufferPool的DirtyPage和CleanPage对应的控制块作为该链表的节点。主要是负责管理页的可用性和释放。

=> 从磁盘加载数据页到BufferPool的时候,也就意味着有FreePage → CleanPage,就会有新的节点加入到 LRUList,而新加入的节点会直接放在链表的头部

=> 当LRUList的某个节点被访问的时候,也会被放在链表的头部。(这里的访问指的是查询和修改)

=> 当BP满了,需要从磁盘加载数据页到BP中,此时会把LRUList尾节点淘汰,释放空间并重新作为节点加入到 FreeList中

LRUList

使用LRUList的优缺点

优点:最近使用的数据会被放在链表头部,保证热数据可以以最快的方式获取到

缺点:如果发生全表扫描或者MySQL预读机制,很容易把真正的热数据

=> 全表扫描,把磁盘所有数据页加载到BufferPool并且放在LRUList头部,真正的热数据会被挤到链表尾部

当空闲页不足的时候就会触发LRU淘汰机制,尾部真正的热数据就会被淘汰

=> 预读机制,和全表扫描原理差不多,就是把一些其他的页也加载到BufferPool中,也会把真正的热数据挤到

链表尾部淘汰

MySQL预读机制

线性预读

从磁盘加载数据页到BufferPool的时候,如果有56个页是在同一个区,那么会把相邻的下一个区所有的数据页都加载到BufferPool中(一个区存放64个页)

随机预读

如果BufferPool中缓存的数据页有13个是属于同一个区的,那么会把这个区所有的数据页从磁盘加载到BufferPool中。

全表扫描淘汰热数据

改进型LRU链表

MySQL目前使用的是这种改进型的LRU算法。把链表分为两部分,分别是热数据区和冷数据区,热数据区占63%,冷数据区占37%。如果有新节点加入链表的时候,会先从midpoint这个位置插入,也就是在热数据区的尾部,冷数据区的头部这个位置。

=> 新加入的节点如果在链表中存活时间超过innodb_old_blocks_time,而且在这段时间被再次访问,就会往热数据区移动

=> 如果存活时间小于innodb_old_blocks_time,就会往冷数据区移动,最终被淘汰

=> 改进后的LRUList,即便是全表扫描或者MySQL预读,也不会影响到真正的热数据。

改进型LRU链表
ChangeBuffer写缓冲区

ChangeBuffer主要是针对非唯一二级索引页的更新优化措施。changeBuffer是BufferPool的一部分,约占20%,最多能占50%。主要是存储对二级索引页的变更操作,包括insert,update,delete操作。这些操作都会被记录到ChageBuffer当中

ChangeBuffer

ChangeBuffer的使用流程

这里需要先判断当前索引页是不是唯一索引

如果是唯一索引,且在BufferPool没有缓存这个索引页的时候,需要从磁盘加载数据页到BufferPool,然后再到BufferPool的缓存页上去做更新操作。如果唯一索引页在BufferPool有缓存,就直接更新

如果不是唯一索引页,且是二级索引页,就需要判断当前这个二级索引页在BufferPool中有没有缓存。如果有缓存,就直接在BufferPool上更新。如果没有缓存。就先把变更操作包括insert,update,delete等先记录到ChangeBuffer中。等这个二级索引页从磁盘加载到BufferPool中的时候,在去执行一个merge操作。完成对数据页的更新。

为什么要有changeBuffer?

操作的是二级且非唯一的索引页,可以先把操作记录缓存起来。等到第一次被查询的时候从磁盘加载后再统一更新。而不是每次对数据页有更新就需要从磁盘加载到BufferPool中,减少磁盘IO次数,降低性能损耗

为什么changeBuffer不能针对唯一索引页?

唯一索引是需要每次在更新之前做唯一性校验的。所以不能直接先缓存更新记录。需要每次更新之前做校验,就需要从磁盘加载数据页到BufferPool

ChangeBuffer的使用场景

=> 针对的是二级索引页,所以当数据库中大多数数据页是非唯一索引页的时候

=> 适用于写大于读的场景。写操作执行之后不会立即去读取这个数据页的数据

LogBuffer日志缓冲区

LogBuffer是用来优化每次更新数据页后需要写入到磁盘redo log文件产生的磁盘IO问题。这里先描述一下对Bufferpool缓存页修改的流程

=> 客户端发送SQL更新数据,首先会把BufferPool中缓存页数据先记录到undo log中,以便事务回滚后恢复到修 改之前的状态。

=> 然后由MySQL底层的执行器对缓存页更新

=> 更新缓存页的物理日志先写入到LogBuffer的redo log buffer中

=> 触发刷盘机制比如log buffer满了,会把日志从redo log buffer先写入到操作系统缓存OS Cache

=> 再由操作系统缓存同步到磁盘的redo log file中,这个文件也叫MySQL的重做日志

=> 最后了由redo log file把更新后的数据给同步到磁盘的数据页上,完成数据的同步操作

先同步到redo log file,最后再同步到磁盘数据页。这个也叫WAL机制,日志先行

LogBuffer作用是什么

就是把对页的操作日志先缓存起来,后面会有一个后台线程定期的去到logBuffer中,把操作日志统一写入到磁盘文件中,而不是每次对BufferPool缓存页更新就把操作日志写入到磁盘中。避免多次磁盘IO,降低MySQL的性能损耗

LogBuffer