MySQLInnoDB存储引擎-内存结构
【MySQL】InnoDB存储引擎 - 内存结构
本文主要介绍 InnoDB存储引擎 在内存中的结构;以及各部分的作用;
1. InnoDB 内存结构
InnoDB 会把查询到的数据缓存到内存中,内存结构的设计可以有效的提高数据库的查询效率;
InnoDB存储引擎中内存结构主要分为:
- Buffer Pool 缓冲池
- Change Buffer 变更缓冲区
- adaptive_hash_index 自适应哈希索引
- Log Buffer 日志缓冲区
InnoDB存储引擎整体架构:
注:蓝色虚线框内为内存结构;
2. 缓冲池
缓冲池主要用来缓存被访问的索引数据页,是主内存中的一片区域,允许直接从内存访问频繁使用的数据从而提高效率。在专用数据库服务器上,通常会将80%的物理内存分配给缓冲池。
缓冲池不仅仅缓存磁盘中的数据页,还存储:锁信息、ChangeBuffer信息、自适应哈希索引、双写缓冲区信息;
2.1 结构
缓冲池是主内存中的一片区域,在这么大的内存空间中如何保证效率就是要解决的问题;缓冲池也采用与表空间类似的方式对数据进行组织;
缓冲池中包含至少⼀个 Instances 实例,Instances 是真正的缓冲池的实例对象,内存操作都是在 Instances 中进行;通过系统变量 innodb_buffer_pool_instances 设置个数,默认为1,最大为64;(可适当增加实例提高并发访问);
每个Instances 中包含至少一个chunk块,在动态调整缓冲池大小时,调整操作的最小单位是chunk,避免在调整大小操作期间复制所有缓冲池中的数据页;每次扩容时仅需扩容一个或多个chunk块,然后加入到 instances 的管理列表;
一段伪代码帮助理解:
// 概念性伪代码,帮助理解
// 每个 Instance 之间的管理相互独立,可以减少互斥冲突
struct Buffer_Pool_Instance {
pthread_mutex_t mutex; // 该实例自己的锁
Chunk* chunks[MAX_CHUNKS_PER_INSTANCE]; // 管理Chunk的指针数组,固定大小,在初始化时计算可能最大的Chunk数量
ulint n_chunks; // 当前拥有的Chunk数量
// ... 其他成员如Free List, LRU List等
};
struct Chunk {
byte* mem; // 指向一大块连续内存的起始地址
// ... 其他元信息
};
数据页在内存中需要被管理,提到管理就是 6 字真言 —— 先描述,再组织;
InnoDB定义了⼀个叫"控制块"的数据结构;“控制块"中有三个重要的信息分别是:
- 指向数据页的内存地址
- 前⼀个控制块的内存地址
- 后⼀下控制块的内存地址
之后再用⼀个双向链表管理每个控制块:
Chunk逻辑结构图:
初始化时以Chunk为单位,控制块 会从Chunk的内存空间从左向右进行初始化,数据页所占的内存会从Chunk的内存空间从右向左进行初始化,直至用尽Chunk的内存空间;
2.2 数据页的管理
当缓冲池初始化完成后,缓冲池中的数据页只是被分配了内存空间,并没有真实的数据,当用户进 行数据查询时真实的数据从磁盘加载到内存中并分配⼀个内存中的数据页,这时内存中数据页的状 态从空间变成了有实际的数据;当用户修改数据时,并不是直接修改磁盘中的数据页,而是修改内 存中的数据页,这时内存中数据页的状态从有实际数据变成了被修改。
在缓冲池中采用三个链表维护内存页,这三个链表也对应着内存中页的三种状态,分别是:
- Free 未使用的页,也可以称做空闲页;
- Clean 已使用但未修改的页,也可以称做干净页;
- Dirty 已修改的页,也可以称做脏页。
对应的三个链表分别是 Free List 、 LRU List 和 Flush List :
- Free List :只管理Free页
- LRU List :管理 Clean 页和 Dirty 页
- Flush List :只管理 Dirty 页
每次操作数据页之后就会把数据页加入到指定的链表,内存中有这么多数据页如何快速找到目标页?
首先第一种办法是通过遍历,这种做法显然不能满足性能要求;
InnoDB采用的是 Page Hash 的方式,也就是每当把磁盘中数据页加载到内存时,用数据页的表空间Id和页号做为Key,当前页在内存中的地址做为Value保存起来,每次查询时就可以通过Key快速定位到目标页,如果内存中没有目标页,则从磁盘中获取。
那缓冲池满了呢?InnoDB根据根据自身的实际场景,使用淘汰策略来淘汰相应的数据页,从而释放出内存空间,以便新的数据页加载到内存中。
2.3 缓冲池淘汰策略
缓冲池使用 LRU 算法管理链表,当有数据页添加到缓冲池时,最近最少使用的页将被淘汰,并将新页添加到列表的中间,这种中点插入策略将列表视为两个子列表:
- 链表头部,是存放最近访问的新页(年轻页)子列表;
- 链表尾部,是存放最近较少访问的旧页子列表
一个数据页首次被加载进缓冲池中时,首先加入到旧子列表头部;当访问的数据页在旧子列表中时,把被访问的页移动到新子列表的头部,使其成为 “新” 页;
数据库运行的过程中,缓冲池中被访问页面的位置不断更新,未访问的页面向列表的尾部移动,从而逐渐"变老” ,最终超出缓冲池容量的页从旧子列表的尾部被淘汰。
为什么要把数据页插⼊到旧子列表头部而不是直接插入到新子列表的头部?
因为InnoDB在读取页时,可能会发生"预读",预读的意思是InnoDB根据当前访问的记录自动推断后面可能会访问哪个页,并把他们提前加载到内存中,从而提高以后查询的效率,预读的页以并不⼀定会被真正的读取,从中间点插⼊可以使其尽快被淘汰。
查看缓冲池信息:
通过使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 BUFFER POOL AND MEMORY 部分查看有关缓冲池的指标;
3. 变更缓冲区 - Change Buffer
变更缓冲区用来缓存对二级素引数据的修改当数据页没有被加载到内存中时先把修改缓存起来等到其他查询操作发生时数据页被加载到内存后再直接修改内存中的数据页,从而达到减少磁盘I/O的目的;
为什么是二级索引?
索引分为聚集索引(主键)和二级索引(自定义);
假设:表中有一个主键( ID )唯一自增的,现在有两条 INSER 语句,都在插入数据,这时两个ID的值是相同的 (id=1,ID的值基于表中当前的ID计算) ,那么在变更缓冲区中就存在两个修改操作,如果以后要合并到缓冲池中,这时就会出现重复的主键值;
与聚集索引不同,⼆级索引通常是不唯⼀的,并且向二级索引中插入数据时由于数据列不同,位置相对随机(数据行连续存储,列与列之间不连续),同样对于删除和更新操作可能会影响不相邻的⼆级索引页,如果每次都从磁盘读取数据就会发生大量的随机I/O,以变更缓冲区的方式先将修改缓存起来,当真正的读取数据时再把修改合并到缓冲池中可以提升效率;
Merge的触发时机:
- 读取对应的数据页时;
- 当系统空闲或者 Slow Shutdown 时,主线程发起 merge ;
- Change buffer 的内存空间即将耗尽时;
- Redo Log 写满时;
关于变更缓冲区的配置,可配置的有:缓冲类型和更改缓冲区的最大大小;(具体配置可在使用时搜索);
在有大量插入、更新和删除的业务场景中,可以考虑增加变更缓冲区大小,读多写少可以考虑减小;
注意:变更缓冲区占用的是缓冲池的内存空间,那么变更缓冲区过大,会导致缓冲池中的数据页更快地淘汰;
查看变更缓冲区信息:使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分查看有关更改缓冲区状态的信息
4. 自适应哈希索引
当使用某一个查询条件达到阈值时,以查询条件为key,B+树页的地址为value建立映射关系,从而对B+树寻路的开销进行了优化,达到提升效率的目的;
为什么要创建自适应哈希索引?
B+树通常只有3到5层,但从根节点到叶节点的寻路涉及到多层页面内记录的比较,即使所有路径上的页面都在内存中,也非常消耗CPU的资源;
InnoDB对寻路的开销进行了优化,比如:寻路结束后将cursor缓存起来方便下次查询复用;尽可能的避免单词寻路开销;
自适应哈希索引相关配置:
自适应哈希索引(AHI)会占用缓冲池⼀部分内存区域,在缓冲池初始化后被初始化,为了避免AHI的锁竞争压力,AHI支持分区,可以使用 innodb_adaptive_hash_index_parts 参数配置分区个数,默认为 8,最大值为 512;
还可以通过设置系统变量 innodb_adaptive_hash_index 开启或关闭自适应哈希索引;
查看自适应哈希索引的信息:
通过使用 SHOW ENGINE InnoDB STATUS 访问 InnoDB 标准监视器输出中 INSERT BUFFER AND ADAPTIVE HASH INDEX 部分查看自适应哈希索引使用信息,如果锁争抢过多,可以考虑增加自适应哈希索引分区数量或禁用自适应哈希索引;
5. 日志缓冲区
日志缓冲区是服务器启动时向操作系统申请的一篇连续的内存区域,存储即将要写入磁盘的日志数据。
在对数据库进行DML操作时,InnoDB会记录对应操作的日志比如为保证数据完整性实现数据库崩溃恢复的Redo Log,这些日志会首先写入Log Buffer中,从而解决同步写磁盘导致的性能问题然后根据不同落盘策略最终写入磁盘;
日志缓冲区可以有效的减少写日志到磁中的IO频率;根据刷盘策略统一进行落盘操作,可以实现⼀次磁盘I/O写入多条日志,从而提升效率;