InnoDB缓存机制和底层数据结构
前言
图片链接没转,后续补上
参考资料
| 资料名称 | 来源地址 |
|---|---|
| 《MySQL王者晋级之路》 | 图书 |
| 《MySQL技术内幕 InnoDB存储引擎》 | 图书 |
| MySQL 5.7官方文档 | https://dev.mysql.com/doc/refman/5.7/en/ |
InnoDB引擎概述
InnoDB是事务安全的MySQL存储引擎。最早由Innobase Oy公司开发,从MySQL 5.5版本开始是默认的表存储引擎(之前版本InnoDB存储引擎仅在Windows下为默认的存储引擎)。该存储引擎是第一个完整支持ACID事务的MySQL存储引擎,其特点是行锁设计、支持MVCC、支持外键、提供一致性非锁定读,同时被设计用来最有效地利用以及使用内存和CPU。
InnoDB体系架构

InnoDB存储引擎有多个内存块,这些内存块组成一个内存池,主要负责如下工作:
- 维护所有进程、线程需要访问的多个内部数据结构
- 缓存磁盘上的数据,方便快速读取,同时在对磁盘文件的数据修改之前在这里缓存
- 重做日志(redo log)缓冲
后台线程的主要作用:
- 负责刷新内存池中的数据,保证缓冲池中的内存缓存的是最近的数据
- 将已修改的数据文件刷新到磁盘文件
InnoDB缓存机制
缓冲池(Buffer Pool)
Buffer Pool是InnoDB在主内存中用于缓存查询到的表和索引数据的区域,它能够使InnoDB直接从内存中处理经常使用的数据,从而加快处理速度。在专用MySQL服务器上,多达80%的物理内存通常分配给缓冲池。
为提高大批量数据的读取效率,Buffer Pool被划分为多个页,每个页包可以含多条行记录。为了提高缓存管理的效率,Buffer Pool被实现为页面的列表,很少使用的数据页使用变种的LRU算法从缓存中老化。
缓冲池的 LRU 算法
普通的LRU算法实现的是末尾淘汰制,当整个链表已满时,淘汰尾部,将新的数据页加入头部;
Buffer Pool的LRU算法与普通的LRU算法不一样,新数据页插入时并不从头部插入,而是从中间位置插入(默认配置下,该位置从表头计算为列表5/8的位置),在该位置,列表被分成了如下两个子列表:
- 在头部是最近被访问的新页列表
- 在尾部是很少被访问的旧页列表
默认情况下,该变种的LRU算法特点如下:
- Buffer Pool的3/8用于旧页列表
- 列表的中点事新页列表的尾部和旧页列表的头部相交的边界
- 当 InnoDB从磁盘读一页数据并放入缓冲池中时,它会将此页插入到列表的中间位置(也就是旧页列表的头部)。发生读页一般是因为用户查询数据,或者InnoDB自动触发的预读(read-ahead)操作。
- 读取旧页列表中的数据会让该页变年轻,并将其移动到新页列表的头部。如果是因为用户查询读造成该页被读取,则该页会立即被标识为年轻,并直接移动到新页列表头部。如果该页因为预读被读取,则首次读取该页并放入缓冲池时不会将该页放入新页列表头部,而是放入列表中点,需要再次读取才能使该页被标识为年轻状态,这样是为了避免预读导致的缓冲池搅动
- 数据库运行过程中,Buffer Pool中不被访问的页因其他被访问的页的移动或新插入的页而老化向尾部移动,最后到达旧页列表尾部并被移出
缓冲池搅动
预读或者表、索引的扫描都会造成大量数据页被读入缓冲池,这些页可能会在短时间内被读取若干次,然后从此不再访问,因此,数据页的大量插入到新页头部可能会将热点数据迅速老化移除出缓冲池。这种情况叫做缓冲池搅动。为解决该问题,MySQL提供了配置参数innodb_old_blocks_time 用来指定该页在放入缓冲池后第一次读之后一定时间内(时间窗口,单位毫秒)读取不会被标识为年轻,也就是不会被移动到列表头部 。
相关参数
innodb_old_blocks_pct 该参数为旧页列表占LRU列表的百分比,默认为37即3/8,取值范围为[5,95]。
innodb_old_blocks_time 指定第一次访问页面之后的时间窗口(以毫秒为单位),在此期间,可以访问页面,而无需移动页到LRU新页列表的前端。innodb_old_blocks_time的默认值是1000。增加这个值会使越来越多的块从缓冲池中老化得更快。
缓冲池状态监控
使用InnoDB标准监视器可以监视缓冲池的状态:
mysql> SHOW ENGINE INNODB STATUS\G
----------------------
BUFFER POOL AND MEMORY
----------------------
Total large memory allocated 17170432 #分配给缓冲池的内存大小(bytes)
Dictionary memory allocated 352153 #分配给InnoDB数据字典的内存大小(bytes)
Buffer pool size 1024 #分配给缓冲池的总页数
Free buffers 761 #缓冲池可用列表的总页数
Database pages 256 #缓冲池LRU列表的总页数
Old database pages 0 #缓冲池LRU旧页列表的总页数
Modified db pages 0 #缓冲池中当前修改的页数
Pending reads 0 #等待读入缓冲池的缓冲池页数
Pending writes: LRU 0, flush list 0, single page 0 #等待写入磁盘的脏页数
Pages made young 0, not young 0
0.00 youngs/s, 0.00 non-youngs/s
Pages read 8439, created 1241, written 34441
0.00 reads/s, 0.00 creates/s, 0.00 writes/s
No buffer pool page gets since the last printout
Pages read ahead 0.00/s, evicted without access 0.00/s, Random read ahead 0.00/s
LRU len: 256, unzip_LRU len: 0
I/O sum[0]:cur[0], unzip sum[0]:cur[0]
| Name | Description |
|---|---|
| Total memory allocated | The total memory allocated for the buffer pool in bytes. |
| Dictionary memory allocated | The total memory allocated for the InnoDB data dictionary in bytes. |
| Buffer pool size | The total size in pages allocated to the buffer pool. |
| Free buffers | The total size in pages of the buffer pool free list. |
| Database pages | The total size in pages of the buffer pool LRU list. |
| Old database pages | The total size in pages of the buffer pool old LRU sublist. |
| Modified db pages | The current number of pages modified in the buffer pool. |
| Pending reads | The number of buffer pool pages waiting to be read into the buffer pool. |
| Pending writes LRU | The number of old dirty pages within the buffer pool to be written from the bottom of the LRU list. |
| Pending writes flush list | The number of buffer pool pages to be flushed during checkpointing. |
| Pending writes single page | The number of pending independent page writes within the buffer pool. |
| Pages made young | The total number of pages made young in the buffer pool LRU list (moved to the head of sublist of “new” pages). |
| Pages made not young | The total number of pages not made young in the buffer pool LRU list (pages that have remained in the “old” sublist without being made young). |
| youngs/s | The per second average of accesses to old pages in the buffer pool LRU list that have resulted in making pages young. See the notes that follow this table for more information. |
| non-youngs/s | The per second average of accesses to old pages in the buffer pool LRU list that have resulted in not making pages young. See the notes that follow this table for more information. |
| Pages read | The total number of pages read from the buffer pool. |
| Pages created | The total number of pages created within the buffer pool. |
| Pages written | The total number of pages written from the buffer pool. |
| reads/s | The per second average number of buffer pool page reads per second. |
| creates/s | The per second average number of buffer pool pages created per second. |
| writes/s | The per second average number of buffer pool page writes per second. |
| Buffer pool hit rate | The buffer pool page hit rate for pages read from the buffer pool memory vs from disk storage. |
| young-making rate | The average hit rate at which page accesses have resulted in making pages young. See the notes that follow this table for more information. |
| not (young-making rate) | The average hit rate at which page accesses have not resulted in making pages young. See the notes that follow this table for more information. |
| Pages read ahead | The per second average of read ahead operations. |
| Pages evicted without access | The per second average of the pages evicted without being accessed from the buffer pool. |
| Random read ahead | The per second average of random read ahead operations. |
| LRU len | The total size in pages of the buffer pool LRU list. |
| unzip_LRU len | The total size in pages of the buffer pool unzip_LRU list. |
| I/O sum | The total number of buffer pool LRU list pages accessed, for the last 50 seconds. |
| I/O cur | The total number of buffer pool LRU list pages accessed. |
| I/O unzip sum | The total number of buffer pool unzip_LRU list pages accessed. |
| I/O unzip cur | The total number of buffer pool unzip_LRU list pages accessed. |
缓冲池配置
MySQL 官方文档对于缓冲池的相关配置参考:
You can configure the various aspects of the buffer pool to improve performance.
- Ideally, you set the size of the buffer pool to as large a value as practical, leaving enough memory for other processes on the server to run without excessive paging. The larger the buffer pool, the more
InnoDBacts like an in-memory database, reading data from disk once and then accessing the data from memory during subsequent reads. See Section 14.8.3.1, “Configuring InnoDB Buffer Pool Size”. - On 64-bit systems with sufficient memory, you can split the buffer pool into multiple parts to minimize contention for memory structures among concurrent operations. For details, see Section 14.8.3.2, “Configuring Multiple Buffer Pool Instances”.
- You can keep frequently accessed data in memory regardless of sudden spikes of activity from operations that would bring large amounts of infrequently accessed data into the buffer pool. For details, see Section 14.8.3.3, “Making the Buffer Pool Scan Resistant”.
- You can control when and how to perform read-ahead requests to prefetch pages into the buffer pool asynchronously in anticipation that the pages will be needed soon. For details, see Section 14.8.3.4, “Configuring InnoDB Buffer Pool Prefetching (Read-Ahead)”.
- You can control when background flushing occurs and whether or not the rate of flushing is dynamically adjusted based on workload. For details, see Section 14.8.3.5, “Configuring Buffer Pool Flushing”.
- You can configure how
InnoDBpreserves the current buffer pool state to avoid a lengthy warmup period after a server restart. For details, see Section 14.8.3.6, “Saving and Restoring the Buffer Pool State”.
变更缓冲(Change Buffer)
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-IIXmsOIv-1574392220787)(innodb缓存方案和底层存储结构.assets/innodb-change-buffer.png)]
Change buffer的主要目的是将对二级索引的数据操作缓存下来,以此减少二级索引的随机IO,并达到操作合并的效果。 在MySQL5.5之前的版本中,由于只支持缓存insert操作,所以最初叫做insert buffer,只是后来的版本中支持了更多的操作类型缓存,才改叫change buffer 。
当执行插入、更新或删除操作时,会触发与表有关的索引的操作,表的索引也会进行相应的插入、更新或删除,这样会影响到对源表的数据的操作速度。当对表进行DML操作时,非聚集索引的值通常是无序的,如果索引所在的页不在缓冲池中,就需要从磁盘中读取该页到缓冲池中进行更新,然后刷新到磁盘,如果有大量的DML操作,就会产生大量的磁盘随机读取,影响性能。
Change Buffer的作用,就是当变更的二级索引的相关页不在缓冲池中时,将二级索引的更改缓存到Change Buffer区,当该索引的页被加载到缓冲池中时,根据Change Buffer区中的更改对其进行更新合并,更新的页之后被刷新到磁盘。InnoDB主线程在服务器接近空闲时以及在缓慢关闭期间合并缓冲的更改。
通过Change Buffer,可以减少磁盘读写,对于具有大量DML操作的应用程序来说该功能很有用。不过Change Buffer也有弊端,那就是它占用了Buffer Pool的空间,从而减少了缓存数据页的可用内存。
相关配置参数:
innodb_change_buffering 该参数可以指定哪些操作可以使用change buffer
可选值包含如下:
-
all缓冲全部操作
-
none不缓存任何操作
-
inserts仅insert操作
-
deletes包含delete标记和update操作(update分两步,删除原有记录,插入新的记录)
-
changesinsert和delete标记操作
-
purges缓冲后台进程的物理删除操作
innodb_change_buffer_max_size 该参数可以设定change buffer占用缓冲池的百分比最大值,默认值为25,最大可以设置为50。innodb_change_buffer_max_size设置是动态的,允许在不重新启动服务器的情况下修改设置。
Change buffer监控:
通过以下指令可以监控Change Buffer状态:
mysql> SHOW ENGINE INNODB STATUS\G
本地使用MySQL 5.7.19版本,以“ INSERT BUFFER AND ADAPTIVE HASH INDEX ”为标题的输出如下所示:
-------------------------------------
INSERT BUFFER AND ADAPTIVE HASH INDEX
-------------------------------------
Ibuf: size 1, free list len 389, seg size 391, 18 merges
merged operations:
insert 26, delete mark 0, delete 0
discarded operations:
insert 0, delete mark 0, delete 0
Hash table size 4441, node heap has 1 buffer(s)
Hash table size 4441, node heap has 1 buffer(s)
Hash table size 4441, node heap has 1 buffer(s)
Hash table size 4441, node heap has 1 buffer(s)
Hash table size 4441, node heap has 2 buffer(s)
Hash table size 4441, node heap has 0 buffer(s)
Hash table size 4441, node heap has 0 buffer(s)
Hash table size 4441, node heap has 1 buffer(s)
0.00 hash searches/s, 0.00 non-hash searches/s
seg size显示了当前Change Buffer的大小为391x16KB,约为6.1MB;free list len表示空闲列表的长度;size表示已合并记录页的数量。
可以看到这里显示了merged operations和discarded operations,并且下面具体显示了Change Buffer中每个操作的次数。insert表示insert buffer,delete mark表示delete buffer,delete表示purge buffer;discarded operations表示当Change Buffer发生merge时,表已经被删除,此时就无需将记录合并到二级索引。
日志缓冲(Log Buffer)
日志缓冲是存储要写入磁盘日志文件的数据的内存区域。日志缓冲的数据会定期刷新到磁盘。大的日志缓冲区可以确保大事务无需在事务提交前将重做日志写入磁盘。因此如果应用程序的事务较多,增加日志缓冲区的大小可以减少磁盘的I/O。
相关参数:
innodb_log_buffer_size 日志缓冲区大小,默认为16MB,单位是byte
innodb_flush_log_at_trx_commit 控制如何将日志缓冲区的内容写入并刷新到磁盘
| value | remark |
|---|---|
| 0 | 每秒将日志写入并刷新到磁盘一次。未刷新日志的事务可能在崩溃中丢失。 |
| 1(default) | 每次事务提交时,日志都会写入并刷新到磁盘。 |
| 2 | 每次事务提交后都会写入日志,并每秒刷新一次到磁盘。未刷新日志的事务可能在崩溃中丢失。 |
innodb_flush_log_at_timeout 每隔N秒写入并刷新日志。innodb_flush_log_at_timeout允许增加刷新之间的超时时间,以减少刷新频率并避免影响binlog提交的性能。innodb_flush_log_at_timeout的默认设置是每秒一次。取值范围[1,2700]。
查询缓存( Query Cache )
【注】从MySQL 5.7.20开始,查询缓存已被弃用,并在MySQL 8.0中被删除。
查询缓存就是将SELECT语句作为Key(select语句的字节不同则被认为是不同的key),查询结果作为VALUE的形式进行缓存。 如果服务器收到相同的语句,则服务器从查询缓存中检索结果,而不是再次解析并执行该语句。查询缓存在会话之间共享,因此可以响应另一个客户端发出的同一查询。
缺点:
- hash性能问题和命中率问题
- 查询缓存容易失效:当表内容发生变化或者表结构发生变化,INSERT, UPDATE, DELETE, TRUNCATE, ALTER TABLE, DROP TABLE, or DROP DATABASE等操作都会导致表的查询缓存失效
- 查询缓存中的结果容易产生重复;因为查询缓存中缓存的是查询结果,所以不同的查询的结果很容易重复
- 查询缓存会增加检查和清理缓存中记录集的开销
- SELECT语句不能被缓存的情况
- 查询语句中加了SQL_NO_CACHE参数;
- 查询语句中含有获得值的函数,包涵自定义函数,如:CURDATE()、GET_LOCK()、RAND()、CONVERT_TZ等;
- 对系统数据库的查询:mysql、information_schema
- 查询语句中使用SESSION级别变量或存储过程中的局部变量;
- 查询语句中使用了LOCK IN SHARE MODE、FOR UPDATE的语句
- 查询语句中类似SELECT …INTO 导出数据的语句;
- 事务隔离级别为:Serializable情况下,所有查询语句都不能缓存;
- 对临时表的查询操作;
- 存在警告信息的查询语句;
- 不涉及任何表或视图的查询语句;
- 某用户只有列级别权限的查询语句;
适合使用场景:
表的读操作非常频繁,而且数据不常更新。
指定SELECT缓存
select语句中可以通过 SQL_CACHE 和 SQL_NO_CACHE 指定缓存或不缓存该查询结果
-
SQL_CACHE
如果查询结果是可缓存的,并且
query_cache_type的值是ON或DEMAND,则查询结果将被缓存。 -
SQL_NO_CACHE
服务器不使用查询缓存。它既不检查查询缓存以查看结果是否已缓存,也不缓存查询结果。
SELECT SQL_CACHE id, name FROM customer;
SELECT SQL_NO_CACHE id, name FROM customer;
相关参数
have_query_cache 查询缓存是否可用
query_cache_size 查询缓存的大小,默认为1048576bytes,即1MB
query_cache_type 查询缓存的类型
| value | remark |
|---|---|
| OFF | 关闭缓存 |
| ON | 启用缓存,但以SELECT SQL_NO_CACHE开头的语句除外 |
| DEMAND | 启用缓存,只缓存以SELECT SQL_CACHE开头的语句 |
状态监控
mysql> SHOW STATUS LIKE 'Qcache%';
+-------------------------+---------+
| Variable_name | Value |
+-------------------------+---------+
| Qcache_free_blocks | 1 | #缓存中相邻内存块的个数
| Qcache_free_memory | 1031872 | #缓存中的空闲内存
| Qcache_hits | 0 | #查询在缓存中命中次数
| Qcache_inserts | 0 | #插入查询缓存次数
| Qcache_lowmem_prunes | 0 | #内存不足并且必须要进行清理以便为更多查询缓存提供空间的次数
| Qcache_not_cached | 14188 | #不适合进行缓存的查询的数量
| Qcache_queries_in_cache | 0 | #当前缓存的查询的数量
| Qcache_total_blocks | 1 | #缓存中块的数量
+-------------------------+---------+
InnoDB存储结构
逻辑存储结构
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-pzoix7WO-1574392220792)(innodb缓存方案和底层存储结构.assets/image-20191120111018106.png)]
InnoDB的逻辑存储结构如上,所有数据都被逻辑地存储在表空间中,其中表空间又由段(segment)、区(extent)、页(page)组成,在一些文档中页也被称为块(block)。
表空间
InnoDB存储引擎中所有数据都是存储在表空间中
系统表空间
系统表空间以ibdata1命名,在安装数据库初始化数据时就是系统在创建一个ibdata1的表空间文件,它会存储所有数据的信息以及回滚段(undo)的信息。MySQL5.6之后,undo表空间可以通过参数单独设置存储位置。
独立表空间
设置参数innodb_file_per_table=1可以使每张表都有一个独立的表空间文件,而不用存储在ibdata1中。值得注意的是独立表空间存放的是B+树数据、索引和插入缓冲等信息,其他数据还是存放在ibdata1中。目前默认使用的就是独立表空间的存储方式。
临时表空间
MySQL5.7把临时表的数据从系统表空间中抽离出来,形成自己的独立表空间,并把临时表的相关检索信息保存在系统信息表的information_schema库下的innodb_temp_table_info表中。
通用表空间
与系统表空间类似,通用表空间是可以存储多个表的数据的共享表空间。多个表放在同一个表空间中,可以减少存储开销。
段
表空间由段组成,常见的段有数据段,回滚段,索引段等。因为InnoDB表是索引组织的,所以索引即数据,数据即索引,数据段就是B+树的叶子节点(Leaf node segment),索引段即B+树的非叶子节点(Non-leaf node segment)。
区
区由连续的页组成,是物理上连续的一块空间,每个区的大小固定是1MB。
页
页是InnoDB最小的的物理存储分配单位,有数据页和回滚页等。一般情况下,一个区由64个页组成,页的默认大小是16KB。从MySQL5.6开始可以自定义调低页的大小为8KB、4KB,MySQL5.7开始可以调高页的大小为32KB或64KB。
行
InnoDB引擎是面向列的(row-oriented),也就是说数据是按行进行存储的。每个页存放的行记录按照行格式进行存放。
行记录格式
在InnoDB 1.0.X之前,InnoDB存储引擎提供了Compact和Redundant两种格式来存放行记录数据。Redundant是mysql5.0版本之前的行记录存储方式,之后仍然支持这个格式是为了兼容之前版本的格式,5.1之后很少用到了,因为Compact的结构设计比它好,因为compact格式消耗的磁盘空间和备份耗时更小,Redundant相比之下大了一些。compact格式更适用于大多数的业务场景。
在InnoDB 1.0.X版本开始又引入了新的文件格式(file format),以前支持Compact和Redundant格式称为Antelope文件格式,新引入的文件格式称为Barracuda文件格式。Barracuda文件格式下拥有两种新的行记录格式:Compressed和Dynamic,同时,Barracuda文件格式也包括了Antelope所有的文件格式。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-qaxXMUuQ-1574392220797)(innodb缓存方案和底层存储结构.assets/image-20191120150117896.png)]
可以通过如下命令查看表的行格式:
mysql> show table status like '%iot_linkage_info%'\G;
*************************** 1. row ***************************
Name: iot_linkage_info
Engine: InnoDB
Version: 10
Row_format: Dynamic # 这行Row_format表示的就是行记录的格式
Rows: 22
Avg_row_length: 744
Data_length: 16384
Max_data_length: 0
Index_length: 65536
Data_free: 0
Auto_increment: 77
Create_time: 2019-11-11 15:41:51
Update_time: 2019-11-15 15:12:52
Check_time: NULL
Collation: utf8mb4_general_ci
Checksum: NULL
Create_options: row_format=DYNAMIC
Comment:
1 row in set (0.00 sec)
MySQL5.7默认使用dynamic行记录格式和Barracuda文件格式
mysql> show variables like '%innodb_file%';
+--------------------------+-----------+
| Variable_name | Value |
+--------------------------+-----------+
| innodb_file_format | Barracuda |
| innodb_file_format_check | ON |
| innodb_file_format_max | Barracuda |
| innodb_file_per_table | ON |
+--------------------------+-----------+
4 rows in set, 1 warning (0.00 sec)
mysql> show variables like '%row_format%';
+---------------------------+---------+
| Variable_name | Value |
+---------------------------+---------+
| innodb_default_row_format | dynamic |
+---------------------------+---------+
1 row in set, 1 warning (0.00 sec)
Dynamic和Compact基本是类似的,但是它们在行溢出数据的处理上却完全不同,这里我们需要先了解一下行溢出的概念。行溢出就是将需要存储的数据存储在当前页面之外,拆分到多个页进行存储,针对大数据类型text或blob字段类型的数据。
dynamic将数据都存放在溢出的页中(off-page),而数据页只存储前20个字节的指针。在Compact格式下,溢出的列只存放768个前缀字节。dynamic这种行格式模式针对溢出列所在的新页利用率会更高,所以生产环境中建议使用dynamic。
Dynamic格式的行溢出处理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-MBmqC6eY-1574392220801)(innodb缓存方案和底层存储结构.assets/image-20191120173440319.png)]
Compact格式的行溢出处理:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-xjaamQy1-1574392220805)(innodb缓存方案和底层存储结构.assets/image-20191120173351771.png)]
Redundant是最早的行记录格式,相比compact要消耗更多空间,不建议使用;Compressed是压缩行格式,对数据和索引页进行压缩,但只是针对物理存储层面的压缩,在内存中是不压缩的,所以当数据读取内存中就需要转换,会增加了CPU消耗,而且效率也低,压缩比例只接近1/2,压缩带来的负面影响也大,数据库的TPS会下降,影响线上业务。
数据页结构
页是InnoDB存储引擎管理数据库的最小磁盘单位。页类型为B-tree Node的页存放的就是表中行的实际数据,下图为页的结构:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-QSi3BukE-1574392220807)(innodb缓存方案和底层存储结构.assets/image-20191120152625275.png)]
InnoDB数据页由七个部分组成,其中File Header、Page Header、File Trailer的大小是固定的,这些空间用来标记页的一些信息。User Records、Free Space和Page Directory为实际的行记录存储空间,因此大小是动态的。
File Header
File Header用于记录页的头信息,大小为38个字节,组成部分如下:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-g2Cpdd9t-1574392220809)(innodb缓存方案和底层存储结构.assets/image-20191120153417851.png)]
InnoDB存储引擎中页的类型:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-RLCBjRNR-1574392220811)(innodb缓存方案和底层存储结构.assets/image-20191120153750560.png)]
Page Header
Page Header用来记录数据页的状态信息,由14个部分组成,共占用56个字节:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-TTMx91UL-1574392220813)(innodb缓存方案和底层存储结构.assets/image-20191120154133018.png)]
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-4wKw1slx-1574392220818)(innodb缓存方案和底层存储结构.assets/image-20191120154150903.png)]
Infimum和Suprenum Record
InnoDB的每个数据页都有两个虚拟的行记录虚拟最小行(Infimum records)和虚拟最大行(Supremum records),用来限定记录的边界:
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-M6qk3xnn-1574392220820)(innodb缓存方案和底层存储结构.assets/image-20191120154754662.png)]
User Record和Free Space
User Record是实际存储行记录的内容。Free Space是空闲空间,是链表的数据结构。在一条记录被删除后,该空间会被加入到空闲链表中。
Page Directory
Page Directory存放了记录的相对位置。
File Trailer
InnoDB利用File Trailer来保证页完整地写入磁盘。File Trailer只有一个FIL_PAGE_END_LSN部分,占用8个字节,前4个字节代表该页的checksum值,后4个字节和File Header中的FIL_PAGE_LSN相同。通过将这两个值与File Header中的FIL_PAGE_SPACE_OR_CHECKSUM和FIL_PAGE_LSN值进行比较是否一致来保证页的完整性(checksum不是等值比较,需要用到InnoDB的checksum函数进行比较)。
会议疑问点
-
Infimum和Suprenum Record怎么保证B+tree节点是双向链表结构?
关于这两个概念,官网有做介绍 https://dev.mysql.com/doc/internals/en/innodb-infimum-and-supremum-records.html 。Infimum和Suprenum Record用来限定记录的边界,Infimum是比页中任何主键值都要小的值,Suprenum 是指比任何可能大值还要大的值,这两个值在页创建时被建立,并且在任何情况下都不会被删除。Infimum和Suprenum与行记录组成单链表结构,查询记录时,从Infimum开始查找,如果找不到结果会直到查到最后的suprenum为止,然后通过Page Header中的FIL_PAGE_NEXT指针跳转到下一个page继续从Infimum开始逐个查找
所以原文档说的Infimum和Suprenum Record保证B+tree节点是双向链表结构应该是不对的,应该是保证节点内部记录的结构是单链表结构
- 关于脏页的刷新机制Checkpoint
缓冲池设计目的是为了协调CPU速度和磁盘速度的鸿沟,页的操作首先都是在缓冲池中完成的,如果一条DML语句改变了页中的记录,那么此时页是脏的,即缓冲池中的页的版本要比磁盘的新,数据库需要将新版本的页从缓冲池刷新到磁盘,如果页每次发生改变都要将其刷新到磁盘,并且热点数据集中在几个页中,那么数据库的性能就会变得很差,InnoDB使用了一些机制来避免这种问题。
InnoDB采用Write Ahead Log策略来防止宕机数据丢失,即事务提交时,先写重做日志,再修改内存数据页,这样就产生了脏页,由于重做日志不可能无限大,缓冲池也不可能无限大,所以脏页需要刷新到磁盘,另外如果重做日志太大,宕机的时候也会使重做全部日志恢复的时间过长。InnoDB为了避免这些问题,使用checkpoint技术来使数据库宕机时只需要重做上次checkpiont之后的日志。
checkpoint主要解决以下问题:
- 缩短数据库恢复时间
- 缓冲池不可用时,将脏页刷新到磁盘
- 重做日志不可用时,将脏页刷新到磁盘
Checkpoint分为两种:
-
Sharp Checkpoint
发生在数据库关闭的时候将所有脏页刷新到磁盘,这是默认的工作方式,即参数innodb_fast_shutdown=1
-
Fuzzy Checkpoint
数据库运行过程中使用的是Fuzzy checkpoint进行页的刷新,即只刷新一部分脏页,而不是刷新所有脏页。
Fuzzy Checkpoint中包含几种情况:
-
Master Thread Checkpoint
master thread是后台线程中的主线程,内部的flush loop刷新循环中会执行每秒或每十秒将缓冲池中的脏页列表中刷新一定比例的页回磁盘,这个过程是异步的,不会阻塞用户的查询线程。
-
FLUSH_LRU_LIST Checkpoint
因为InnoDB存储引擎需要保证LRU列表中需要有差不多100个空闲页可供使用。在InnoDB1.1.x版本之前,需要检查LRU列表中是否有足够的可用空间操作发生在用户查询线程中,显然这会阻塞用户的查询操作。倘若没有100个可用空闲页,那么InnoDB存储引擎会将LRU列表尾端的页移除。如果这些页中有脏页,那么需要进行Checkpoint,而这些页是来自LRU列表的,因此称为FLUSH_LRU_LIST Checkpoint。
而从MySQL 5.6版本,也就是InnoDB1.2.x版本开始,这个检查被放在了一个单独的Page Cleaner线程中进行,并且用户可以通过参数innodb_lru_scan_depth控制LRU列表中可用页的数量,该值默认为1024,如:
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_lru_scan_depth'; +-----------------------+-------+ | Variable_name | Value | +-----------------------+-------+ | innodb_lru_scan_depth | 1024 | +-----------------------+-------+ -
Async/Sync Flush Checkpoint
指的是重做日志文件不可用的情况,这时需要强制将一些页刷新回磁盘。重做日志不可用的情况是因为当前事务数据库系统对重做日志的设计都是循环使用的,并不是让其无限增大,因为无限增大会造成成本和管理的问题。重做日志可以被重用的部分是指这些重做日志已经不需要在数据库恢复时使用到,可以这样理解,重做日志中的操作已经刷新到磁盘,所以数据库也就在恢复时可以将这部分覆盖重用。但是若此时重做日志还需使用,那就必须强制产生Checkpoint,将缓冲池中的页至少刷新到当前重做日志的位置。
- Dirty Page too much
即脏页的数量太多,导致InnoDB存储引擎强制进行Checkpoint。其目的总的来说还是为了保证缓冲池中有足够可用的页。其可由参数innodb_max_dirty_pages_pct控制, innodb_max_dirty_pages_pct值为75表示当缓冲池中脏页的数量占据75%时,强制进行Checkpoint,刷新一部分的脏页到磁盘。在InnoDB 1.0.x版本之前,该参数默认值为90,之后的版本都为75。
mysql> SHOW GLOBAL VARIABLES LIKE 'innodb_max_dirty_pages_pct; +----------------------------+-------+ | Variable_name | Value | +----------------------------+-------+ | innodb_max_dirty_pages_pct | 75 | +----------------------------+-------+ -
- 关于Change buffer的一些常见问题官网文档有详细解答:
https://dev.mysql.com/doc/refman/5.7/en/faqs-innodb-change-buffer.html
- 查询缓存为什么在8.0被删除的原因,MySQL开发者 Matt Lord 在这篇博客中有做说明:
https://mysqlserverteam.com/mysql-8-0-retiring-support-for-the-query-cache/

浙公网安备 33010602011771号