目錄
- 頁(Page)是 Innodb 存儲引擎用于管理數據的最小磁盤單位
- B+樹的一般高度
- 記錄在頁中的存儲
- innodb ibd文件
- innodb 頁類型
- 分析`ibd`文件
- 查看數據表的行格式
- 查看ibd文件
- 分析 ibd的第4個頁:`B-tree Node`類型
- 先分析File Header(38字節-描述頁信息)
- 再分析Page Header(56字節-記錄頁的狀態信息)
- 分析Infimum + Supremum Record (26字節-兩個虛擬行記錄)
- User Record(表中的數據記錄)
- COMPACT行記錄格式
- File Tailer(最后8字節)
- 附:二進制文件查看小技巧
- 頁分裂/頁合并
- innodb數據的存儲
- 頁合并
- 頁分裂
- 附加:mysql頻繁的插入刪除導致的問題
- 1. 性能下降
- 2. 碎片化
- 3. 鎖定和阻塞
- 4. 日志文件增長
- 5. 資源競爭
頁(Page)是 Innodb 存儲引擎用于管理數據的最小磁盤單位
innoDB 中頁的默認大小是 16KB
假設一行記錄的數據大小為1k,那么單個葉子節點(頁)中的記錄數=16K/1K=16
- File Header: 文件頭部,頁的一些通用信息(38字節)
- page Header: 頁面頭部,數據頁專有的一些信息(56字節)
- infimum+supremum: 行記錄最小值和最大值,兩個虛擬的行記錄(26字節)
- user recorders: 實際存儲的行記錄內容(不確定)
- free space: 頁中尚未使用的空間(不確定)
- Page Directory: 頁中的某些記錄的相對位置(不確定)
- File Tailer: 校驗頁是否完整(8字節)
B+樹的一般高度
即非葉子節點能存放多少指針?
假設主鍵ID為bigint類型,長度為8字節,而指針大小在InnoDB源碼中設置為6字節,這樣一共14字節,一個頁中能存放多少這樣的單元,其實就代表有多少指針,即16kb/14b=1170;那么可以算出一棵高度為2的B+樹,大概能存放1170*16=18720條這樣的數據記錄(即1170個索引,每個索引定位葉子節點的一頁數據,一頁能存儲16行原始數據)。
根據同樣的原理我們可以算出一個高度為3的B+樹大概可以存放:1170*1170*16=21,902,400
行數據。所以在InnoDB中B+樹高度一般為1-3層,它就能滿足千萬級
的數據存儲。在查找數據時一次頁的查找代表一次IO,所以通過主鍵索引查詢通常只需要1-3次邏輯IO操作即可查找到數據。
記錄在頁中的存儲
- 當一個記錄需要插入頁的時候,會從
Free space
劃分空間到User recorders
Free Space
部分的空間全部被User Records
部分替代掉之后,也就意味著這個頁使用完了,如果還有新的記錄插入的話,就需要去申請新的頁了
innodb ibd文件
ibd文件是以頁
為單位進行管理的,頁通常是以16k為單位,所以ibd文件通常是16k的整數倍
innodb 頁類型
名稱 | 十六進制 | 解釋 |
---|---|---|
FIL_PAGE_INDEX | 0x45BF | B+樹葉節點 |
FIL_PAGE_UNDO_LOGO | 0x0002 | UNDO LOG頁 |
FIL_PAGE_INODE | 0x0003 | 索引節點 |
FIL_PAGE_IBUF_FREE_LIST | 0x0004 | InsertBuffer空閑列表 |
FIL_PAGE_TYPE_ALLOCATED | 0x0000 | 該頁的最新分配 |
FIL_PAGE_IBUF_BITMAP | 0x0005 | InsertBuffer位圖 |
FIL_PAGE_TYPE_SYS | 0x0006 | 系統頁 |
FIL_PAGE_TYPE_TRX_SYS | 0x0007 | 事務系統數據 |
FIL_PAGE_TYPE_FSP_HDR | 0x0008 | FILE SPACE HEADER |
FIL_PAGE_TYPE_XDES | 0x0009 | 擴展描述頁 |
FIL_PAGE_TYPE_BLOB | 0x000A | BLOB頁 |
分析ibd
文件
mubi@mubideMacBook-Pro bin $ mysql -uroot -p123456
mysql: [Warning] Using a password on the command line interface can be insecure.
Welcome to the MySQL monitor. Commands end with ; or \g.
Your MySQL connection id is 109
Server version: 5.6.40 MySQL Community Server (GPL)Copyright (c) 2000, 2015, Oracle and/or its affiliates. All rights reserved.Oracle is a registered trademark of Oracle Corporation and/or its
affiliates. Other names may be trademarks of their respective
owners.Type 'help;' or '\h' for help. Type '\c' to clear the current input statement.mysql> use test;
Reading table information for completion of table and column names
You can turn off this feature to get a quicker startup with -ADatabase changed
mysql> select * from tb_user;
+------------+--------+----------+-------+-------------+----------+
| id | userID | password | name | phone | address |
+------------+--------+----------+-------+-------------+----------+
| 1 | 00001 | 123456 | zhang | 15133339999 | Shanghai |
| 2 | 00002 | 123456 | wang | 15133339999 | Beijing |
| 4 | 0003 | NULL | abc | NULL | NULL |
| 6 | 0003 | NULL | abc | NULL | NULL |
| 7 | 0004 | 123456 | tom | 110 | beijing |
| 10 | 0004 | 123456 | tom | 110 | beijing |
| 2147483647 | 0004 | 123456 | tom | 110 | beijing |
+------------+--------+----------+-------+-------------+----------+
7 rows in set (0.00 sec)mysql>
查看數據表的行格式
mysql> show table status like 'tb_user'\G;
*************************** 1. row ***************************Name: tb_userEngine: InnoDBVersion: 10Row_format: CompactRows: 6Avg_row_length: 2730Data_length: 16384
Max_data_length: 0Index_length: 16384Data_free: 0Auto_increment: 2147483647Create_time: 2020-03-21 16:10:06Update_time: NULLCheck_time: NULLCollation: utf8_general_ciChecksum: NULLCreate_options:Comment:
1 row in set (0.00 sec)ERROR:
No query specifiedmysql>
查看ibd文件
使用py_innodb_page_info
工具(https://github.com/happieme/py_innodb_page_info
)
注意到文件大小114688
字節(114688 = 16 * 1024 * 7)即有7
個頁(要分析哪個頁直接定位到二進制文件到開始,然后分析即可)
分析 ibd的第4個頁:B-tree Node
類型
page offset 00000003, page type <B-tree Node>, page level <0000>
>>> hex(3 * 16 * 1024)
'0xc000'
>>> hex(4 * 16 * 1024)
'0x10000'
>>>
先分析File Header(38字節-描述頁信息)
- 2D A1 2D 57 -> 數據頁的checksum值
- 00 00 00 03 -> 頁號(偏移量),當前是第3頁
- FF FF FF FF -> 目前只有一個數據頁,無上一頁
- FF FF FF FF -> 目前只有一個數據頁,無下一頁
- 00 00 00 04 6F 65 24 CF -> 該頁最后被修改的LSN
- 45 BF -> 頁的類型,0x45BF代表數據頁,剛好這頁是數據頁
- 00 00 00 00 00 00 00 00 -> 獨立表空間,該值為0
- 00 00 00 06 -> 表空間的SPACE ID
再分析Page Header(56字節-記錄頁的狀態信息)
參見:innodb-page-header
標識 | 字節數 | 解釋 | 本次值:說明 |
---|---|---|---|
PAGE_N_DIR_SLOTS | 2 | number of directory slots in the Page Directory part; initial value = 2 | 00 02,2個槽位 |
PAGE_HEAP_TOP | 2 | record pointer to first record in heap | 01 ED,堆第一個開始位置的偏移量,也即空閑偏移量 |
PAGE_N_HEAP | 2 | number of heap records; initial value = 2 | 80 0A |
PAGE_FREE | 2 | record pointer to first free record | 01 1C |
PAGE_GARBAGE | 2 | “number of bytes in deleted records” | 00 20,刪除的記錄字節 |
PAGE_LAST_INSERT | 2 | record pointer to the last inserted record | 01 C5,最后插入記錄的位置偏移 |
PAGE_DIRECTION | 2 | either PAGE_LEFT, PAGE_RIGHT, or PAGE_NO_DIRECTION | 00 02,自增長的方式進行行記錄的插入,方向向右 |
PAGE_N_DIRECTION | 2 | number of consecutive inserts in the same direction, for example, “last 5 were all to the left” | 00 02 |
PAGE_N_RECS | 2 | number of up[ser records | 00 07,共7條有效記錄數 |
PAGE_MAX_TRX_ID | 8 | the highest ID of a transaction which might have changed a record on the page (only set for secondary indexes) | 00 00 00 00 00 00 00 00 |
PAGE_LEVEL | 2 | level within the index (0 for a leaf page) | 00 00 |
PAGE_INDEX_ID | 8 | identifier of the index the page belongs to | 00 00 00 00 00 00 00 16 |
PAGE_BTR | 10 | “file segment header for the leaf pages in a B-tree” (this is irrelevant here) | 00 00 00 06 00 00 00 02 00 F2 |
PAGE_LEVEL | 10 | “file segment header for the non-leaf pages in a B-tree” (this is irrelevant here) | 00 00 00 06 00 00 00 02 00 32 |
0xc000 + 01 ED
=0xC1ED
地址后面的都是空閑的
0xc000 + 01 C5
=0xC1C5
最后一條記錄
分析Infimum + Supremum Record (26字節-兩個虛擬行記錄)
infimum: n. 下確界;
supremum: n. 上確界;
Infimum和Suprenum Record用來限定記錄的邊界,Infimum
是比頁中任何主鍵值都要小的值,Suprenum
是指比任何頁中可能大值還要大的值,這兩個值在頁創建時被建立,并且在任何情況下都不會被刪除。Infimum和Suprenum與行記錄組成單鏈表結構,查詢記錄時,從Infimum開始查找,如果找不到結果會直到查到最后的Suprenum為止,然后通過Page Header中的FIL_PAGE_NEXT
指針跳轉到下一個page繼續從Infimum開始逐個查找
#Infimum偽行記錄
01 00 02 00 20/*recorder header*/
69 6E 66 69 6D 75 6D 00/*只有一個列的偽行記錄,記錄內容就是Infimum(多了一個0x00字節)
*/
#Supremum偽行記錄
08 00 0B 00 00/*recorder header*/
73 75 70 72 65 6D 75 6D/*只有一個列的偽行記錄,記錄內容就是Supremum*/
infimum行記錄的recorder header部分,最后2個字節位00 20
表示下一個記錄的位置的偏移量
User Record(表中的數據記錄)
用戶所有插入的記錄都存放在這里,默認情況下記錄跟記錄之間沒有間隙,但是如果重用了已刪除記錄的空間,就會導致空間碎片。每個記錄都有指向下一個記錄的指針,但是沒有指向上一個記錄的指針。記錄按照主鍵順序排序:即用戶可以從數據頁最小記錄開始遍歷,直到最大的記錄,這包括了所有正常的記錄和所有被delete-marked記錄,但是不會訪問到被刪除的記錄(PAGE_FREE)
COMPACT行記錄格式
-
行格式的首部是一個非NULL變長字段長度列表,而且是按照列的順序逆序放置的。當列的長度小于255字節,用1字節表示,若大于255個字節,用2個字節表示,變長字段的長度最大不可以超過2個字節(這也很好地解釋了為什么MySQL中varchar的最大長度為65535,因為2個字節為16位,即
pow(2,16)-1=65536
)。第二個部分是NULL標志位,該位指示了該行數據中是否有NULL值,1個字節表示;該部分所占的字節應該為bytes;接下去的部分是為記錄頭信息(record header),固定占用5個字節(40位),每位的含義如下 -
預留位1 1(bit位) 沒有使用
-
預留位2 1 沒有使用
-
delete_mask 1 標記該記錄是否被刪除
-
min_rec_mask 1 標記該記錄是否為B+樹的非葉子節點中的最小記錄
-
n_owned 4 表示當前槽管理的記錄數
-
heap_no 13 表示當前記錄在記錄堆的位置信息
-
record_type 3 表示當前記錄的類型,0表示普通記錄,1表示B+樹非葉節點記錄,2表示最小記錄,3表示最大記錄
-
next_record 16 表示下一條記錄的相對位置
File Tailer(最后8字節)
7E 75 29 30 6F 65 24 CF
注意到File Header該頁最后被修改的LSN:00 00 00 04 6F 65 24 CF
,可以看到后4個字節和File Tailer
的后4個字節相同
附:二進制文件查看小技巧
- 使用python可以方便的進行二進制相關的轉換
- hex(16) # 10進制轉16進制
- oct(8) # 10進制轉8進制
- bin(8) # 10進制轉2進制
>>> hex(6 * 16 * 1024)
'0x18000'
>>> hex(3 * 16 * 1024)
'0xc000'
>>>
- vscode可以安裝
hexdump for VSCode
插件
頁分裂/頁合并
innodb-page-merging-and-page-splitting
InnoDB不是按行的來操作的,它可操作的最小粒度是頁,頁加載進內存后才會通過掃描頁來獲取行/記錄,curd操作則會產生頁合并,頁分裂操作。
innodb數據的存儲
在 InnoDB 存儲引擎中,所有的數據都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(extent)、頁(page), 頁中存放實際的數據記錄行
MySQL 使用 InnoDB 存儲表時,會將表的定義和數據相關記錄、索引等信息分開存儲,其中前者存儲在.frm
文件中,后者存儲在.ibd
文件中(ibd文件既存儲了數據也存儲了索引)
在創建表時,會在磁盤上的 datadir 文件夾中生成一個 .frm
的文件,這個文件中包含了表結構相關的信息
頁合并
刪除記錄時會設置record的flaged標記為刪除,當一頁中刪除記錄超過MERGE_THRESHOLD(默認頁體積的50%)時,InnoDB會開始尋找最靠近的頁(前或后)看看是否可以將兩個頁合并以優化空間使用。例如:合并操作使得頁#5保留它之前的數據,并且容納來自頁#6的數據。頁#6變成一個空頁,可以接納新數據。
頁分裂
頁可能填充至100%,在頁填滿了之后,下一頁會繼續接管新的記錄。但如果下一頁也沒有足夠空間去容納新(或更新)的記錄,那么就必須創建新的頁了,如下
- 創建新頁
- 判斷當前頁(頁#10)可以從哪里進行分裂(記錄行層面)
- 移動記錄行
- 重新定義頁之間的關系
#9 #10 #11 #12 #13 ...#8 #10 #14 #11 #12 #13 ...
如上,頁#10沒有足夠空間去容納新記錄,頁#11也同樣滿了, #10要分列為兩列, 且頁的前后指針關系要發生改變
附加:mysql頻繁的插入刪除導致的問題
1. 性能下降
頻繁的插入和刪除操作會增加數據庫的工作量,特別是在高并發環境下,可能導致查詢響應時間變長。
解決方案:
優化查詢:確保你的查詢是有效且高效的,使用適當的索引。
批量操作:盡可能使用批量插入(INSERT INTO … VALUES (), (), …)和批量刪除(DELETE FROM WHERE id IN (…)) 而不是單條記錄操作。
分區表:對于非常大的表,考慮使用分區表,這可以改善查詢性能和減少單個表的維護負擔。
2. 碎片化
頻繁的插入和刪除操作會導致數據文件和索引文件碎片化,這會降低查詢效率。
解決方案:
定期重建索引:使用OPTIMIZE TABLE命令來重建表的索引,這可以幫助減少碎片。
歸檔舊數據:定期歸檔舊數據到另一個表或數據庫中,減少主表的負擔。
3. 鎖定和阻塞
在高并發環境下,頻繁的插入和刪除操作可能導致行級鎖或表級鎖,從而引起阻塞和死鎖。
解決方案:
減少鎖的粒度:考慮使用較低級別的隔離級別(如READ COMMITTED),或者使用樂觀鎖策略。
鎖優化:分析并優化事務的設計,確保事務盡可能短,避免長事務。
使用鎖定的最小范圍:盡可能在WHERE子句中指定具體的條件來減少鎖定的范圍。
4. 日志文件增長
頻繁的寫入操作會增加二進制日志(如binlog)和重做日志(如InnoDB的redo log)的大小,這可能會影響磁盤I/O性能。
解決方案:
配置合適的日志文件大小:調整max_binlog_size或innodb_log_file_size參數以控制日志文件的大小。
定期清理日志文件:使用PURGE BINARY LOGS命令來刪除舊的二進制日志文件。
5. 資源競爭
在高負載情況下,頻繁的插入和刪除可能會增加CPU和內存的使用率,尤其是在多核服務器上。
解決方案:
硬件升級:如果可能,增加服務器的CPU核心數或內存容量。
負載均衡:使用數據庫復制或分片策略來分散負載。
監控和調優:定期監控數據庫性能,根據需要調整配置。