【华为云MySQL技术专栏】InnoDB大对象存储格式解析

举报
GaussDB 数据库 发表于 2024/09/10 14:56:36 2024/09/10
【摘要】 1. 背景在MySQL中,大字段是经常使用到的对象,例如:字符类型,包括日志、博客内容以及二进制类型的视频文件等。在InnoDB中,大字段也叫大对象(Large Object,简称LOB),通常认为不会高频全量访问。InnoDB的数据是按照聚簇索引进行组织的,当聚簇索引的数据行中存在大对象时,InnoDB为了提升聚簇索引B+树中数据行的访问效率,会对数据行中大对象的存储格式进行优化。本文将基...

1. 背景

在MySQL中,大字段是经常使用到的对象,例如:字符类型,包括日志、博客内容以及二进制类型的视频文件等。在InnoDB中,大字段也叫大对象(Large Object,简称LOB),通常认为不会高频全量访问。InnoDB的数据是按照聚簇索引进行组织的,当聚簇索引的数据行中存在大对象时,InnoDB为了提升聚簇索引B+树中数据行的访问效率,会对数据行中大对象的存储格式进行优化。

本文将基于MySQL 8.0.38的代码,介绍InnoDB的DYNAMIC行格式中LOB的存储格式。

2. 大对象的存储形式

InnoDB中,大对象的存储形式主要有两种:

1) 内联存储在InnoDB聚簇索引的行记录中;

2) 以链表的形式存在溢出页中,同时,根据不同的LOB大小,链表的格式也有差异。

2.1 大对象溢出页存储的条件

InnoDB中,以16KB大小的页面为例,为了保证每个数据页面中至少有两条记录,每条记录的长度不能超过8126个字节。如果超过了,就需要对记录中的某些符合条件的字段采用溢出页(即数据并不是存储在聚簇索引中)的形式进行存储,这个判断过程如下:

步骤1,若主键记录的物理长度大于8126个字节,则顺序遍历主键记录中的每一个字段;

步骤2,接着找到一个新的、最长的且没有在溢出页的字段(主要判断字段类型,例如:VARCHAR, TEXT等),同时,能够满足以下条件的字段且不会存放在溢出页中:

a) 字段是固定长度;

b) 字段为NULL;

c) 字段的长度小于等于40个字节;

d) 字段为非大对象类型。例如,VARCHAR类型,且长度小于或者等于255。

步骤3,对满足条件的字段进行溢出页存储,存储后该字段在聚簇索引行记录中的长度更新为20;

步骤4,反之再次进入步骤1,直到步骤1中的条件不满足或者步骤2中无法找到可存储到溢出页的字段。

以上过程在InnoDB中对应的核心函数dtuple_convert_big_rec如下:

big_rec_t *dtuple_convert_big_rec(dict_index_t *index, upd_t *upd,

dtuple_t *entry) {

...

while (page_zip_rec_needs_ext(

rec_get_converted_size(index, entry), dict_table_is_comp(index->table),

dict_index_get_n_fields(index), dict_table_page_size(index->table))) {

...

for (ulint i = dict_index_get_n_unique_in_tree(index);

i < dtuple_get_n_fields(entry); i++) {

ulint savings;

dfield = dtuple_get_nth_field(entry, i);

ifield = index->get_field(i);

/* Skip fixed-length, NULL, externally stored,

or short columns */

if (ifield->fixed_len || dfield_is_null(dfield) ||

dfield_is_ext(dfield) || dfield_get_len(dfield) <= local_len ||

dfield_get_len(dfield) <= BTR_EXTERN_LOCAL_STORED_MAX_SIZE) {

goto skip_field;

}

savings = dfield_get_len(dfield) - local_len;

/* Check that there would be savings */

if (longest >= savings) {

goto skip_field;

}

/* In DYNAMIC format, store locally any non-BLOB columns whose maximum length does not exceed 256 bytes.*/

if (!DATA_BIG_COL(ifield->col)) {

goto skip_field;

}

longest_i = i;

longest = savings;

skip_field:

continue;

}

/* 将longest_i对应的字段进行溢出页存储. */

...

}

2.2 大对象溢出页存储示例

示例1:存在表t1,其定义如下:

CREATE TABLE `t1` (

`a` int DEFAULT NULL,

`b` blob,

`c` blob,

`d` blob

) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci


CASE 1:

insert into t1 values(1,repeat('a',32768),repeat('a',8000),repeat('a',32768));

b,d字段会被存储到溢出页中,c字段虽然有8000个字节,但因b,d字段优先被溢出页存储,因此,c字段不会被溢出页存储。

CASE 2:

insert into t1 values(1,repeat('a',7000),repeat('a',8000),repeat('a',7000));

b,c字段会被存储到溢出页中,d字段不会被溢出页存储。过程是这样的:首先,选择c进行溢出页存储,存储完毕后c字段长度变成20,然后选择b字段进行溢出页存储,完毕后b字段长度为20,d字段保留在主键记录中。

示例2:存在表t1,其包括一个INT列和32个VARCHAR (256)类型的列。当向该表中插入一个满行(每个列的值都达到字段定义的长度)记录时,此时所有VARCHAR类型的字段总长度为8192,但是如果一个字段被溢出页存储后,则总长度小于8126。因此,该记录中第一个VARCHAR (256)的列会被溢出页存储,其他的字段都不会被溢出页存储。

从上述分析以及示例中可以看到,定义为大字段(BLOB, TEXT等)类型列的数据不一定会被溢出成底层的大对象存储,定义为VARCHAR类型的列的数据也可能会被缓存,这主要取决于行以及某些字段大小是否符合上述InnoDB的约束。

3. 大对象溢出页存储格式

3.1 大对象引用字段(LOB reference,简称LOB ref)

在主键记录中,当某字段被溢出页存储时,则在该字段中不会存储真正的数据,而是会写入大对象的引用,InnoDB可以通过该大对象引用字段找到数据存储的真实位置。

LOB ref有20个字节大小,其包含的内容如下:

图1:LOB ref结构

space_id(4):标识溢出页所属的表空间;

page_no(4):标识溢出页第一个页面的page no;

version(4):标识当前LOB字段值的版本号,从1开始累加,主要用于LOB的多版本读,后续文章再详细介绍该字段的使用场景;

info bits: 一些标识信息,一共4个字节,目前只用了3个bit,主要是用于LOB的更新,包括:

a) BTR_EXTERN_OWNER_FLAG(128UL):标识该列的数据是否“真正”拥有溢出页,例如:在InnoDB中,一些UPDATE操作会被转换成DELETE + INSERT,即先将旧的行记录打上“删除”的标签,然后插入新的行记录,如果这个UPDATE不涉及大对象的修改,那么我们可以让新行记录“继承”旧行记录的溢出页存储内容,这样一来,旧行记录将不再保留溢出页,即便该行依然拥有LOB ref。在Purge的时候,被标记为“删除”的旧行指向的溢出页数据也不会被清理,后续文章会详细介绍该字段的使用;

b) BTR_EXTERN_INHERITED_FLAG(64UL):标识该列的数据溢出页内容,是否“继承”自其它行,如果是“继承”自其它行,则在回滚的时候不需要真正清理数据;

c) BTR_EXTERN_BEING_MODIFIED_FLAG(32UL):标识该字段是否正在被修改,这个主要用于READ UNCOMMITED隔离级别时防止读到中间状态的LOB内容;

len:标识LOB对象的总长度。

LOB ref的初始值如下:

/** A BLOB field reference has all the bits set to zero, except the "being

* modified" bit. */

const byte field_ref_almost_zero[FIELD_REF_SIZE] = {

0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0, 0x20, 0, 0, 0, 0, 0, 0, 0,

};

0x20对应32UL,表示该LOB正在被修改。

3.2 溢出页

InnoDB 8.0中溢出页的格式主要包括两种,如果记录的总长度小于2个页面的长度,那么只需要链表即可(这种场景比较简单,本文不介绍)。否则,InnoDB会使用更复杂的数据结构来对LOB的内容进行组织,用于实现LOB的高效更新以及多版本查询,如下图2所示:


图2:InnoDB溢出页存储格式


在这种场景中,我们称溢出页的第一个页面为first page(对应数据结构first_page_t);其他的数据页面被称为data page(对应数据结构data_page_t),管理data page的页面被称为index page(对应数据结构node_page_t),其主要是通过index entry这种结构进行管理。

3.2.1 first page

first page同正常的InnoDB数据页面一样,从FIL_PAGE_DATA(38)字节开始写first page的真实内容,first page除了存放真实数据以外,主要包括以下两部分内容:

1)对该页面数据的描述信息(固定长度:58个字节)

first page的页面描述信息主要包括如下字段:

VERSION: 1个字节,当前LOB格式的版本号,当前是0;

FLAG: 1个字节,但是目前只有一个bit位有使用,标记该字段是否允许partial update,后续会详细介绍JSON的partial update;

LOB VERSION: 4个字节,当前页面数据的版本号,从1开始累加,和blob ref的version字段的作用类似;

LAST TRXID: 6个字节,最后一次修改这个页面数据的事务id;

LAST UNDO NO: 4个字节,最后一次修改这个页面数据的事务id的undo no;

DATA_LEN: 4个字节,描述当前页面写入的数据长度

TRX_ID: 6个字节,创建该页面的事务id,对应于insert或者非partial update操作;

INDEX_LIST:16个字节,存放有正在使用数据页面的管理节点链表的首地址;

FREE_LIST_NODES:16个字节,存放所有未使用的管理节点链接的首地址;

2)index entry数组

在first page中一共有10个index entry,其中第一个index entry指向自身,其他9个指向9个其他的数据页面(假定数据足够大,需要使用10个以上的页面进行存储)。


图3:LOB存储示例

图片来源:https://dev.mysql.com/blog-archive/mysql-8-0-innodb-introduces-lob-index-for-faster-updates/

如图3所示,如果一个LOB字段的内容长度为81920,需要6个页面存储数据,每个数据页面均有一个index entry进行管理。因此,在first page中会有6个正在使用的index entry。

第一个index entry指向page 5,即first page本身,长度为15680个字节,故在first page中,除去10个index entry的空间,其他的15680个字节也可以存储数据。第二个到第五个index entry分别指向page 6, 7, 8, 9,将这些page依次全部装满(16327个字节)。第六个index entry指向page 10,存储剩下的932个字节。first page并不知道data page的位置,需要通过index entry进行遍历。

所有管理data page的index entry会通过双向链表串起来,其首地址存放在first page中LOB_INDEX_LIST中,所有空闲(即不存在data page)的index entry也会通过双向链表串联起来,其首地址存放在first page的LOB_INDEX_FREE_NODES。

index entry主要包括以下字段:

PREV:6个字节,前一个index_entry表空间地址;

NEXT: 6个字节,后一个index_entry表空间地址;

VERSION: 16个字节,当前页面的版本链双向链表,用于LOB的MVCC,后续文章中介绍partial update操作会详细描述该字段;

TRXID:6个字节,创建该index entry的事务id;

TRX_UNDO_NO:4个字节,创建该index entry的事务的undo no;

TRXID_MODIFIER:6个字节,修改该index entry的事务id;

TRX_UNDO_NO_MODIFIER: 4个字节,修改该index entry的事务的undo no;

PAGE_NO: 4个字节,该index entry对应的data page的page no;

DATA_LEN: 4个字节,该index entry对应的data page的数据长度;

LOB_VERSION:4个字节,该index entry以及data page的版本,从1开始累加,LOB的多版本的版本号指的就是这个。

TRXID与TRXID_MODIFIER的使用和LOB的update有关系后续详细介绍。

3.2.2 data page以及index page

data page

如上所述,每个index entry管理着一个data page,data page主要作用就是存储真实数据,除此之外,其页面内容前11个字节还存储以下3个字段:

VERSION:1个字节长度,存储当前data page格式的版本号。当前是0,为未来扩展data page的格式使用;

DATA_LEN:4个字节长度,当前页面数据部分的长度;

TRX_ID:6个字节长度,标识修改以及创建该页面的事务id;

index page

当LOB字段的长度超过10个页面时,first page的10个index entry就不够用了,此时会新分配一个新的页面,该页面中的所有内容被划分为index entry来使用,如下图4所示:

图4 index page 格式

index page只有一个额外的字段VERSION,标识当前页面格式的版本。图4中红色的index entry表示已经被使用的,绿色的表示尚未使用的。

3.2.3 小结

溢出页作为InnoDB大对象的复杂存储机制,其first page是大对象访问的入口,通过其内部的index entry以及关联的index page来快速访问、更新存储数据的data page。除此之外,为优化小数据量溢出的快速访问,first page自身也保存10个index entry,同时也能存小部分数据。

4. 总结

本文介绍了InnoDB大对象的存储格式,包括InnoDB会将数据行中的字段按照大对象格式进行存储的场景,InnoDB大对象溢出页存储常见存储格式,并详细介绍了InnoDB大对象的常见组织管理方式。后续文章中将结合InnoDB大对象的存储格式,介绍大对象更新和查询方式。

【版权声明】本文为华为云社区用户原创内容,转载时必须标注文章的来源(华为云社区)、文章链接、文章作者等基本信息, 否则作者和本社区有权追究责任。如果您发现本社区中有涉嫌抄袭的内容,欢迎发送邮件进行举报,并提供相关证据,一经查实,本社区将立刻删除涉嫌侵权内容,举报邮箱: cloudbbs@huaweicloud.com
  • 点赞
  • 收藏
  • 关注作者

评论(0

0/1000
抱歉,系统识别当前为高风险访问,暂不支持该操作

全部回复

上滑加载中

设置昵称

在此一键设置昵称,即可参与社区互动!

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。

*长度不超过10个汉字或20个英文字符,设置后3个月内不可修改。