如下图,我用不同颜色表示了MySQL中数据在内存和外存中的位置。其中SQL层主要会维护自己元信息(frm),而InnoDB层也会维护自己的元信息以及其他数据,本文主要关注InnoDB的数据。
内存结构简述
内存中主要就是Buffer Pool以及一些其他的辅助缓存,比较重要的有Change Buffer,AHI,这些结构本文暂不讨论,这里先讨论BufferPool。
BufferPool的空间由srv_buf_pool_size
(总大小)和srv_buf_pool_instances
控制。
关于bufferpool,按照层级划分有如下结构:
最大的是buf_pool_t表示一个bufferpool instance,其中安装buf_chunk_t进行页面分配,默认大小为128MB(在8中可动态调整);具体的数据块就由buf_block_t表示,需要区分下面三个概念:
- frame代表内存的虚拟地址空间的一个16k单元,其是一个内存地址,指向实际的数据。
- page(buf_page_t)是该页上的相关信息。
- Block代表一个Control Block(
buf_block_t
),对于每个frame对应一个ControlBlock结构进行控制信息管理,但这些信息不写回内存。
每个BufferPool将不同类型的Page组织成对应的List,便于快速找到需要类型的page;
Free List:空闲Page所在的链表,便于快速取出一个可用页。
LRU List:最近访问过的Page所在的链表,在InnoDB中,LRUList之上还有一个PageHash,避免List的顺序遍历;同时将List分为Young和Old,并采用了更加精细的LRU策略,避免频繁更改List。Page通过
buf_page_get_gen
读取,有不同的读取模式,默认的是BUF_GET
,大概的读逻辑如下图,新页都是通过buf_LRU_add_block(TRUE)
先放到OLD区:Flush List:在mtr_commit时,将相应的脏页挂在FlushList上,等待刷盘;FlushList是LRUList的一个子集;FlushList中的页是按照page->oldest_modification_lsn降序排列(MySQL5.7);checkpoint_lsn会根据每个FlushList的oldest_modification_lsn进行判断。
另外,BufferPool内还有一个page_hash,这样定位page时,不需要每次都遍历list。
除BuffPool之外,还有一个dictionary cache,dictionary cache是全局共享对象的cache,这其中的数据同样在系统表空间ibdata1中存在。比如,这里不做进一步讨论。
- tablespace defintion
- schema definition
- table definition
- stored program definition
- character set definition
- collation definition
外存结构简述
在外存中,按照表空间(space)进行组织;当启动了innodb_file_per_table
参数后,每个数据表对应一个文件(参看系统表:INFORMATION_SCHEMA.INNODB_SYS_DATAFILES
)。
而对于全局共享的对象,需要放在共享的表空间SysSpace(全局变量:innodb_data_file_path
,默认为ibdata1,spaceid=0)中,这之中除了每个表空间都有的对象(ibuf_bitmap、inode等)外,其中还有如下对象:
公共对象
Change Buffer Tree
Insert Buffer是二级索引变更的缓存;之前只有insert操作,后来也支持了delete/update/purge操作,成为ChangeBuffer;但是命名上没有改变,在内存中还是叫InsertBuffer;在外存中,放在ibuf文件中。
Transaction Sys Header:事务信息
Dict :目录信息
- 0号回滚段
- Double Write Buffer
除了space0是共享的以外,我们还可以通过create tablespace创建General Tablespace,同样是全局共享的。
InnoDB Instrinsic Tables:InnoDB引擎内部的表,没有undo和redo,用户不可创建,只供引擎内部使用。
类似地,InnoDB在space中逐级的切分下去,直到最后的Record。每个space有若干个file或者diskpartition;每个file分为若干个segment;其中有LeafNodeSegment、NonLeafNodeSegment、RollbackSegment,见General Tablespace。每个segment中有若干个固定大小的page,Page可以选择进行压缩:
- 对于uncompressed的表空间,page是16Kb;
- 对于compressed的表空间,page是1~16Kb。
注意Segment概念只是逻辑上的概念,并不是物理上的结构。物理上Page的组织形式是Extent,每次分配按照Extent进行分配,这有点像BufferPool的chunk。
另外对于Record,InnoDB需要与SQL层进行数据转换(
row_sel_store_mysql_field
vsrow_mysql_store_col_in_innobase_format
),
- 对于一个整形数据,MySQL采用的是小端存储,InnoDB采用大端存储。
- 有精度的浮点数,MySQL和InnoDB存储方式相同。
ibd文件
关于InnoDB的文件格式,可以通过innodb_ruby学习。
基于以上的讨论,当我们打开参数(innodb_file_per_table
)时,对于一个ibd文件就是对于的表空间。
这里我创建一个测试表t1,包含一个一级索引和一个二级索引,如下:
mysql> show create table t1\G
*************************** 1. row ***************************
Table: t1
Create Table: CREATE TABLE `t1` (
`id` int(11) NOT NULL AUTO_INCREMENT,
`salary` int(11) DEFAULT NULL,
`name` varchar(255) DEFAULT '',
PRIMARY KEY (`id`),
KEY `i1` (`salary`)
) ENGINE=InnoDB AUTO_INCREMENT=103 DEFAULT CHARSET=utf8
1 row in set (0.00 sec)
mysql> select *
-> from
-> INFORMATION_SCHEMA.INNODB_SYS_TABLES
-> join INFORMATION_SCHEMA.INNODB_SYS_DATAFILES on INFORMATION_SCHEMA.INNODB_SYS_TABLES.space = INFORMATION_SCHEMA.INNODB_SYS_DATAFILES.space
-> join INFORMATION_SCHEMA.INNODB_SYS_INDEXES on INFORMATION_SCHEMA.INNODB_SYS_TABLES.TABLE_ID = INFORMATION_SCHEMA.INNODB_SYS_INDEXES.TABLE_ID
-> where INFORMATION_SCHEMA.INNODB_SYS_TABLES.TABLE_ID = 40\G
*************************** 1. row ***************************
TABLE_ID: 40
NAME: testdb/t1
FLAG: 33
N_COLS: 6
SPACE: 23
FILE_FORMAT: Barracuda
ROW_FORMAT: Dynamic
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Single
SPACE: 23
PATH: ./testdb/t1.ibd
INDEX_ID: 41
NAME: PRIMARY
TABLE_ID: 40
TYPE: 3
N_FIELDS: 1
PAGE_NO: 3
SPACE: 23
MERGE_THRESHOLD: 50
*************************** 2. row ***************************
TABLE_ID: 40
NAME: testdb/t1
FLAG: 33
N_COLS: 6
SPACE: 23
FILE_FORMAT: Barracuda
ROW_FORMAT: Dynamic
ZIP_PAGE_SIZE: 0
SPACE_TYPE: Single
SPACE: 23
PATH: ./testdb/t1.ibd
INDEX_ID: 42
NAME: i1
TABLE_ID: 40
TYPE: 0
N_FIELDS: 1
PAGE_NO: 4
SPACE: 23
MERGE_THRESHOLD: 50
2 rows in set (0.00 sec)
表空间的头部几个重要的页的信息:
FSP_HDR页:该页是对整个表空间进行管理的头部页
IBUF_BITMAP页:为了避免ibuf的合并造成数据页的分裂合并,在ibufbitmap中保存了叶子节点的可用空间与其他状态(内容比较复杂,将在独立的changebuffer章节阐述)。
INODE页:外存空间的管理基本单位是Page,而连续的Page在逻辑上有组织成Segment;Segment只是一个逻辑的组织单位,一个Segment在物理上对应若干个file segment inode,简称INODE。这里的INODE页中包含很多inode entry,每个inode entry可以创建一个逻辑的segment,如下图,INODE entry的头部标记一个。
INDEX页:索引页,这是最常见的页类型;
在InnoDB中有多种页类型,以上我就介绍了3种。那么,对于一个表的独立表空间中的页是如何管理的呢?表未添加二级索引初始化时,为
只有一个索引页,即主键索引。
然后插入了1000条数据,
此时,在原索引页之后多了一个索引页,由prev和next看出,这是索引数据页。
然后创建了一个新的索引i2
在原索引页之后,追加了一个新的索引页,存放二级索引。
那么,一个表的索引页只是这么追加,如何找到每个索引的root page呢?
这个信息是在单独的InnoDB的系统表中存储的,主要有以下四个:
SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_TABLES ;
SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_COLUMNS ;
SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_INDEXES;
SELECT * FROM INFORMATION_SCHEMA.INNODB_SYS_FIELDS;
在INNODB_SYS_INDEXES中,根据了(spaceid,pageno),定位每个索引的root。
而在ibd文件内部,由FSP_HDR、INODE和XDES页中的定义了一些外存的list来管理表空间的使用。