轉自:https://draveness.me/mysql-innodb
作為一名開發(fā)人員,在日常的工作中會難以避免地接觸到數據庫,無論是基于文件的 sqlite 還是工程上使用非常廣泛的 MySQL、PostgreSQL,但是一直以來也沒有對數據庫有一個非常清晰并且成體系的認知,所以最近兩個月的時間看了幾本數據庫相關的書籍并且閱讀了 MySQL 的官方文檔,希望對各位了解數據庫的、不了解數據庫的有所幫助。
本文中對于數據庫的介紹以及研究都是在 MySQL 上進行的,如果涉及到了其他數據庫的內容或者實現會在文中單獨指出。
數據庫的定義
很多開發(fā)者在最開始時其實都對數據庫有一個比較模糊的認識,覺得數據庫就是一堆數據的集合,但是實際卻比這復雜的多,數據庫領域中有兩個詞非常容易混淆,也就是數據庫和實例:
- 數據庫:物理操作文件系統(tǒng)或其他形式文件類型的集合;
- 實例:MySQL 數據庫由后臺線程以及一個共享內存區(qū)組成;
對于數據庫和實例的定義都來自于 MySQL 技術內幕:InnoDB 存儲引擎 一書,想要了解 InnoDB 存儲引擎的讀者可以閱讀這本書籍。
數據庫和實例
在 MySQL 中,實例和數據庫往往都是一一對應的,而我們也無法直接操作數據庫,而是要通過數據庫實例來操作數據庫文件,可以理解為數據庫實例是數據庫為上層提供的一個專門用于操作的接口。

在 Unix 上,啟動一個 MySQL 實例往往會產生兩個進程,mysqld 就是真正的數據庫服務守護進程,而 mysqld_safe 是一個用于檢查和設置 mysqld 啟動的控制程序,它負責監(jiān)控 MySQL 進程的執(zhí)行,當 mysqld 發(fā)生錯誤時,mysqld_safe 會對其狀態(tài)進行檢查并在合適的條件下重啟。
MySQL 的架構
MySQL 從第一個版本發(fā)布到現在已經有了 20 多年的歷史,在這么多年的發(fā)展和演變中,整個應用的體系結構變得越來越復雜:

最上層用于連接、線程處理的部分并不是 MySQL 『發(fā)明』的,很多服務都有類似的組成部分;第二層中包含了大多數 MySQL 的核心服務,包括了對 SQL 的解析、分析、優(yōu)化和緩存等功能,存儲過程、觸發(fā)器和視圖都是在這里實現的;而第三層就是 MySQL 中真正負責數據的存儲和提取的存儲引擎,例如:InnoDB、MyISAM 等,文中對存儲引擎的介紹都是對 InnoDB 實現的分析。
數據的存儲
在整個數據庫體系結構中,我們可以使用不同的存儲引擎來存儲數據,而絕大多數存儲引擎都以二進制的形式存儲數據;這一節(jié)會介紹 InnoDB 中對數據是如何存儲的。
在 InnoDB 存儲引擎中,所有的數據都被邏輯地存放在表空間中,表空間(tablespace)是存儲引擎中最高的存儲邏輯單位,在表空間的下面又包括段(segment)、區(qū)(extent)、頁(page):

同一個數據庫實例的所有表空間都有相同的頁大??;默認情況下,表空間中的頁大小都為 16KB,當然也可以通過改變 innodb_page_size 選項對默認大小進行修改,需要注意的是不同的頁大小最終也會導致區(qū)大小的不同:

從圖中可以看出,在 InnoDB 存儲引擎中,一個區(qū)的大小最小為 1MB,頁的數量最少為 64 個。
如何存儲表
MySQL 使用 InnoDB 存儲表時,會將表的定義和數據索引等信息分開存儲,其中前者存儲在 .frm 文件中,后者存儲在 .ibd 文件中,這一節(jié)就會對這兩種不同的文件分別進行介紹。

.frm 文件
無論在 MySQL 中選擇了哪個存儲引擎,所有的 MySQL 表都會在硬盤上創(chuàng)建一個 .frm 文件用來描述表的格式或者說定義;.frm 文件的格式在不同的平臺上都是相同的。
<pre>CREATE TABLE test_frm( column1 CHAR(5), column2 INTEGER);
</pre>
當我們使用上面的代碼創(chuàng)建表時,會在磁盤上的 datadir 文件夾中生成一個 test_frm.frm 的文件,這個文件中就包含了表結構相關的信息:

MySQL 官方文檔中的 11.1 MySQL .frm File Format 一文對于
.frm文件格式中的二進制的內容有著非常詳細的表述,在這里就不展開介紹了。
.ibd 文件
InnoDB 中用于存儲數據的文件總共有兩個部分,一是系統(tǒng)表空間文件,包括 ibdata1、ibdata2 等文件,其中存儲了 InnoDB 系統(tǒng)信息和用戶數據庫表數據和索引,是所有表公用的。
當打開 innodb_file_per_table 選項時,.ibd 文件就是每一個表獨有的表空間,文件存儲了當前表的數據和相關的索引數據。
如何存儲記錄
與現有的大多數存儲引擎一樣,InnoDB 使用頁作為磁盤管理的最小單位;數據在 InnoDB 存儲引擎中都是按行存儲的,每個 16KB 大小的頁中可以存放 2-200 行的記錄。
當 InnoDB 存儲數據時,它可以使用不同的行格式進行存儲;MySQL 5.7 版本支持以下格式的行存儲方式:

Antelope 是 InnoDB 最開始支持的文件格式,它包含兩種行格式 Compact 和 Redundant,它最開始并沒有名字;Antelope 的名字是在新的文件格式 Barracuda 出現后才起的,Barracuda 的出現引入了兩種新的行格式 Compressed 和 Dynamic;InnoDB 對于文件格式都會向前兼容,而官方文檔中也對之后會出現的新文件格式預先定義好了名字:Cheetah、Dragon、Elk 等等。
兩種行記錄格式 Compact 和 Redundant 在磁盤上按照以下方式存儲:

Compact 和 Redundant 格式最大的不同就是記錄格式的第一個部分;在 Compact 中,行記錄的第一部分倒序存放了一行數據中列的長度(Length),而 Redundant 中存的是每一列的偏移量(Offset),從總體上上看,Compact 行記錄格式相比 Redundant 格式能夠減少 20% 的存儲空間。
行溢出數據
當 InnoDB 使用 Compact 或者 Redundant 格式存儲極長的 VARCHAR 或者 BLOB 這類大對象時,我們并不會直接將所有的內容都存放在數據頁節(jié)點中,而是將行數據中的前 768 個字節(jié)存儲在數據頁中,后面會通過偏移量指向溢出頁。

但是當我們使用新的行記錄格式 Compressed 或者 Dynamic 時都只會在行記錄中保存 20 個字節(jié)的指針,實際的數據都會存放在溢出頁面中。

當然在實際存儲中,可能會對不同長度的 TEXT 和 BLOB 列進行優(yōu)化,不過這就不是本文關注的重點了。
想要了解更多與 InnoDB 存儲引擎中記錄的數據格式的相關信息,可以閱讀 InnoDB Record Structure
數據頁結構
頁是 InnoDB 存儲引擎管理數據的最小磁盤單位,而 B-Tree 節(jié)點就是實際存放表中數據的頁面,我們在這里將要介紹頁是如何組織和存儲記錄的;首先,一個 InnoDB 頁有以下七個部分:

每一個頁中包含了兩對 header/trailer:內部的 Page Header/Page Directory 關心的是頁的狀態(tài)信息,而 Fil Header/Fil Trailer 關心的是記錄頁的頭信息。
在頁的頭部和尾部之間就是用戶記錄和空閑空間了,每一個數據頁中都包含 Infimum 和 Supremum 這兩個虛擬的記錄(可以理解為占位符),Infimum 記錄是比該頁中任何主鍵值都要小的值,Supremum 是該頁中的最大值:

User Records 就是整個頁面中真正用于存放行記錄的部分,而 Free Space 就是空余空間了,它是一個鏈表的數據結構,為了保證插入和刪除的效率,整個頁面并不會按照主鍵順序對所有記錄進行排序,它會自動從左側向右尋找空白節(jié)點進行插入,行記錄在物理存儲上并不是按照順序的,它們之間的順序是由 next_record 這一指針控制的。
B+ 樹在查找對應的記錄時,并不會直接從樹中找出對應的行記錄,它只能獲取記錄所在的頁,將整個頁加載到內存中,再通過 Page Directory 中存儲的稀疏索引和 n_owned、next_record 屬性取出對應的記錄,不過因為這一操作是在內存中進行的,所以通常會忽略這部分查找的耗時。
InnoDB 存儲引擎中對數據的存儲是一個非常復雜的話題,這一節(jié)中也只是對表、行記錄以及頁面的存儲進行一定的分析和介紹,雖然作者相信這部分知識對于大部分開發(fā)者已經足夠了,但是想要真正消化這部分內容還需要很多的努力和實踐。下文是詳細分析。
innodb數據存儲詳細分析
本文主要介紹InnoDB存儲引擎的邏輯存儲結構
邏輯存儲結構
Tablespace
- Tablespace是InnoDB存儲引擎邏輯存儲結構的
最高層,所有數據都存放在Tablespace中 - 分類
System TablespaceSeparate TablespaceGeneral Tablespace
System Tablespace
-
System Tablespace即我們常見的共享表空間,變量為innodb_data_file_path,一般為ibdata1文件 - 里面存放著
undo logs,change buffer,doublewrite buffer等信息(后續(xù)將詳細介紹),在沒有開啟file-per-table的情況下,還會包含所有表的索引和數據信息 - 沒有開啟
file-per-table時存在的問題- 所有的表和索引都會在
System Tablespace中,占用空間會越來越大 -
碎片越來越多(如truncate table時,占用的磁盤空間依舊保留在System Tablespace)
- 所有的表和索引都會在
| <pre>12345678910111213141516171819</pre> | <pre>mysql> SHOW VARIABLES LIKE 'innodb_data_file_path';+-----------------------+------------------------+| Variable_name | Value |+-----------------------+------------------------+| innodb_data_file_path | ibdata1:12M:autoextend |+-----------------------+------------------------+1 row in set (0.01 sec)mysql> SHOW VARIABLES LIKE '%datadir%';+---------------+-----------------+| Variable_name | Value |+---------------+-----------------+| datadir | /var/lib/mysql/ |+---------------+-----------------+1 row in set (0.01 sec)mysql> system sudo ls -lh /var/lib/mysql/ibdata1[sudo] password for zhongmingmao:-rw-r----- 1 mysql mysql 76M May 6 20:00 /var/lib/mysql/ibdata1</pre> |
| --- | --- |
Separate Tablespace
- MySQL參考手冊中并沒有
Separate Tablespace這個術語,這里只為了行文方便,表示在開啟file-per-table的情況下,每個表有自己獨立的表空間,變量為innodb_file_per_table - 里面存放在
每個表的索引和數據信息,后綴一般為.ibd - 默認初始大小為
96KB - 好處
- 避免
System Tablespace越來越大 - 減少碎片(
truncate table,操作系統(tǒng)會自動回收空間)
- 避免
| <pre>123456789101112131415161718192021222324252627282930313233</pre> | <pre>mysql> use testReading table information for completion of table and column namesYou can turn off this feature to get a quicker startup with -ADatabase changedmysql> show tables;+----------------+| Tables_in_test |+----------------+| t |+----------------+1 row in set (0.00 sec)mysql> SHOW VARIABLES LIKE 'innodb_file_per_table';+-----------------------+-------+| Variable_name | Value |+-----------------------+-------+| innodb_file_per_table | ON |+-----------------------+-------+1 row in set (0.00 sec)mysql> SHOW VARIABLES LIKE '%datadir%'; +---------------+-----------------+| Variable_name | Value |+---------------+-----------------+| datadir | /var/lib/mysql/ |+---------------+-----------------+1 row in set (0.01 sec)mysql> system sudo ls -lh /var/lib/mysql/testtotal 112K-rw-r----- 1 mysql mysql 61 Apr 28 10:18 db.opt-rw-r----- 1 mysql mysql 8.4K May 7 17:03 t.frm-rw-r----- 1 mysql mysql 96K May 7 17:03 t.ibd</pre> |
| --- | --- |
General Tablespace
-
General Tablespace是MySQL 5.7.6引入的新特性,具體內容請參照下面鏈接
15.7.9 InnoDB General Tablespaces
Segment
- Segment分為三種
-
Leaf node segment:數據段,B+Tree的葉子節(jié)點 -
Non-Leaf node segment:索引段,B+Tree的非葉子節(jié)點 -
Rollback segment:回滾段,存放undo log,默認是位于System Tablespace
-
- InnoDB中的
B+Tree索引,由Leaf node segment和Non-Leaf node segment組成 - 一個Segment由
多個Extent和Page組成
Extent
-
Extent是由連續(xù)頁(默認頁大小為16KB)組成,在默認頁大小時,為64個連續(xù)頁,大小為64*16KB=1MB- 不同頁大?。?code>4KB*256 or
8KB*128or16KB*64or32KB*64or64KB*64
- 不同頁大?。?code>4KB*256 or
- 為了保證
頁的連續(xù)性,InnoDB可以一次性從磁盤申請4個Extent - 為了
節(jié)省磁盤空間,如表的數據量很?。?code>Leaf node segment和Non-Leaf node segment都很小)或Rollback segment,Segment一開始不會直接申請Extent,而是先用32個碎片頁(用于葉子節(jié)點)來存放數據,用完之后才繼續(xù)對Extent(1MB)的申請
Page
-
Page是InnoDB磁盤管理的最小單位,變量為innodb_page_size
| <pre>1234567</pre> | <pre>mysql> SHOW VARIABLES LIKE 'innodb_page_size';+------------------+-------+| Variable_name | Value |+------------------+-------+| innodb_page_size | 16384 |+------------------+-------+1 row in set (0.17 sec)</pre> |
| --- | --- |
Row
- InnoDB存儲引擎的數據是
按行進行存放的 - 行記錄格式
Row_FORMAT將在后續(xù)詳細介紹
接下來是Page數據頁詳解,這是最重要的一部分。
本文主要介紹InnoDB存儲引擎的數據頁結構
數據頁結構
File Header
參考鏈接:Fil Header
- 總共
38 Bytes,記錄頁的頭信息
| 名稱 | 大?。˙ytes) | 描述 |
|---|---|---|
| FIL_PAGE_SPACE | 4 | 該頁的checksum值 |
| FIL_PAGE_OFFSET | 4 | 該頁在表空間中的頁偏移量
|
| FIL_PAGE_PREV | 4 | 該頁的上一個頁 |
| FIL_PAGE_NEXT | 4 | 該頁的下一個頁 |
| FIL_PAGE_LSN | 8 | 該頁最后被修改的LSN |
| FIL_PAGE_TYPE | 2 | 該頁的類型,0x45BF為數據頁
|
| FIL_PAGE_FILE_FLUSH_LSN | 8 | 獨立表空間中為0
|
| FIL_PAGE_ARCH_LOG_NO | 4 | 該頁屬于哪一個表空間 |
Page Header
參考鏈接:Page Header
- 總共
56 Bytes,記錄頁的狀態(tài)信息
| 名稱 | 大?。˙ytes) | 描述 |
|---|---|---|
| PAGE_N_DIR_SLOTS | 2 | 在Page Directory中Slot的數量,初始值為2
|
| PAGE_HEAP_TOP | 2 | 堆中第一個記錄的指針 |
| PAGE_N_HEAP | 2 | 堆中的記錄數,初始值為2
|
| PAGE_FREE | 2 | 指向可重用空間的首指針 |
| PAGE_GARBAGE | 2 | 已標記為刪除(deleted_flag)的記錄的字節(jié)數 |
| PAGE_LAST_INSERT | 2 | 最后插入記錄的位置 |
| PAGE_DIRECTION | 2 | 最后插入的方向,PAGE_LEFT(0x01),PAGE_RIGHT(0x02),PAGE_NO_DIRECTION(0x05)
|
| PAGE_N_DIRECTION | 2 | 一個方向上連續(xù)插入記錄的數量 |
| PAGE_N_RECS | 2 | 該頁中記錄(User Record)的數量 |
| PAGE_MAX_TRX_ID | 8 | 修改該頁的最大事務ID(僅在輔助索引中定義) |
| PAGE_LEVEL | 2 | 該頁在索引樹中位置,0000代表葉子節(jié)點
|
| PAGE_INDEX_ID | 8 | 索引ID,表示該頁屬于哪個索引
|
| PAGE_BTR_SEG_LEAF | 10 | B+Tree葉子節(jié)點所在Leaf Node Segment的Segment Header(無關緊要) |
| PAGE_BTR_SEG_TOP | 10 | B+Tree非葉子節(jié)點所在Non-Leaf Node Segment的Segment Header(無關緊要) |
Infimum + Supremum Records
參考鏈接:The Infimum and Supremum Records
- 每個數據頁中都有兩個
虛擬的行記錄,用來限定記錄(User Record)的邊界(Infimum為下界,Supremum為上界) -
Infimum和Supremum在頁被創(chuàng)建是自動創(chuàng)建,不會被刪除 - 在
Compact和Redundant行記錄格式下,Infimum和Supremum占用的字節(jié)數是不一樣的
User Records
參考鏈接:User Records
- 存儲
實際插入的行記錄 - 在
Page Header中PAGE_HEAP_TOP、PAGE_N_HEAP的HEAP,實際上指的是Unordered User Record List- InnoDB不想每次都
依據B+Tree鍵的順序來插入新行,因為這可能需要移動大量的數據 - 因此InnoDB插入新行時,通常是插入到當前行的后面(
Free Space的頂部)或者是已刪除行留下來的空間
- InnoDB不想每次都
- 為了保證訪問B+Tree記錄的
順序性,在每個記錄中都有一個指向下一條記錄的指針,以此構成了一條單向有序鏈表
Free Space
- 空閑空間,數據結構是
鏈表,在一個記錄被刪除后,該空間會被加入到空閑鏈表中
Page Directory
參考鏈接:Page Directory
- 存放著
行記錄(User Record)的相對位置(不是偏移量) - 這里的
行記錄指針稱為Slot或Directory Slot,每個Slot占用2Byte -
并不是每一個行記錄都有一個Slot,一個Slot中可能包含多條行記錄,通過行記錄中n_owned字段標識 -
Infimum的n_owned總是1,Supremum的n_owned為[1,8],User Record的n_owned為[4,8] -
Slot是按照索引鍵值的順序進行逆序存放(Infimum是下界,Supremum是上界),可以利用二分查找快速地定位一個粗略的結果,然后再通過next_record進行精確查找 -
B+Tree索引本身并不能直接找到具體的一行記錄,只能找到該行記錄所在的頁- 數據庫把頁載入到
內存中,然后通過Page Directory再進行二分查找 - 二分查找時間復雜度很低,又在內存中進行查找,這部分的時間基本開銷可以忽略
- 數據庫把頁載入到
File Trailer
參考鏈接:Fil Trailer
- 總共
8 Bytes,為了檢測頁是否已經完整地寫入磁盤 - 變量
innodb_checksums,InnoDB從磁盤讀取一個頁時是否會檢測頁的完整性 - 變量
innodb_checksum_algorithm,檢驗和算法
微信公眾號【黃小斜】作者是螞蟻金服 JAVA 工程師,專注于 JAVA 后端技術棧:SpringBoot、SSM全家桶、MySQL、分布式、中間件、微服務,同時也懂點投資理財,堅持學習和寫作,相信終身學習的力量!關注公眾號后回復”架構師“即可領取 Java基礎、進階、項目和架構師等免費學習資料,更有數據庫、分布式、微服務等熱門技術學習視頻,內容豐富,兼顧原理和實踐,另外也將贈送作者原創(chuàng)的Java學習指南、Java程序員面試指南等干貨資源
