大綱
1.為什麼不能直接更新磁碟上的資料
2.為什麼要引入資料頁的概念
3.一行資料在磁碟上是如何儲存的
4.一行資料中的NULL值是如何處理的
5.一行資料的資料頭儲存的是什麼
6.一行資料的真實資料如何儲存
7.資料在物理儲存時的行溢位和溢位頁
8.資料頁的物理儲存結構
9.表空間的物理儲存結構
10.InnoDB儲存模型及讀寫機制總結
前面介紹了MySQL的資料快取機制和記憶體資料更新機制,接下來介紹MySQL的表空間、資料區、資料頁等磁碟上的物理檔案
1.為什麼不能直接更新磁碟上的資料
為何MySQL要設計一套複雜的資料存取機制,即基於記憶體、日誌、磁碟上的資料檔案來完成資料讀寫?對於增改請求為何不直接更新磁碟檔案?因為來一個請求就直接對磁碟檔案進行隨機讀寫,然後更新磁碟檔案裡的資料,這會導致執行請求的效能極差。
我們知道磁碟的隨機讀寫效能是很差的,所以直接更新磁碟檔案必然導致資料庫完全無法抗下稍微高併發場景。於是MySQL才設計了一套資料快取機制和記憶體資料更新機制。首先透過記憶體更新資料,然後寫redo日誌以及提交事務,最後透過後臺執行緒不定時重新整理記憶體裡的資料到磁碟檔案。透過這種方式可保證每個更新請求都更新記憶體,然後順序寫日誌檔案。
由於更新記憶體的效能是極高的,而且往磁碟檔案順序寫日誌的效能也比較高(順序寫的效能遠高於隨機寫),所以這套機制可讓MySQL在16核32G的機器上抗下每秒幾千的讀寫請求。
2.為什麼要引入資料頁的概念
當MySQL要更新資料時,並不是直接去更新磁碟檔案的。而是首先把磁碟上的一些資料載入到記憶體裡,然後對記憶體的資料進行更新,同時寫redo日誌。
但MySQL並非每次都把磁碟裡的一條資料載入到記憶體裡去進行更新。然後下次要更新別的資料時,再從磁碟裡載入另外一條資料到記憶體裡。因為這樣每次一條一條的資料載入到記憶體進行更新,效率就太低了。
所以InnoDB儲存引擎引入了資料頁的概念。具體就是把資料組織成一頁一頁的結構,然後每一頁有16KB。這樣每次載入磁碟的資料到記憶體時,至少載入一頁資料,甚至會透過預讀機制載入多頁資料。
假設要更新一條id=1的資料:
update xxx set xxx=xxx where id = 1;
那麼此時MySQL會把id=1這條資料所在的一頁資料都載入到記憶體裡,這一頁資料可能還包含了id=2等其他資料。然後當MySQL更新完id=1的資料後,如果接著需要更新id=2的資料時,就不用再次讀取磁碟裡的資料了。
這就是資料頁的意義。磁碟和記憶體之間的資料交換透過資料頁來執行,包括記憶體裡更新後的髒資料被刷回磁碟時,也是以資料頁為單位進行。
3.一行資料在磁碟上是如何儲存的
(1)行格式
(2)變長欄位在磁碟中是怎麼儲存的
(3)引入變長欄位列表後,如何讀取變長欄位
(4)如果有多個變長欄位,如何存放它們的長度
(1)行格式
我們建立表的時候可以指定表的行使用什麼樣的儲存格式。比如下面的語句就指定使用compact格式進行儲存。可以在建表的時候指定一個行儲存的格式,也可以後續修改行儲存的格式。
create table table_name (columns) row_format=compact
alter table table_nale row_format=compact
在compact行儲存格式下,每一行實際儲存時,格式如下。除了每個欄位的值以外,還包含一些額外的資訊。這些額外的資訊就是用來描述這一行資料的。
變長欄位長度列表,null值列表,資料頭,column01的值,column02的值...column0n的值...
(2)變長欄位在磁碟中是怎麼儲存的
假設有兩行資料,它的幾個欄位型別為varchar(10),char(1),char(1)。第一個欄位是varchar(10),其長度是可變的。第一行資料可能類似"hello a a",第一個欄位是hello,後兩個欄位是a。第二行資料可能類似"hi a a",第一個欄位是hi,後兩個欄位也是a。
這時如果要把這兩行資料寫入磁碟檔案,且要求這兩行資料要挨在一起。那麼磁碟檔案裡可能就有類似這樣的資料:"hello a a hi a a"。其實我們平時看到表裡的多行資料,落地到磁碟時,都是按上面的方式緊挨著儲存的。
現在問題來了,假設要從磁碟檔案讀取上面形式的資料。也就是把"hello a a"這一行資料讀取出來,那就比較困難了。因為沒有標記也沒有分隔符。所以MySQL引入了變長欄位的長度列表,來解決標記和分隔符的問題。
也就是說,在儲存"hello a a"這行資料時,要帶上一些額外的附加資訊。比如第一個就是這一行資料裡的變長欄位的長度列表。由於只有這個"hello"是varchar(10)型別的變長欄位的值,而且它的長度是5,對應的十六進位制是0x05,所以這個0x05就是這一行資料對應的變長欄位的長度列表。
因此,需要在"hello a a"前面補充如下的額外資訊:"0x05 null值列表 資料頭 hello a a"。於是上面挨著的兩行資料放在一起儲存在磁碟檔案裡的格式就類似為:"0x05 null值列表 資料頭 hello a a 0x02 null值列表 資料頭 hi a a"。
(3)引入變長欄位列表後,如何讀取變長欄位
現在假設要讀取"hello a a"這行資料。
一.首先要確定表的欄位型別
MySQL可知表裡的三個欄位的型別是varchar(10) char(1) char(1)。
二.然後讀取第一個欄位的值
由於第一個欄位是變長的,所以從磁碟讀出資料時,會從開頭的變長欄位的長度列表讀到一個0x05的十六進位制的數字。由此可知第一個變長欄位的長度是5,於是便可以按照長度5去讀取出第一個欄位的值"hello"。
三.接著讀取後面兩個欄位的值
由於後續兩個欄位都是char(1),長度都是固定的1個字元。於是就依次按照長度為1讀取出後續兩個欄位的值,分別是"a"和"a"。這樣就把該行資料"hello a a"讀取出來了。
如果要讀取第二行資料,也是先看一下第二行資料的變長欄位列表。發現第一個變長欄位的長度是0x02,於是讀取長度為2的欄位值,即"hi"。然後再讀取兩個長度固定為1的字元值,都是"a"。這樣也把該行資料"hi a a"讀出來了。
(4)如果有多個變長欄位,如何存放它們的長度
比如一行資料有5個欄位:varchar(10) varchar(5) varchar(20) char(1) char(1),其中3個是變長欄位。假設這一行資料是這樣的:"hello hi hao a a"。那麼這一行資料會如何在磁碟中儲存?
首先會在資料開頭的變長欄位長度列表中儲存幾個變長欄位的長度。需要注意的是,變長欄位的長度是逆序儲存的。也就是先存放varchar(20)欄位的長度,然後再存放varchar(5)欄位的長度,最後才存放varchar(10)欄位的長度。所以這一行資料實際儲存在磁碟檔案是長這樣的:"0x03 0x02 0x03 null值列表 頭欄位 hello hi hao a a"。
4.一行資料中的NULL值如何處理
(1)為什麼一行資料裡的NULL值不能直接儲存
(2)NULL值是以二進位制bit位來儲存的
(3)磁碟上的一行資料會怎麼讀出來
(1)為什麼一行資料裡的NULL值不能直接儲存
磁碟上儲存的每一行資料裡除了有變長欄位的長度列表外,還有另外一塊特殊的資料區域,就是NULL值列表。
一行資料中有些欄位值是NULL,NULL表示的是什麼值都沒有。如果在磁碟上儲存時按"NULL"字串儲存,這樣就太浪費空間了。
(2)NULL值是以二進位制bit位來儲存的
NULL值在磁碟上並不是透過字串來儲存的,而是透過bit位來儲存的。具體來說,假設一行資料裡有多個值是NULL。那麼這多個NULL值,會以bit位的形式存放在NULL值列表中。
舉個例子,有一張表如下:有5個欄位,其中4個變長欄位、1個定長欄位,name欄位不能為NULL。
create table customer (
name varchar(10) not null,
address varchar(20),
gender char(1),
job varchar(30),
school varchar(50)
) row_format=compact;
在customer表裡有一行資料:"jack NULL m NULL xx_school"。現在已經知道一行資料在磁碟上的compact儲存格式為:
變長欄位的長度列表 null值列表 資料頭 column01的值 column02的值 ... column0n的值
一.先看變長欄位長度列表應該存放的內容
由於多個變長欄位按照逆序存放,所以會先放school欄位的長度,再放job欄位、address欄位、name欄位的長度。
二.如果某變長欄位的值是NULL
那麼就不用在變長欄位長度列表裡存放這個變長欄位的值長度。所以只有name和school兩個變長欄位有值。即需要把name和school的長度按照逆序放在變長欄位長度列表中:
0x09 0x04 NULL值列表 資料頭 column1=value1 column2=value2 ... columnN=valueN
三.對於所有允許值為NULL的欄位,每個欄位都有一個二進位制bit位的值
如果bit值是1,則說明該欄位是NULL;如果bit值是0,則說明該欄位不是NULL。
四.上面的表4個欄位都允許為NULL,所以每個欄位都有一個bit位
對於資料"jack NULL m NULL xx_school"來說,4個bit位應該就是1010。
五.存放NULL值列表和存放變長欄位的長度列表一樣,也按逆序存放
所以4個bit位是0101,最後這一行資料是:
0x09 0x04 0101 資料頭 column1=value1 column2=value2 ... columnN=valueN
六.實際存放NULL值列表時按照8個bit位的整數倍來存放
不會按4個bit位來存放,如果不足8個bit位則進行高位補0。所以最後的資料是:
0x09 0x04 00000101 資料頭 column1=value1 column2=value2 ... columnN=valueN
(3)磁碟上的一行資料會怎麼讀出來
對於讀取磁碟上儲存的資料:
0x09 0x04 00000101 資料頭 column1=value1 column2=value2 ... columnN=valueN
首先要把變長欄位長度列表和NULL值列表讀出來。透過分析可知有多少個變長欄位,哪些欄位是NULL。如果NULL值列表的某個bit位是1,則說明其對應的欄位值是NULL。對於變長欄位的值,就按變長欄位長度列表來獲得長度再根據長度去讀取。對於固定長度欄位的值,則直接按固定長度進行讀取。
5.一行資料的資料頭儲存的是什麼
在磁碟上儲存資料時:
一.每一行資料都會有變長欄位的長度列表
變長欄位的長度列表,會透過逆序存放這一行資料裡的變長欄位的長度。
二.每一行資料都可能會有NULL值列表
允許為NULL的欄位會有一個bit位標識該欄位是否為NULL且逆序存放。
三.每一行資料還會有40個bit位的資料頭
這個資料頭是用來描述這行資料的
這40個bit位裡面:
第一個和第二個bit位,都是預留位,沒有含義;
第三個bit位是delete_mask:標識這行資料是否被刪除。刪除一行資料時不會馬上把資料從磁碟上清理,而會在資料頭進行標記;
第四個bit位是min_rec_mask:標記B+樹裡每一層的非葉子節點裡的最小值;
接著有4個bit位的n_owned:這是一個記錄數;
接著有13個bit位的heap_no:代表當前這行資料在記錄堆裡的記錄;
接著有3個bit位的record_type:指的是這行資料的型別。0代表普通型別,1代表B+樹非葉子節點,2代表最小值,3代表最大值;
最後有16個bit位的next_record:指向該行資料下一條資料的指標;
6.一行資料的真實資料如何儲存
現已知一行資料在磁碟檔案中儲存時:首先會包含自己的變長欄位的長度列表,然後是NULL值列表,接著是資料頭,最後才是真實資料。
那麼儲存真實資料時,是怎麼處理的呢?比如一行資料是"jack NULL m NULL xx_school"。那麼它的真實儲存大概如下:
0x09 0x04 00000101 00000000000000000000100000000000000011001 jack m xx_school
一開始是變長欄位的長度,使用了十六進位制來儲存。然後是NULL值列表,指出了允許NULL的欄位誰是NULL。接著是40個bit位的資料頭,最後是真實資料值放在後面。
讀取時先讀取第一個欄位。根據變長欄位的長度列表知道長度是4,先讀取出jack值。然後發現第二個欄位是NULL,不用讀取。接著第三個欄位是定長欄位,直接讀取1個字元即可,也就是m值。接著第四個欄位是NULL,也不用讀取。第五個欄位是變長欄位。根據變長欄位的長度列表知道長度是9,讀取出xx_school。
但在磁碟上儲存時,真實資料並不是以字串的形式儲存在磁碟上的。而是根據資料庫指定的字符集編碼,對字串進行編碼之後再儲存。所以一行資料最終會如下所示進行儲存:
0x09 0x04 00000101 00000000000000000000100000000000000011001 616161 636320 62626262
此外在實際儲存一行資料時,還會在真實資料部分,加入一些隱藏欄位。
一.首先有一個DB_ROW_ID欄位
這是該行的唯一標識,是資料庫針對該行設定的一個標識,不是主鍵ID。當沒有給表指定主鍵和唯一索引時,該表會自動加一個ROW_ID作為主鍵。
二.接著有一個DB_TRX_ID欄位
這是事務ID,代表著這是哪個事務更新的資料。
三.最後有個DB_ROLL_PTR欄位
這是回滾指標,用來進行事務回滾。
所以加上隱藏欄位後,一行資料可能看起來如下:
0x09 0x04 00000101 00000000000000000000100000000000000011001 00000000094C (DB_ROW_ID) 000000000032D (DB_TRX_ID) EA0000010078E (DB_ROL_PTR) 616161 636320 62626262
7.資料在物理儲存時的行溢位和溢位頁
每一行的資料都是放在一個資料頁裡的,這個資料頁預設的大小是16KB。但如果一行資料的大小超過了資料頁的大小時怎麼辦?
比如有一個表的欄位型別是varchar(65532),意思就是最大可以包含65532個字元(65532位元組),遠大於16KB。
這時MySQL是這樣處理的:首先會在一個資料頁裡儘量儲存這一行所有欄位資料,然後在特別長的欄位中會僅僅包含一部分資料,同時包含一個20位元組的指標指向其他資料頁,那些資料頁會用連結串列串聯起來,存放這個特別長的欄位的資料。
行溢位指的是:一行資料儲存的內容太多,一個資料頁放不下時,只能溢位這個資料頁。把資料存放到其他資料頁裡,那些其他資料頁就叫做溢位頁。除了varchar(65532)這種欄位,其他如text、blob欄位,都可能出現溢位,然後一行資料會儲存在多個資料頁裡。
總結:當往資料庫插入一行資料時,實際上是在記憶體裡插入一個有複雜儲存結構的一行資料。然後隨著一些條件的發生,這行資料會被刷到磁碟檔案裡去。在磁碟檔案裡儲存時,這行資料會按照這個複雜的儲存結構去存放。每一行的資料都放在資料頁裡。如果一行資料太大,則會產生行溢位,導致一行資料溢位到多個資料頁裡。那麼這行資料在Buffer Pool可能就是存在於多個快取頁裡的,刷入磁碟時也會用磁碟的多個資料頁來存放這行資料。
8.資料頁的物理儲存結構
在MySQL中進行資料操作的最小單位是資料頁,預設大小是16KB。當一行的資料量比16KB大時,會發生行溢位使用其他資料頁存放。存放該行資料的資料頁會使用一個20位元組大小的指標指向其他溢位頁。
存放一行的資料頁會被拆分成多個部分:包括檔案頭、資料頁頭、最小記錄和最大記錄、多個資料行、空閒空間、資料頁目錄、檔案尾部。下圖所示包含了一個資料頁的各個部分:
檔案頭會佔38個位元組;
資料頁頭會佔56個位元組;
最大記錄和最小記錄會佔26個位元組;
資料行區域大小是不固定的;
空閒區域的大小也是不固定的;
資料頁目錄的大小也是不固定的;
檔案尾部會佔8個位元組;
一開始資料庫初始化完後,資料頁是空的,沒有一行資料,對應於"多個資料行"的區域是空的。接著如果要插入一行資料,由於資料頁和快取頁是對應的,所以會往一個空閒快取頁的多個資料行進行資料寫入,並更新空閒區域。
9.表空間的物理儲存結構
(1)什麼是表空間
簡單來說,我們平時建立的那些表,其實都對應一個表空間的。表空間在磁碟上都會對應著"表名.ibd"這樣的一個磁碟資料檔案。
在物理層面,表空間就是對應一些磁碟上的資料檔案。系統的表空間可能會對應多個磁碟檔案。我們建立的表對應的表空間,通常對應一個"表名.ibd"的資料檔案。
在表空間的磁碟檔案裡,會有很多很多的資料頁。一個資料頁只有16KB,為了便於管理這麼多資料頁,表空間引入了資料區(extent)。一個資料區對應著連續的64個資料頁。每個資料頁16KB,所以一個資料區是1MB。一組資料區也就是256個資料區會被劃分為一個組,所以一組資料區有256MB。
表空間的第一組資料區的第一個資料區的前3個資料頁,都是固定的,裡面存放了一些描述性的資料。比如FSP_HDR資料頁,存放了表空間和這一組資料區的一些屬性;比如IBUF_BITMAP資料頁,存放了這一組資料頁的所有insert buffer資訊;比如INODE資料頁,裡面存放了一些特殊資訊。
然後表空間的其他各組資料區的第一個資料區的頭兩個資料頁,也都是固定的,裡面存放了一些特殊資訊:比如XDES資料頁就是用來存放這一組資料區的一些相關屬性的。
(2)表空間總結
平時建立的表都是有對應的表空間,每個表空間對應磁碟上的資料檔案。表空間裡有很多資料區組,256個資料區分成一組。每個資料區包又有64個資料頁,所以每個資料區的大小是1MB。
表空間的第一組資料區的第一個資料區的頭三個資料頁,存放特殊資訊。表空間的其他組資料區的第一個資料區的頭兩個資料頁,也存放特殊資訊。
當InnoDB執行增刪改查操作時,會從磁碟上的表空間的資料檔案裡,載入資料頁到Buffer Pool的快取頁裡。
下圖展示了一個表空間內部的儲存結構:一個表空間內部會包含一組一組的資料區,每一組資料區包含256個資料區,每個資料區又包含64個資料頁。
10.InnoDB儲存模型及讀寫機制總結
在邏輯層面上,InnoDB的資料是插入一個一個的表中。在物理層面上,InnoDB的資料是插入一個一個的表空間。
表空間對應著磁碟檔案,磁碟檔案裡存放的就是資料。在磁碟檔案存放資料時,會被拆分為一個一個的資料區組。每個資料區組包含256個資料區,每個資料區包含64個資料頁。每個資料頁包含一行一行的資料。
資料頁又包含了:檔案頭、資料頁頭、最小記錄和最大記錄、多個資料行、空閒空間、資料頁目錄、檔案尾部。
每個資料行又附加了真實資料外的很多資訊:變長欄位的長度列表、null值列表、資料頭、真實資料和隱藏欄位。
透過資料頁、資料區、資料行附加的特殊資訊,可以讓InnoDB在磁碟檔案裡實現B+索引、事務等複雜的機制。
當資料庫執行增刪改查時:必須把磁碟檔案裡的一個資料頁載入到記憶體Buffer Pool中的快取頁裡,然後增刪改查都針對快取頁裡的資料進行。
所以要讀寫資料時:會根據表找到一個表空間,透過表空間就可以找到對應的磁碟檔案。透過磁碟檔案就可以從裡面找一個資料區組中的一個資料區。然後從該資料區中找一個資料頁出來。最後就可以把這個資料頁從磁碟載入到Buffer Pool快取頁裡。