目录
一、页的大小
二、页的分类
三、页头和页尾
3.1 页头--File Header
3.2 页尾--File Trailer
3.3 LSN
四、数据行
五、页中数据的查询
六、事务和索引在页中的记录
一、页的大小
前面介绍了每个数据页默认大小为16KB,是操作系统“数据块” 4KB 的整数倍,那么只要保证页的大小是操作系统“数据块”大小的整数倍,我们也可以修改数据页的大小。
MySQL提供了一个专门的系统变量来控制页的大小,可以通过系统变量 innodb_page_size 进行调整与查看,在调整页大小时需要保证设置的值是操作系统“数据块”大小的整数倍,从而保证通过操作系统和磁盘交互时“数据块”的完整性,不被分割和浪费,所以规定了 innodb_page_size 的可以设置的值,分别为4096、8192、16384、32768、65536,对应 4KB 、8KB、16KB、32KB、64KB。
二、页的分类
InnoDB在不同的使用场景下定义多种不同类型的页,常用的有数据页、Undo Log页、Change Buffer页、Extent Descriptor(XDES)页、InnoDB段信息页等,其中最需要我们关注的就是数据页,由于InnoDB中有个概念叫“索引即数据”,所以也叫做索引页。
不论哪种类型的页都具有页头(File Header)和页尾(File Trailer)两个信息。
三、页头和页尾
页头和页尾用来描述文件相关信息,如下图所示:
3.1 页头--File Header
页号:FIL_PAGE_OFFSET 占用 4byte ,相当于页的身份证号,通过这个长度可以计算出每个InnoDB表中最多能有 2^(4*8)- 1 约42亿 个页,表空间的第一个页编号从0开始,之后的编号分别是1 2 3 4....以此类推,具体页的偏移量计算工程为 页号 * 每页大小。那么按照每个页默认16KB来计算,一个表空间最大的容量为 64TB ,这也是InnoDB表空间最大容量是 64TB 的原因。
上一页页号:FIL_PAGE_PREV
下一页页号:FIL_PAGE_NEXT 多个页通过这两个信息组成双向链表,即便不同的页地址不相连,也能通过链表连接。
表空间ID:FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID,当前页属于哪个表空间。
页类型:FIL_PAGE_TYPE,数据页对应的页类型是 FIL_PAGE_INDEX = 0x45BF
最近一次修改的LSN:FIL_PAGE_LSN ,占用8byte
已被刷到磁盘的LSN:FIL_PAGE_FILE_FLUSH_LSN,占用8byte
校验和:FIL_PAGE_SPACE_OR_CHKSUM,用于页的完整性校验
3.2 页尾--File Trailer
最近一次修改的LSN
校验和:对应页头中的校验和。
如果在数据传输的过程中数据丢失或者异常中断,导致一个数据页不完整就可以使用页头页尾中的校验和来进行验证,验证算法默认使用 CRC32。
3.3 LSN
LSN:是“log Sequence Number” 的缩写,表示日志序号。用一个任意的、不断增加的值表示日志中记录的操作对应的时间点,用8字节的无符号整型表示。
四、数据行
数据行主要存储真实数据,为了方便数据的管理与描述,InnoDB在每个数据行中还添加了一些额外(管理)信息,于是每个 DYNAMIC 数据行都可以划分为两部分,一部分存储额外信息,一部分存储真实数据,额外信息部分包含变长字段列表和NULL值列表两个大小不确定的区域,以及固定占5字节及40BIT的的头信息区域,头信息中存储了行的基本信息,包括行在页内的位置 heap_no、行类型 record_type、下一行的地址偏移量 next_record 等六项信息,如下图所示:
数据行通过下一行的地址偏移量,将页中所有数据行组成一个单项链表,这里需要注意的是,地址偏移量指向的是下一行中真实数据的起始地址,这样做的好处是,向右是真实数据,向左是头信息,而无需额外的长度计算。
在了解了行的基本结构之后,那么当遍历页中的行时,从哪里开始哪里结束呢?为了解决这个问题,每当创建新页时都会自动分配两个行,一个是行类型为 2 的最小行 Infimun,heap_no 的值固定为0号, 和一个行类型为 3 的最大行 Supermun,heap_no 的值固定为1号,且这两个行不存储任何真实数据,而是作为数据行链表的头和尾,虽然不存储真实数据,但它们的数据结构和真实数据行一模一样,只不过数据区域存放的是代表他们身份的固定字符串 Infimun 和 Supermun ,新页中没有数据时,最小行的 next_record 直接连接最大行,最大行不连接任何行。
当一个新页插入数据时,heap_no 会从2开始递增,表示当前记录在页面堆中的相对位置,如果是真实数据则 record_type 为0,如果是索引目录(B+树非叶子节点)数据则 record_type 为 1,再将最小行连接第一个数据行,最后一行真实数据行连接最大行,这样数据行就形成了一个单向链表,更多的数据插入后,会按照主键从小到大的顺序连接,为了使页的结构更加清晰通常将页中有数据行的区域称为 用户数据区 User Records ,把未被数据行占用的区域称为 空闲区 Free Space。
五、页中数据的查询
从头开始遍历时最简单的方法,也可以实现数据的查找,当按主键或索引查找某条数据时,从头行 Infimun 开始,沿着链表顺序一个个查找,但一个页有 16KB ,通常会存在数百行数据,每次都要遍历数百行无法满足高效查询。
为了提高查询效率,InnoDB采用二分查找的方式进行查找。
具体实现方法是,在每一个页中加入一个叫做 页目录 Page Directory 的结构,将页内包括头行、尾行在内的所有行进行分组,约定头行单独为一组,其他每个组最多八条数据,同时把每个组最后一行在页中的地址,按主键从小到大的顺序记录在页目录中,页目录中每一个位置称为一个槽,每个槽都对应了一个分组,这样在插入数据行完成连接后,一旦最后一个分组中的数据行超过八个时,就会分裂出一个新的分组,为了快速判断每个分组是否达到八个的上限,在每个分组最后一行用 n_owned 记录这个分组的行数,与此同时在页目录中创建一个新的槽,后续插入的行都遵守这个规则。
后续在查询某行时,就可以通过二分查找,先找到对应的槽,然后再槽内最多八个数据行中进行遍历,从而大幅度提高查询效率。
六、事务和索引在页中的记录
如下图所示: