第五章:盛放记录的大盒子-InnoDB数据页结构
不同类型的页
页是InnoDB管理存储空间的基本单位,大小一般为16kb。InnoDB设计了很多页来管理不同的信息:比如Insert Buffer存放的是表空间头部信息,INODE页存放INODE信息,还有存放undo日志信息的页等等
重点是存放表中记录的页,官方叫做索引页(INDEX)。实际上就是数据页我们可以这样简单认为
数据页结构
我们写的记录在页中的存储
会按照指定的行格式存储在页的User Records部分,但是一开始没有User Records部分,插入记录的时候才会从Free Space部分申请一个空间大小为记录大小的部分划分到User Records部分,如果Free Space用完了,那么这页也用完了,于是需要申请新的页
记录头信息的秘密
InnoDB为了管理好存储在User Records中的记录,试了很大劲的在记录头信息中来管理
记录头中的信息有几个属性:
Delete-mask :
标记着当前记录是否被删除,为0的时候代表没有删除,为1的时候则是已经被删除掉了
个人思考:这个参数有点像是之前的逻辑删除,也就是不直接删除,而是用0或者1来代表删除状态
实际上和真实的存储引擎的操作是一样的!!wnia又被我碰对了。
被删除的记录不会马上从磁盘上移除,因为移除需要重新排列其他记录消耗了性能,所以只是打上一个删除标记而已。这不就是我们的逻辑删除嘛
所以被删除掉的记录,或者说打上了标记的记录,都会组成一个垃圾链表,这个垃圾链表中记录占用的空间称之为可重用空间,之后如果有新的记录插入到这个表中的话,可以覆盖这些垃圾链表中的可重用空间
注意:给记录打上标签和移动到垃圾链表中其实是两个阶段。
Min-rec-mask:
B+树的煤层非叶子节点最小记录都会添加该标记。具体什么含义之后再看看
n-owned:
这个是主角!但是什么含义也是后面再看看啊哈哈💦
Heap-no:
表示当前记录在本页中的位置。但是我们插入的记录是从2开始的。0和1 的位置已经被设计师提前插入了两条记录,称之为伪记录或者虚拟记录,一个代表最小记录,一个代表最大记录
记录的大小就是主键的大小对于一条完整的记录来说。
但是最小记录和最大记录已经被伪记录提前预定好了,不管我们插入多少条记录都不会影响它们
这两条伪记录单独存放在Infimum、Supremumd 的部分
而且它们的位置也是最靠前的
record-type:
表示当前记录的类型,共有4中类型的记录。0------普通记录,1------B+非叶节点记录,2------最小记录,3------最大记录。
我们插入的记录都是普通记录,record-type的值都是0。
最小记录和最大记录是2和3,为1的情况则是索引的重点
Next-record:
非常重要,表示当前记录的真实数据到下一条记录的真实数据的地址偏移量。
比如第一条记录的next-record值为32,那么从这条记录往后找32个字节便是第二条记录的真实数据,其实这个就是一个链表。通过地址值的形式形似地将两个数据"链接起来"
注意:下一条记录并不是按照插入顺序的下一条记录,而是按照主键值由小到大的顺序的下一条记录。
规定最小记录(Infimum记录)的下一条记录就是本页中主键值最小的记录,类似的,Supremum记录的下一条记录就是本页中主键值最大的用户记录
那么通过这个next-record的属性将整个表中的记录连接成了一个链表,连接的顺序按照主键的递增方向连接
其中最大记录的next-record的值为0,也就是最大记录后面就没有下一个记录了
删除记录,那么整个链表也会跟着变化,比如删除第二条数据,那么第二条记录的next-record就为0了,并且第一条记录的指针指向第三条记录
总之InnoDB始终会维持一条链表的记录由主键值的递增方向连接。
虽然主键值为2的记录被删除了,但是没有回收其存储空间,如果重新插入呢?
InnoDB直接复用了之前的被删除记录的存储空间
注意:当数据页中存在多条被删除的记录,这些记录的next-record会将这些被删除的记录组成一个垃圾链表,之后重用这部分存储空间
Page Directory(页目录)
现在我们知道了,记录在页中是存放成一个主键递增顺序的链表
如果想查找某一条记录呢?
比如: SELECT * FROM page_demo WHERE c1 = 3;
那么直接遍历整个链表,通过比较节点的值或者看主键的位置即可
但是这样对性能来说并不高,设计InnoDB的哥们为了提高性能从书中的目录中找到了灵感
我们查找一本书的某个内容的时候先看目录找到对应的页码,再根据页码查找内容
InnoDB页也有类似的目录:
(1)将所有的目录划分成几个组
(2)每一组的最后一条记录的n-owned属性表示该记录拥有多少记录,也就是组内的记录总数(主角登场!!)
(3)将每个组的最后一条记录的地址偏移量单独取出来按照顺序放在页的尾部,那么这尾部就是所谓的Page Directory,也就是页目录。
页目录中的地址偏移量称之为槽。也就是说目录是由槽组成的
槽中的值就是从页面中的0字节开始数位置,最大记录就是数112个字节,最小记录就是数99个字节
有几个槽就代表有几个分组,最小记录的n-owned属性为1,也就是以最小记录结尾的分组中只有一条记录,最大记录的n-onwed值为5,说明该组中只有5条记录,包括最大记录本身和插入的记录
InnoDB规定最小记录所在的分组只能有一条记录,最大记录的分组记录条数是1~8,剩下的分组则是4~8
分组经过:
(1)一开始只有最大记录和最小记录,分别属于两个分组
(2)每次插入一条记录,都会从页目录中(也就是那些槽中)找到主键值比本记录大并且差值最小的槽,然后增加该槽对应的n-owned值加1(也就是分组末尾的记录的属性加一),表示组内有添加了一条记录,知道该组的记录数为8
个人理解:其实也就是按主键递增顺序插入的变种嘛
(3)一个组的记录数满了,再插入会拆分组,并且新增一个槽来记录这个新增分组的末尾,也就是最大记录的偏移量
好了现在你有了槽了,那么就可以利用槽来进行快速查找。
具体就是通过二分法找到记录所在的槽,并且找到槽对应的组中的主键值最小的记录,然后通过记录的next-record遍历组中的所有记录
个人思考:实际上这个槽就是相当于变相减少了链表的节点数,利用分组的办法将很多节点划分成一个大节点,然后用槽来标记这个大节点在链表中所处的位置
之后再利用快速遍历的二分法先确定槽,再由槽确定具体的一条记录,还是很有说法的
其实所谓的二分化不过也就是在一个区间内进行二分查找,每查找一次可以划去本次区间一半的节点,然后再剩下的一半中再次二分,这样查找速度是很快的。
具体查找就是比较区间中值和目标值的关系,比大小从而确定划去的是左半边还是右半边的节点,没有说特别复杂
这种方法算是相当折中巧妙的,一开始直接二分没有多高的效率,而是先大节点的二分然后对单独组的遍历,这种组合的查找方式更加高效!!!
有点东西啊,InnoDB!
总结:通过分组划定各个大节点,使用槽来标定大节点在链表中的位置,然后通过二分法先确定大节点的位置再遍历这个大节点得到具体某一条记录
Page Header(页面头部)
用于表示数据页中存储记录的状态信息,比如本页中存储了多少记录,有多少个槽等等
其中有两个:
PAGE_DIRECTION
新插入的记录的主键值大于上一条记录的主键值,那么就说插入方向是右边,反之就是左边。其实就是表示最后一条插入记录的主键是不是更大的
PAGE_N_DIRECTION
连续插入几个新的记录的方向都是一致的话,InnoDB就会将同一个方向插入的记录的条数记录下来,用这个状态量表示。如果最后一条记录的方向改变的话,这个状态的值会被清零
总结:
其实也就是相当于比主键值大小的"箭头":
新插入的更大: > 那么方向就是右边
有多个 > 那么就记录下来,直到有一个小的刷新了这个记录
File Header(文件头部)
Page Header是专门针对数据页的各种状态信息的东西。那么File Header就是对各种类型的页通用的状态信息的东西。不同类型的页都会以File Header 作为第一个组成部分
上面记录的是各种页都通用的信息,比如页的编号是多少,上一个页或者下一个页是谁?
比较重要的几个状态信息:
FIT-PAGE-SPACE-OR-CHKSUM
当前页面的校验和:通过某种算法对一个很长的字节串计算出一个代表值,这个代表值就是校验和。通过校验和的不同可以说明两个不同的字节串
也就是制作一个字节串的替身,通过比较这个替身来比较字节串本身
FIT-PAGE-OFFSET
页号,通过页号可以定位唯一一个页
FIT-PAGE-TYPE
代表当前页的类型,我们熟知的数据页的类型是FIL-PAGE-INDEX,也就是索引页
FIL-PAGE-PREV和FIL-PAGE-NEXT
一张表可能需要多个页来存储,分散多个不连续的页需要将这些页关联起来,使用这两个状态量来关联页
个人思考:PREV和NEXT,不就是链表中的双方向的指针嘛?!man!!
PREV来从本页指向前一页,NEXT从本页指向后一页
File Trailer
InnoDB会从内存中将页的数据同步给磁盘,但是磁盘的速度很慢,如果同步了一半中断了怎么办?
所以为了检测一个页的完整性,使用这个状态量来表示。
这个状态量由两个部分组成,页的校验和 + 页面最后修改时的日志序列位置
Trailer也就是尾部,尾部的校验和是和头部的校验和对应的。同步成功的话那么首位的校验应该是一致的,如果同步中出错了,那么校验和也不对。
日志序列位置LSN暂时先不用管,反正也是一个校验页的完整性的
File Trailer和 File Header都是所有类型的页都通用的
总结
(1)InnoDB为了不同的目的设计出了不同的类型的页,存放记录的页叫做数据页(更加专业的说法是索引页)
(2)一个数据页可以分为7个部分
File Header:表示页的通用信息
Page Header:表示数据页的专有信息
Infimum + Supremum:虚拟的伪记录,分别表示最大和最小记录
个人理解:这个伪记录感觉就像是链表中的锚点一样,锚定了头和尾
User Records : 真实存储的记录部分,大小不固定
Free Space: 没有使用的空闲区域
Page Direcotory:页目录,存放该页面中所有的槽(其实就是槽的集合)
个人思考:其实你看名字就知道了,页目录,顾名思义就是快速定位页中某个位置的字段,于是乎很自然的汇总该页所有的槽(用于快速定位的相对位置)
File Trailer : 检验页面数据是否完整,使页中的所有数据串联成一个单链表
(4)InnoDB将页中的数据划分成很多组,建立槽的概念来快速查找记录:
通过二分法确定槽 + 遍历槽中的所有记录
(5) 每个数据页的File Header部分都有前后页的指针,所有的页面形成了一个双链表
(6)保证页面同步的完整性:头部和尾部的属性中都有校验和来对比验证同步前后页面的数据是否发生变化,还有LSN值,如果校验和和LSN值出现了问题,说明同步过程中有问题