富陽區(qū)建設(shè)局網(wǎng)站直通車怎么開效果最佳
「mysql是怎樣運(yùn)行的」第五章 盛放記錄的大盒子—InnoDB數(shù)據(jù)頁結(jié)構(gòu)
文章目錄
- 「mysql是怎樣運(yùn)行的」第五章 盛放記錄的大盒子---InnoDB數(shù)據(jù)頁結(jié)構(gòu)
- @[toc]
- 一、不同類型的頁介紹
- 二、數(shù)據(jù)頁結(jié)構(gòu)的快速瀏覽
- 三、記錄在頁中的存儲
- 記錄頭信息的秘密
- 四、Page Directory(頁目錄)
- 五、Page Header(頁面頭部)
- 六、File Header(文件頭部)
- 七、File Trailer(文件尾部)
- 八、總結(jié)
文章目錄
- 「mysql是怎樣運(yùn)行的」第五章 盛放記錄的大盒子---InnoDB數(shù)據(jù)頁結(jié)構(gòu)
- @[toc]
- 一、不同類型的頁介紹
- 二、數(shù)據(jù)頁結(jié)構(gòu)的快速瀏覽
- 三、記錄在頁中的存儲
- 記錄頭信息的秘密
- 四、Page Directory(頁目錄)
- 五、Page Header(頁面頭部)
- 六、File Header(文件頭部)
- 七、File Trailer(文件尾部)
- 八、總結(jié)
一、不同類型的頁介紹
前邊我們簡單提了一下頁的概念,它是InnoDB管理存儲空間的基本單位,一個頁的大小一般是16KB。InnoDB為了不同的目的而設(shè)計了許多種不同類型的頁,比如存放表空間頭部信息的頁,存放Insert Buffer信息的頁,存放INODE信息的頁,存放undo日志信息的頁等等等等。
我們聚焦的是那些存放我們表中記錄的那種類型的頁,官方稱這種存放記錄的頁為索引(INDEX)頁,鑒于我們還沒有了解過索引是個什么東西,而這些表中的記錄就是我們?nèi)粘?谥兴Q的數(shù) 據(jù),所以目前還是叫這種存放記錄的頁為數(shù)據(jù)頁吧。
二、數(shù)據(jù)頁結(jié)構(gòu)的快速瀏覽
數(shù)據(jù)頁代表的這塊16KB大小的存儲空間可以被劃分為多個部分,不同部分有不同的功能,各個部分如圖所示:
從圖中可以看出,一個InnoDB數(shù)據(jù)頁的存儲空間大致被劃分成了7個部分,有的部分占用的字節(jié)數(shù)是確定的,有的部分占用的字節(jié)數(shù)是不確定的。下邊我們用表格的方式來大致描述一下這7個部分都存儲 一些啥內(nèi)容(快速的瞅一眼就行了,后邊會詳細(xì)嘮叨的):
名稱 | 中文名 | 占用空間大小 | 簡單描述 |
---|---|---|---|
File Header | 文件頭部 | 38 字節(jié) | 頁的一些通用信息 |
Page Header | 頁面頭部 | 56 字節(jié) | 數(shù)據(jù)頁專有的一些信息 |
Infimum + Supremum | 最小記錄和最大記錄 | 26 字節(jié) | 兩個虛擬的行記錄 |
User Records | 用戶記錄 | 不確定 | 實(shí)際存儲的行記錄內(nèi)容 |
Free Space | 空閑空間 | 不確定 | 頁中尚未使用的空間 |
Page Directory | 頁面目錄 | 不確定 | 頁中的某些記錄的相對位置 |
File Trailer | 文件尾部 | 8 字節(jié) | 校驗(yàn)頁是否完整 |
三、記錄在頁中的存儲
在頁的7個組成部分中,我們自己存儲的記錄會按照我們指定的行格式存儲到User Records部分。但是在一開始生成頁的時候,其實(shí)并沒有User Records這個部分,每當(dāng)我們插入一條記錄,都會從Free Space部分,也就是尚未使用的存儲空間中申請一個記錄大小的空間劃分到User Records部分,當(dāng)Free Space部分的空間全部被User Records部分替代掉之后,也就意味著這個頁使用完了,如果還有新的 記錄插入的話,就需要去申請新的頁了,這個過程的圖示如下:
為了更好的管理在User Records中的這些記錄,InnoDB可費(fèi)了一番力氣呢,在哪費(fèi)力氣了呢?不就是把記錄按照指定的行格式一條一條擺在User Records部分么?其實(shí)這話還得從記錄行格式的記錄頭信 息中說起。
記錄頭信息的秘密
從圖中可以看到,我們特意把記錄頭信息的5個字節(jié)的數(shù)據(jù)給標(biāo)出來了,說明它很重要,我們再次先把這些記錄頭信息中各個屬性的大體意思瀏覽一下(我們目前使用Compact行格式進(jìn)行演示):
名稱 | 大小(單位:bit) | 描述 |
---|---|---|
預(yù)留位1 | 1 | 沒有使用 |
預(yù)留位2 | 1 | 沒有使用 |
delete_mask | 1 | 標(biāo)記該記錄是否被刪除 |
min_rec_mask | 1 | B+樹的每層非葉子節(jié)點(diǎn)中的最小記錄都會添加該標(biāo)記 |
n_owned | 4 | 表示當(dāng)前記錄擁有的記錄數(shù) |
heap_no | 13 | 表示當(dāng)前記錄在記錄堆的位置信息 |
record_type | 3 | 表示當(dāng)前記錄的類型,0 表示普通記錄,1 表示B+樹非葉節(jié)點(diǎn)記錄,2 表示最小記錄,3 表示最大記錄 |
next_record | 16 | 表示下一條記錄的相對位置 |
四、Page Directory(頁目錄)
現(xiàn)在我們了解了記錄在頁中按照主鍵值由小到大順序串聯(lián)成一個單鏈表,那如果我們想根據(jù)主鍵值查找頁中的某條記錄該咋辦呢?比如說這樣的查詢語句:
SELECT * FROM page_demo WHERE c1 = 3;
最笨的辦法:從Infimum記錄(最小記錄)開始,沿著鏈表一直往后找,總有一天會找到(或者找不到[攤手]),在找的時候還能投機(jī)取巧,因?yàn)殒湵碇懈鱾€記錄的值是按照從小到大順序排列的,所以當(dāng)鏈表的某個節(jié)點(diǎn)代表的記錄的主鍵值大于你想要查找的主鍵值時,你就可以停止查找了,因?yàn)樵摴?jié)點(diǎn)后邊的節(jié)點(diǎn)的主鍵值依次遞增。
這個方法在頁中存儲的記錄數(shù)量比較少的情況用起來也沒啥問題,比方說現(xiàn)在我們的表里只有4條自己插入的記錄,所以最多找4次就可以把所有記錄都遍歷一遍,但是如果一個頁中存儲了非常多的記 錄,這么查找對性能來說還是有損耗的,所以我們說這種遍歷查找這是一個笨辦法。但是設(shè)計InnoDB的大叔們是什么人,他們能用這么笨的辦法么,當(dāng)然是要設(shè)計一種更6的查找方式嘍,他們從書的目錄中找到了靈感。
我們平常想從一本書中查找某個內(nèi)容的時候,一般會先看目錄,找到需要查找的內(nèi)容對應(yīng)的書的頁碼,然后到對應(yīng)的頁碼查看內(nèi)容。設(shè)計InnoDB的大叔們?yōu)槲覀兊挠涗浺仓谱髁艘粋€類似的目錄,他們的制作過程是這樣的:
- 將所有正常的記錄(包括最大和最小記錄,不包括標(biāo)記為已刪除的記錄)劃分為幾個組。
- 每個組的最后一條記錄(也就是組內(nèi)最大的那條記錄)的頭信息中的n_owned屬性表示該記錄擁有多少條記錄,也就是該組內(nèi)共有幾條記錄。
- 將每個組的最后一條記錄的地址偏移量單獨(dú)提取出來按順序存儲到靠近頁的尾部的地方,這個地方就是所謂的Page Directory,也就是頁目錄(此時應(yīng)該返回頭看看頁面各個部分的圖)。頁面目錄 中的這些地址偏移量被稱為槽(英文名:Slot),所以這個頁面目錄就是由槽組成的。
比方說現(xiàn)在的page_demo表中正常的記錄共有6條,InnoDB會把它們分成兩組,第一組中只有一個最小記錄,第二組中是剩余的5條記錄,看下邊的示意圖:
- 現(xiàn)在
頁目錄
部分中有兩個槽,也就意味著我們的記錄被分成了兩個組,槽1
中的值是112
,代表最大記錄的地址偏移量(就是從頁面的0字節(jié)開始數(shù),數(shù)112個字節(jié));槽0
中的值是99
,代表最小記錄的地址偏移量。 - 注意最小和最大記錄的頭信息中的
n_owned
屬性- 最小記錄的
n_owned
值為1
,這就代表著以最小記錄結(jié)尾的這個分組中只有1
條記錄,也就是最小記錄本身。 - 最大記錄的
n_owned
值為5
,這就代表著以最大記錄結(jié)尾的這個分組中只有5
條記錄,包括最大記錄本身還有我們自己插入的4
條記錄。
- 最小記錄的
99
和112
這樣的地址偏移量很不直觀,我們用箭頭指向的方式替代數(shù)字,這樣更易于我們理解,所以修改后的示意圖就是這樣:
是的,設(shè)計InnoDB
的大叔們對每個分組中的記錄條數(shù)是有規(guī)定的:對于最小記錄所在的分組只能有 *1* 條記錄,最大記錄所在的分組擁有的記錄條數(shù)只能在 *1~8* 條之間,剩下的分組中記錄的條數(shù)范圍只能在是 *4~8* 條之間。所以分組是按照下邊的步驟進(jìn)行的:
- 初始情況下一個數(shù)據(jù)頁里只有最小記錄和最大記錄兩條記錄,它們分屬于兩個分組。
- 之后每插入一條記錄,都會從
頁目錄
中找到主鍵值比本記錄的主鍵值大并且差值最小的槽,然后把該槽對應(yīng)的記錄的n_owned
值加1,表示本組內(nèi)又添加了一條記錄,直到該組中的記錄數(shù)等于8個。 - 在一個組中的記錄數(shù)等于8個后再插入一條記錄時,會將組中的記錄拆分成兩個組,一個組中4條記錄,另一個5條記錄。這個過程會在
頁目錄
中新增一個槽
來記錄這個新增分組中最大的那條記錄的偏移量。
數(shù)據(jù)頁中查找指定主鍵值的記錄的過程
一個數(shù)據(jù)頁中查找指定主鍵值的記錄的過程分為兩步:
- 通過二分法確定該記錄所在的槽,并找到該槽中主鍵值最小的那條記錄。
- 通過記錄的
next_record
屬性遍歷該槽所在的組中的各個記錄。
五、Page Header(頁面頭部)
設(shè)計InnoDB的大叔們?yōu)榱四艿玫揭粋€數(shù)據(jù)頁中存儲的記錄的狀態(tài)信息,比如本頁中已經(jīng)存儲了多少條記錄,第一條記錄的地址是什么,頁目錄中存儲了多少個槽等等,特意在頁中定義了一個叫Page Header的部分,它是頁結(jié)構(gòu)的第二部分,這個部分占用固定的56個字節(jié),專門存儲各種狀態(tài)信息,具體各個字節(jié)都是干嘛的看下表:
名稱 | 占用空間大小 | 描述 |
---|---|---|
PAGE_N_DIR_SLOTS | 2 字節(jié) | 在頁目錄中的槽數(shù)量 |
PAGE_HEAP_TOP | 2 字節(jié) | 還未使用的空間最小地址,也就是說從該地址之后就是Free Space |
PAGE_N_HEAP | 2 字節(jié) | 本頁中的記錄的數(shù)量(包括最小和最大記錄以及標(biāo)記為刪除的記錄) |
PAGE_FREE | 2 字節(jié) | 第一個已經(jīng)標(biāo)記為刪除的記錄地址(各個已刪除的記錄通過next_record 也會組成一個單鏈表,這個單鏈表中的記錄可以被重新利用) |
PAGE_GARBAGE | 2 字節(jié) | 已刪除記錄占用的字節(jié)數(shù) |
PAGE_LAST_INSERT | 2 字節(jié) | 最后插入記錄的位置 |
PAGE_DIRECTION | 2 字節(jié) | 記錄插入的方向 |
PAGE_N_DIRECTION | 2 字節(jié) | 一個方向連續(xù)插入的記錄數(shù)量 |
PAGE_N_RECS | 2 字節(jié) | 該頁中記錄的數(shù)量(不包括最小和最大記錄以及被標(biāo)記為刪除的記錄) |
PAGE_MAX_TRX_ID | 8 字節(jié) | 修改當(dāng)前頁的最大事務(wù)ID,該值僅在二級索引中定義 |
PAGE_LEVEL | 2 字節(jié) | 當(dāng)前頁在B+樹中所處的層級 |
PAGE_INDEX_ID | 8 字節(jié) | 索引ID,表示當(dāng)前頁屬于哪個索引 |
PAGE_BTR_SEG_LEAF | 10 字節(jié) | B+樹葉子段的頭部信息,僅在B+樹的Root頁定義 |
PAGE_BTR_SEG_TOP | 10 字節(jié) | B+樹非葉子段的頭部信息,僅在B+樹的Root頁定義 |
在這里我們先嘮叨一下PAGE_DIRECTION
和PAGE_N_DIRECTION
的意思:
-
PAGE_DIRECTION
假如新插入的一條記錄的主鍵值比上一條記錄的主鍵值大,我們說這條記錄的插入方向是右邊,反之則是左邊。用來表示最后一條記錄插入方向的狀態(tài)就是
PAGE_DIRECTION
。 -
PAGE_N_DIRECTION
假設(shè)連續(xù)幾次插入新記錄的方向都是一致的,
InnoDB
會把沿著同一個方向插入記錄的條數(shù)記下來,這個條數(shù)就用PAGE_N_DIRECTION
這個狀態(tài)表示。當(dāng)然,如果最后一條記錄的插入方向改變了的話,這個狀態(tài)的值會被清零重新統(tǒng)計。
六、File Header(文件頭部)
上邊嘮叨的Page Header是專門針對數(shù)據(jù)頁記錄的各種狀態(tài)信息,比方說頁里頭有多少個記錄了呀,有多少個槽了呀。我們現(xiàn)在描述的File Header針對各種類型的頁都通用,也就是說不同類型的頁都會 以File Header作為第一個組成部分,它描述了一些針對各種頁都通用的一些信息,比方說這個頁的編號是多少,它的上一個頁、下一個頁是誰啦吧啦吧啦~ 這個部分占用固定的38個字節(jié),是由下邊這 些內(nèi)容組成的:
名稱 | 占用空間大小 | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 字節(jié) | 頁的校驗(yàn)和(checksum值) |
FIL_PAGE_OFFSET | 4 字節(jié) | 頁號 |
FIL_PAGE_PREV | 4 字節(jié) | 上一個頁的頁號 |
FIL_PAGE_NEXT | 4 字節(jié) | 下一個頁的頁號 |
FIL_PAGE_LSN | 8 字節(jié) | 頁面被最后修改時對應(yīng)的日志序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2 字節(jié) | 該頁的類型 |
FIL_PAGE_FILE_FLUSH_LSN | 8 字節(jié) | 僅在系統(tǒng)表空間的一個頁中定義,代表文件至少被刷新到了對應(yīng)的LSN值 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 字節(jié) | 頁屬于哪個表空間 |
對照著這個表格,我們看幾個目前比較重要的部分:
-
FIL_PAGE_SPACE_OR_CHKSUM
這個代表當(dāng)前頁面的校驗(yàn)和(checksum)。啥是個校驗(yàn)和?就是對于一個很長很長的字節(jié)串來說,我們會通過某種算法來計算一個比較短的值來代表這個很長的字節(jié)串,這個比較短的值就稱為
校驗(yàn)和
。這樣在比較兩個很長的字節(jié)串之前先比較這兩個長字節(jié)串的校驗(yàn)和,如果校驗(yàn)和都不一樣兩個長字節(jié)串肯定是不同的,所以省去了直接比較兩個比較長的字節(jié)串的時間損耗。 -
FIL_PAGE_OFFSET
每一個
頁
都有一個單獨(dú)的頁號,就跟你的身份證號碼一樣,InnoDB
通過頁號來可以唯一定位一個頁
。 -
FIL_PAGE_TYPE
這個代表當(dāng)前
頁
的類型,我們前邊說過,InnoDB
為了不同的目的而把頁分為不同的類型,我們上邊介紹的其實(shí)都是存儲記錄的數(shù)據(jù)頁
,其實(shí)還有很多別的類型的頁,具體如下表:類型名稱 十六進(jìn)制 描述 FIL_PAGE_TYPE_ALLOCATED
0x0000 最新分配,還沒使用 FIL_PAGE_UNDO_LOG
0x0002 Undo日志頁 FIL_PAGE_INODE
0x0003 段信息節(jié)點(diǎn) FIL_PAGE_IBUF_FREE_LIST
0x0004 Insert Buffer空閑列表 FIL_PAGE_IBUF_BITMAP
0x0005 Insert Buffer位圖 FIL_PAGE_TYPE_SYS
0x0006 系統(tǒng)頁 FIL_PAGE_TYPE_TRX_SYS
0x0007 事務(wù)系統(tǒng)數(shù)據(jù) FIL_PAGE_TYPE_FSP_HDR
0x0008 表空間頭部信息 FIL_PAGE_TYPE_XDES
0x0009 擴(kuò)展描述頁 FIL_PAGE_TYPE_BLOB
0x000A BLOB頁 FIL_PAGE_INDEX
0x45BF 索引頁,也就是我們所說的 數(shù)據(jù)頁
我們存放記錄的數(shù)據(jù)頁的類型其實(shí)是
FIL_PAGE_INDEX
,也就是所謂的索引頁
。 -
FIL_PAGE_PREV
和FIL_PAGE_NEXT
我們前邊強(qiáng)調(diào)過,
InnoDB
都是以頁為單位存放數(shù)據(jù)的,有時候我們存放某種類型的數(shù)據(jù)占用的空間非常大(比方說一張表中可以有成千上萬條記錄),InnoDB
可能不可以一次性為這么多數(shù)據(jù)分配一個非常大的存儲空間,如果分散到多個不連續(xù)的頁中存儲的話需要把這些頁關(guān)聯(lián)起來,FIL_PAGE_PREV
和FIL_PAGE_NEXT
就分別代表本頁的上一個和下一個頁的頁號。這樣通過建立一個雙向鏈表把許許多多的頁就都串聯(lián)起來了,而無需這些頁在物理上真正連著。需要注意的是,并不是所有類型的頁都有上一個和下一個頁的屬性,不過我們本集中嘮叨的數(shù)據(jù)頁
(也就是類型為FIL_PAGE_INDEX
的頁)是有這兩個屬性的,所以所有的數(shù)據(jù)頁其實(shí)是一個雙鏈表,就像這樣:
七、File Trailer(文件尾部)
我們知道InnoDB
存儲引擎會把數(shù)據(jù)存儲到磁盤上,但是磁盤速度太慢,需要以頁
為單位把數(shù)據(jù)加載到內(nèi)存中處理,如果該頁中的數(shù)據(jù)在內(nèi)存中被修改了,那么在修改后的某個時間需要把數(shù)據(jù)同步到磁盤中。但是在同步了一半的時候中斷電了咋辦,這不是莫名尷尬么?為了檢測一個頁是否完整(也就是在同步的時候有沒有發(fā)生只同步一半的尷尬情況),設(shè)計InnoDB
的大叔們在每個頁的尾部都加了一個File Trailer
部分,這個部分由8
個字節(jié)組成,可以分成2個小部分:
-
前4個字節(jié)代表頁的校驗(yàn)和
這個部分是和
File Header
中的校驗(yàn)和相對應(yīng)的。每當(dāng)一個頁面在內(nèi)存中修改了,在同步之前就要把它的校驗(yàn)和算出來,因?yàn)?code>File Header在頁面的前邊,所以校驗(yàn)和會被首先同步到磁盤,當(dāng)完全寫完時,校驗(yàn)和也會被寫到頁的尾部,如果完全同步成功,則頁的首部和尾部的校驗(yàn)和應(yīng)該是一致的。如果寫了一半兒斷電了,那么在File Header
中的校驗(yàn)和就代表著已經(jīng)修改過的頁,而在File Trialer
中的校驗(yàn)和代表著原先的頁,二者不同則意味著同步中間出了錯。 -
后4個字節(jié)代表頁面被最后修改時對應(yīng)的日志序列位置(LSN)
這個部分也是為了校驗(yàn)頁的完整性的,只不過我們目前還沒說
LSN
是個什么意思,所以大家可以先不用管這個屬性。
這個File Trailer
與File Header
類似,都是所有類型的頁通用的。
八、總結(jié)
- InnoDB為了不同的目的而設(shè)計了不同類型的頁,我們把用于存放記錄的頁叫做
數(shù)據(jù)頁
。 - 一個數(shù)據(jù)頁可以被大致劃分為7個部分,分別是
File Header
,表示頁的一些通用信息,占固定的38字節(jié)。Page Header
,表示數(shù)據(jù)頁專有的一些信息,占固定的56個字節(jié)。Infimum + Supremum
,兩個虛擬的偽記錄,分別表示頁中的最小和最大記錄,占固定的26
個字節(jié)。User Records
:真實(shí)存儲我們插入的記錄的部分,大小不固定。Free Space
:頁中尚未使用的部分,大小不確定。Page Directory
:頁中的某些記錄相對位置,也就是各個槽在頁面中的地址偏移量,大小不固定,插入的記錄越多,這個部分占用的空間越多。File Trailer
:用于檢驗(yàn)頁是否完整的部分,占用固定的8個字節(jié)。
- 每個記錄的頭信息中都有一個
next_record
屬性,從而使頁中的所有記錄串聯(lián)成一個單鏈表
。 InnoDB
會為把頁中的記錄劃分為若干個組,每個組的最后一個記錄的地址偏移量作為一個槽
,存放在Page Directory
中,所以在一個頁中根據(jù)主鍵查找記錄是非常快的,分為兩步:- 通過二分法確定該記錄所在的槽。
- 通過記錄的next_record屬性遍歷該槽所在的組中的各個記錄。
- 每個數(shù)據(jù)頁的
File Header
部分都有上一個和下一個頁的編號,所以所有的數(shù)據(jù)頁會組成一個雙鏈表
。 - 為保證從內(nèi)存中同步到磁盤的頁的完整性,在頁的首部和尾部都會存儲頁中數(shù)據(jù)的校驗(yàn)和和頁面最后修改時對應(yīng)的
LSN
值,如果首部和尾部的校驗(yàn)和和LSN
值校驗(yàn)不成功的話,就說明同步過程出現(xiàn)了問題。
參考
mysql是怎樣運(yùn)行的