在數據庫系統中,通常包含數據字典(data dictionary)用于記錄數據庫對象的元數據(表,分區,觸發器,存儲過程,函數的定義),我們可以通過information_schema(i_s)數據庫下的視圖(view)或者SHOW語句來訪問數據字典。例如,可以通過show create table sakila.film
語句獲取film表的DDL語句。或者使用的innodb_indexes, innodb_tables,… 視圖獲取表元數據信息。 在MySQL 8.0之前的版本,數據字典信息存儲在基于文件的元數據文件,不支持事務的表,和存儲引擎相關的數據字典中。
在MySQL 5.7版本,sakila.film表,除了film.ibd文件,還有film.frm和film.TRG文件。.TRG文件保存了film表相關的觸發器元數據,而.frm文件則存儲了film表的元數據,包括字段名,字段數據類型等信息。基于文件的數據字典不支持事務,在管理上有一系列的問題,因此從MySQL8.0版本開始,MySQL引入一個基于事務的數據字典來存儲數據對象的信息。詳細請參考:
https://minervadb.com/index.php/2018/06/11/mysql-8-0-data-dictionary/
https://dev.mysql.com/doc/refman/8.0/en/data-dictionary.html
Data dictionary tables are created in a single InnoDB tablespace named mysql.ibd, which resides in the MySQL data directory. The mysql.ibd tablespace file must reside in the MySQL data directory and its name cannot be modified or used by another tablespace.
注意:是不允許通過SQL語句直接訪問數據字典的:
root@localhost [testcase]> select * from mysql.tables;
ERROR 3554 (HY000): Access to data dictionary table ‘mysql.tables’ is rejected.
但可以通過一些方法繞過限制: https://lefred.be/content/mysql-8-0-data-dictionary-tables-and-why-they-should-stay-protected/
其中一個重要的改變就是,從8.0版本開始,MySQL廢棄了.frm文件(還包括.par,.TRN,.TRG, .isl,db.opt,ddl_log.log ),MySQL使用字典信息序列化格式(serialized form)來存儲數據字典,這部分數據稱為serialized dictionary information (SDI),MySQL將SDI存放對應表的表空間中,并引入新的Page如:FIL_PAGE_SDI來存放SDI的數據。對于非InnoDB表,如MyISAM,SDI會存放在.sdi文件中(一種json格式的文本文件)。MySQL還提供了ibd2sdi外部命令用于從表空間文件(ibd)中讀取的SDI信息。
我們可以把SDI記錄看成是一張普通表,可以把FIL_PAGE_SDI頁當做FIL_PAGE_INDEX (ClusteredKeyLeafPage)來解析,而SDI"表"的定義偽代碼如下:
CREATE TABLE SID {type bytes(2) not null, // 對應ibd2sdi的-t, --type=#參數,請參考ibd2sdi命令官方文檔。id bytes(4) not null, // 對應ibd2sdi的-i, --id=#參數,請參考ibd2sdi命令官方文檔。/*DB_TRX_ID bytes(6) not null,*//*DB_ROLL_PTR bytes(7) not null,*/uncomp_len bytes(5) not null, // zip_data解壓后的長度(字節數);comp_len bytes(6) not null, // zip_data的長度(字節數);zip_data bytes(variable-length) not null, //通過zip壓縮后的SDI記錄內容;PRIMARY KEY (type, id)
}
FIL_PAGE_SDI頁的位置位于ibd文件的page(3),我們通過案例來解析該頁的內容:
public class SdiPage1 {public static void main(String[] args) throws Exception {String fileName = "D:\\Data\\mysql\\8.0.18\\data\\sakila\\film.ibd";try (IbdFileParser parser = new IbdFileParser(fileName)) {SdiPage page = (SdiPage) parser.getPage(3);List<SdiRecord> records = page.getUserRecords();StringBuilder buff = new StringBuilder();int rows = 0;for(SdiRecord record: records) {byte[] unZipData = record.getUnZipDataRaw();byte[] zipData = record.getZipDataRaw();String json = toPretty(new String(unZipData));String format = "%11s : ";buff.append("\n*************************** ").append(++rows).append(". row ***************************\n").append(String.format(format, "type")).append(record.getType()).append("\n").append(String.format(format, "id")).append(record.getId()).append("\n").append(String.format(format, "DB_TRX_ID")).append(ParserHelper.toHexString(record.getTrxIdRaw())).append("\n").append(String.format(format, "DB_ROLL_PTR")).append(ParserHelper.toHexString(record.getRollPrtRaw())).append("\n").append(String.format(format, "unzip_len")).append(record.getUncompressedLen()).append(" , actual : ").append(unZipData.length).append("\n").append(String.format(format, "zip_len")).append(record.getCompressedLen()).append(" , actual : ").append(zipData.length).append("\n").append("\n*************************** Content ***************************\n").append(json).append("\n");}System.out.println(buff);}}public static String toPretty(String jsonString) {JsonElement jsonElement = JsonParser.parseString(jsonString);Gson gson = new GsonBuilder().setPrettyPrinting().create();String prettyJson = gson.toJson(jsonElement);return prettyJson;}
}
/*
程序輸出:
*************************** 1. row ***************************type : 1id : 521DB_TRX_ID : 000000738097
DB_ROLL_PTR : 020000028a15a9unzip_len : 16801 , actual : 16801zip_len : 1884 , actual : 1884*************************** Content ***************************
{"mysqld_version_id": 80018,"dd_version": 80017,"sdi_version": 80016,"dd_object_type": "Table","dd_object": {"name": "film","mysql_version_id": 80018,"created": 20210319003202,"last_altered": 20210319003202,...
}}*************************** 2. row ***************************type : 2id : 392DB_TRX_ID : 00000073803d
DB_ROLL_PTR : 82000001230535unzip_len : 372 , actual : 372zip_len : 233 , actual : 233*************************** Content ***************************
{"mysqld_version_id": 80018,"dd_version": 80017,"sdi_version": 80016,"dd_object_type": "Tablespace","dd_object": {"name": "sakila/film","comment": "","options": "encryption\u003dN;","se_private_data": "flags\u003d16417;id\u003d387;server_version\u003d80018;space_version\u003d1;state\u003dnormal;","engine": "InnoDB","files": [{"ordinal_position": 1,"filename": ".\\sakila\\film.ibd","se_private_data": "id\u003d387;"}]}
}
*/
與ibd2sid命令輸出比對,程序解析符合預期,解析細節可以看SdiPage.class,做法就是當作ClusteredKeyLeafPage來解析,zip_data通過JDK的java.util.zip.Inflater解壓。到此ibd解析系列告一段落,未來如果精力允許,而且知識儲備足夠,會挑戰一下redo或者undo表空間結構的解析。