只是做了简单的总结和简述,很多地方并没有那么详尽,参考书本《MySQL是怎么运行的:从根上理解MySQL》
InnDB是怎么存储数据的
- InnDB页的记录结构
- COMPACT行格式
- 其他行格式
- InnDB的数据页结构
- File Header
- Page Header
- Free Space(User Records Infimum+ supremum)
- Page Directory
- File Trailer
- B+树 索引
InnDB页的记录结构
InnDB是一个将表中数据存储于硬盘中的存储引擎。与之区别的:Memory(存储于内存中),与之相似的:MylSAM;
对于InnDB来说,数据是存储于磁盘,但是真正处理数据的过程发生在内存中,所以需要经过磁盘IO将 磁盘中的数据加载到内存。InnDB采取了很优秀的方式:将数据划分为若干个页,以页作为磁盘和内存之间交互的基本单位,InnoDB中页的大小一般为 16 KB。 也就是在一般情况下,一次最少从磁盘中读取16KB的内容到内存中,一次最少把内存中的16KB内容刷新到磁盘中。
我们平时是以记录为单位来向表中插入数据的,这些记录在磁盘上的存放方式也被称为行格式或者记录格式。InnDB拥有着多种行格式,原理上大体相同,主要探讨COMPACT行格式。
COMPACT行格式
如图,我们可以把一条记录分为 记录的额外信息
和记录的真实数据
两大部分。
- 记录的额外信息(简述)
这部分信息是为了描述记录而不得不添加的额外的一些信息,额外信息分外三类变长字段长度列表
、NULL值列表
、记录头信息
。边长字段长度列表
:用于存储可变长(VARCHAR、TEXT)字段的真实数据占用的字节长度,并按照列的顺序逆序存放 。NULL值列表
: 顾名思义,存储的即字段可为NULL的列的位置,逆序存放
(二进制位1时,表示该列位NULL,反之则为0)记录头信息
: 有多个字段,部分字段介绍如下:
字段名 | 介绍 |
delete_mask | 标记记录是否被删除(删除则标记为1) |
min_rec_mask | B+树每层非叶子节点的最小记录都会添加该标记 |
n_owned | 表示到本条记录拥有的记录数 |
heap_no | 表示当前记录在堆中的位置序号 |
record_type | 表示记录类型,0为普通记录,1为B+树非叶子节点记录(目录记录),2表示最小记录,3表示最大记录 |
next_record | 表示下一条记录的位置 |
- 记录的真实数据
对于InnDB来说,真实数据除了我们自己定义的数据字段之外还有2个或3个隐藏字段(取决于我们是否定义了主键或Unique键)
列名 | 描述 |
row_id | 行ID,唯一标识记录,当我们定义了主键或者Unique键则不会有该字段 |
tx_id | 事务ID,用于确保ACID |
roll_poniter | 回滚指针,同样是用于确保ACID |
实际上这几个列的真正名称其实是:DB_ROW_ID、DB_TRX_ID、DB_ROLL_PTR
其他行格式
除COMPACT之外还有Redundant等行格式,MySQL5.7默认行格式为Dynamic,与COMPACT相似,区别在于针对行溢出
处理方式不同。
COMPACT会存储溢出数据的一部分数据(768字节),把剩余的数据分散存储至其他的页面,在记录真实的数据处使用20个字节存储指向这些页的地址。
Dynamic则会把所有的字节都从存储到其他的页面中,只在真实数据处存储其他页面的的地址
InnDB的数据页结构
数据页代表的就是这块16KB大小的存储空间
File Header
文件头部针对各种类型的页都通用,它描述了一些针对各种页都通用的一些信息,比方说这个页的编号是多少,它的上一个页、下一个页是谁。
Page Header
存储各种状态信息,具体各个字节的含义可以参考下方表格:
对于PAGE_N_DIR_SLOTS
到PAGE_LAST_INSERT
以及PAGE_N_RECS
的含义可先看Page Directory
理解。
Free Space(User Records Infimum+ supremum)
Free Spcae区域其实就是用于分配User Records区域的内存区域,当Free Space区域内存全部分配完成,则表明该页面数据量已达最大,需要分页。
infimum+supremum区域为最小记录和最大记录的存储区域
记录头信息中的next_record列 表示从当前记录的真实数据到下一条记录的真实数据的地址偏移量 可以通过一条记录找到它的下一条记录Infimum记录(也就是最小记录) 的下一条记录就是本页中主键值最小的用户记录,而本页中主键值最大的用户记录的下一条记录就是 Supremum记录(也就是最大记录)
Page Directory
InnoDB会把页中的记录划分为若干个组,每个组的最后一个记录的地址偏移量作为一个槽,存放在Page Directory中,所以在一个页中根据主键查找记录是非常快的,分为两步:
- 通过二分法确定该记录所在的槽。
- 通过记录的next_record属性遍历该槽所在的组中的各个记录。
具体的记录槽分配过程这里就不作过多复述了。
File Trailer
这个部分是和File Header中的校验和相对应的。每当一个页面在内存中修改了,在同步之前就要把它的校验和算出来,因为File Header在页面的前边,所以校验和会被首先同步到磁盘,当完全写完时,校验和也会被写到页的尾部,如果完全同步成功,则页的首部和尾部的校验和应该是一致的。如果写了一半儿断电了,那么在File Header中的校验和就代表着已经修改过的页,而在File Trailer中的校验和代表着原先的页,二者不同则意味着同步中间出了错。
用于校验页的完整性
B+树 索引
通过InnDB页的介绍我们知道各个数据页可以组成一个双向链表,而每个数据页中的记录又按照主键从小到大的顺序组成了单向链表,每个数据页都会为存储在其中的记录生成页目录,通过主键查找记录,可以在页目录中使用二分法快速定位相对应的槽,然后在遍历对应槽中的记录。
InnDB形成B+树索引的过程:
- 每当为某个表创建一个B+树索引(聚簇索引不是人为创建的,默认就有)的时候,都会为这个索引创建一个根节点页面。最开始表中没有数据的时候,每个B+树索引对应的根节点中既没有用户记录,也没有目录项记录。
- 随后向表中插入用户记录时,先把用户记录存储到这个根节点中。
- 当根节点中的可用空间用完时继续插入记录,此时会将根节点中的所有记录复制到一个新分配的页,比如页a中,然后对这个新页进行页分裂的操作,得到另一个新页,比如页b。这时新插入的记录根据键值(也就是聚簇索引中的主键值,二级索引中对应的索引列的值)的大小就会被分配到页a或者页b中,而根节点便升级为存储目录项记录的页。
- 一个B+树索引的根节点自诞生之日起,便不会再移动