InnoDB数据的存储

InnoDB数据的存储

我们知道MySQL可以分成两层,数据的读取和写入工作是由底层存储引擎负责的。本文主要介绍InnoDB底层是如何存放数据的。

1 InnoDB逻辑存储结构

如下图所示,InnoDB中所有数据都被逻辑地存放在一个空间,称之为表空间。表空间又由段、区、页组成。

  • 表空间

    默认情况下,InnoDB将所有表都放在同一个表空间,也可以通过参数innodb_file_per_table,将每张表放到一个单独表空间。

  • 表空间由各个段组成,常见的段有数据段、索引段、回滚段等。在InnoDB中,数据是存放在主键B+树索引的叶子节点中,因此数据段即为B+树索引叶子节点,索引段即为B+树索引非叶子节点。

  • 区是由连续页组成的空间,任何情况下区的大小都固定为1MB。默认情况下InnoDB页大小为16KB,因此1个区有64个连续页。需要注意的一点是,在每个段开始的时候,先用32个页大小的碎片页来存放数据,在使用完这些碎片页之后才申请64个连续页。这样做的目的在于,对于一些小表来说,可以节省磁盘空间。

  • 页是InnoDB磁盘管理的最小单位,InnoDB中每个页默认大小为16KB。InnoDB中常见的页类型有:数据页、undo页、系统页、事务缓存页、插入缓冲位图页、插入缓冲空闲列表页、未压缩的二进制大对象页、压缩的二进制大对象页。

  • InnoDB是面向行存储的,即页中的数据是逻辑上一行接一行排列的,实际上是通过链表相连。

2 InnoDB行记录格式

InnoDB提供Compact和Redundant两种格式来存放行记录数据。Redundant是为了兼容以前版本而保留的,在MySQL5.1之后默认的行格式是Compact。

2.1 Compact行记录格式

Compact是MySQL5.0中引入的,其设计目标在于高效地存储数据(一个页中存放的行数据越多,越高效)。

  • 变长字段长度列表**:列长度小于255字节,用1字节表示,若大于255字节,用2字节表示。它有一下特点:

    • 逆序存放,比如值为01 04 03分别表示第3、2、1列边长字段占用字节数为1、4、3个字节。
    • 变长字段的值为NULL,长度列表里面不会记录其字节数为0。

    问题:怎么知道什么时候1字节表示1列,什么时候2字节表示1列?

    详情可看文末。

  • NULL标志位:通常大小为1字节,将所有没有被NOT NULL修饰的列放到一起(节约空间),然后和标志位二进制值一一对应,二进制值为1,代表该列值为NULL,为0,代表不为NULL。需要注意的是,这里也是逆序存放。下面看一个例子:一张表有4列,第2列被NOT NULL修饰,此时他们和NULL标志位的对应关系如下图所示:

  • 记录头信息:一共占用40字节空间,如下表所示:

  • 真实数据列

    • 隐藏列:InnoDB的每张表都默认被添加了一些隐藏列:db_row_id,db_trx_id和db_roll_ptr,分别表示行ID,事务ID和回滚指针。如果用户建表时指定了表主键,则不会创建db_row_id隐藏列。这3个隐藏列放在真实数据列的最前面。

    • NULL值:如果某字段数据为NULL,则不被存储。

    • char(M):指定字符数的字段,如果真实数据长度不够,会用0x20填充。还需要注意的一点是,如果char字段采用的是变长编码字符集时,该列占用的字节数会被加到变长字段长度列表,并且char(M)至少占用M个字节,即char(10)使用utf8字节符集时,该列存储的数据占用的字节长度范围为10~30。

      即使我们向该列中存储一个空字符串也会占用10 字节,这主要是希望在将来更新该列时, 在新值的字节长度大于旧值的字节长度但不大于10 个字节时,可以在该记录处直接更新,而不是在存储空间中再重新分配一个新的记录空间,导致原有的记录空间成为所谓的碎片。

2.2 Redundant 行记录格式

Redundant是MySQL5.0之前使用的一种行格式,其结构如下图所示:

  • 字段长度偏移列表:1个或2个字节长度,它和Compact相比有2处不同:

    • 没有“变长”两个字,也就是说所有列的长度信息都会存储在这里
    • 多了“偏移”两个字,它存储的时各个字段的偏移量,字段长度需要计算两个偏移量之间的差值获取

    问题:什么时候1个字节,什么时候2个字节?

    • 当记录的真实数据占用的字节数不大于127,每个列对应的偏移量占1个字节

      为什么是127,不是225?因为最高位被看作NULL标志位,如果为1,表示该列为NULL。

    • 当记录的真实数据占用的字节数大于127,不大于32767,每个列对应的偏移量占2个字节

    • 有没有记录的真实数据大于3 2767 的情况呢?有, 不过此时记录的一部分已经存放到了所谓的溢出页中。

    问题:那怎么知道用的是1字节表示长度还是2字节表示长度呢?

    记录头中有一个1byte_offs_flag属性,1表示使用1字节,0表示使用2字节。

  • 记录头信息:一共48个字节

  • 真实数据

    • Null值的处理:varchar不占用空间,char占用,用0x00填充。
    • char(M):无论是定长字符集还是变长字符集,char(M)占用的空间是M乘以该字符集下每个字符最多占用字节数。比如对于utf8字符集,char(10)直接占用30字节空间。

2.3 行溢出数据的存储

InnoDB的页大小是16KB,也就是16384个字节,如果一行记录中某列占用字节数大于16284,那么这一页空间连一行数据都存不下。因此,对于占用存储空间非常多的列需要单独存储。对于Compact和Redundant类型,溢出列的记录方式如下图所示:

如上图所示,该行记录只存储溢出列前768个字节,其余数据放到其他页中存储,然后用20字节的指针指向页地址。

那么什么时候列会产生溢出呢?通过简单的计算可以得出溢出的临界点为8099,即列空间大小小于8099不会溢出,如果大于等于8099就会溢出。

2.4 Compressed和Dynamic行记录格式

我现在使用的MySQL 版本是5.7,其默认行格式就是DYNAMIC 。 这两个行格式与COMPACT 行格式挺像,只不过在处理溢出列的数据时有点儿分歧:它们不会在记录的真实数据处存储该溢出列真实数据的前768 字节,而是把该列的所有真实数据都存储到溢出页中,只在记录的真实数据处存储20 字节大小的指向溢出页的地址(当然,这20 字节还包括真实数据占用的字节数)。如下图所示:

总之,Redundant是一种比较原始的行格式,它是非紧凑的,而compact、dynamic和compressed行格式是较新的,它们是紧凑的(即占用空间小)。

3 InnoDB数据页结构

如上图所示,一个InnoDB数据页大致可以分成7个部分,下表简单描述了这些部分的大致功能:

3.1 File Header

File Header用来记录页的一些头信息,8个部分,共占用38个字节。

3.2 Page Header

Page Header里面存放一些页的状态信息,有14个部分,共占用56个字节。

3.3 Infimum和Supremurm记录

在InnoDB中,每各页中会有两个虚拟记录,用来限定记录的边界,即最小、最大记录。页面中任何记录的主键值都大于Infimum记录,都小于suprememurm记录。因此这两条伪记录分别处于记录链表的头部和尾部。

3.4 User Record和Free Space

  • User Record:即存储行记录的内容,所有记录链表相连

  • Free Space:即空闲空间,所有空闲空间也是个链表数据结构。一条记录被删除后,该空间会被加入到空闲列表中。

    下图展示了页中每条记录的存放方式:

从上图可以看出,记录按照主键从小到大形成了一个单向链表。

3.5 Page directory

从名字就可以看出来,这部分存放的时一个目录。从前面我们知道,所有记录是通过链表主键递增顺序一个个串在一起的,因此要查找某个记录,就需要顺序遍历链表。

Page目录存在的目的就是,帮助我们根据行记录的逐渐,快速定位该条记录所在位置。

Page 目录中存在很多指向页中记录的指针,这个指针称之为槽,如下图所示:

上图中有2个槽,槽将所有记录分成了两组,槽指向的是分组中主键值最大的那条记录,即分组的最后一条记录。关于槽,有以下几点需要注意:

  • 槽中记录的时地址偏移量,而不是绝对地址。
  • 槽指向的记录中n_owned数值为该分组中记录总数,分组中其余记录的n_owned值为0。
  • 每个槽占用2字节,按照对应记录的大小顺序排列。槽对应记录的主键越小,它的位置越靠近File Trailer。

下图展示了一个记录数更多的示例:

当需要查找某条记录时,先通过二分法确定该记录所在分组对应的槽,然后遍历该分组中的记录。

3.6 File Trailer

InnoDB的数据是存储在磁盘上的,但是磁盘速度太慢了,需要以页为单位把数据加载到内存中处理。如果该页在内存中被修改了,那么在修改后的某个时间还需要把数据刷新到磁盘中。

但是,如果刷新过程中,断电了咋办?为了检测页的完整性,InnoDB在页的末尾增加了File Trailer部分(8个字节),可以分成2部分:

  • 前4个字节:代表页的校验和。

    这个部分与File Header 中的校验和相对应.每当一个页面在内存中发生修改时,在刷新之前就要把页面的校验和算出来。因为File Header 在页面的前边,所以File Header 中的校验和会被首先刷新到磁盘,当完全写完后,校验和也会被写到页的尾部.如果页面刷新成功,则页首和页尾的校验和应该是一致的。如果刷新了一部分后断电了,那么File Header 中的校验和就代表着己经修改过的页,而Fi1e Trai1er 中的校验和代表着原先的页, 二者不同则意味着刷新期间发生了错误。

  • 后4个字节:最后修改时对应的LSN的后4个字节。正常情况下应该与File Header 部分的FIL_PAGE_LSN 的后4 字节相同。

4 拓展问题

  1. Compact行记录格式下,如何确定“变长字段长度列表”长度是1字节还是2字节?

    用1字节还是2字节来表示变长字段的真实数据占用的字节数, InnoDB 有它的一套规则。为了更清楚的描述这套规则,我们引入W、M和L这几个符号:

    • W:表示所使用的字符集中,一个字符最多占用W个字节。对于utf8字符集,W就是3;对于gbk字符集,W就是2;对于ascii字符集,W就是1。
    • M:字段类型VARCHAR(M)中的M,表示最多存储M个字符。
    • L:该字段实际存储的字符串占用的字节数。

    判断1字节还是2字节的规则如下:

    • 如果M×W255M\times W\leq 255,那么使用1字节

    • 如果M×W>255M\times W > 255,分两种情况

      • 如果L127L\leq 127,则用1字节来表示真实数据占用的字节数
      • 如果L>127L>127,则用2字节来表示真实数据占用的字节数

      问什么是127?因为如果用2字节表示长度,对于某个字节怎么知道它是一个字段长度还是半个字段长度?如果该字节第一位为0,说明该字节表示的是完整字节段长度,否则说明它表示的是半个字段长度。

    **总之:**如果变长字段允许存储的最大字节数M×WM\times W超过255,并且真实数据占用的字节数L超过127,则使用2字节表示真实数据占用的字节数,否则使用1字节。

打赏
  • 版权声明: 本博客所有文章除特别声明外,著作权归作者所有。转载请注明出处!
  • Copyrights © 2021-2022 Yin Peng
  • 引擎: Hexo   |  主题:修改自 Ayer
  • 访问人数: | 浏览次数:

请我喝杯咖啡吧~

支付宝
微信