從上層的角度來看,InnoDB層的文件,除了redo日誌外,基本上具備至關統一的結構,都是固定block大小,廣泛使用的btree結構來管理數據。只是針對不一樣的block的應用場景會分配不一樣的頁類型。一般默認狀況下,每一個block的大小爲 UNIV_PAGE_SIZE,在不作任何配置時值爲16kb,你還能夠選擇在安裝實例時指定一個塊的block大小。對於壓縮表,能夠在建表時指定block size,但在內存中表現的解壓頁依舊爲統一的頁大小。 node
從物理文件的分類來看,有日誌文件、主系統表空間文件ibdata、undo tablespace文件、臨時表空間文件、用戶表空間。 mysql
日誌文件主要用於記錄redo log,InnoDB採用循環使用的方式,你能夠經過參數指定建立文件的個數和每一個文件的大小。默認狀況下,日誌是以512字節的block單位寫入。因爲現代文件系統的block size一般設置到4k,InnoDB提供了一個選項,可讓用戶將寫入的redo日誌填充到4KB,以免read-modify-write的現象;而Percona Server則提供了另一個選項,支持直接將redo日誌的block size修改爲指定的值。 算法
ibdata是InnoDB最重要的系統表空間文件,它記錄了InnoDB的核心信息,包括事務系統信息、元數據信息,記錄InnoDB change buffer的btree,防止數據損壞的double write buffer等等關鍵信息。咱們稍後會展開描述。 sql
undo獨立表空間是一個可選項,一般默認狀況下,undo數據是存儲在ibdata中的,但你也能夠經過配置選項 innodb_undo_tablespaces 來將undo 回滾段分配到不一樣的文件中,目前開啓undo tablespace 只能在install階段進行。在主流版本進入5.7時代後,咱們建議開啓獨立undo表空間,只有這樣才能利用到5.7引入的新特效:online undo truncate。 數組
MySQL 5.7 新開闢了一個臨時表空間,默認的磁盤文件命名爲ibtmp1,全部非壓縮的臨時表都存儲在該表空間中。因爲臨時表的自己屬性,該文件在重啓時會從新建立。對於雲服務提供商而言,經過ibtmp文件,能夠更好的控制臨時文件產生的磁盤存儲。 安全
用戶表空間,顧名思義,就是用於本身建立的表空間,一般分爲兩類,一類是一個表空間一個文件,另一種則是5.7版本引入的所謂General Tablespace,在知足必定約束條件下,能夠將多個表建立到同一個文件中。除此以外,InnoDB還定義了一些特殊用途的ibd文件,例如全文索引相關的表文件。而針對空間數據類型,也構建了不一樣的數據索引格式R-tree。 數據結構
在關鍵的地方本文註明了代碼函數,建議讀者邊參考代碼邊閱讀本文,本文的代碼部分基於MySQL 5.7.11版本,不一樣的版本函數名或邏輯可能會有所不一樣。請讀者閱讀本文時儘可能選擇該版本的代碼。 函數
InnoDB 的每一個數據文件都歸屬於一個表空間,不一樣的表空間使用一個惟一標識的space id來標記。例如ibdata1, ibdata2… 歸屬系統表空間,擁有相同的space id。用戶建立表產生的ibd文件,則認爲是一個獨立的tablespace,只包含一個文件。 加密
每一個文件按照固定的 page size 進行區分,默認狀況下,非壓縮表的page size爲16Kb。而在文件內部又按照64個Page(總共1M)一個Extent的方式進行劃分並管理。對於不一樣的page size,對應的Extent大小也不一樣,對應爲: spa
儘管支持更大的Page Size,但目前還不支持大頁場景下的數據壓縮,緣由是這涉及到修改壓縮頁中slot的固定size(其實實現起來也不復雜)。在不作聲明的狀況下,下文咱們默認使用16KB的Page Size來闡述文件的物理結構。
爲了管理整個Tablespace,除了索引頁外,數據文件中還包含了多種管理頁,以下圖所示,一個用戶表空間大約包含這些頁來管理文件,下面會一一進行介紹。
InnoDB 管理頁
首先咱們先介紹基於文件的一個基礎結構,即文件鏈表。爲了管理Page,Extent這些數據塊,在文件中記錄了許多的節點以維持具備某些特徵的鏈表,例如在在文件頭維護的inode page鏈表,空閒、用滿以及碎片化的Extent鏈表等等。
在InnoDB裏鏈表頭稱爲FLST_BASE_NODE,大小爲FLST_BASE_NODE_SIZE(16個字節)。BASE NODE維護了鏈表的頭指針和末尾指針,每一個節點稱爲FLST_NODE,大小爲FLST_NODE_SIZE(12個字節)。相關結構描述以下:
FLST_BASE_NODE:
FLST_NODE:
如上所述,文件鏈表中使用6個字節來做爲節點指針,指針的內容包括:
該鏈表結構是InnoDB表空間內管理全部page的基礎結構,下圖先感覺下,具體的內容能夠繼續往下閱讀。
InnoDB 表空間page管理
文件鏈表管理的相關代碼參閱:include/fut0lst.ic, fut/fut0lst.cc
數據文件的第一個Page類型爲FIL_PAGE_TYPE_FSP_HDR,在建立一個新的表空間時進行初始化(fsp_header_init),該page同時用於跟蹤隨後的256個Extent(約256MB文件大小)的空間管理,因此每隔256MB就要建立一個相似的數據頁,類型爲FIL_PAGE_TYPE_XDES ,XDES Page除了文件頭部外,其餘都和FSP_HDR頁具備相同的數據結構,能夠稱之爲Extent描述頁,每一個Extent佔用40個字節,一個XDES Page最多描述256個Extent。
FSP_HDR頁的頭部使用FSP_HEADER_SIZE個字節來記錄文件的相關信息,具體的包括:
在文件頭使用FLAG(對應上述FSP_SPACE_FLAGS)描述了建立表時的以下關鍵信息:
除了上述描述信息外,其餘部分的數據結構和XDES PAGE(FIL_PAGE_TYPE_XDES)都是相同的,使用連續數組的方式,每一個XDES PAGE最多存儲256個XDES Entry,每一個Entry佔用40個字節,描述64個Page(即一個Extent)。格式以下:
XDES_STATE表示該Extent的四種不一樣狀態:
經過XDES_STATE信息,咱們只須要一個FLIST_NODE節點就能夠維護每一個Extent的信息,是處於全局表空間的鏈表上,仍是某個btree segment的鏈表上。
第2個page類型爲FIL_PAGE_IBUF_BITMAP,主要用於跟蹤隨後的每一個page的change buffer信息,使用4個bit來描述每一個page的change buffer信息。
因爲bitmap page的空間有限,一樣每隔256個Extent Page以後,也會在XDES PAGE以後建立一個ibuf bitmap page。
關於change buffer,這裏咱們不展開討論,感興趣的能夠閱讀以前的這篇月報:
MySQL · 引擎特性 · Innodb change buffer介紹(http://mysql.taobao.org/monthly/2015/07/01/)
數據文件的第3個page的類型爲FIL_PAGE_INODE,用於管理數據文件中的segement,每一個索引佔用2個segment,分別用於管理葉子節點和非葉子節點。每一個inode頁能夠存儲FSP_SEG_INODES_PER_PAGE(默認爲85)個記錄。
每一個Inode Entry的結構以下表所示:
從上文咱們能夠看到,InnoDB經過Inode Entry來管理每一個Segment佔用的數據頁,每一個segment能夠看作一個文件頁維護單元。Inode Entry所在的inode page有可能存放滿,所以又經過頭Page維護了Inode Page鏈表。
在ibd的第一個Page中還維護了表空間內Extent的FREE、FREE_FRAG、FULL_FRAG三個Extent鏈表;而每一個Inode Entry也維護了對應的FREE、NOT_FULL、FULL三個Extent鏈表。這些鏈表之間存在着轉換關係,以高效的利用數據文件空間。
當建立一個新的索引時,實際上構建一個新的btree(btr_create),先爲非葉子節點Segment分配一個inode entry,再建立root page,並將該segment的位置記錄到root page中,而後再分配leaf segment的Inode entry,並記錄到root page中。
當刪除某個索引後,該索引佔用的空間須要能被從新利用起來。
建立Segment
首先每一個Segment須要從ibd文件中預留必定的空間(fsp_reserve_free_extents),一般是2個Extent。但若是是新建立的表空間,且當前的文件小於1個Extent時,則只分配2個Page。
當文件空間不足時,須要對文件進行擴展(fsp_try_extend_data_file)。文件的擴展遵循必定的規則:若是當前小於1個Extent,則擴展到1個Extent滿;當表空間小於32MB時,每次擴展一個Extent;大於32MB時,每次擴展4個Extent(fsp_get_pages_to_extend_ibd)。
在預留空間後,讀取文件頭Page並加鎖(fsp_get_space_header),而後開始爲其分配Inode Entry(fsp_alloc_seg_inode)。首先須要找到一個合適的inode page。
咱們知道Inode Page的空間有限,爲了管理Inode Page,在文件頭存儲了兩個Inode Page鏈表,一個連接已經用滿的inode page,一個連接還沒有用滿的inode page。若是當前Inode Page的空間使用完了,就須要再分配一個inode page,並加入到FSP_SEG_INODES_FREE鏈表上(fsp_alloc_seg_inode_page)。對於獨立表空間,一般一個inode page就足夠了。
當拿到目標inode page後,從該Page中找到一個空閒(fsp_seg_inode_page_find_free)未使用的slot(空閒表示其不歸屬任何segment,即FSEG_ID置爲0)。
一旦該inode page中的記錄用滿了,就從FSP_SEG_INODES_FREE鏈表上轉移到FSP_SEG_INODES_FULL鏈表。
得到inode entry後,遞增頭page的FSP_SEG_ID,做爲當前segment的seg id寫入到inode entry中。隨後進行一些列的初始化。
在完成inode entry的提取後,就將該inode entry所在inode page的位置及頁內偏移量存儲到其餘某個page內(對於btree就是記錄在根節點內,佔用10個字節,包含space id, page no, offset)。
Btree的根節點其實是在建立non-leaf segment時分配的,root page被分配到該segment的frag array的第一個數組元素中。
Segment分配入口函數: fseg_create_general
分配數據頁
隨着btree數據的增加,咱們須要爲btree的segment分配新的page。前面咱們已經講過,segment是一個獨立的page管理單元,咱們須要將從全局得到的數據空間歸入到segment的管理中。
Step 1:空間擴展
當斷定插入索引的操做可能引發分裂時,會進行悲觀插入(btr_cur_pessimistic_insert),在作實際的分裂操做以前,會先對文件進行擴展,並嘗試預留(tree_height / 16 + 3)個Extent,大多數狀況下都是3個Extent。
這裏有個意外場景:若是當前文件還不超過一個Extent,而且請求的page數小於1/2個Extent時,則若是指定page數,保證有2個可用的空閒Page,或者分配指定的page,而不是以Extent爲單位進行分配。
注意這裏只是保證有足夠的文件空間,避免在btree操做時進行文件Extent。若是在這一步擴展了ibd文件(fsp_try_extend_data_file),新的數據頁並未初始化,也未加入到任何的鏈表中。
在斷定是否有足夠的空閒Extent時,自己ibd預留的空閒空間也要歸入考慮,對於普通用戶表空間是2個Extent + file_size * 1%。這些新擴展的page此時並未進行初始化,也未加入到,在頭page的FSP_FREE_LIMIT記錄的page no標識了這類未初始化頁的範圍。
Step 2:爲segment分配page
隨後進入索引分裂階段(btr_page_split_and_insert),新page分配的上層調用棧:
在傳遞的參數中,有個hint page no,一般是當前須要分裂的page no的前一個(direction = FSP_DOWN)或者後一個page no(direction = FSP_UP),其目的是將邏輯上相鄰的節點在物理上也儘可能相鄰。
在Step 1咱們已經保證了物理空間有足夠的數據頁,只是還沒進行初始化。將page分配到當前segment的流程以下(fseg_alloc_free_page_low):
1. 計算當前segment使用的和佔用的page數
· 使用的page數存儲包括FSEG_NOT_FULL鏈表上使用的page數(存儲在inode entry的FSEG_NOT_FULL_N_USED中) + 已用滿segment的FSEG_FULL鏈表上page數 + 佔用的frag array page數量;
· 佔用的page數包括FSEG_FREE、FSEG_NOT_FULL、FSEG_FULL三個鏈表上的Extent + 佔用的frag array page數量。
2. 根據hint page獲取對應的xdes entry (xdes_get_descriptor_with_space_hdr)
3. 當知足以下條件時該hint page能夠直接拿走使用:
· Extent狀態爲XDES_FSEG,表示屬於一個segment
· hint page所在的Extent已被分配給當前segment(檢查xdes entry的XDES_ID)
· hint page對應的bit設置爲free,表示還沒有被佔用
· 返回hint page
4. 當知足條件:1) xdes entry當前是空閒狀態(XDES_FREE);2) 該segment中已使用的page數大於其佔用的page數的7/8 (FSEG_FILLFACTOR);3) 當前segment已經使用了超過32個frag page,即表示其inode中的frag array可能已經用滿。
· 從表空間分配hint page所在的Extent (fsp_alloc_free_extent),將其從FSP_FREE鏈表上移除
· 設置該Extent的狀態爲XDES_FSEG,寫入seg id,並加入到當前segment的FSEG_FREE鏈表中。
· 返回hint page
5. 當以下條件時:1) direction != FSP_NO_DIR,對於Btree分裂,要麼FSP_UP,要麼FSP_DOWN;2)已使用的空間小於已佔用空間的7/8; 3)當前segment已經使用了超過32個frag page
· 嘗試從segment獲取一個Extent(fseg_alloc_free_extent),若是該segment的FSEG_FREE鏈表爲空,則須要從表空間分配(fsp_alloc_free_extent)一個Extent,並加入到當前segment的FSEG_FREE鏈表上
· direction爲FSP_DOWN時,返回該Extent最後一個page,爲FSP_UP時,返回該Extent的第一個Page
6. xdes entry屬於當前segment且未被用滿,從其中取一個空閒page並返回
7. 若是該segment佔用的page數大於實用的page數,說明該segment還有空閒的page,則依次先看FSEG_NOT_FULL鏈表上是否有未滿的Extent,若是沒有,再看FSEG_FREE鏈表上是否有徹底空閒的Extent。從其中取一個空閒Page並返回
8. 當前已經實用的Page數小於32個page時,則分配獨立的page(fsp_alloc_free_page)並加入到該inode的frag array page數組中,而後返回該block
9. 當上述狀況都不知足時,直接分配一個Extent(fseg_alloc_free_extent),並從其中取一個page返回。
上述流程看起來比較複雜,但能夠總結爲:
1. 對於一個新的segment,老是優先填滿32個frag page數組,以後纔會爲其分配完整的Extent,能夠利用碎片頁,並避免小表佔用太多空間。
2. 儘可能得到hint page;
3. 若是segment上未使用的page太多,則儘可能利用segment上的page。
上文提到兩處從表空間爲segment分配數據頁,一個是分配單獨的數據頁,一個是分配整個Extent
表空間單獨數據頁的分配調用函數fsp_alloc_free_page:
1. 若是hint page所在的Extent在鏈表XDES_FREE_FRAG上,能夠直接使用;不然從根據頭page的FSP_FREE_FRAG鏈表查看是否有可用的Extent;
2. 未能從上述找到一個可用Extent,直接分配一個Extent,並加入到FSP_FREE_FRAG鏈表中;
3. 從得到的Extent中找到描述爲空閒(XDES_FREE_BIT)的page。
4. 分配該page (fsp_alloc_from_free_frag)
· 設置page對應的bitmap的XDES_FREE_BIT爲false,表示被佔用;
· 遞增頭page的FSP_FRAG_N_USED字段;
· 若是該Extent被用滿了,就將其從FSP_FREE_FRAG移除,並加入到FSP_FULL_FRAG鏈表中。同時對頭Page的FSP_FRAG_N_USED遞減1個Extent(FSP_FRAG_N_USED只存儲未滿的Extent使用的page數量);
· 對Page內容進行初始化(fsp_page_create)。
表空間Extent的分配函數fsp_alloc_free_extent:
1. 一般先經過頭page看FSP_FREE鏈表上是否有空閒的Extent,若是沒有的話,則將新的Extent(例如上述step 1對文件作擴展產生的新page,從FSP_FREE_LIMIT算起)加入到FSP_FREE鏈表上(fsp_fill_free_list):
· 一次最多加4個Extent(FSP_FREE_ADD);
· 若是涉及到xdes page,還須要對xdes page進行初始化;
· 若是Extent中存在相似xdes page這樣的系統管理頁,這個Extent被加入到FSP_FREE_FRAG鏈表中而不是FSP_FREE鏈表;
· 取鏈表上第一個Extent爲當前使用;
2. 將得到的Extent從FSP_FREE移除,並返回對應的xdes entry(xdes_lst_get_descriptor)。
回收Page
數據頁的回收分爲兩種,一種是整個Extent的回收,一種是碎片頁的回收。在刪除索引頁或者drop索引時都會發生。
當某個數據頁上的數據被刪光時,咱們須要從其所在segmeng上刪除該page(btr_page_free -->fseg_free_page --> fseg_free_page_low),回收的流程也比較簡單:
1. 首先若是是該segment的frag array中的page,將對應的slot設置爲FIL_NULL, 並返還給表空間(fsp_free_page):
· page在xdes entry中的狀態置爲空閒;
· 若是page所在Extent處於FSP_FULL_FRAG鏈表,則轉移到FSP_FREE_FRAG中;
· 若是Extent中的page徹底被釋放掉了,則釋放該Extent(fsp_free_extent),將其轉移到FSP_FREE鏈表;
· 從函數返回;
2. 若是page所處於的Extent當前在該segment的FSEG_FULL鏈表上,則轉移到FSEG_NOT_FULL鏈表;
3. 設置Page在xdes entry的bitmap對應的XDES_FREE_BIT爲true;
4. 若是此時該Extent上的page所有被釋放了,將其從FSEG_NOT_FULL鏈表上移除,並加入到表空間的FSP_FREE鏈表上(而非Segment的FSEG_FREE鏈表)。
釋放Segment
當咱們刪除索引或者表時,須要刪除btree(btr_free_if_exists),先刪除除了root節點外的其餘部分(btr_free_but_not_root),再刪除root節點(btr_free_root)
因爲數據操做都須要記錄redo,爲了不產生很是大的redo log,leaf segment經過反覆調用函數fseg_free_step來釋放其佔用的數據頁:
1. 首先找到leaf segment對應的Inode entry(fseg_inode_try_get);
2. 而後依次查找inode entry中的FSEG_FULL、或者FSEG_NOT_FULL、或者FSEG_FREE鏈表,找到一個Extent,注意着裏的鏈表元組所指向的位置其實是描述該Extent的Xdes Entry所在的位置。所以能夠快速定位到對應的Xdes Page及Page內偏移量(xdes_lst_get_descriptor);
3. 如今咱們能夠將這個Extent安全的釋放了(fseg_free_extent,見後文);
4. 當反覆調用fseg_free_step將全部的Extent都釋放後,segment還會最多佔用32個碎片頁,也須要依次釋放掉(fseg_free_page_low)
5. 最後,當該inode所佔用的page所有釋放時,釋放inode entry:
· 若是該inode所在的inode page中當前被用滿,則因爲咱們即將釋放一個slot,須要從FSP_SEG_INODES_FULL轉移到FSP_SEG_INODES_FREE(更新第一個page);
· 將該inode entry的SEG_ID清除爲0,表示未使用;
· 若是該inode page上所有inode entry都釋放了,就從FSP_SEG_INODES_FREE移除,並刪除該page。
non-leaf segment的回收和leaf segment的回收基本相似,但要注意btree的根節點存儲在該segment的frag arrary的第一個元組中,該Page暫時不能夠釋放(fseg_free_step_not_header)
btree的root page在完成上述步驟後再釋放,此時才能完全釋放non-leaf segment
ibd文件中真正構建起用戶數據的結構是BTREE,在你建立一個表時,已經基於顯式或隱式定義的主鍵構建了一個btree,其葉子節點上記錄了行的所有列數據(加上事務id列及回滾段指針列);若是你在表上建立了二級索引,其葉子節點存儲了鍵值加上彙集索引鍵值。本小節咱們探討下組成索引的物理存儲頁結構,這裏默認討論的是非壓縮頁,咱們在下一小節介紹壓縮頁的內容。
每一個btree使用兩個Segment來管理數據頁,一個管理葉子節點,一個管理非葉子節點,每一個segment在inode page中存在一個記錄項,在btree的root page中記錄了兩個segment信息。
當咱們須要打開一張表時,須要從ibdata的數據詞典表中load元數據信息,其中SYS_INDEXES系統表中記錄了表,索引,及索引根頁對應的page no(DICT_FLD__SYS_INDEXES__PAGE_NO),進而找到btree根page,就能夠對整個用戶數據btree進行操做。
索引最基本的頁類型爲FIL_PAGE_INDEX。能夠劃分爲下面幾個部分。
Page Header
首先無論任何類型的數據頁都有38個字節來描述頭信息(FIL_PAGE_DATA, or PAGE_HEADER),包含以下信息:
Index Header
緊隨FIL_PAGE_DATA以後的是索引信息,這部分信息是索引頁獨有的。
Segment Info
隨後20個字節描述段信息,僅在Btree的root Page中被設置,其餘Page都是未使用的。
10個字節的inode信息包括:
經過上述信息,咱們能夠找到對應segment在inode page中的描述項,進而能夠操做整個segment。
系統記錄
以後是兩個系統記錄,分別用於描述該page上的極小值和極大值,這裏存在兩種存儲方式,分別對應舊的InnoDB文件系統,及新的文件系統(compact page)
Compact的系統記錄存儲方式爲:
兩種格式的主要差別在於不一樣行存儲模式下,單個記錄的描述信息不一樣。在實際建立page時,系統記錄的值已經初始化好了,對於老的格式(REDUNDANT),對應代碼裏的infimum_supremum_redundant,對於新的格式(compact),對應infimum_supremum_compact。infimum記錄的固定heap no爲0,supremum記錄的固定Heap no 爲1。page上最小的用戶記錄前節點老是指向infimum,page上最大的記錄後節點老是指向supremum記錄。
具體參考索引頁建立函數:page_create_low
用戶記錄
在系統記錄以後就是真正的用戶記錄了,heap no 從2(PAGE_HEAP_NO_USER_LOW)開始算起。注意Heap no僅表明物理存儲順序,不表明鍵值順序。
根據不一樣的類型,用戶記錄能夠是非葉子節點的Node指針信息,也能夠是隻包含有效數據的葉子節點記錄。而不一樣的行格式存儲的行記錄也不一樣,例如在早期版本中使用的redundant格式會被如今的compact格式使用更多的字節數來描述記錄,例如描述記錄的一些列信息,在使用compact格式時,能夠改成直接從數據詞典獲取。由於redundant屬於漸漸被拋棄的格式,本文的討論中咱們默認使用Compact格式。在文件rem/rem0rec.cc的頭部註釋描述了記錄的物理結構。
每一個記錄都存在rec header,描述以下(參閱文件include/rem0rec.ic)
在記錄頭信息以後的數據視具體狀況有所不一樣:
· 對於彙集索引記錄,數據包含了事務id,回滾段指針;
· 對於二級索引記錄,數據包含了二級索引鍵值以及彙集索引鍵值。若是二級索引鍵和彙集索引有重合,則只保留一份重合的,例如pk (col1, col2),sec key(col2, col3),在二級索引記錄中就只包含(col2, col3, col1);
· 對於非葉子節點頁的記錄,彙集索引上包含了其子節點的最小記錄鍵值及對應的page no;二級索引上有所不一樣,除了二級索引鍵值外,還包含了彙集索引鍵值,再加上page no三部分構成。
Free space
這裏指的是一塊完整的未被使用的空間,範圍在頁內最後一個用戶記錄和Page directory之間。一般若是空間足夠時,直接從這裏分配記錄空間。當斷定空閒空間不足時,會作一次Page內的重整理,以對碎片空間進行合併。
Page directory
爲了加快頁內的數據查找,會按照記錄的順序,每隔4~8個數量(PAGE_DIR_SLOT_MIN_N_OWNED ~ PAGE_DIR_SLOT_MAX_N_OWNED)的用戶記錄,就分配一個slot (每一個slot佔用2個字節,PAGE_DIR_SLOT_SIZE),存儲記錄的頁內偏移量,能夠理解爲在頁內構建的一個很小的索引(sparse index)來輔助二分查找。
Page Directory的slot分配是從Page末尾(倒數第八個字節開始)開始逆序分配的。在查詢記錄時。先根據page directory 肯定記錄所在的範圍,而後在據此進行線性查詢。
增長slot的函數參閱 page_dir_add_slot
頁內記錄二分查找的函數參閱 page_cur_search_with_match_bytes
FIL Trailer
在每一個文件頁的末尾保留了8個字節(FIL_PAGE_DATA_END or FIL_PAGE_END_LSN_OLD_CHKSUM),其中4個字節用於存儲page checksum,這個值須要和page頭部記錄的checksum相匹配,不然認爲page損壞(buf_page_is_corrupted)
InnoDB當前存在兩種形式的壓縮頁,一種是Transparent Page Compression,還有一種是傳統的壓縮方式,下文分別進行闡述。
這是MySQL5.7新加的一種數據壓縮方式,其原理是利用內核Punch hole特性,對於一個16kb的數據頁,在寫文件以前,除了Page頭以外,其餘部分進行壓縮,壓縮後留白的地方使用punch hole進行 「打洞」,在磁盤上表現爲不佔用空間 (但會產生大量的磁盤碎片)。 這種方式相比傳統的壓縮方式具備更好的壓縮比,實現邏輯也更加簡單。
對於這種壓縮方式引入了新的類型FIL_PAGE_COMPRESSED,在存儲格式上略有不一樣,主要表如今從FIL_PAGE_FILE_FLUSH_LSN開始的8個字節被用做記錄壓縮信息:
打洞後的page其實際存儲空間須要是磁盤的block size的整數倍。
這裏咱們不展開闡述,具體參閱我以前寫的這篇文章:MySQL · 社區動態 · InnoDB Page Compression(http://mysql.taobao.org/monthly/2015/08/01/)
當你建立或修改表,指定row_format=compressed key_block_size=1|2|4|8 時,建立的ibd文件將以對應的block size進行劃分。例如key_block_size設置爲4時,對應block size爲4kb。
壓縮頁的格式能夠描述以下表所示:
在內存中一般存在壓縮頁和解壓頁兩份數據。當對數據進行修改時,一般先修改解壓頁,再將DML操做以一種特殊日誌的格式記入壓縮頁的mlog中。以減小被修改過程當中重壓縮的次數。主要包含這幾種操做:
· Insert: 向mlog中寫入完整記錄
· Update:
· Delete-insert update,將舊記錄的dense slot標記爲刪除,再寫入完整新記錄
· In-place update,直接寫入新更新的記錄
· Delete: 標記對應的dense slot爲刪除
頁壓縮參閱函數 page_zip_compress
頁解壓參閱函數 page_zip_decompress
這裏咱們將全部非獨立的數據頁統稱爲系統數據頁,主要存儲在ibdata中,以下圖所示:
InnoDB 系統數據頁
ibdata的三個page和普通的用戶表空間同樣,都是用於維護和管理文件頁。其餘Page咱們下面一一進行介紹。
FSP_IBUF_HEADER_PAGE_NO
Ibdata的第4個page是Change Buffer的header page,類型爲FIL_PAGE_TYPE_SYS,主要用於對ibuf btree的Page管理。
FSP_IBUF_TREE_ROOT_PAGE_NO
用於存儲change buffer的根page,change buffer目前存儲於Ibdata中,其本質上也是一顆btree,root頁爲固定page,也就是Ibdata的第5個page。
IBUF HEADER Page 和Root Page聯合起來對ibuf的數據頁進行管理。
首先Ibuf btree本身維護了一個空閒Page鏈表,鏈表頭記錄在根節點中,偏移量在PAGE_BTR_IBUF_FREE_LIST處,實際上利用的是普通索引根節點的PAGE_BTR_SEG_LEAF字段。Free List上的Page類型標示爲FIL_PAGE_IBUF_FREE_LIST
每一個Ibuf page重用了PAGE_BTR_SEG_LEAF字段,以維護IBUF FREE LIST的先後文件頁節點(PAGE_BTR_IBUF_FREE_LIST_NODE)。
因爲root page中的segment字段已經被重用,所以額外的開闢了一個Page,也就是Ibdata的第4個page來進行段管理。在其中記錄了ibuf btree的segment header,指向屬於ibuf btree的inode entry。
關於ibuf btree的構建參閱函數 btr_create
FSP_TRX_SYS_PAGE_NO/FSP_FIRST_RSEG_PAGE_NO
ibdata的第6個page,記錄了InnoDB重要的事務系統信息,主要包括:
在5.7版本中,回滾段既能夠在ibdata中,也能夠在獨立undo表空間,或者ibtmp臨時表空間中,一個可能的分佈以下圖所示(摘自我以前的這篇文章[http://mysql.taobao.org/monthly/2015/04/01/])。
InnoDB Undo 回滾段結構
因爲是在系統剛啓動時初始化事務系統,所以第0號回滾段頭頁老是在ibdata的第7個page中。
事務系統建立參閱函數 trx_sysf_create
InnoDB最多能夠建立128個回滾段,每一個回滾段須要單獨的Page來維護其擁有的undo slot,Page類型爲FIL_PAGE_TYPE_SYS。描述以下:
回滾段頭頁的建立參閱函數 trx_rseg_header_create
實際存儲undo記錄的Page類型爲FIL_PAGE_UNDO_LOG,undo header結構以下
undo頁內結構及其與回滾段頭頁的關係參閱下圖:
InnoDB Undo 頁內結構
關於具體的Undo log如何存儲,本文不展開描述,可閱讀我以前的這篇文章:MySQL · 引擎特性 · InnoDB undo log 漫遊(http://mysql.taobao.org/monthly/2015/04/01/)
FSP_DICT_HDR_PAGE_NO
ibdata的第8個page,用來存儲數據詞典表的信息 (只有拿到數據詞典表,才能根據其中存儲的表信息,進一步找到其對應的表空間,以及表的彙集索引所在的page no)
Dict_Hdr Page的結構以下表所示:
dict_hdr頁的建立參閱函數 dict_hdr_create
double write buffer
InnoDB使用double write buffer來防止數據頁的部分寫問題,在寫一個數據頁以前,老是先寫double write buffer,再寫數據文件。當崩潰恢復時,若是數據文件中page損壞,會嘗試從dblwr中恢復。
double write buffer存儲在ibdata中,你能夠從事務系統頁(ibdata的第6個page)獲取dblwr所在的位置。總共128個page,劃分爲兩個block。因爲dblwr在安裝實例時已經初始化好了,這兩個block在Ibdata中具備固定的位置,Page64 ~127 劃屬第一個block,Page 128 ~191劃屬第二個block。
在這128個page中,前120個page用於batch flush時的髒頁回寫,另外8個page用於SINGLE PAGE FLUSH時的髒頁回寫。
對於大字段,在知足必定條件時InnoDB使用外部頁進行存儲。外部存儲頁有三種類型:
1. FIL_PAGE_TYPE_BLOB:表示非壓縮的外部存儲頁,結構以下圖所示:
2. FIL_PAGE_TYPE_ZBLOB:壓縮的外部存儲頁,若是存在多個blob page,則表示第一個
FIL_PAGE_TYPE_ZBLOB2:若是存在多個壓縮的blob page,則表示blob鏈隨後的page;
結構以下圖所示:
而在記錄內只存儲了20個字節的指針以指向外部存儲頁,指針描述以下:
外部頁的寫入參閱函數 btr_store_big_rec_extern_fields
MySQL 5.7版本引入了新的數據頁以支持表空間加密及對空間數據類型創建R-TREE索引。本文對這種數據頁不作深刻討論,僅僅簡單描述下,後面咱們會單獨開兩篇文章分別進行介紹。
數據加密頁
從MySQL5.7.11開始InnoDB支持對單表進行加密,所以引入了新的Page類型來支持這一特性,主要加了三種Page類型:
· FIL_PAGE_ENCRYPTED:加密的普通數據頁
· FIL_PAGE_COMPRESSED_AND_ENCRYPTED:數據頁爲壓縮頁(transparent page compression) 而且被加密(先壓縮,再加密)
· FIL_PAGE_ENCRYPTED_RTREE:GIS索引R-TREE的數據頁並被加密
對於加密頁,除了數據部分被替換成加密數據外,其餘部分和大多數表都是同樣的結構。
加解密的邏輯和Transparent Compression相似,在寫入文件前加密(os_file_encrypt_page --> Encryption::encrypt),在讀出文件時解密數據(os_file_io_complete --> Encryption::decrypt)
祕鑰信息存儲在ibd文件的第一個page中(fsp_header_init --> fsp_header_fill_encryption_info),當執行SQL ALTER INSTANCE ROTATE INNODB MASTER KEY時,會更新每一個ibd存儲的祕鑰信息(fsp_header_rotate_encryption)
默認安裝時,一個新的插件keyring_file被安裝而且默認Active,在安裝目錄下,會產生一個新的文件來存儲祕鑰,位置在$MYSQL_INSTALL_DIR/keyring/keyring,你能夠經過參數keyring_file_data來指定祕鑰的存放位置和文件命名。 當你安裝多實例時,須要爲不一樣的實例指定keyring文件。
開啓表加密的語法很簡單,在CREATE TABLE或ALTER TABLE時指定選項ENCRYPTION=‘Y’來開啓,或者ENCRYPTION=‘N’來關閉加密。
關於InnoDB表空間加密特性,參閱該commit及官方文檔。
R-TREE索引頁
在MySQL 5.7中引入了新的索引類型R-TREE來描述空間數據類型的多維數據結構,這類索引的數據頁類型爲FIL_PAGE_RTREE。
R-TREE的相關設計參閱官方WL#6968, WL#6609, WL#6745
MySQL5.7引入了臨時表專用的表空間,默認命名爲ibtmp1,建立的非壓縮臨時表都存儲在該表空間中。系統重啓後,ibtmp1會被從新初始化到默認12MB。你能夠經過設置參數innodb_temp_data_file_path來修改ibtmp1的默認初始大小,以及是否容許autoExtent。默認值爲 「ibtmp1:12M:autoExtent」。
除了用戶定義的非壓縮臨時表外,第1~32個臨時表專用的回滾段也存放在該文件中(0號回滾段老是存放在ibdata中)(trx_sys_create_noredo_rsegs),
關於日誌文件的格式,網上已經有不少的討論,在以前的系列文章中我也有專門介紹過,本小節主要介紹下MySQL5.7新的修改。
首先是checksum算法的改變,當前版本的MySQL5.7能夠經過參數innodb_log_checksums來開啓或關閉redo checksum,但目前惟一支持的checksum算法是CRC32。而在以前老版本中只支持效率較低的InnoDB自己的checksum算法。
第二個改變是爲Redo log引入了版本信息(WL#8845),存儲在ib_logfile的頭部,從文件頭開始,描述以下
每次切換到下一個iblogfile時,都會更新該文件頭信息(log_group_file_header_flush)
新的版本支持兼容老版本(recv_find_max_checkpoint_0),但升級到新版本後,就沒法在異常狀態下in-place降級到舊版本了(除非作一次clean的shutdown,並清理掉iblogfile)。