深刻理解InnoDB -- 存儲篇

本文分享InnoDB如何規劃表空間,如何存儲表空間元信息以及用戶數據。html

思考一個問題,若是給你一個文件,讓你存儲MySql的數據,你會怎麼作?mysql

下面是一種比較合理的思路。首先把文件劃分紅大小相等的塊(InnoDB中的頁),每次取一塊使用。爲了管理這些塊信息,咱們也拿出一塊空間,存儲每一塊空間的位置,偏移量,以及已經使用和剩餘未使用的塊(InnoDB中的FSP HEADER PAGE ,文件管理頁)
而後根據不一樣的邏輯創建對應的對象,如索引對象,回滾信息對象(InnoDB中的段),這些對象從上面分好的塊中申請空間使用,並管理屬於本身的塊,固然,這些對象信息也須要拿出一塊空間存儲起來(InnoDB中的INODE PAGE)。 這就是InnoDB中段和頁的概念git

下面來明確幾個核心概念github

  • 表空間
    InnoDB將全部數據(包括表數據,索引,回滾信息,插入緩衝索引頁,系統事務信息,二次寫緩衝)邏輯地放在一個空間中,稱爲共享表空間。
    默認表空間的存儲文件爲data目錄下的ibdata1,初始化爲10M。算法


  • 一個索引(InnoDB都是B+索引)由兩個段管理,葉子節點段(leaf segment)和非葉子節點段(non leaf segment)
    回滾數據也是經過段管理。sql


  • InnoDB申請空間的最小單位,由連續頁組成的空間,大小爲1MB,保持不變。
    InnoDB一次從磁盤中申請4~5個區。數據庫

  • InnoDB訪問的最小單位,默認16KB。一個區中一共有64個連續的頁。
    緩衝池是以頁爲管理單位,每次讀取或刷新一頁數據。
    參數: innodb_page_size,能夠將頁大小設置爲4K,8K。vim

InnoDB將表空間按Page切分,這些Page主要分爲兩類:存儲表空間元信息的管理頁(如FIL_PAGE_TYPE_FSP_HDR)和存儲表空間用戶數據的索引頁(如FIL_PAGE_INDEX,FIL_PAGE_INODE)。數組

FIL Header

全部頁都有兩個統一的結構,FIL Header,佔據頁面的前38個字節,FIL Trailer,佔據頁面末尾8字節。bash

FIL Header結構以下

變量 字節 描述
FIL_PAGE_SPACE 4 所在表空間ID(space id)
FIL_PAGE_OFFSET 4 該頁在表空間的偏移量(page no)
FIL_PAGE_PREV 4 前驅節點的偏移量(僅對索引頁有效)
FIL_PAGE_NEXT 4 後繼節點的偏移量(僅對索引頁有效)
FIL_PAGE_LSN 8 頁最後刷新到磁盤的LSN
FIL_PAGE_TYPE 2 頁的類型
FIL_PAGE_FILE_FLUSH_LSN 8 僅在第一個Page(FSP HEADER PAGE)使用,用來判斷數據庫是否正常關閉
FIL_PAGE_SPACE_ID 8 僅在第一個Page使用,保存數據庫關閉時歸檔重作日誌的編號

InnoDB中每個表空間都會有一個惟一的space id,共享表空間的space id就是0。
每一個頁都有一個32位序號page no,稱爲偏移量,即離表空間初始位置的偏移量。由於每一個頁大小爲16kb,因此第0個頁的偏移量爲0,第一個頁的偏移量爲16384,以此類推。
經過space id和page no,InnoDB能夠定位任何一個頁。

FIL_PAGE_TYPE標誌頁的類型,InnoDB經常使用頁類型以下
FIL_PAGE_TYPE_ALLOCATED:該頁爲最新分配
FIL_PAGE_IBUF_BITMAP:Insert Buffer位圖頁
FIL_PAGE_TYPE_SYS:系統頁
FIL_PAGE_TYPE_TRX_SYS:事務系統數據頁
FIL_PAGE_TYPE_FSP_HDR:FSP HEADER PAGE頁
FIL_PAGE_TYPE_XDES:擴展描述頁
FIL_PAGE_IBUF_FREE_LIST:Insert Buffer空閒列表頁
FIL_PAGE_UNDO_LOG:Undo Log頁
FIL_PAGE_INDEX:B+樹葉子節點頁
FIL_PAGE_INODE:B+樹索引節點頁
FIL_PAGE_TYPE_BLOB:BLOB頁

FIL Trailer

FIL Trailer是在文件末尾的最後8個字節, 低位4個字節是用來表示Page頁中數據的checksum,最後4字節和FIL Header中的FIL_PAGE_LSN相同
下面說到的頁都有FIL Header,FIL Trailer,再也不重複說明。

如今看一下關鍵的關鍵的管理頁。

FSP HEADER PAGE

表空間第1頁就是文件管理頁FSP HEADER PAGE,存儲表空間關鍵元數據信息。由FSP HEADER、XDES ENTRIES構成。

FSP HEADER

FSP HEADER主要存儲表空間元信息,維護關鍵結構分配信息,主要變量以下:

變量 字節 描述
FSP_SIZE 4 表空間大小,以Page數量計算
FSP_FREE_LIMIT 4 當前已經使用的位置
FSP_FREE 16 空閒區鏈表
FSP_FREE_FRAG 16 部分能夠用碎片區鏈表
FSP_FULL_FRAG 16 已經徹底使用的碎片區鏈表
FSP_SEG_INODES_FULL 16 已經徹底使用的INODE PAGE鏈表
FSP_SEG_INODES_FREE 16 部分可用的INODE PAGE鏈表

區具體能夠分爲區(extent)和碎片區(frag extent)
碎片區是比較特殊的區,用於分配碎片頁。

XDES ENTRIES

接下來是區描述符XDES ENTRIES,每一個區描述符需佔用40個字節,用於追蹤64個頁的使用狀態。
每一個FSP HEADER PAGE只能管理256個區的信息(也就是16384個頁),所以每隔16384個頁,會有一個相似FSP HEADER PAGE的Page來描述隨後的區信息。

XDES ENTRIES主要變量以下

變量 字節 描述
XDES_FLST_NODE 12 維護鏈表先後節點信息
XDES_STATE 4 標識該區是屬於FSP_FREE,FSP_FRAG_FREE或FSP_FRAG_FREE_FULL或XDES_SEG(某個段)
XDES_BITMAP 16 標識區中64個頁的使用狀態

XDES_BITMAP使用位圖方式保存,每一個頁的使用狀態佔用2位(預留一位)。

一個區能夠屬於FSP_FREE,FSP_FRAG_FREE或FSP_FRAG_FREE_FULL或者某一個段。區的分配實現了一套相似於借還的機制。段向表空間租借區,只有段退還該空間時,該區才能從新出如今FSP_FREE/FSP_FULL_FRAG/FSP_FULL中。

INODE PAGE

表空間文件的第3個page的類型爲FIL_PAGE_INODE,管理表空間的段。

INODE PAGE由SEGMENT INODE組成,每一個SEGMENT INODE爲192字節,對應一個段。

SEGMENT INODE結構主要變量以下:

變量 字節 描述
FSEG_FREE 16 未使用的extend鏈表
FSEG_FULL 16 已徹底使用的extend鏈表
FSEG_NOT_FULL 16 部分可用的extend鏈表
FSEG_FRAG_ARR[0] 4 碎片頁數組首頁地址
...
FSEG_FRAG_ARR[31] 4 碎片頁數組尾頁地址

爲節省空間,每一個segment都先從FSP HEADER的FSP_FREE_FRAG中分配32個碎片頁(FSEG_FRAG_ARR),當這些32個頁面不夠使用時,再申請區。

每一個INODE PAGE默承認存儲85個SEGMENT INODE。每一個索引使用2個segment,分別用於管理葉子節點和非葉子節點。
因此一個INODE PAGE最多能夠保存42個索引信息(一個索引使用兩個段)。若是表空間有超過42個索引,則必須再分配一個INODE PAGE。INODE PAGE的分配是從碎片區中申請,但它的位置不是固定的。爲了找到索引的INODE ENTRY,InnoDB定義了SEGMENT HEADER,結構以下

變量 字節 描述
FSEG_HDR_SPACE 4 INODE PAGE所在表空間ID
FSEG_HDR_PAGE_NO 4 INODE PAGE所在表空間的偏移量
FSEG_HDR_OFFSET 2 INODE ENTRY在頁的偏移量

對於用戶表,其索引的Root Page中保存了兩個SEGMENT HEADER,分別指向葉子節點的SEGMENT INODE和非葉子節點的SEGMENT INODE。

鏈表結構

InnoDB的鏈表都是雙向鏈表,如FSP HEADER中變量FSP_FREE,FSP_FREE_FRAG,FSP_FULL_FRAG,FSP_SEG_INODES_FULL,他們都是鏈表頭結構FLST_BASE_NODE,維護了鏈表的頭指針和末尾指針,

變量 字節 描述
FLST_LEN 4 鏈表長度
FLST_FIRST 6 鏈表首節點地址
FLST_LAST 6 鏈表尾節點地址

它們指向的節點爲XDES ENTRIES的XDES_FLST_NODE,每一個節點的結構體稱爲FLST_NODE

變量 字節 描述
FLST_PREV 6 鏈表前驅節點地址
FLST_NEXT 6 鏈表後繼節點地址

下面是一個表空間的示意圖,請理解該圖

第2個Page是FIL_PAGE_IBUF_BITMAP,主要用於跟蹤隨後的每一個PAGE的change buffer信息,使用4個bit來描述每一個page的change buffer信息。
因爲FIL_PAGE_IBUF_BITMAP的空間有限,一樣每隔256個Extent Page以後,也會在XDES PAGE以後建立一個FIL_PAGE_IBUF_BITMAP。

其餘的表空間元信息Page,如 FSP_TRX_SYS_PAGE_NO,共享表空間第6個Page,記錄了InnoDB重要的事務系統信息。 FSP_DICT_HDR_PAGE_NO,共享表空間第8個Page,存儲了SYS_TABLES,SYS_TABLE_IDS,SYS_COLUMNS,SYS_INDEXES和SYS_FIELDS等數據詞典表的Root Page(b+樹Root節點所在Page)。 有興趣的同窗能夠自行了解

索引組織表

上面說了InnoDB經過索引頁來存放行記錄,那麼這些行記錄是怎麼組織的呢
(這裏說的索引頁,包括了B+樹葉子節點頁FIL_PAGE_INDEX和B+樹索引節點頁FIL_PAGE_INODE)

彙集索引

InnoDB中,表都是根據彙集索引順序組織存放的,這種存儲方式的表稱爲索引組織表。
而InnoDB中主鍵索引使用的是B+索引(經過B+樹組織的索引)

當咱們須要打開一張表時,須要從表空間的數據詞典表中加載元數據信息,其中SYS_INDEXES系統表中記錄了用戶表中全部索引Root Page對應的page no,進而找到B+樹Root Page,就能夠對整個用戶數據B+樹進行操做。

B+是爲磁盤和其餘直接存取輔助設備設計的一種多路平衡查找樹。
看一個例子

B+樹有如下特色
一、B+樹不只是多叉樹,並且每一個非葉子節點只存儲鍵值,不存儲數據,這樣每一個非葉子節點所能保存的鍵值大大增長,能夠下降B+的樹深度。
該特性應用到索引上,可使每次加載的節點包括更多的索引數據,也能夠減小IO操做(每次讀取樹的下一層都須要一次IO)。
因此B+索引具備高扇出性,在數據庫中,B+樹的高度通常都在2~4層,查找某一個鍵值的行記錄最多隻須要2到4次IO。

  1. B+樹中,全部數據按鍵值的大小順序存放在同一層的葉子節點上,由各葉子節點指針進行鏈接。
    每次查找數據都須要查找到葉子節點,查找次數都相同,因此查詢速度很穩定。

  2. B+樹全部的葉子節點數據構成了一個有序鏈表,在查詢大小區間的數據時候很是方便。
    如上面例子中的B+樹,若是要查詢[22,89]範圍數據,再須要找到鍵值22,再遍歷到數據鍵值89就能夠了。
    而遍歷全部數據,只須要遍歷全部的葉子節點便可,而不須要遍歷每一層數據,這有利於數據庫作全表掃描。

注意:B+樹全部的葉子節點數據構成了一個有序鏈表,這個是邏輯上的有序,而非物理存儲是順序(維護成本太高)。
InnoDB中,Page的FIL Header維護了上下Page的偏移量,組成雙向鏈表,而Page中行記錄的記錄頭中維護了下一行記錄的位置,組成單向鏈表。

B+樹的查找 相似於二叉查找樹。起始於根節點,自頂向下遍歷樹,根據目標值與鍵值比較結果向下查找對應子樹。
但B+的數據都存儲在葉子節點,因此就算某個非葉子節點的鍵值與所查的關鍵字相等時,並不中止查找,而是繼續沿着這個節點左邊的指針向下,一直查到該關鍵字所在的葉子節點爲止。

B+樹的平衡
對於插入和刪除操做,B+經過分裂和合並節點維持平衡(類型紅黑樹的旋轉), InnoDb中B+樹的鍵值和數據都存放在Page中,所以Page也須要合併和分裂,有興趣的同窗能夠自行了解。

輔助索引

InnoDB中彙集索引和輔助索引都是B+索引。但輔助索引葉子節點的數據不是存儲實際的數據,而是主鍵的值。要想拿到實際的數據須要再經過主鍵索引找到對應的行記錄而後才能拿到實際的數據,這個過程稱爲回表。
若是查詢語句能夠從輔助索引(包括聯合索引)中獲取到全部須要的列,這時不須要再經過主鍵索引找到對應的行記錄,這種狀況稱爲覆蓋索引。

聯合索引

聯合索引也是B+索引。 聯合索引中列的順序很重要。
InnoDB首先根據聯合索引中最左邊的、也就是第一列進行排序,在第一列排序的基礎上,再對聯合索引中後面的第二列進行排序,依此類推。
因此若是想使用聯合索引的第n列,必須先使用聯合索引前面的第1列到第n-1列。

(group, score),可能出現如下排序(1, 46), (1,58), (2,23), (2,96), (3,25), (3,67)。 若是要使用該索引的score列,查詢語句中必須先使用該索引的group列,如where group = 2 and score = 96,InnoDB經過group查詢後,再經過score查詢。
這個規則稱爲最左前綴匹配原則。

頁結構

下面看一下InnoDB索引頁如何保持用戶數據,索引頁由如下部分組成

變量 字節 描述
Page Header 56 頁頭,記錄頁的一些狀態信息
Infimun/Supremum Records 26 系統記錄
User Records 不肯定 用戶記錄,即行記錄
Free Space 不肯定 空閒空間
Page Directory 不肯定 頁目錄

Page Header

變量 字節 描述
PAGE_N_DIR_SLOTS 2 page directory中槽的數量
PAGE_HEAP_TOP 2 堆中空閒空間的偏移量
PAGE_N_HEAP 2 記錄數據數量,包含用戶記錄,系統記錄以及標記刪除的記錄
PAGE_FREE 2 刪除記錄的鏈表
PAGE_GARBAGE 2 已標記刪除記錄數量
PAGE_N_RECS 2 用戶記錄數量,不包含系統記錄以及標記刪除的記錄
PAGE_MAX_TRX_ID 8 最近一次修改該Page記錄的事務ID
PAGE_LEVEL 2 當前頁在索引樹的位置
PAGE_INDEX_ID 8 索引id,表示當前頁屬於那個索引
PAGE_BTR_SEG_LEAF 10 B+樹葉子節點所在段的segment header,僅在B+樹的Root Page中定義
PAGE_BTR_SEG_TOP 10 B+樹非葉子節點所在段的segment header,僅在B+樹的Root Page中定義

PAGE_LAST_INSERT,PAGE_DIRECTION,PAGE_N_DIRECTION等變量並未在表中列出,他們用於進行頁的分裂操做。

當記錄被刪除(不只是將記錄的deleted_flag設置爲1,而是完全刪除),會放到PAGE_FREE鏈表中(鏈表經過記錄頭信息next_record串聯),若是這個頁上有記錄要插入,會先檢查PAGE_FREE鏈表空間是否知足,若是空間知足,直接從PAGE_FREE鏈表空間分配,若是空間不夠,再從空閒空間(PAGE_HEAP_TOP)分配。當空閒空間不足時,會調用函數btr_page_reorganize_low進行頁的從新組織,即根據頁中記錄主鍵的順序從新進行整理,這樣就能整理出碎片的空間。若仍是空間不足,則進行分裂操做。
注意:檢查PAGE_FREE鏈表空間時,僅檢查第一個節點的可用空間,不會經過next_record進行遍歷。

頁記錄是根據主鍵順序排序的,這個排序是邏輯上的,而非物理上的(開銷過大)。

Infimun和Supremum Records
系統虛擬的記錄,Infimun表示比任何主鍵值都小的值,Supremum表示比任何可能的值都大的值。

User Records
行記錄以鏈表的形式存放在 User Records 中,行記錄格式中的記錄頭中的next_record 存放着下一條記錄的地址

Free Space 隨着記錄愈來愈多,Free Space空間愈來愈小,User Records空間愈來愈大。當Free Space的所有空間都被分配完了,這個頁也就使用完了,須要申請新的頁。

Page Directory
B+索引自己不能定位具體的一條記錄,只能找到該記錄所在的頁。
InnoDB將頁載入到內存後,能夠遍歷頁全部的記錄找到目標記錄,但這樣作太慢了。
(Page的記錄非物理順序存儲,沒法經過物理地址二分查詢)

InnoDB將頁中數據進行分組,將每一個組最後一條數據的偏移量按順序存儲起來,組成目錄。
每一個偏移量也被稱爲一個槽(Slot,兩個字節)。這些偏移量都會被存儲到靠近頁的尾部的地方,被稱爲Page Directory。
這樣InnoDB能夠經過Page Directory進行二叉查找定位目標所在分組,再遍歷該組數據就能夠。

每一個槽能夠包括4~8條記錄,每一個記錄的記錄頭中n_owned變量,維護該記錄所在槽的記錄數量。
(例外,第1個槽僅包含一個1記錄,即Infimun,最後一個槽可包含1~8個記錄)

行格式

上面說了InnoDB索引頁如何保存用戶數據,即表的行記錄,下面看看每一行的存儲格式

InnodB中行記錄存儲方法有Compress,Redundant,Compressed,Dynamic。
Redundant行格式是MySql5.0以前使用的,如今基本不會再使用,這裏就不介紹了。

compact格式下,一行記錄依次爲如下內容:
變長字段長度列表,NULL標誌位,記錄頭信息,rowID,TransactionID,RollPointer,列1數據,列2數據,... 其中rowID,TransactionID,RollPointer由InnoDB生成,
注意,若是表中已經指定主鍵,則不生成rowID。
(TransactionID,RollPointer用於實現MVCC功能,將在事務篇解析)

變長字段長度列表
若列的長度小於255字節,用1字節表示
若列的長度大於255字節,用2字節表示(varchar最大限制爲65535)

NULL標誌位
佔用字節爲(可爲NULL的列數量/8)向上取整,字節中哪一位爲1,表示該行數據對應列爲NULL值

注意,變長字段長度列表和NULL標誌位都是是按列定義的倒敘保存的,他們都是可選的,若是表中沒有變長字段和容許NULL值的字段,那麼這兩個都是佔用0字節長度

char(N)中的N指定的是字符,UTF-8下CHAR(10)類型的列,最小能夠存儲10字節的字符,最大能夠存儲30字節的字符。
因此對於多字節的字符,char類型在InnoDB存儲引擎內部被視爲變長字符類型。也意味着這些CHAR數據類型的長度會記錄在變長長度列表中。

記錄頭信息
固定5字節,結構以下

變量 字節 描述
() 2 預留位
deleted_flag 1 該行是否已被刪除
min_rec_flag 1 若是該行記錄是預約義爲最小的記錄,爲1
n_owned 4 該記錄所在Slot擁有的記錄數
heap_no 13 索引堆中該條記錄的索引號
record_type 3 記錄類型,000(普通),001(B+Tree節點指針),010(Infimum),011(Supremum)
next_record 16 頁中下一條記錄的相對位置

看一例子

create table mytest (
    t1 varchar(10),
    t2 varchar(10),
    t3 char(10),
    t4 varchar(10)
) engine=INNODB charset=LATIN1 ROW_format=compact;
insert into mytest3 values('d','11',NULL,'fff');
複製代碼

使用vim打開ibd文件,在「命令」模式中輸入「:%!xxd」命令,將文本轉換爲16進制,找到supremum字符串,後面的就是列數據了。
(ibd中可能有多個頁,能夠依照行的實際數據判斷哪一個是本身要找的數據。)

00010070: 7375 7072 656d 756d 0302 0104 0000 10ff  supremum........
00010080: ef00 0000 0002 0100 0000 0010 2281 0000  ............"... 00010090: 010a 0110 6431 3166 6666 0000 0000 0000 ....d11fff...... 複製代碼

解析以下

03 02 01   // 變長字段長度列表, d列-03,c列-null,不記錄  b列-02  a列-01
04  // null標準位, 二進制-00000100,逆序,d,c-null,b,a
00 00 10 ff ef  // Record Header,固定5字節 
00 0000 0002 01  // RowID ,InnoDB自動建立,6字節 
00 0000 0010 22  // TransactionID
81 0000 010a 0110  // Roll Pointer
64  // 列1數據  'a'
31 31  // 列2數據 '11'
66 6666  // 列4數據  'fff'
複製代碼

行溢出 對於佔用字節數很是大的列,在記錄的真實數據中只會存儲一小部分數據(768個字節),剩餘的數據分散在其餘溢出頁中(BLOG類型的頁),記錄的真實數據中記錄這些頁的地址,以便找到他們。
注意:行溢出與列定義的類型無關。若是varchar過長會發生行溢出,而text,blog不夠長則不會發生行溢出。

InnoDB 1.0.x引入新的行格式,之前支持的Compress 和Redundant稱爲Antelope文件格式,新的文件格式爲Barracuda文件格式,有兩個行記錄格式:Compressed和Dynamic。他們是Compact的變種形式。他們基本沒什麼本質上的區別,惟一的區別就是對於行溢出的處理不一樣。Compressed在數據頁只存儲一個指向溢出頁的地址,全部的實際數據都存放在溢出頁中。
而Compressed還能夠是zlib算法對行數據進行壓縮,所以對於BLOB,TEXT,VARCHAR這類大長度類型的數據可以很是有效的存儲。

參考文檔:
《MySQL技術內幕InnoDB存儲引擎》
《MYSQL內核:INNODB存儲引擎》
Innodb表空間
深度 | 解析InnoDB引擎
Chapter 22 InnoDB Storage ngine
InnoDB 文件系統之文件物理結構

若是您以爲本文不錯,歡迎關注個人微信公衆號,您的關注是我堅持的動力!

相關文章
相關標籤/搜索