深刻理解InnoDB -- 存储篇

本文分享InnoDB如何规划表空间,如何存储表空间元信息以及用户数据。html

思考一个问题,若是给你一个文件,让你存储MySql的数据,你会怎么作?mysql

下面是一种比较合理的思路。首先把文件划分红大小相等的块(InnoDB中的页),每次取一块使用。为了管理这些块信息,咱们也拿出一块空间,存储每一块空间的位置,偏移量,以及已经使用和剩余未使用的块(InnoDB中的FSP HEADER PAGE ,文件管理页)
而后根据不一样的逻辑创建对应的对象,如索引对象,回滚信息对象(InnoDB中的段),这些对象从上面分好的块中申请空间使用,并管理属于本身的块,固然,这些对象信息也须要拿出一块空间存储起来(InnoDB中的INODE PAGE)。 这就是InnoDB中段和页的概念git

下面来明确几个核心概念github

  • 表空间
    InnoDB将全部数据(包括表数据,索引,回滚信息,插入缓冲索引页,系统事务信息,二次写缓冲)逻辑地放在一个空间中,称为共享表空间。
    默认表空间的存储文件为data目录下的ibdata1,初始化为10M。算法


  • 一个索引(InnoDB都是B+索引)由两个段管理,叶子节点段(leaf segment)和非叶子节点段(non leaf segment)
    回滚数据也是经过段管理。sql


  • InnoDB申请空间的最小单位,由连续页组成的空间,大小为1MB,保持不变。
    InnoDB一次从磁盘中申请4~5个区。数据库

  • InnoDB访问的最小单位,默认16KB。一个区中一共有64个连续的页。
    缓冲池是以页为管理单位,每次读取或刷新一页数据。
    参数: innodb_page_size,能够将页大小设置为4K,8K。vim

InnoDB将表空间按Page切分,这些Page主要分为两类:存储表空间元信息的管理页(如FIL_PAGE_TYPE_FSP_HDR)和存储表空间用户数据的索引页(如FIL_PAGE_INDEX,FIL_PAGE_INODE)。数组

FIL Header

全部页都有两个统一的结构,FIL Header,占据页面的前38个字节,FIL Trailer,占据页面末尾8字节。bash

FIL Header结构以下

变量 字节 描述
FIL_PAGE_SPACE 4 所在表空间ID(space id)
FIL_PAGE_OFFSET 4 该页在表空间的偏移量(page no)
FIL_PAGE_PREV 4 前驱节点的偏移量(仅对索引页有效)
FIL_PAGE_NEXT 4 后继节点的偏移量(仅对索引页有效)
FIL_PAGE_LSN 8 页最后刷新到磁盘的LSN
FIL_PAGE_TYPE 2 页的类型
FIL_PAGE_FILE_FLUSH_LSN 8 仅在第一个Page(FSP HEADER PAGE)使用,用来判断数据库是否正常关闭
FIL_PAGE_SPACE_ID 8 仅在第一个Page使用,保存数据库关闭时归档重作日志的编号

InnoDB中每个表空间都会有一个惟一的space id,共享表空间的space id就是0。
每一个页都有一个32位序号page no,称为偏移量,即离表空间初始位置的偏移量。由于每一个页大小为16kb,因此第0个页的偏移量为0,第一个页的偏移量为16384,以此类推。
经过space id和page no,InnoDB能够定位任何一个页。

FIL_PAGE_TYPE标志页的类型,InnoDB经常使用页类型以下
FIL_PAGE_TYPE_ALLOCATED:该页为最新分配
FIL_PAGE_IBUF_BITMAP:Insert Buffer位图页
FIL_PAGE_TYPE_SYS:系统页
FIL_PAGE_TYPE_TRX_SYS:事务系统数据页
FIL_PAGE_TYPE_FSP_HDR:FSP HEADER PAGE页
FIL_PAGE_TYPE_XDES:扩展描述页
FIL_PAGE_IBUF_FREE_LIST:Insert Buffer空闲列表页
FIL_PAGE_UNDO_LOG:Undo Log页
FIL_PAGE_INDEX:B+树叶子节点页
FIL_PAGE_INODE:B+树索引节点页
FIL_PAGE_TYPE_BLOB:BLOB页

FIL Trailer

FIL Trailer是在文件末尾的最后8个字节, 低位4个字节是用来表示Page页中数据的checksum,最后4字节和FIL Header中的FIL_PAGE_LSN相同
下面说到的页都有FIL Header,FIL Trailer,再也不重复说明。

如今看一下关键的关键的管理页。

FSP HEADER PAGE

表空间第1页就是文件管理页FSP HEADER PAGE,存储表空间关键元数据信息。由FSP HEADER、XDES ENTRIES构成。

FSP HEADER

FSP HEADER主要存储表空间元信息,维护关键结构分配信息,主要变量以下:

变量 字节 描述
FSP_SIZE 4 表空间大小,以Page数量计算
FSP_FREE_LIMIT 4 当前已经使用的位置
FSP_FREE 16 空闲区链表
FSP_FREE_FRAG 16 部分能够用碎片区链表
FSP_FULL_FRAG 16 已经彻底使用的碎片区链表
FSP_SEG_INODES_FULL 16 已经彻底使用的INODE PAGE链表
FSP_SEG_INODES_FREE 16 部分可用的INODE PAGE链表

区具体能够分为区(extent)和碎片区(frag extent)
碎片区是比较特殊的区,用于分配碎片页。

XDES ENTRIES

接下来是区描述符XDES ENTRIES,每一个区描述符需占用40个字节,用于追踪64个页的使用状态。
每一个FSP HEADER PAGE只能管理256个区的信息(也就是16384个页),所以每隔16384个页,会有一个相似FSP HEADER PAGE的Page来描述随后的区信息。

XDES ENTRIES主要变量以下

变量 字节 描述
XDES_FLST_NODE 12 维护链表先后节点信息
XDES_STATE 4 标识该区是属于FSP_FREE,FSP_FRAG_FREE或FSP_FRAG_FREE_FULL或XDES_SEG(某个段)
XDES_BITMAP 16 标识区中64个页的使用状态

XDES_BITMAP使用位图方式保存,每一个页的使用状态占用2位(预留一位)。

一个区能够属于FSP_FREE,FSP_FRAG_FREE或FSP_FRAG_FREE_FULL或者某一个段。区的分配实现了一套相似于借还的机制。段向表空间租借区,只有段退还该空间时,该区才能从新出如今FSP_FREE/FSP_FULL_FRAG/FSP_FULL中。

INODE PAGE

表空间文件的第3个page的类型为FIL_PAGE_INODE,管理表空间的段。

INODE PAGE由SEGMENT INODE组成,每一个SEGMENT INODE为192字节,对应一个段。

SEGMENT INODE结构主要变量以下:

变量 字节 描述
FSEG_FREE 16 未使用的extend链表
FSEG_FULL 16 已彻底使用的extend链表
FSEG_NOT_FULL 16 部分可用的extend链表
FSEG_FRAG_ARR[0] 4 碎片页数组首页地址
...
FSEG_FRAG_ARR[31] 4 碎片页数组尾页地址

为节省空间,每一个segment都先从FSP HEADER的FSP_FREE_FRAG中分配32个碎片页(FSEG_FRAG_ARR),当这些32个页面不够使用时,再申请区。

每一个INODE PAGE默承认存储85个SEGMENT INODE。每一个索引使用2个segment,分别用于管理叶子节点和非叶子节点。
因此一个INODE PAGE最多能够保存42个索引信息(一个索引使用两个段)。若是表空间有超过42个索引,则必须再分配一个INODE PAGE。INODE PAGE的分配是从碎片区中申请,但它的位置不是固定的。为了找到索引的INODE ENTRY,InnoDB定义了SEGMENT HEADER,结构以下

变量 字节 描述
FSEG_HDR_SPACE 4 INODE PAGE所在表空间ID
FSEG_HDR_PAGE_NO 4 INODE PAGE所在表空间的偏移量
FSEG_HDR_OFFSET 2 INODE ENTRY在页的偏移量

对于用户表,其索引的Root Page中保存了两个SEGMENT HEADER,分别指向叶子节点的SEGMENT INODE和非叶子节点的SEGMENT INODE。

链表结构

InnoDB的链表都是双向链表,如FSP HEADER中变量FSP_FREE,FSP_FREE_FRAG,FSP_FULL_FRAG,FSP_SEG_INODES_FULL,他们都是链表头结构FLST_BASE_NODE,维护了链表的头指针和末尾指针,

变量 字节 描述
FLST_LEN 4 链表长度
FLST_FIRST 6 链表首节点地址
FLST_LAST 6 链表尾节点地址

它们指向的节点为XDES ENTRIES的XDES_FLST_NODE,每一个节点的结构体称为FLST_NODE

变量 字节 描述
FLST_PREV 6 链表前驱节点地址
FLST_NEXT 6 链表后继节点地址

下面是一个表空间的示意图,请理解该图

第2个Page是FIL_PAGE_IBUF_BITMAP,主要用于跟踪随后的每一个PAGE的change buffer信息,使用4个bit来描述每一个page的change buffer信息。
因为FIL_PAGE_IBUF_BITMAP的空间有限,一样每隔256个Extent Page以后,也会在XDES PAGE以后建立一个FIL_PAGE_IBUF_BITMAP。

其余的表空间元信息Page,如 FSP_TRX_SYS_PAGE_NO,共享表空间第6个Page,记录了InnoDB重要的事务系统信息。 FSP_DICT_HDR_PAGE_NO,共享表空间第8个Page,存储了SYS_TABLES,SYS_TABLE_IDS,SYS_COLUMNS,SYS_INDEXES和SYS_FIELDS等数据词典表的Root Page(b+树Root节点所在Page)。 有兴趣的同窗能够自行了解

索引组织表

上面说了InnoDB经过索引页来存放行记录,那么这些行记录是怎么组织的呢
(这里说的索引页,包括了B+树叶子节点页FIL_PAGE_INDEX和B+树索引节点页FIL_PAGE_INODE)

汇集索引

InnoDB中,表都是根据汇集索引顺序组织存放的,这种存储方式的表称为索引组织表。
而InnoDB中主键索引使用的是B+索引(经过B+树组织的索引)

当咱们须要打开一张表时,须要从表空间的数据词典表中加载元数据信息,其中SYS_INDEXES系统表中记录了用户表中全部索引Root Page对应的page no,进而找到B+树Root Page,就能够对整个用户数据B+树进行操做。

B+是为磁盘和其余直接存取辅助设备设计的一种多路平衡查找树。
看一个例子

B+树有如下特色
一、B+树不只是多叉树,并且每一个非叶子节点只存储键值,不存储数据,这样每一个非叶子节点所能保存的键值大大增长,能够下降B+的树深度。
该特性应用到索引上,可使每次加载的节点包括更多的索引数据,也能够减小IO操做(每次读取树的下一层都须要一次IO)。
因此B+索引具备高扇出性,在数据库中,B+树的高度通常都在2~4层,查找某一个键值的行记录最多只须要2到4次IO。

  1. B+树中,全部数据按键值的大小顺序存放在同一层的叶子节点上,由各叶子节点指针进行链接。
    每次查找数据都须要查找到叶子节点,查找次数都相同,因此查询速度很稳定。

  2. B+树全部的叶子节点数据构成了一个有序链表,在查询大小区间的数据时候很是方便。
    如上面例子中的B+树,若是要查询[22,89]范围数据,再须要找到键值22,再遍历到数据键值89就能够了。
    而遍历全部数据,只须要遍历全部的叶子节点便可,而不须要遍历每一层数据,这有利于数据库作全表扫描。

注意:B+树全部的叶子节点数据构成了一个有序链表,这个是逻辑上的有序,而非物理存储是顺序(维护成本太高)。
InnoDB中,Page的FIL Header维护了上下Page的偏移量,组成双向链表,而Page中行记录的记录头中维护了下一行记录的位置,组成单向链表。

B+树的查找 相似于二叉查找树。起始于根节点,自顶向下遍历树,根据目标值与键值比较结果向下查找对应子树。
但B+的数据都存储在叶子节点,因此就算某个非叶子节点的键值与所查的关键字相等时,并不中止查找,而是继续沿着这个节点左边的指针向下,一直查到该关键字所在的叶子节点为止。

B+树的平衡
对于插入和删除操做,B+经过分裂和合并节点维持平衡(类型红黑树的旋转), InnoDb中B+树的键值和数据都存放在Page中,所以Page也须要合并和分裂,有兴趣的同窗能够自行了解。

辅助索引

InnoDB中汇集索引和辅助索引都是B+索引。但辅助索引叶子节点的数据不是存储实际的数据,而是主键的值。要想拿到实际的数据须要再经过主键索引找到对应的行记录而后才能拿到实际的数据,这个过程称为回表。
若是查询语句能够从辅助索引(包括联合索引)中获取到全部须要的列,这时不须要再经过主键索引找到对应的行记录,这种状况称为覆盖索引。

联合索引

联合索引也是B+索引。 联合索引中列的顺序很重要。
InnoDB首先根据联合索引中最左边的、也就是第一列进行排序,在第一列排序的基础上,再对联合索引中后面的第二列进行排序,依此类推。
因此若是想使用联合索引的第n列,必须先使用联合索引前面的第1列到第n-1列。

(group, score),可能出现如下排序(1, 46), (1,58), (2,23), (2,96), (3,25), (3,67)。 若是要使用该索引的score列,查询语句中必须先使用该索引的group列,如where group = 2 and score = 96,InnoDB经过group查询后,再经过score查询。
这个规则称为最左前缀匹配原则。

页结构

下面看一下InnoDB索引页如何保持用户数据,索引页由如下部分组成

变量 字节 描述
Page Header 56 页头,记录页的一些状态信息
Infimun/Supremum Records 26 系统记录
User Records 不肯定 用户记录,即行记录
Free Space 不肯定 空闲空间
Page Directory 不肯定 页目录

Page Header

变量 字节 描述
PAGE_N_DIR_SLOTS 2 page directory中槽的数量
PAGE_HEAP_TOP 2 堆中空闲空间的偏移量
PAGE_N_HEAP 2 记录数据数量,包含用户记录,系统记录以及标记删除的记录
PAGE_FREE 2 删除记录的链表
PAGE_GARBAGE 2 已标记删除记录数量
PAGE_N_RECS 2 用户记录数量,不包含系统记录以及标记删除的记录
PAGE_MAX_TRX_ID 8 最近一次修改该Page记录的事务ID
PAGE_LEVEL 2 当前页在索引树的位置
PAGE_INDEX_ID 8 索引id,表示当前页属于那个索引
PAGE_BTR_SEG_LEAF 10 B+树叶子节点所在段的segment header,仅在B+树的Root Page中定义
PAGE_BTR_SEG_TOP 10 B+树非叶子节点所在段的segment header,仅在B+树的Root Page中定义

PAGE_LAST_INSERT,PAGE_DIRECTION,PAGE_N_DIRECTION等变量并未在表中列出,他们用于进行页的分裂操做。

当记录被删除(不只是将记录的deleted_flag设置为1,而是完全删除),会放到PAGE_FREE链表中(链表经过记录头信息next_record串联),若是这个页上有记录要插入,会先检查PAGE_FREE链表空间是否知足,若是空间知足,直接从PAGE_FREE链表空间分配,若是空间不够,再从空闲空间(PAGE_HEAP_TOP)分配。当空闲空间不足时,会调用函数btr_page_reorganize_low进行页的从新组织,即根据页中记录主键的顺序从新进行整理,这样就能整理出碎片的空间。若仍是空间不足,则进行分裂操做。
注意:检查PAGE_FREE链表空间时,仅检查第一个节点的可用空间,不会经过next_record进行遍历。

页记录是根据主键顺序排序的,这个排序是逻辑上的,而非物理上的(开销过大)。

Infimun和Supremum Records
系统虚拟的记录,Infimun表示比任何主键值都小的值,Supremum表示比任何可能的值都大的值。

User Records
行记录以链表的形式存放在 User Records 中,行记录格式中的记录头中的next_record 存放着下一条记录的地址

Free Space 随着记录愈来愈多,Free Space空间愈来愈小,User Records空间愈来愈大。当Free Space的所有空间都被分配完了,这个页也就使用完了,须要申请新的页。

Page Directory
B+索引自己不能定位具体的一条记录,只能找到该记录所在的页。
InnoDB将页载入到内存后,能够遍历页全部的记录找到目标记录,但这样作太慢了。
(Page的记录非物理顺序存储,没法经过物理地址二分查询)

InnoDB将页中数据进行分组,将每一个组最后一条数据的偏移量按顺序存储起来,组成目录。
每一个偏移量也被称为一个槽(Slot,两个字节)。这些偏移量都会被存储到靠近页的尾部的地方,被称为Page Directory。
这样InnoDB能够经过Page Directory进行二叉查找定位目标所在分组,再遍历该组数据就能够。

每一个槽能够包括4~8条记录,每一个记录的记录头中n_owned变量,维护该记录所在槽的记录数量。
(例外,第1个槽仅包含一个1记录,即Infimun,最后一个槽可包含1~8个记录)

行格式

上面说了InnoDB索引页如何保存用户数据,即表的行记录,下面看看每一行的存储格式

InnodB中行记录存储方法有Compress,Redundant,Compressed,Dynamic。
Redundant行格式是MySql5.0以前使用的,如今基本不会再使用,这里就不介绍了。

compact格式下,一行记录依次为如下内容:
变长字段长度列表,NULL标志位,记录头信息,rowID,TransactionID,RollPointer,列1数据,列2数据,... 其中rowID,TransactionID,RollPointer由InnoDB生成,
注意,若是表中已经指定主键,则不生成rowID。
(TransactionID,RollPointer用于实现MVCC功能,将在事务篇解析)

变长字段长度列表
若列的长度小于255字节,用1字节表示
若列的长度大于255字节,用2字节表示(varchar最大限制为65535)

NULL标志位
占用字节为(可为NULL的列数量/8)向上取整,字节中哪一位为1,表示该行数据对应列为NULL值

注意,变长字段长度列表和NULL标志位都是是按列定义的倒叙保存的,他们都是可选的,若是表中没有变长字段和容许NULL值的字段,那么这两个都是占用0字节长度

char(N)中的N指定的是字符,UTF-8下CHAR(10)类型的列,最小能够存储10字节的字符,最大能够存储30字节的字符。
因此对于多字节的字符,char类型在InnoDB存储引擎内部被视为变长字符类型。也意味着这些CHAR数据类型的长度会记录在变长长度列表中。

记录头信息
固定5字节,结构以下

变量 字节 描述
() 2 预留位
deleted_flag 1 该行是否已被删除
min_rec_flag 1 若是该行记录是预约义为最小的记录,为1
n_owned 4 该记录所在Slot拥有的记录数
heap_no 13 索引堆中该条记录的索引号
record_type 3 记录类型,000(普通),001(B+Tree节点指针),010(Infimum),011(Supremum)
next_record 16 页中下一条记录的相对位置

看一例子

create table mytest (
    t1 varchar(10),
    t2 varchar(10),
    t3 char(10),
    t4 varchar(10)
) engine=INNODB charset=LATIN1 ROW_format=compact;
insert into mytest3 values('d','11',NULL,'fff');
复制代码

使用vim打开ibd文件,在“命令”模式中输入“:%!xxd”命令,将文本转换为16进制,找到supremum字符串,后面的就是列数据了。
(ibd中可能有多个页,能够依照行的实际数据判断哪一个是本身要找的数据。)

00010070: 7375 7072 656d 756d 0302 0104 0000 10ff  supremum........
00010080: ef00 0000 0002 0100 0000 0010 2281 0000  ............"... 00010090: 010a 0110 6431 3166 6666 0000 0000 0000 ....d11fff...... 复制代码

解析以下

03 02 01   // 变长字段长度列表, d列-03,c列-null,不记录  b列-02  a列-01
04  // null标准位, 二进制-00000100,逆序,d,c-null,b,a
00 00 10 ff ef  // Record Header,固定5字节 
00 0000 0002 01  // RowID ,InnoDB自动建立,6字节 
00 0000 0010 22  // TransactionID
81 0000 010a 0110  // Roll Pointer
64  // 列1数据  'a'
31 31  // 列2数据 '11'
66 6666  // 列4数据  'fff'
复制代码

行溢出 对于占用字节数很是大的列,在记录的真实数据中只会存储一小部分数据(768个字节),剩余的数据分散在其余溢出页中(BLOG类型的页),记录的真实数据中记录这些页的地址,以便找到他们。
注意:行溢出与列定义的类型无关。若是varchar过长会发生行溢出,而text,blog不够长则不会发生行溢出。

InnoDB 1.0.x引入新的行格式,之前支持的Compress 和Redundant称为Antelope文件格式,新的文件格式为Barracuda文件格式,有两个行记录格式:Compressed和Dynamic。他们是Compact的变种形式。他们基本没什么本质上的区别,惟一的区别就是对于行溢出的处理不一样。Compressed在数据页只存储一个指向溢出页的地址,全部的实际数据都存放在溢出页中。
而Compressed还能够是zlib算法对行数据进行压缩,所以对于BLOB,TEXT,VARCHAR这类大长度类型的数据可以很是有效的存储。

参考文档:
《MySQL技术内幕InnoDB存储引擎》
《MYSQL内核:INNODB存储引擎》
Innodb表空间
深度 | 解析InnoDB引擎
Chapter 22 InnoDB Storage ngine
InnoDB 文件系统之文件物理结构

若是您以为本文不错,欢迎关注个人微信公众号,您的关注是我坚持的动力!

相关文章
相关标签/搜索