日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

MySQL InnoDB 是如何存储数据的

發布時間:2023/12/20 数据库 26 豆豆
生活随笔 收集整理的這篇文章主要介紹了 MySQL InnoDB 是如何存储数据的 小編覺得挺不錯的,現在分享給大家,幫大家做個參考.

InnoDB 是怎么存儲數據的

本文是《MySQL 是怎樣運行的 —— 從根兒上理解 MySQL》讀書總結,強烈推薦這本書;
CSDN 不能顯示 SVG,可能有圖片加載不出來,可以到 我的博客 上看。

數據目錄

眾所周之,MySQL 的數據是存儲在硬盤中的,而操作系統管理硬盤中的數據的方式就是文件系統,所以通俗的來說,MySQL 中的數據是存在一個個文件中的,這些文件 的目錄就叫 數據目錄

通過 SHOW VARIABLES LIKE 'datadir' 可以查看這個目錄:

進入這個目錄,你會發現,每個數據庫對應該目錄下的一個子目錄,比如 MySQL 中有一個 hotsong 的庫,Data 目錄下就會有一個 hotsong 的文件夾,這個文件夾里面存儲的是一些 ibd 類型的文件,數據庫里每張表對應一個 ibd 文件:

PS C:\ProgramData\MySQL\MySQL Server 8.0\Data\hotsong> ls 目錄: C:\ProgramData\MySQL\MySQL Server 8.0\Data\hotsongMode LastWriteTime Length Name ---- ------------- ------ ---- -a---- 2019/9/7 20:44 114688 hotsong.ibd -a---- 2019/9/7 22:37 12582912 singer.ibd -a---- 2019/9/7 21:17 114688 singer_type.ibd -a---- 2020/10/6 9:21 130023424 songs.ibd

這里是 MySQL 8.0 的樣子,但如果你使用的是更早的版本,你還會看到一種 .frm 的文件,這種文件用來描述表結構,8.0 之后, 表結構信息以 SDI 的形式放在了 .ibd 文件中,你可以使用官方提供 的工具 idb2sdi 從 ibd 文件中提取表結構信息,結果會以 json 形式輸出

在 8.0 之前,ibd 文件里保存的僅僅是該表的數據,但是再往前,MySQL 5.6.6 之前,MySQL 服務器中所有表的數據都會被放在一個地方,叫系統表空間, 對應數據目錄下的 ibdata1 文件,這是一個自擴展文件,但是你也可以在服務器啟動時使用相關參數指定服務器使用自定義的文件。

在 5.6.6 之后,InnoDB 引如 獨立表空間 空間的概念,每張表使用單獨的文件存儲數據和表結構,也就是上面的 ibd 和 frm 文件,服務器啟動時,可以通過 innodb_file_per_table 設置只使用系統表空間(值為 0)或者是使用獨立表空間(值為1).

服務啟動后,通過 ALERT 語句,存儲在兩種表空間中的數據可以相互移動。

需要注意的是,不是說使用了獨立表空間系統表空間就沒用了,因為系統表空間除了可以存儲表數據外,還存儲了許多 MySQL 服務運行所必要的公共信息。

數據目錄總結

MySQL 的數據是存儲在磁盤的,或者可以說是存儲在文件中的,這些文件的目錄叫做數據目錄,每個數據庫對應數據目錄下的一個子目錄,每個表中數據存放的地方叫表空間,在 5.6.6 之前,所有數據都被存放在一個地方,叫系統表空間,數據庫子目錄下只有 frm 文件,用來描述表結構,在 5.6.6 之后,InnoDB 默認將每個表的數據放在一個單獨的 ibd 文件中,稱為獨立表空間,在 8.0 之后,InnoDB 將描述表結構的 frm 信息以 sdi 的形式也放在了 ibd 文件中,所以 8.0 之后,數據庫子目錄下就只有 ibd 了。

5.6.6 之后,系統表空間默認只存儲一些必要的公共信息,對應數據目錄下的 ibdata1 文件,但他仍然很重要。

InnoDB 的數據存放在數據目錄下的某個文件中,這是把 InnoDB 看作一個黑盒,從操作系統的角度得到的一個宏大的結論,但每條記錄是以怎樣的形式組織在這個文件中的,就需要深入了解表空間和記錄的具體結構了。

聚簇索引和頁

眾所周之,InnoDB 中每張表都一定會有一個聚簇索引,如果該表設置了主鍵,那就會以主鍵建立聚簇索引,如果沒有設置主鍵,InnoDB 會選取一個唯一非 NULL 的列建立聚簇索引,如果找不到適合建立聚簇索引的列,InnoDB 會給表插入一個隱藏列 row_id, 并以此建立聚簇索引。

為什么 InnoDB 如此執著非要建一個聚簇索引呢?原因是聚簇索引的葉子節點會存儲表中的完整數據,換句話說,InnoDB 中的數據是存儲在聚簇索引葉子節點中的。

InnoDB 的聚簇索引是一顆 B+ 樹,B+ 樹的每個節點占一頁,“頁” 是 InnoDB 中內存分配的基本單位,大小為 16KB,InnoDB 中有許多不同種類的頁,如移除頁,索引頁等,B+ 的樹節點類型就是索引頁,它的結構如下圖所示:

從上往下依次是:

  • 文件頭(File Header):占 38 字節,用來描述數據頁的一些狀態信息。
  • 頁頭(Page Header): 占 56 字節,記錄了存儲在頁中的記錄的一些狀態。
  • Infimum + Supermum: 占 26 字節,該頁中兩條預添加的記錄,Infimum 表示該頁中的最小記錄,Supermum表示一個最大記錄。
  • User Records: 用戶記錄。
  • Free Space: 空閑空間。
  • 頁目錄(Page Directory): 用來加快頁內記錄查找速度。
  • 文件尾(File Tialer): 用于校驗數據。

File Header

文件頭(File Header):占 38 字節,用來描述數據頁的一些狀態信息,它的結構如下:

從左到右依次表示:

  • 該頁的校驗和
  • 頁號
  • 上一個頁的頁號
  • 下一個頁的頁號
  • 頁面被最后修改時對應的日志序列號(LSN)
  • 頁面類型
  • 僅在系統表空間的第一個頁上使用
  • 頁屬于哪個表空間
  • 這里面比較重要的是 FIL_PAGE_PREV 和 FIL_PAGE_NEXT ,這兩個字段可以看作指向上一個頁和下一個頁的指針,我們知道 B+ 樹的葉子節點是通過雙指針串聯起來的,但實際上,InnoDB 的索引里,它的非葉子節點也可以看作是串連起來的。

    Page Header

    頁頭(Page Header): 占 56 個字節,他記錄了存儲在頁中的記錄的一些狀態,結構如下:

    從上到下依次為:

  • 頁目錄中槽的數量(后面會說)
  • 還未使用的最小地址空間,也就是該地址之后就是 Free Space 了。
  • 第一位表示本記錄是否為緊湊型記錄,后 15 為表示本頁堆中的記錄數。
  • 已刪除記錄鏈表的頭節點的偏移。
  • 已刪除的記錄占用的字節數。
  • 最后插入的記錄的位置。
  • 記錄插入方向。
  • 一個方向連續插入的記錄數。
  • 用戶記錄數(PAGE_N_HEAP 中的記錄數包含已經刪除了的記錄和Infimum + Supermum, 但這里不包含)
  • 當前頁的最大事務 ID。
  • 該頁在 B+ 樹中所處的層級。
  • B+ 樹葉子節點的頭部信息(只在 B+ 樹的更頁面定義)
  • B+ 樹非葉子節點頭部信息(只在 B+ 樹的更頁面定義)
  • 關于第一個 PAGE_N_DIR_SLOTS , 他與頁目錄有關,在后面會說到,關于 3, 4, 5, 9 他們都涉及到了記錄的刪除,當我們執行 DELETE 語句時,InnoDB 并不會真的把這條記錄從磁盤刪除,因為這還涉及到緊湊數據,每次都真正刪除花銷太大,所以 InnoDB 會修改這條記錄上的一個標記位,并將這些已經刪除的記錄鏈在一起(事實上正常記錄也是鏈在一起的,在說記錄格式時會講到),4 PAGE_FREE 所記錄的就是這個鏈表的頭節點在 User Records 中的偏移。

    關于 7,8,記錄插入方向描述的是新插入記錄的主鍵值與最后一次插入記錄主鍵值的大小關系。

    User Records

    到這兒就需要說一下 InnoDB 的記錄行格式了。

    InnoDB 行格式

    行格式,也就是每條記錄在 InnoDB 中的真實樣子,InnoDB 有四種行格式,分別是:COMPACT, REDUNDANT, DYNAMIC, COMPRESSED,通過 ROW_FORMAT 可以修改表的行格式,如:

    ALTER TABLE table_name ROW_FORMAT=COMPACT

    這里以 COMPACT 格式為例,它的結構如下:

    變長字段長度列表

    顧名思義,這個結構用來存儲這一行里變長字段的長度,唯一需要注意的是這個列表是按表結構逆序排序的,假如一個表結構如下:

    CREATE TABLE `hotsong` (`id` int(11) NOT NULL AUTO_INCREMENT,`name` varchar(50) NOT NULL,`song_id` int(11) NOT NULL,`download_link` varchar(100) DEFAULT NULL,`singer` varchar(20) DEFAULT NULL,PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci;

    其中 name, download_link, singer 的類型是 varchar, 屬于變長字段,如果向該表中插入一條記錄,其中 name, download_link, singer 的長度分別為 1, 2, 3, 則在變長字段長度列表中會存儲 3, 2, 1。

    對于列表中該使用多少字節存儲一個字段的長度,這取決于表采用的字符集和該類型能存儲的最大字節數,如上表,字符集是 utf8mb4, 該字符集最多使用四個字節表示一個字符,而定義中, name 最多存儲 50 個字符,所以這些字段能存儲最多 4 * 50 = 200 字節的數據,所以用一個字節就可以表示其長度了,而 download_link 最多存儲 100 個字符,則需要兩個字節來表示其長度了。這里最多也只會使用兩個字節,如果某個字段長度特別長,就需要使用溢出字段了,也就是在這一頁中只會存部分數據。

    變長字段列表只會存不為 NULL 的列的長度,NULL 列會表現在下面的 NULL 值列表中。

    NULL 值列表

    很好理解,NULL 值列表類似于一個 BitMap 表明了這一行中哪寫列是 NULL,這些為 NULL 的列是不會占額外的空間的,存記錄時, InnoDB 會去查看表格式,看允許為 NULL 的列有多少個,如上面的 hotsong 表,只有兩個字段允許為 NULL,那 NULL 值列表就會占用一個字節(必須占用整數字節,高位填0),最低兩位用來表示 singer 和 download_link, 這里和長度列表一樣,也是逆序排列的,值為 0 時,代表該字段不為 NULL。

    記錄頭信息

    記錄頭信息里,我們暫且關注這幾個字段:

    • delete_flag: 標識這條記錄是否被刪除,在 Page Header 那已經說過了,這就是那個標志位,為 1 表示被刪除。
    • n_owner: 與頁目錄有關,頁中的記錄會被分成若干組,這個字段表示這一組中的記錄數。
    • heap_no: 這條記錄在頁堆中的偏移。
    • record_type: 記錄類型:
      • 0: 普通記錄
      • 1:B+ 樹非葉子節點目錄項記錄
      • 2:Infimum 記錄
      • 3:Supermum 記錄
    • next_record: 下一個記錄(主鍵大小上的下一條)的相對位置,通過這個字段,頁面中的每條記錄都像是使用鏈表連起來了。

    回到索引頁的 User Records 上,通過上面行格式的介紹,我們知道每一條記錄的長度是不一樣的,并且他們通過 next_record 鏈在了一起,所以記錄在 User Records 中是像下面這樣存儲的:

    記錄一條一條緊密排列,這個結構被稱之為 Heap(堆), 記錄在這個堆中的相對位置就是上面記錄頭信息里的 heap_no, next_record 指的也是下一條記錄的偏移,而不是真的一個鏈表指針。

    除此之外,InnoDB 的設計者在每一個堆中加入了兩條特殊記錄:Infimum 和 Supermum,他們的 heap_no 分別為 0 和 1,這兩條記錄很簡單,只有記錄頭信息和代表這兩個單詞的記錄體,這兩個特殊記錄代表了這一頁中最大和最小的記錄,也就是說,通過 Infimum 的 next_record 找到的是堆中的第一條用戶記錄,堆中的最后一條用戶記錄的 next_record 指向了 Supermum, 如果把緊密排列的堆變成鏈表的樣子,他應該是這樣的:

    關于 next_record 他還有一個非常重要的特性,就是它允許為負,表示當前記錄的下一條記錄在它前面,這里的下一條是主鍵大小排列上的下一條,比如頁中有一個主鍵值為 5 的記錄 A(長度為 lenAlen_AlenA?),我們又插入了一條主鍵值為 6 的記錄 B(長度為 lenBlen_BlenB?),那么 A 的 next_record 就是 +lenA+len_A+lenA? 表示沿著記錄 A 向后尋找 lenAlen_AlenA? 個字節就是記錄 B,但這時如果我們又插入了一條主鍵值為 4 的記錄 C,那 C 的 next_record 就是 ?(lenA+lenB)-(len_A + len_B)?(lenA?+lenB?) 也就是向前找能找到 C 的下一條記錄 A。

    這樣的好處是通過 next_record 頁中的所有記錄會組成一個按主鍵排序的有序鏈表,但在物理上,記錄還是按插入順序緊密排列的,配合下面的頁目錄,能提高頁內記錄的檢索速度。

    Page Directory

    我們知道,索引的存在是為了快速定位到記錄所在的頁,但定位到頁后呢,一頁里可能包含許多記錄,遍歷頁中的所有記錄同樣是不可接受的,所以 InnoDB 設計了頁目錄,相當于頁索引,它的工作原理如下:

  • 將所有未刪除的記錄(包括Infimum 和 Supermum)劃分為多個組。
  • 將每組中的最后一條記錄的偏移提取出來放在 Page Directory 中。
  • 當查找頁中的某條記錄時,先通過二分法查找到該記錄在哪一組中,然后找到這一組中最小的那條記錄,沿著 next_record 往下遍歷這一組的記錄。
  • 這里 Page Directory 中的每一個偏移量被叫做一個 , 一個槽占 2 字節,記錄分組的原則是:

    • Infimum 獨占一組
    • Supermum 那一組只能有 1~8條記錄
    • 其他組只能有 4 ~ 8 條記錄

    這樣一來,頁中最多遍歷 8 次,就可以找到(確認找不到)某條記錄了,能這樣做的前提,還是通過 next_record 記錄組成了一個有序鏈表。

    還有一個有趣的問題,槽中記錄的是一組中最大的記錄的偏移,但定位到組后,需要的是最小的記錄,該怎么辦呢?上一個槽的下一條記錄不就是嗎。

    總結

    第一節 數據目錄 我們站在操作系統的角度,說 InnoDB 是把數據存儲在數據目錄下的文件中的,這一節,我們從聚簇索引的一個節點(頁)出發,說明了一條記錄是怎樣被存放的,關鍵點如下:

  • 所有數據被存放在表聚簇索引的葉子節點上。
  • 索引的一個節點就是一頁,大小為 16KB,頁是 InnoDB 內存分配的基本單位。
  • InnoDB 中,頁有很多種,索引的節點對應的頁類型叫索引頁。
  • 索引頁由文件頭,頁頭,用戶記錄,頁目錄,文件尾等部分組成。
  • 通過文件頭,頁和頁可以以雙鏈表的形式連接起來。
  • 頁頭記錄了頁中的一些統計信息。
  • 用戶記錄段是存儲用戶記錄的地方,每條記錄被緊密地存儲在這,稱為堆。
  • 每條用戶記錄都有一個重要的 next_record 字段,他能保證緊密排列地用戶記錄能按主鍵大小組織成一個有序鏈表。
  • 有兩條特殊的記錄 Infimum 和 Supermum被安排在堆中,他們處于堆中最前的位置,但分別表示最大最小的記錄。
  • 頁目錄是為了提高頁內記錄檢索的速度而存在的,堆中的記錄最多會 8 個為一組,每一組中最大的記錄偏移量會被存放在頁目錄中,稱為槽,查找記錄時,會先通過二分法定位到組,然后在組內遍歷。
  • 文件尾用來校驗數據。
  • 最后,放上索引頁的整體圖:

    接下來,我們要把頁和數據目錄結合起來,了解頁是怎么在表空間中組織的。

    InnoDB 表空間

    在 MySQL 5.6.6 之后, InnoDB 有了獨立表空間的概念,每張表對應一個獨立表空間(一個 ibd 文件),而系統表空間(ibdata1)則主要用來存儲一些公有的信息,這一節,我們以頁為單位,看一看 InnoDB 是怎么在表空間中管理每個頁的。

    頁回顧

    上面多次說過,頁是 InnoDB 分配內存的基本單位,一頁大小 16KB,頁有許多不同的類型,如:

    • Index 頁,上面已經說過。
    • Inode 頁,用來存儲段信息。
    • XDES 頁,存儲區信息。
    • FPS_HDR 頁,存儲表空間頭部信息。
    • IBUF_BITMAP 頁:存儲 Change Buffer 相關的內容。

    除了這幾個,其實還有許多種類型的頁,但其余的和本文關系不大,我們只關心這幾種頁就好了。

    上面說 Index 頁時講了它的格式,事實上,File Header 和 File Trailer 是所有頁面類型所共有的,在后面介紹其他頁面類型的結構時,就不贅述了。

    區 , 組和段

    前面說過,頁是 InnoDB 分配存儲空間最小的單位,但問題在于頁太小了,只有 16KB,在表中數據非常多時,如果繼續以頁為單位分配,就可能造成頁與頁間的物理距離過大,雖然頁和頁之間是通過指針連接的,但在使用傳統機械硬盤時,物理距離大就意味著根據一個頁的 Next 指針找到下一個頁磁頭需要移動更多的距離(隨機 IO),造成頁和頁雖然在邏輯上連續,但在物理上分散,這樣不利于高效地數據讀寫。為了盡量避免這種情況,InnoDB 會盡量讓邏輯上相連的頁在物理內存上也連續(順序IO),具體做法就是當表中的數據量很大時,就以更大的 區(extent)為單位為表分配存儲空間,InnoDB 規定連續的 64 個頁是一個區,也就是一個區占 1M 的空間。同時,為了方便管理這些區,將連續的 256 個區被劃分為一,每一組的開始幾個頁面類型是固定的:

    對于表空間中第一個組的前三個頁面類型是固定的,他們依次是:

  • FPS_HDR 頁:記錄表空間的整體屬性和這一組中 256 個區的整體屬性。
  • IBUF_BITMAP 頁:存儲 Change Buffer 相關的內容。
  • INODE 頁:存儲與段相關的內容。
  • 對于其他組,它的前兩個頁面類型是固定的,依次是:

  • XDES 頁:記錄這一組中說有區的屬性
  • IBUF_BITMAP 頁:存儲 Change Buffer 相關的內容。
  • 所以獨立表空間的結構類似于下圖:

    藍色的表示一個組,大小為 256 MB, 綠色的表示一個區,大小為 1M, 紅色的表示一個頁,大小為 16KB。

    區存在的意義是盡量讓頁面鏈表中相鄰的頁在物理位置上也相鄰,這樣在掃描葉子節點的大量記錄時,才可以使用順序IO。

    引入區是為了加快掃描葉子節點時的速度,但事實上不管是葉子節點還是非葉子節點,他們的頁類型都是 Index, 非葉子節點間也是有鏈表連起來的,只是我們一般用不到這些指針而已,所以如果把葉子節點和非葉子節點都放在區里面,掃描的性能又會大打折扣了,為此,InnoDB 引入了,這是一個邏輯上的概念,每個索引(聚簇索引或二級索引)都有兩個段,分別用來存放葉子節點和非葉子節點。

    當表中的數據很少時,段會以頁為單位申請存儲空間,這些零散的頁所在的區叫做碎片區,它直屬于表空間。當表中的數據占了 32 個零散的頁面后,段會以完整的區為單位分配存儲空間,但之前存儲在零散頁面的數據并不會被移動過去。這樣做的目的是盡量減少浪費。

    所以段是一些零散的頁面以及一些完整的區構成的集合

    區的分類

    有了段后,區就可以被分為下面幾類:

  • 空閑區(FREE):完全沒有被使用的區。
  • 有空閑的碎片區(FREE_FRAG): 區中的部分頁面被用作段的零散頁面,但還有空閑的頁。
  • 無空閑的碎片區(FUEE_FRAG): 所有頁面都被用了的碎片區。
  • 完整分配給某個段的區(FSEG):當表中的數據占了 32 頁后,段會以完整的區為單位分配空間,這些區就是 FSEG。
  • XDES Entry

    為了管理這些區,InnoDB 設計了一個大小為 40 字節的 XDES Entry ,它的結構如下:

    XDES Entry 結構List Node 結構
    • Segment ID: 對于一個 FSEG 類型的區,Segment ID 用來標識它被分配給了哪個段。
    • List Node: 通過這個結構,XDES Entry 能連成一個鏈表。
      • Prev Node Page Number, Prev Node Offset: 上一個 XDES Entry 所在的頁面和在頁面內的偏移,通過這兩個字段,可以在表空間中找到上一個 XDES Entry.
      • NextNode Page Number, Next Node Offset: 下一個 XDES Entry 。
      • 這個鏈表鏈接的是相同狀態的區對應的 XDES Entry, 也就是說,如果一個 XDES Entry 對應的區是 FREE 狀態的,那么根據它的 Next 和 Prev 指針拿到的 XDES Entry 對應的區也是 FREE 狀態的。如此一來,不同類型區對應的 XDES Entry 就會被組織成不同的鏈表,通過這些鏈表的頭節點(保存在固定的地方),我們就可以快速獲得一個需要的區或碎片頁(由于 FSEG 類型的區已經分配給段了,所以這里的鏈表不包括這種類型的,FSEG 類型的區會在段內鏈接成別的鏈表,馬上會說到)。
    • State:表示這個區的狀態。
    • Page State Bitmap: 沒兩位對應區中的一頁的狀態, 00 表示這一頁空閑, 01表示不空閑。

    有了 XDES Entry 后,向表空間申請頁插入新記錄的的過程就是這樣的了:

  • 如果表中數據不多(不足 32 頁),就從 FREE_FRAG 鏈表中找到一個 FREE_FRAG 狀態的區,并通過 Page State Bitmap 找到一個空閑的頁分配給表(實際上是分配給索引或者說分配給段)之后把記錄插進去,如果沒有 FREE_FRAG 狀態的區,就通過 FREE 鏈表找到一個 FREE 狀態的區,將其中的一頁分配給段,并將這個區對應的 XDES Entry 從 FREE 鏈表移動到 FREE_FRAG 鏈表中。
  • 如果表中的數據到了 32 頁,就需要以區為單位給段分配空間,這時只需要根據 FREE 列表找到一個 FREE 分配給段即可。
  • Inode Entry

    類似于 XDES Entry,InnoDB 為每個段設計了一個 Inode Entry 結構,這個結構記錄了該段的一些必要信息,它的結構如下:

    上面說過,段是由一些整塊的區和一些零碎的頁組成的邏輯上的結構,Inode Entry 記錄的就是這些信息,對于段中整塊的區,InnoDB 將其分成了三類:

    • FREE 區:完全沒有使用的區,剛剛分配的。
    • NOT_FULL 區:還有空頁面的區。
    • FULL 區:沒有空頁面的區。

    這三類區對應的 XDES Entry 結構也會組成一個鏈表(從上面的區的分類來看,他們都是 FSEG 類型的),Inode Entry 中的 List Base Node For FREE List, List Base Node For NOT_FULL List, List Base Node For FULL List 對應的就是這三個鏈表的頭節點, NOT_FULL_N_USED 字段儲存的就是 NOT_FULL 鏈表中已經使用了多少頁面了。

    Magic Number 字段用來標記 Inode Entry 是不是已經被初始化了,值為 97937874 時,表示已經初始化了(確實是 Magic Number)

    下面的 32 個 Fragment Array Entry 每個占四字節,用來存儲段中 32 個零碎頁的頁號。

    Segment ID 用來記錄這個段的唯一 ID。

    小結

    頁是分配存儲空間的最小單位,但頁太小了,在數據量特別大時,如果依然以頁為單位分配,可能導致邏輯上相鄰的兩個頁在物理上相隔很遠,這樣在遍歷葉子節點時就會造成大量的隨機 IO,為此,InnoDB 規定當表中數據占用空間小于 32 頁時,從碎片區中以頁為單位分配,當超過 32 個頁后,就以更大的區(連續的 64 個頁)為單位分配存儲空間,每個區由一個 XDES Entry 結構管理,不同狀態的 XDES Entry 結構通過 List Node 鏈接成一個鏈表,也就看一看作是不同狀態的區鏈成了不同的鏈表,在分配區或碎片頁時,就可以直接從對應鏈表獲取到對應的區了。

    為了進一步減少隨機 IO,InnoDB 還引入一個邏輯上的概念 “段”,每個索引對應兩個段,分別是葉子節點段和非葉子節點段,每個段實際上是一些碎片頁和一些整塊的區(FSEG 狀態)的集合,每個段由一個 Inode Entry 結構管理,在段里,完整的區也會被分成三類,每類使用單獨的鏈表鏈接。

    頁面類型

    上面說的 XDES Entry, Inode Entry 是被存儲在特定的頁面類型中的,他們分別是 XDES 頁, Inode 頁 和 FSP_HDR 頁, 他們的結構如下:

    XDES 頁INODE 頁FPS_HDR 頁

    除了熟悉的 XDES Entry 和 Inode Entry 外, File Header 和 File Tialer 是所有頁面共有的,在索引頁那已經說過,剩下的就是 INODE 頁的 List Node For INODE Page List 和 FSP_HDR 頁的 File Space Header 了。

    List Node For INODE Page List

    上面說過,INODE 頁是表空間的第一組(第一個區)的第三個頁,里面的核心結構式 Inode Entry, 用來描述段信息,但這樣一頁只能有 85 個 Inode Entry, 如果一張表里的段數量超過85個(索引數量超過 42 )時,就需要額外的 INODE 頁來存儲這些 Inode Entry 了, 根據這個 List Node For INODE Page List 字段就找到別的 INDOE 頁,它的結構如下:

    File Space Header

    關于 FSP_HDR 頁面,前面也說過了,它類似于 XDES 頁面,存儲了本組 256 個區的信息,除此之外,他是表空間的第一個頁面,因此還存儲了表空間的一些通用信息,這些信息就被存儲在 File Space Header 里,它的結構如下:

  • Space ID: 表空間 ID
  • Size:表空間擁有的頁面數
  • FREE Limit: 未被初始化的最小頁號,大于或等于該頁號的區對應的 XDES Entry 都沒被加入 FREE 鏈表, 每個表空間對應的其實是一個自增長的 ibd 文件(當然可以在建表時直接指定一個非常的文件),這些表空間中可能有大量未使用的區,InnoDB 不會把所有空閑區一股腦的加入 FREE 鏈表,而是會等到空閑區不夠時,再加一批到鏈表中,加入到鏈表中的就是被初始化了,反之就是未初始化。
  • Space Flags: 一些標志字段
  • FRAG_N_USED: 類似于 Inode Entry 中的 NOT_FULL_N_USED, 表示 FREE_FRAG鏈表中有多少頁面被使用了。
  • List Base Node for FREE List: 上面說 FREE 鏈表的根節點被保存在固定的地方,就是這。
  • List Base Node for FREE_FRAG List:FREE_FRAG 鏈表根節點。
  • List Base Node for FULL_FRAG List:FULL_FRAG 鏈表根節點。
  • Next Unused Segment ID: 每個段都有一個唯一 ID,這個字段表示下一個可以分配的段ID。
  • List Base Node for SEG_INODES_FULL:上面說,INODE 類型的頁面可能有多個,由 List Node For INODE Page List 連接,這些 INODE 頁也會依據有沒有滿鏈成兩個鏈表:SEG_INODES_FULL 和 SEG_INODES_FREE, 這個字段就是 SEG_INODES_FULL 鏈表的頭節點。
  • List Base Node for SEG_INODES_FREE: SEG_INODES_FREE 鏈表的頭節點。
  • 總結

    最后,祭上大圖吧

    [外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-tgY4L5xG-1614441836399)(https://pic.stackoverflow.wiki/uploadImages/103/116/47/193/2021/02/27/22/43/16d6dfe6-2353-406b-87d0-61c9d5f24837.svg)]

    系統表空間

    上面介紹了獨立表空間的結構,它對應于數據庫里的每一張表,但還有一些問題沒有解決,比如如何確定哪張表對應哪個表空間等,這就需要系統表空間,一個 MySQL 服務只會對應一個系統表空間,它是 MySQL 服務的第一個表空間, Space ID 為 0, 記錄了整個系統屬性的相關信息,第一個組中的前七個頁面類型分別為:

  • FSP_HDR
  • IBUF_BITMAP
  • SYS_insert buffer header
  • INODE_insert buffer root
  • TRX_SYS: 存儲事務系統相關信息
  • SYS_first rollback segment: 第一個回滾段信息
  • SYS_data dictionary header: 數據字段頭部信息
  • 這里簡單介紹與 Change Buffer 相關的 IBUF_BITMAP, SYS_insert buffer header 和 INODE_insert buffer root 以及數據字典相關的 SYS_data dictionary header, 其他字段都用于事務。

    Change Buffer

    其實 IBUF_BITMAP 類型的頁面在獨立表空間也一直出現過,它實質上也是一棵 B+ 樹,當我們往表中插入一條記錄時,首先完整的記錄會被插入到聚簇索引的葉子節點上,其次還需要更新所有二級索引,但這些索引隨機處在表空間的不同地方,每次修改這些索引可能引起許多隨機 IO,這會影響數據寫入的效率,為此,當執行二級索引寫入操作時,如果 InnoDB 發現二級索引對應的頁面沒在內存中,就會暫時把修改數據寫到 Change Buffer 里,等服務器空閑時,再把數據寫到二級索引對應的頁里。

    其中,系統表空間的 SYS_insert buffer header 字段用于存儲 Change Buffer 的頭部信息, INODE_insert buffer root 用于存儲 Change Buffer 的根節點。

    數據字典

    InnoDB 的數據字典保存了許多重要的元數據, 包括:

    • 表對應的表空間;
    • 表中有多少列,每一列的類型是什么;
    • 表中有多少索引,索引的字段,索引根節點對應的頁面;
    • 外鍵信息等……

    這些信息是為了更好的管理用戶信息而存在的,InnoDB 將他們放在一些內部表中,比較重要的有:

  • SYS_TABLES: 存儲所有表信息
  • SYS_COLUMNS: 存儲所有列信息
  • SYS_INDEXS: 存儲所有索引xinx
  • SYS_FIELDS: 存儲所有索引對應的列信息
  • SYS_TABLESPACES: 存儲所有表空間信息
  • ……
  • 其中,前四個表被稱為四個基本表,使用這四個表,我們就可以獲取其他系統表和用戶數據了,比如更具表名就可以在 SYS_TABLES 表里獲取到 Table ID, 根據 ID 到 SYS_COLUMNS 就可以獲取到所有列信息,還可以到 SYS_FIELDS 和 SYS_INDEXS 獲取到索引信息……

    具體操作要看這四張表的具體結構

    其他表可以使用這四張表定位,那這四張表該怎么定位呢?答案是硬編碼,這四張表的信息被硬編碼到了系統表空間的第七頁上,也就是 SYS_data dictionary header 關于這一頁的結構就不贅述了。

    需要注意的是,這些內部系統表用戶是不能直接訪問的,但 InnoDB 為了用戶能更好的使用存儲引擎,提供了這些內部表的映射,對應數據庫 information_schema, 這里面有一些 INNODB 開頭的表, 如 INNODB_TABLES 的表結構如下:

    CREATE TEMPORARY TABLE `INNODB_TABLES` (`TABLE_ID` bigint(21) unsigned NOT NULL DEFAULT '0',`NAME` varchar(655) NOT NULL DEFAULT '',`FLAG` int(11) NOT NULL DEFAULT '0',`N_COLS` int(11) NOT NULL DEFAULT '0',`SPACE` bigint(21) NOT NULL DEFAULT '0',`ROW_FORMAT` varchar(12) DEFAULT NULL,`ZIP_PAGE_SIZE` int(11) unsigned NOT NULL DEFAULT '0',`SPACE_TYPE` varchar(10) DEFAULT NULL,`INSTANT_COLS` int(11) NOT NULL DEFAULT '0' ) ENGINE=MEMORY DEFAULT CHARSET=utf8;
    • TABLE_ID: 表ID
    • NAME:表名
    • FLAG:有關表格式和存儲特性的位級信息數據,包括行格式,壓縮頁大小(如果適用)以及DATA DIRECTORY子句是否與CREATE TABLE或ALTER TABLE一起使用等,參考 24.32.22 The INFORMATION_SCHEMA INNODB_SYS_TABLES Table
    • N_COLS: 表中有多少列
    • SPACE:表所屬表空間 ID
    • ROW_FORMAT:行格式,默認為 Dynamic
    • ZIP_PAGE_SIZE: 壓縮頁大小
    • SPACE_TYPE: 表所屬的表空間類型。可能的值包括:System(系統表空間)、General(普通表空間)、Single(獨立表空間)
    • INSTANT_COLS:8.0 之后的新特性,表示插入的列的個數,參考 MySQL8.0 - 新特性 - Instant Add Column

    總結

    InnoDB 的完整數據存放在聚簇索引的葉子節點上,索引的一個節點就是一頁,為了減少隨機 IO,當表中的數據很多時,會一次性分配連續的 64 頁,稱為一個區,每個區由一個 XDES Entry 結構管理,根據區的狀態,這些 XDES Entry 會鏈成不同的鏈表,鏈表頭節點保存在表空間的第一個頁面上,除此之外,為了盡可能保證葉子節點在物理內存上連續, InnoDB 把葉子節點和非葉子節點通過段分開,每個段由 Inode Entry 管理。

    當定位到頁后,InnoDB 還提供了頁目錄來提高頁內檢索速度。

    MySQL 服務共有的信息被存儲在系統表空間中,最重要的是 InnoDB 數據字典,通過它,我們才可以獲取到表空間中的記錄。

    參考

    小孩子 - MySQL 是怎么運行的

    總結

    以上是生活随笔為你收集整理的MySQL InnoDB 是如何存储数据的的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。