1.前言

  在innodb中页是存储空间的基本单位,一个页的大小一般是16kb。innodb为了不同的目的而设计了多种不同类型的页,比如存放表空间头部信息的页,存放change buffer信息的页,存放inode信息的页,存储undo日志信息的页等等,这里这要说说存放表中记录的那种类型的页,官方称这种存放记录的页为索引(index)页,有时候也会称之为数据页。

从索引设计上 规避 页分裂_存储空间

上图就是innodb数据页结构的示意图

从索引设计上 规避 页分裂_从索引设计上 规避 页分裂_02

2.页存储着记录

  在上面7个部分中,我们自己的数据(记录)会按照指定的行格式存储到User Records部分。但是在一开始生成页的时候,其实并没有User Records部分,每当插入一条记录时,都会从Free Space部分(也就是尚未使用的存储空间)申请一个记录大小的空间,并将这个空间划分到User Records部分。当Free Space部分的空间全部被User records部分替代掉之后,也就意味着这个页使用完了,此时如果还有新的记录插入,就需要申请新的页了。

  2.1 记录头信息

  由于页中存储的都是记录,因为在了解页之前,我们先看看记录,而关于记录在上一节的行格式中已经提到了,一条记录主要是由记录额外信息和记录真实信息两部分组成,其中记录真实信息就是我们所看到的各个列下面的数据(这个就很简单了),那么主要说说记录额外信息了,关于记录额外额外信息主要有1)变长字段字段长度列表 2)NULL值 3)记录头信息三部分组成,其中关于1和2这里就不说了,主要就看3中所谓的记录头信息。

  看如下结构如下:

从索引设计上 规避 页分裂_User_03

 

  各个属性解释:

名称

大小

描述

预留位1

1

没有使用

预留位2

1

没有使用

delete_flag

1

标记该记录是否被删除

min_rec_flag

1

B+树中每层非叶子节点中的最小的目录项记录都会添加该标记

n_owned

4

一个页面中的记录会被分成若干组,每个组中的记录有一个"

带头大哥",其余记录都是“小弟”。“带头大哥”记录的n_owned

值代表该组中所有的记录条数,"小弟"记录的n_owned都是0

heap_no

13

表示当前记录在页面堆中的相对位置

record_type

3

表示当前记录的类型,0表示普通记录,1表示B+树非叶子节点

的目录项记录,2表示infimum记录,3表示Supremum记录

next_record

16

表示下一条记录的相对位置

 

 

 

 

 

 

 

 

 

 

 

 

 

这里模拟一下几条记录,介绍一条记录按照如下格式进行记录的话:

从索引设计上 规避 页分裂_从索引设计上 规避 页分裂_04

从索引设计上 规避 页分裂_从索引设计上 规避 页分裂_05

这里有4条记录,按照上面的记录格式进行填写的

  • delete_flag:这个属性用来标记当前记录是否被删除,占用1比特。值为0时表示记录没有被删除,值为1时表示记录已经被删除。对应这个属性来说,它只是进行对记录进行标记,因此当我们对一条记录delete操作时,其实只是对这条记录做了一个删除的标记,并没有真正的从磁盘上进行删除。这些被删除的记录之所以不从磁盘上删除,是因为移除了它们之后,还需要在磁盘上重新排列其他记录,这就会带来性能上的消耗,所以只打一个删除标记就能避免这个问题。另外所有被删除掉的记录会组成一个垃圾链表,记录在这个链表中占用的空间称为可重用空间,之后若有新的记录插入到表中,它们就可能覆盖掉被删除的这些记录占用的空间。  
  • heap_no:我们向表中插入的记录从本质上来说都是放到数据页的User Records部分,这些记录一条一条地亲密无间地排列着,innodb的设计者把记录一条一条亲密无间排列的结构称之为堆(heap).为了方便管理这个堆,它们把一条记录的在堆中的相对位置称之为heap_no.在页面前边的记录heap_no相对较小。在页面后面的记录heap_no相对较大,每新申请一条记录的存储空间时,该条记录比物理位置在它前面的那条记录的heap_no值大1.另外一点需要注意的是,堆中记录的heap_no值在分配之后就不发生改动了,即使之后删除了堆中的某条记录,这条被删除记录的heap_no值也仍然保持不变。
  • recore_type:这个属性表示当前记录的类型,一共是4中类型的记录,其中0表示普通记录。1表示B+树非叶子节点的目录项记录,2表示infimum记录,3表示Supremum记录
  • next_record:这个属性非常重要,它表示从当前记录的真实数据到下一条记录的真实数据的距离。如果该属性值为正数,说明当前记录的下一条记录在当前记录的后面,如果该属性值为负数,说明当前记录的下一条记录在当前记录的前面,比如,第1条记录的next_record值为32,意味着从第1条记录的真实数据的地址处向后找32字节便是下一条记录。在比如,第四条的next_record值为-111,意味着从第4条记录的真实数据的地址处向前找111字节便是下一条记录的真实数据,这个有点类似于链表,可以通过一条记录找到下一条记录。

从索引设计上 规避 页分裂_User_06

  这里使用箭头替代next_record的值,从上图可以看出,记录按照主键从小到大的顺序形成一个单向链表。Supremum记录的next_record值为0,也就是说Supremum记录之后就没有下一条记录了,这也意味着Supremum记录就是这个单向链表中的最后一个节点,如果从表中删除一条记录,这个由记录组成的单向链表也是会跟着变化。

从索引设计上 规避 页分裂_存储空间_07

所以无论怎么对页中的记录进行增删改查操作,innodb始终会维护记录的一个单向链表,链表中的各个节点是按照主键值由小到大的顺序链接起来的。

这里再看一个有意思的地方:主键值为2的记录被删除掉了,但是却没有回收存储空间(该记录的heap_no也未发生改变),如果我们在把这条记录插入到表中,会发生什么呢?

从索引设计上 规避 页分裂_存储空间_08

这里可以看到,innodb并没有因为新的记录的插入而为它申请新的存储空间,而是直接复用了原理被删除记录的存储空间。