面试官:Innodb B+树中每个叶节点是如何存储数据的?

246 阅读6分钟

介绍

前面介绍的几篇文章内容都有提及到数据页,在innodb中内存与磁盘的交互绝大部分也都是以页为最小颗粒度的,本篇文章将详细介绍页的数据结构以及在页中如何查询、存储数据。

页是innodb中最小的管理单位,它的默认大小为16kb,在innodb 1.2版本开始可以通过innodb_page_size设置页的大小,但只能为4k的整数倍,设置后不可更改。
页的类型有很多种,如:

  • 数据页(FIL_PAGE_INDEX)
  • undo页(FIL_PAGE_UNDO_LOG)
  • change buffer空闲页(FIL_PAFE_IBUF_FREE_LIST)
  • 溢出页(FIL_PAGE_TYPE_BLOB)等

在B+树中存储数据的就是数据页,因为在innodb中索引即数据,所以它叫FIL_PAGE_INDEX页。

页的结构

image.png

innodb的页都是上图的结构,header(页头)、tailer(页尾)是所有类型数据页都具备的;而中间的body则根据不同的类型存储不同的内容;其中file header占用38字节,file tailer占用8字节,根据页的大小剩余都是body占用。接下来我们以FIL_PAGE_INDEX页为例详细讲解。

数据页(FIL_PAGE_INDEX)结构

image.png FIL_PAGE_INDEX页的组成如上图,具体含义如下:

  • File Header:文件头;
  • File Trailer:文件尾;
  • User Records:行记录;
  • Infimun + Supremum Records:头记录和尾记录;
  • Free Space:空闲区域;
  • Page Directory:页目录;
  • Page Header:页头。

为了方便大家理解,我会按照上述含义的顺序详细分析每个部分的具体内容。

File Header

image.png

数据页头的存储内容如上图所示,各个参数含义以及占用空间大小如下:

  • FIL_PAGE_SPACE_OR_CHKSUM:占用4字节,目前这个属性存储的是校验和(checksum值),古老版本存储的是表空间编号;
  • FIL_PAGE_OFFSET:占用4个字节,存储页号;
  • FIL_PAGE_PREV:占用4个字节,存储上一个页;虽然绝大部分的页都是连续的地址,但也有一部分不是,所以还是需要指针把所有页连起来;
  • FIL_PAGE_NEXT:占用4个字节,存储下一个页;
  • FIL_PAGE_LSN:占用8个字节,页被修改时对应的LSN;
  • FIL_PAGE_TYPE:占用2个字节,页的类型;
  • FIL_PAGE_FILE_FLUSH_LSN:占用8个字节,仅在系统表空间的第一个页中存储,代表文件至少被刷新到了哪个LSN;
  • FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID:占用4个字节,归属的表空间编号。

根据上述参数的解释可以知道,一个innodb中最多有2的32次方个表,每个表最多能容纳2的32次方个页,存储数据的大小最多是64T,如果每个页的大小设置为4kb,最多存储的数据还会更少。

File Trailer

由于操作系统传输单于数据块通常是4kb,一个16kb的数据页会包含4个数据块,发生异常情况可能就会发生一个页仅传输了一部分数据块,此时一个页就是不完整的,这时就需要一个校验方法来检验一个页是否完整。而File Trailer就是用来进行校验数据页的完整性,它仅有一个属性FIL_PAGE_END_LSN占用8个字节,其中前4个字节代表这个页的checksum值,后4个字节与File Header中的FIL_PAGE_LSN的后4个字节相同,通过checksum函数来与File Header中的这两个值进行比较以此来保证数据页的完整性。默认每次读取磁盘都会进行页的完整性校验,可以通过设置innodb_checksums来关闭校验,在Mysql 5.6.6版本中可以通过innodb_checksum_algorithm来设置checksum函数的算法,默认是使用crc32。

User Records

每当有一条记录插入时都会从Free Space区域申请一定空间写入当前数据行,在innodb中有四种行格式默认为DYNAMIC,它除了存储真实的数据信息还会存储一些额外信息,结构如图:

image.png

  • 变成字段长度列表:用来存储表结构中各个变长字段在该行中真实占用的长度;
  • 空值列表:用来存储哪些字段是空值;
  • deleted_flag:删除标识;
  • n_owned:在页面中记录会被分成n组,每组最后一条记录会用这个字段记录该组有多少行,占用4个字节,规定每组最多8行,当该组再插入一个行会分裂成2个组;
  • heap_no:标识当前记录在页面中的相对位置,占用13个字节;
  • record_type:0:代表普通记录,1:代表B+树非叶子结点,2:Infimum记录,3:Supremum记录;
  • next_record:表示下一条记录的相对位置。

Infimun + Supremum Records

每当创建一个新页时会自动创建2个行,一个最小行Infimun,一个最大行Supremum;它俩不存储任何信息,仅作为数据行链表的头和尾,其他用户行则根据主键大小从小到大连接起来。 如图所示:

image.png

Free Space

Free Space指的就是空闲空间,它也是一个链表数据结构,当页中的数据被删除(deleted_flag=1)后就会加入到空闲链表中。

Page Directory

通过上面可以看到在一个页中所有的行都是由链表连接起来,如果想查找一个数据从头节点遍历到尾节点时间复杂度为O(n),这种性能是无法容忍的。此时innodb采用了页目录(Page Directory)来解决查询性能问题,它的思路就是把行分为多个组,每组最多8行数据(java语言中的一些三列表也是用8作为分割线,大家可以思考一下原因),使用一个槽目录来记录每组中最后一条记录的相对位置;由于用户行在链表中是顺序存储的,所以此时可以采用二分法时间复杂度O(logN)来查询目标数据行在哪个组中,再在组中进行遍历即可查询到具体数据。如图:

image.png

每个目录至少有两个槽,Infimun会单独占用一个槽,其他数据行超过8行就会分裂出来一个槽。

Page Header

前面所有的结构都了解了,剩下的这个Page Header理解起来就简单很多了,它主要是用来记录数据页的状态信息,由14个部分组成共需要56个字节,如图所示:

image.png

为了方便大家理解,每个部分我直接写了中文并标明了占用位置。

总结

本篇文章介绍了页,它是innodb对磁盘的最小管理单位,一个页中包含文件头、文件尾、行记录、头记录和尾记录、空闲区域、页目录、页头共7个部分,并且深度分析了每个部分的具体组成。


创作不易,觉得文章写得不错帮忙点个赞吧!如转载请标明出处~