我们继续探讨FIL_PAGE_INDEX页的解析, 本章我们聚焦于解析FIL_PAGE_INDEX页存储的记录。接前文,INDEX_Header结构后面接着的是FSEG Header,FSEG Header用于InnoDB段管理的,跟记录读取无关,这里不做讨论,这部分mysql-file-parser也进行了解析,网上也有一些介绍,有兴趣可以自行研究。
FSEG_Header结构接下来的就是System_Records, System_Records包含Infimum和Supremum记录,我们看结构:
94+========== Infimum Record =========+
| Info Flags (4 bits) |
| Number of Records Owned (4 bits) |
95+-----------------------------------+
| Order (13 bits) |
| Record Type (3 bits) |
97+-----------------------------------+
| Next Record Offset (2 bytes) |
99+-----------------------------------+
| Content (8 bytes) | //固值为: "infimum "
107+========== Supremum Record ========+
| Info Flags (4 bits) |
| Number of Records Owned (4 bits) |
108+-----------------------------------+
| Order (13 bits) |
| Record Type (3 bits) |
110+-----------------------------------+
| Next Record Offset(2 bytes) |
112+-----------------------------------+
| Content (8 bytes) | //固值为: "supremum"
120+-----------------------------------+
* 前面的数字为页内偏移(bytes)
前文提到,InnoDB用户数据在页内的存储,物理上也是无序存放的,这和堆表类似,但是InnoDB会预留预留一些空间,来存储一个单向链表(singly-linked)结构,来让页内的数据按主键有序串连起来。这个单向链表的头就是Infimum Record,而尾就是Supremum Record。FIL_PAGE_INDEX的记录逻辑结构如下:
[infimum record | supremum record]
\ /
\ [Record 1] /
\ | \ /
\ | [record 2] /
[record 3]--/ \ /
[record ...]
Extra Bytes可以看成是某一记录在页内存储的元数据。注意:根据前面概述中介绍,不同行格式,Extra Bytes的是不同的,我们只讨论COMPACT/DYNAMIC行格式的Extra Bytes结构,因为它们是相同的, 都是有5个字节, 分别代表如下:
--------------------------------------------------------------------------------+
REC_NEW_INFO_BITS(Info Flags) |
--------------------------------------------------------+-----------------------+-----------------+--------------------+-----------------+-----------------+
1 bit | 1 bit | 1 bit | 1 bit | 4 bits | 13 bits | 3 bits | 2 bytes |
-----------------------+--------+-----------------------+-----------------------+-----------------+--------------------+-----------------+-----------------+
REC_INFO_INSTANT_FLAG | unused | REC_INFO_DELETED_FLAG | REC_INFO_MIN_REC_FLAG | REC_NEW_N_OWNED | REC_NEW_HEAP_NO | REC_NEW_STATUS | REC_NEXT |
-----------------------+--------+-----------------------+-----------------------+-----------------+--------------------+-----------------+-----------------+
所有的FIL_PAGE_INDEX的遍历记录方式都是一致的,就是通过单链表,从infimum开始,通过Extra Bytes的next_offset找到下一条记录,直到supremum。infimum的Next Record Offset是一个指针,指向的是下一条记录的位置,这个下一条记录位置到底是什么位置?记录的开始?结尾?都不是,而是记录的Extra Bytes结构的结尾。
Infimum Record User Records Supremum record
+=Extra Bytes=+ +=Extra Bytes=+ +=Extra Bytes=+
| ... | | ... | | ... |
+-------------+ +-------------+ +-------------+
| REC_NEXT | | REC_NEXT | | REC_NEXT |
+=============+ ----> +=============+ ----> +=============+
| Content | | Content | | Content |
+=============+ +=============+ +=============+
我们通过一个案例来演示这一个过程:
public class IdxPage2 {
// SUPREMUM_REC_NEXT记录的结束位置, 根据System_Records结构图, 固定为112;
public static final int SUPREMUM_REC_NEXT_END_POS = 112;
// INFIMUM_REC_NEXT记录的结束位置, 根据System_Records结构图, 固定为99;
public static final int INFIMUM_REC_NEXT_END_POS = 99;
public static void main(String[] args) throws IOException, Exception {
String fileName = "D:\\Data\\mysql\\8.0.18\\data\\sakila\\film.ibd";
try (IbdFileParser parser = new IbdFileParser(fileName)) {
IndexPage page = (IndexPage) parser.getPage(4);
byte[] pageRaw = page.getPageRaw(); // 获取page(4)的页的数据(字节)
SystemRecords systemRecords = page.getSystemRecords();
RecordExtra infimumExtra = systemRecords.getInfimumExtra();
RecordExtra supremumExtra = systemRecords.getSupremumExtra();
StringBuilder buff = new StringBuilder();
buff.append("INSTANT_FLAG DELETED_FLAG MIN_REC_FLAG N_OWNED HEAP_NO RECORD_TYPE REC_NEXT(OffSet|Pos)\n")
.append("------------ ------------ ------------ ------- ------- -------------------- --------------------\n");
// Extra的next是一个有符号的整数(Int16), 如果你插入的数据是无序(记录的主键无序)的,偏移有可能是个负数。
// 这个相对偏移, 也就是INFIMUM_REC_NEXT结束的位置 + INFIMUM_REC_NEXT_END_POS
int infimumRecOffset = infimumExtra.getNextRecordOffset();
int infimumRecNextPos = infimumRecOffset + INFIMUM_REC_NEXT_END_POS;
buff.append(dumpRecordExtra(infimumExtra));
int currentPos = infimumRecNextPos;
while (currentPos > SUPREMUM_REC_NEXT_END_POS) {
int extraEndPos = currentPos;
int extraStartPos = currentPos - IndexPage.REC_N_NEW_EXTRA_BYTES;// 5 bytes for DYNAMIC
byte[] extraRaw = Arrays.copyOfRange(pageRaw, extraStartPos, extraEndPos);
RecordExtra extra = new RecordExtra(extraRaw);
int nextOffset = extra.getNextRecordOffset();
int nextPos = nextOffset + extraEndPos;
extra.setNextRecordPos(nextPos);
buff.append(dumpRecordExtra(extra));
if (nextPos == SUPREMUM_REC_NEXT_END_POS) {
break;
}
currentPos = nextPos;
}
buff.append(dumpRecordExtra(supremumExtra));
System.out.println(buff);
}
}
public static String dumpRecordExtra(RecordExtra extra) {
StringBuilder buff = new StringBuilder()
.append(String.format("%12s ", extra.getInstantFlag()))
.append(String.format("%12s ", extra.getDeletedFlag()))
.append(String.format("%12s ", extra.getMinRecFlag()))
.append(String.format("%7d ", extra.getRecordsOwned()))
.append(String.format("%7d ", extra.getRecordHeapNo()))
.append(String.format("%20s ", RecordExtra.getRecordType( extra.getRecordStatus())))
.append(String.format("%20s", extra.getNextRecordOffset() + " | " + extra.getNextRecordPos()))
.append("\n");
return buff.toString();
}
}
程序输出:
INSTANT_FLAG DELETED_FLAG MIN_REC_FLAG N_OWNED HEAP_NO RECORD_TYPE REC_NEXT(OffSet|Pos)
------------ ------------ ------------ ------- ------- -------------------- --------------------
false false false 1 0 REC_STATUS_INFIMUM 27 | 126
false false true 0 2 REC_STATUS_NODE_PTR 12 | 138
false false false 0 3 REC_STATUS_NODE_PTR 12 | 150
false false false 0 4 REC_STATUS_NODE_PTR 12 | 162
false false false 4 5 REC_STATUS_NODE_PTR 12 | 174
false false false 0 6 REC_STATUS_NODE_PTR 12 | 186
false false false 0 7 REC_STATUS_NODE_PTR 12 | 198
false false false 0 8 REC_STATUS_NODE_PTR 12 | 210
false false false 0 9 REC_STATUS_NODE_PTR 12 | 222
false false false 0 10 REC_STATUS_NODE_PTR 12 | 234
false false false 0 11 REC_STATUS_NODE_PTR 12 | 246
false false false 0 12 REC_STATUS_NODE_PTR -134 | 112
false false false 0 1 REC_STATUS_SUPREMUM 0 | 112
其中记录类型:
REC_STATUS_ORDINARY = 0 // 叶节点记录
REC_STATUS_NODE_PTR = 1 // 非叶节点记录
REC_STATUS_INFIMUM = 2 // infimum记录
REC_STATUS_SUPREMUM = 3 // supremum记录
因为案例的page(4)的page_level=1,也就是说存放的是非叶子节点,所以记录类型都是REC_STATUS_NODE_PTR,我们可以看到最后一条记录指向的位置(next_pos)为112,通过System_Records的结构图,可知这是supermum记录的"Next Record Offset(2 bytes)"位置,说明记录到此遍历完成。
我们可以看到Extra Bytes中有一个记录删除位(DELETED_FLAG),没错,和很多系统类似,InnoDB删除记录时通常只是置删除标志,而不是清空内容,后面也会对这个删除标志位做进一步得讨论。