目录
InnoDB内存结构
主要组成
缓冲池
缓冲池的作用
缓冲池的结构
缓冲池中页与页之间连接方式分析
缓冲池如何组织数据
控制块初始化
页面初始化
缓冲池中页的管理
缓冲区淘汰策略
查看缓冲池信息
总结
变更缓冲区-Chang Buffer
变更缓冲区的作用
主要配置选项
查看当前变更缓冲区信息
自适应哈希
自适应哈希作用
自适应哈希的配置
查看自适应哈希信息
日志缓冲区
日志缓冲区的作用
日志缓冲区写入磁盘与内存比较
InnoDB内存结构总结
编辑
InnoDB内存结构
主要组成
内存结构主要构成
- Buffer Pool缓冲区
- Change Buffer变更缓冲区
- adaptive_hash_index自适应哈希索引
- Log Buffer 日志缓冲区
缓冲池
缓冲池的作用
缓冲池谓语InnoDB内存区域,主要用于缓存表和索引的常用数据。通过将频繁访问的数据(例如数据页、索引页)保存到内存中,缓冲池减少了磁盘的I/O操作,从而提高数据库性能。
缓冲池的结构
InnoDB的缓冲池至少包含一个Instance实例,其中一个Instance是真正的缓冲池事例对象,内存操作都是在Instance实例中进行
每个Instance实例中都有一个Chunk(服务器运行状态下动态调整块的大小),然后没Chunk块有包含若干个从磁盘上加载到内存的page数据页
缓冲池中页与页之间连接方式分析
链接机制
- 数据页在内存中的链接并未直接使用物理地址,而是通过链表中的指针和偏移量来表示
- 每个数据页头信息中包含一个next_record字段,该字段记录了下一行数据的偏移量,根据偏移量可以找到下一条数据
数据页的逻辑连接
- 数据页通过B+树索引逻辑连接,叶子节点为实际数据页,非叶子节点存储指向子节点的指针
双向链表
- 所有控制块都是通过双向链表连接,形成缓冲池LRU链表
- 头结点:最近访问的数据页
- 尾结点:最少访问的数据页,优先淘汰
链表节点的功能
- 数据是否为脏页
- 数据页的访问次数
- 数据页的最后访问时间
缓冲池如何组织数据
- 加载数据页
- 从磁盘中加载数据到缓冲池
- 为每一个加载的数据页分配一个控制块
- 控制块记录数据页的指针,并插入到LRU链表的头部
-
访问数据页:
- 通过控制块找到目标数据页
- 更新控制块的访问信息,将其移动到 LRU 链表头部
-
淘汰数据页:
- 当缓冲池空间不足时,从 LRU 链表尾部开始淘汰数据页
- 如果是脏页,先写回磁盘再释放
内存中的数据页和磁盘上的数据页的关系
磁盘中的数据页加载到内存后,缓冲区有一个内存页与之对应;内存中是使用控制块对其进行管理,控制块中是有一个指针指向了内存中真实的数据页
设置缓冲区大小
通过系统变量Innodb_buffer_pool_size进行设置,默认是128MB
缓冲池中instance数量
SHOW VARIABLES LIKE 'innodb_buffer_pool_instances';
设置(全局配置或配置文件中配置)
//全局配置
SET GLOBAL innodb_buffer_pool_instances = <数量>;//配置文件
[mysqld]
innodb_buffer_pool_instances = <数量>
根据缓冲区大小进行设置,如果缓冲区小于1GB,那么自动设置为1;如果缓冲区大于1GB那么默认值是8。
最大的实例数量是64
chunk的作用
chunk是缓冲池中最小的分配管理单位,每个chunk都有若干个页组成,页是最小的数据存储单位。一个chunk一般是1MB,一个数据页是16KB,那么一个chunk中有64页
chunk的核心作用:支持缓存池大小动态调整;提升缓存池的内存管理效率;减少大规模内存分配和锁竞争
控制块初始化
控制块在缓冲池初始化时与页面一起分配,每个页面分配一个对应的控制块,控制块的数量等于缓冲池中页面的数量
控制块是使用双向链表组织,开始缓冲池分配控制块数组,初始化每个控制块的结构体,将默认值填充到各个字段,然后建立双向链表,初始化链表的头尾指针
struct ControlBlock {void* page_ptr; // 指向页面的指针bool is_dirty; // 脏页标志int access_count; // 访问计数uint64_t timestamp; // 最后访问时间戳ControlBlock* prev; // 指向前一个控制块ControlBlock* next; // 指向后一个控制块
};
页面初始化
- 分配缓冲池实例的内存区域。
- 将内存区域按照 16KB 切分成若干页面。
- 初始化每个页面的头部元信息(如页面类型、下一页偏移)。
- 将页面与控制块建立一一对应关系。
内存先进行初始化,然后建立控制块与内存中缓存数据页之间的关系,从左边开始第一个控制块指向第一个缓冲数据页的内存地址
控制块与页面的关系
每个控制块通过指针指向缓冲池中的一个页面;控制块和页面的数量相等,每个控制块管理一个页面
首先分配页面内存后,记录页面的地址;然后将页面地址存储到对应的控制块中;最后将控制块加入到双向链表中
配置缓冲池提升性能
- 配置缓冲池大小
- 配置 多个缓冲池实例
- 防止缓冲区扫描
- 配置缓冲区预读取
- 配置缓冲区的刷新策略
- 保存和恢复缓冲池的状态
缓冲池中页的管理
分析
页的三种状态
- Free(空闲页)
- 含义:没有使用过的页,可以用来存储新的数据;这些页不包含任何有效的数据,完全空闲
- 作用:当需要加载新的数据页到缓冲池中的时候,InnoDB会优先从Free List中获取空闲页
- Clean(干净页)
- 含义:已经使用但是没有被修改的页,与磁盘中的页内容一致,所以是不需要写回磁盘
- 作用:clean页通通常都是存储在LRU(最近最少使用)列表中,用于提供快速访问
- Dirty(脏页)
- 含义:已经被修改但是还没有写回磁盘的页,这些页的内容与磁盘中的数据不同,需要在某些条件下写回磁盘从而确保数据的持久性
- 作用:Dirty页一般被放在Flush List中,等待后台线程触发特定条件后将其刷新到磁盘中
三个链表管理页
- Free List
- 管理所有的空闲页
- 管理:当需要加载新的数据时候,从Free List中分配页;如果Free List中没有空闲页,会通过替换策略从LRU List中淘汰页
- 动态变化:LRU List 中淘汰的页被释放后,会重新加入到Free List
- LRU List
- 管理Clean 和 Dirty 页
- 管理思路:页的访问会更新其在LRU列表中的位置,频繁访问的页会移动到列表前部,最少使用的页会逐渐移动到尾部;缓冲区需要更多空间的时候,LRU列表尾部的页会被释放或者写回磁盘
- 优化策略:使用中点插入策略,将新加载的也插入的LRU列表的中部,避免新页短时间占用较高优先级(下文详细分析)
- Flush List
- 专门管理Dirty页
- 管理:当一个页被修改的时候,会被标记为Dirty然后加入到Flush List中;后台线程或者触发条件(例如缓冲池不足)就会定期扫描Flush List并将脏页写回到磁盘上
- 动态变化:当Dirty页被刷新到磁盘后,会从Flush List中移除
分析页管理中三种操作
- 读操作(加载数据页)
- 首先检查需要的页是否在缓冲池中,如果命中缓存,那么从缓冲池中读取;如果没有命中缓存,则从磁盘加载页到缓冲池;
- 从Free List分配空闲页,或者从LRU List替换最少使用的页
- 写操作(修改数据页)
- 修改缓冲池中数据页后,该页被标记为Dirty,然后将脏页加入到Flush List中,等待后续刷新磁盘
- 替换页
- 缓冲区不足的时候,从LRU List 尾部选择使用最少的页;如果是脏页那么先写回磁盘,再释放,释放后的页加入到Free List重新分配
总结
每个缓冲池通过三个列表管理三种内存页。Free未使用的页、Clean干净页、Dirty脏页
拓展1:内存如何快速找到目标页
InnoDB使用page Hash方式,当磁盘数据加载到内存的时候,用数据页的表空间ID和页号作为Key,页在内存的地址为Value保存起来,每次查询的时候通过Key快速定位到目标页,如果内存中没有目标页,则从磁盘中获取
缓冲区淘汰策略
分析
LRU机制分析
- 页访问
- 如果该页已在缓冲池中,InnoDB 会将其移动到 LRU 列表的头部,标记为最近使用。
- 如果该页不在缓冲池中,InnoDB 会从磁盘加载该页到缓冲池,并插入到 LRU 列表中。
- 页插入
- 新加载的页并不会直接插入到 LRU 列表的头部,而是通过中点插入策略
- 页淘汰
- 如果待淘汰页是
Clean
页,则直接从 LRU 列表移除。 - 如果是
Dirty
页,则需先将其写回磁盘,然后再移除。
- 如果待淘汰页是
总结
淘汰策略采用LRU
拓展1:页的插入为什么在中部而不是新列表的头部
如果直接将新页插入到列表的头部,容易受到顺序扫描或者批量数据加载的影响,从而会导致缓冲池被新加载的数据被污染,频繁访问的页反而会被淘汰,所以使用中点插入解决该问题
InnoDB在读取页的时候会有预读,也就是根据当前访问记录自动推断后面可能访问哪些页,然后一起加载到内存,从而提高查询效率,从中间点插入可以让其尽快淘汰
查看缓冲池信息
SHOW ENGINE INNODB STATUS\G;
- Buffer pool size:缓冲池的总大小(以页为单位)。
- Free buffers:空闲页的数量。
- Database pages:已加载到缓冲池的数据库页数量。
- Modified db pages:脏页(已被修改但尚未写回磁盘的页)数量。
- Pages read:自服务器启动以来从磁盘读取的页数。
- Pages written:自服务器启动以来写入磁盘的页数。
- Pages created:自服务器启动以来新创建的页数。
- Read requests:逻辑读请求的总数。
- Write requests:写请求的总数。
总结
- 缓冲池的主要作用是用于缓存各种数据,最主要的就是缓存从磁盘中加载数据页,从而提高读取效率
- 缓冲池为方便数据组织定义了不同的数据结构:Buffer Pool中包含至少一个Instance,Instance包含至少一个Chunk,Chunk管理若干个个从磁盘中加载到内存的Page页
- 缓冲池使用三种链表管理三种内存页,分别是Free List 、LRU List和FLush List
- 淘汰策略是LRU算法
变更缓冲区-Chang Buffer
变更缓冲区的作用
分析
变更缓冲区
- 存储内容:用于缓存次级索引页的膝盖内容(例如INSERT\UPDATA\DELETE);如果修改涉及的次索引页还没有被加载到缓冲池中,那么变更缓冲池就会临时保存这些修改,而不是立刻就加载索引页到缓冲池中并写入磁盘
写入磁盘触发条件
- 当涉及的索引页不在缓冲池中的时候,变更缓冲区会暂时存储修改操
- 当后面其他操作需要访问这些索引页的时候,InnoDB会将索引页从磁盘加载到缓冲池中,并将变更缓冲区的修改合并到加载页中
总结
-
减少磁盘I/O:次级索引修改频繁的时候,变更缓冲区避免了每次修改都触发磁盘读取操作;批量合并修改后再写入磁盘,有效减少随机磁盘访问
-
提高读写性能,通过合并操作减少了磁盘的随机写入次数,优化了磁盘写入性能
-
对于不经常访问的索引页,变更缓冲区延迟了其加载时间,降低了缓冲池的压力
拓展:主键索引和二级索引
- 主键索引就像一本书的目录,唯一且直接指向内存
- 二级索引就像术后的关键词索引表,可以针对于多列进行加速查询,但是需要回表找到数据行
- 二级索引不是唯一的,针对的是非主键列,允许多个数据行有相同值
- 二级索引的存在让查询更加灵活高效
主要配置选项
分析
缓冲类型
控制变更缓冲区的行为,指定变更缓冲区缓存哪些类型
- all:缓存所有次级索引的修改操作(默认值)
- insert:进缓存次级层插入操作
- deletes:次级的删除操作
- changes:缓存插入和删除操作
- purges:仅缓存清除操作
- none:禁用变更缓冲区
// 动态调整
SET GLOBAL innodb_change_buffering = 'inserts';// 修改配置文件[mysqld]
innodb_change_buffering = inserts
变更缓冲池大小
范围1-50(表示占缓冲池大小的百分比)
// 动态调整
SET GLOBAL innodb_change_buffer_max_size = 30; -- 设置为 30%//配置文件
[mysqld]
innodb_change_buffer_max_size = 30
总结
主要配置项就是缓冲类型和更改缓冲区的最大大小
查看当前变更缓冲区信息
SHOW ENGINE INNODB STATAUS\G//命令同上,找到对应的信息即可
自适应哈希
自适应哈希作用
分析
工作原理分析
- InnoDB动态监控缓冲池页的访问情况
- 如果某些表或者索引页被频繁访问,并且通过创建哈希表可以显著提升查询性能,InnoDB会自动为其生成哈希索引
- 构建哈希索引
- 自适应哈希索引是基于B+树索引动态生成;其将频繁访问的页或行的索引转化为哈希表的条目,映射到内存的位置
- 自适应哈希索引存储在内存中,全部依赖缓冲池空间
启动和关闭命令
SET GLOBAL innodb_adaptive_hash_index = 1;SET GLOBAL innodb_adaptive_hash_index = 0;
总结
自适应哈希可以提升查询速度,通过减少索引层次的遍历,显著提升查询效率;同时基于缓冲池的构建,直接利用已有的数据和索引页
拓展:使用自适应哈希的原因
- 提升查询性能:B+树已经非常高效,但是其需要多层遍历;自适应哈希索引通过直接映射的方式减少查询路径上的层次
- 减少系统资源消耗:针对于高频访问的路径,B+树多次比较和遍历会大量消耗CPU资源;自适应哈希通过避免遍历和比较,降低CPU资源的利用
- 简化高频查询:针对于某些频繁重复查询的模式,自适应哈希索引可以显著减少查询时间
拓展:自适应哈希KV设置
Key来源:自适应哈希Key是查询条件的索引键值;Value则是该Key对应缓冲池中的B+树页的内存地址。自适应哈希索引通过将Key映射到该Value,可以直接跳转到缓冲池中的内存位置,避免了遍历B+树的操作
映射过程分析:当某一个索引键频繁的被访问,自适应哈希索引会记录该Key和Value的关系;查询的时候直接通过哈希值找到对应的Value,无需要再去访问B+树的多级结构
拓展:自适应哈希索引存储位置
自适应哈希是存储在缓冲池中的内存区域中,缓冲池初始化后分配一部分空间用于存储自适应哈希;
为了避免高并发访问自适应哈希引起锁的竞争,InnoDB支持对哈希索引进行分区,该分区机制可以显著减少多线程访问自适应哈希索引时的冲突
自我理解&&
例如在图书馆找一本书的场景为被背景。传统B+树索引则每次读者找书,都逐层开始招数,即使手里有目录,但还是需要一页一页的查找;自适应哈希则是将那些热门书籍放在(缓冲区)的快速取书区中,所以此时找书的时候就不需要去按照目录逐层寻找。
高并发场景下就会出现问题,因为设置快速取书区只有一张桌子,当多个人去找不同的热门书籍的时候,这些人就会排队,所以也就导致了锁竞争
分区解决该问题的思路,图书馆中设置多个桌子,根据热门数据的类型将其放在不同的桌子,那么当读者来找书的时候,找到不同的桌子,然后在自己的分区中上锁,这样就提高了效率
技术角度理解&&
自适应哈希的分区机制,也就是把一个大的快速指引区域(一个单个哈希表)拆分成多个小的指引区域;通过哈希函数计算,将不同查询条件(Key)分配到不同区
这样做的好处就在于当多个线程访问的时候,如果其查询的数据分配到了不同的分区,也是可以同时工作的,不会相互阻塞。
自适应哈希的配置
启用和禁用
// 临时关闭和启用SET GLOBAL innodb_adaptive_hash_index = OFF; -- 禁用
SET GLOBAL innodb_adaptive_hash_index = ON; -- 启用// 配置文件[mysqld]
innodb_adaptive_hash_index = 0 # 禁用
分区数量配置
通过调整哈希索引分区数量,从而减少高并发访问时的锁竞争;每个分区都有一个独立的哈希表,线程访问的时候只需要锁定对应分区即可,不需要整个哈希表
SET GLOBAL innodb_adaptive_hash_index_parts = 16; -- 设置为 16 个分区// 配置文件[mysqld]
innodb_adaptive_hash_index_parts = 16
缓冲池大小设置
SET GLOBAL innodb_buffer_pool_size = 8G; -- 设置缓冲池为 8GB
查看自适应哈希信息
查看自适应哈希的状态
- Hash table size:哈希表大小。
- Used cells:已使用的哈希表条目。
- Total searches 和 Successful searches:表示查询命中率,越接近 100%,AHI 的效果越好
查询性能统计
命中率计算
日志缓冲区
日志缓冲区的作用
总结
-
缓存事务日志
- 在事务执行过程中,InnoDB 会将所有需要写入到 Redo Log 的信息先写入日志缓冲区
- 这些信息包括事务对表数据的修改,用于在数据库崩溃时进行恢复
-
提高写入效率
- 直接将每次事务的日志写入磁盘会产生大量的随机 I/O 操作,影响性能
- 日志缓冲区通过将多个事务的日志批量写入磁盘,显著减少磁盘写入次数,从而提高性能
-
事务持久性
- Redo Log 是 InnoDB 支持事务持久性的核心组件。即使数据库崩溃,也可以通过 Redo Log 恢复已提交的事务
- 日志缓冲区确保在事务提交时,相关日志已经持久化到磁盘
- 崩溃恢复
- 当 MySQL 服务意外中断或崩溃时,日志缓冲区中的数据如果已经被刷新到磁盘的 Redo Log 文件,InnoDB 可以利用这些日志恢复数据
日志缓冲区写入磁盘与内存比较
日志缓冲区工作流程分析
当事务修改表中数据的时候,InnoDB会先修改操作记录未日志条目,然后写入日志缓冲区;日志缓冲区中的日志会定期或者在事务提交的时候刷新到磁盘中;刷新的时间则是由参数innodb_flush_log_at_trx_commit决定;当日志从缓冲区刷新到磁盘后,事务的修改才被认为是持久化
日志通过缓冲区写入的原因分析
根本原因就是尽量减少磁盘I/O,先写入到缓冲区中,然后统一刷新到磁盘中,这样就实现了一次磁盘I/O写入多条日志,从而提高效率