MySQL Server 層和存儲引擎層是怎么交互數(shù)據(jù)的?
MySQL 存儲引擎是用插件方式實現(xiàn)的,所以在源碼里分為兩層:server 層、存儲引擎層。
server 層負責解析 SQL、選擇執(zhí)行計劃、條件過濾、排序、分組等各種邏輯。
存儲引擎層做的事情比較單一,負責寫數(shù)據(jù)、讀數(shù)據(jù)。寫數(shù)據(jù)就是把 MySQL 傳給存儲引擎的數(shù)據(jù)存到磁盤文件或者內(nèi)存中(對于 Memory 引擎是存儲到內(nèi)存),讀數(shù)據(jù)就是把數(shù)據(jù)從磁盤或者內(nèi)存讀出來返回給 server 層。
server 層和引擎層是相對獨立的兩個模塊,它們之間要配合完成工作,就會存在數(shù)據(jù)交互的過程,今天我們就以 server 層從存儲引擎層讀取數(shù)據(jù)來講講這個起著關鍵作用的數(shù)據(jù)交互過程。
1. 原理說明
在源碼里,數(shù)據(jù)庫中的每個表都會對應 TABLE 類的一個實例,實例中有個 record 屬性,record 屬性是一個有著 2 個元素的數(shù)組,server 層每次調(diào)用引擎層的方法讀取數(shù)據(jù)時,都會用table->record[0] 的形式把第 1 個元素的地址傳給引擎層。引擎層從磁盤或者內(nèi)存中讀取數(shù)據(jù)之后,把引擎層的數(shù)據(jù)格式轉(zhuǎn)換為 server 層的數(shù)據(jù)格式,然后寫入到這個地址對應的內(nèi)存空間里,server 層就可以拿這個數(shù)據(jù)來干各種事情了(比如:WHERE 條件篩選、分組、排序等)。
整個交互過程就是這么簡單,既然這么簡單,那還值得單獨寫篇文章來叨叨這個嗎?
當然是值得的,臺上一分鐘,臺下十年功這句話大家應該都耳熟能詳了,這個交互過程之所以這么簡單,是因為 server 層前期做了足夠的準備工作,才讓這個過程看起來像百度的搜索框那么簡單。
為了一探究竟,接下來就是我們往前追溯準備工作(也就是前戲階段)的時間了。
2. 前戲階段
創(chuàng)建表時,會計算出來每個字段在記錄(也就是我們常說的行)中的 Offset,以及一條記錄的最大長度(包含存儲變長字段的長度需要占用的字節(jié)數(shù))。
當我們第一次查詢某個表的時候,MySQL 會從 frm 文件中讀取字段、索引等信息,以及剛剛提到的字段 Offset 、一條記錄的最大長度。
接下來會根據(jù)記錄的最大長度,為第 1 小節(jié)中提到的 TABLE 類實例的 record 屬性申請內(nèi)存,record 數(shù)組的兩個元素 record[0]、record[1] 占用的字節(jié)數(shù)都等于記錄的最大長度。
在源碼里,每個字段都對應 Field 子類的一個實例,實例中有個 ptr 屬性,指向每個字段在 record[0] 中對應的內(nèi)存地址。對于變長字段,F(xiàn)ield 子類實例中還會存儲內(nèi)容長度占用的字節(jié)數(shù)。
存儲引擎從磁盤或者內(nèi)存中讀取一條記錄的某個字段后,會判斷字段的類型,如果是定長字段,把字段內(nèi)容經(jīng)過相應的格式轉(zhuǎn)換后寫入 ptr 指向的內(nèi)存空間。
如果是變長字段,先把內(nèi)容長度寫入 ptr 指向的內(nèi)存空間,然后緊挨著把字段內(nèi)容經(jīng)過相應的格式轉(zhuǎn)換后寫入內(nèi)容長度之后的內(nèi)存空間。
抽象的東西就寫到這里為止了,接下來會用一個實際的表為例子,并且通過一張圖來展示 record[0] 的內(nèi)存布局,以便有個直觀的了解。
3. 實例分析
這是示例表:
CREATE TABLE `t_recbuf` (
`id` int(10) unsigned NOT NULL AUTO_INCREMENT,
`i1` int(10) unsigned DEFAULT '0',
`str1` varchar(32) DEFAULT '',
`str2` varchar(255) DEFAULT '',
`c1` char(11) DEFAULT '',
`e1` enum('北京','上海','廣州','深圳') DEFAULT '北京',
`s1` set('吃','喝','玩','樂') DEFAULT '',
`bit1` bit(8) DEFAULT b'0',
`bit2` bit(17) DEFAULT b'0',
`blob1` blob,
`d1` decimal(10,2) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;
這是 record[0] 的內(nèi)存布局:
示例表和內(nèi)存布局圖都有了,接下來我們對照著圖來分析一下各個字段對應的內(nèi)存空間的情況:
字段 NULL 值標記區(qū)域
這個區(qū)域是標記一條具體的記錄中,定義表結(jié)構(gòu)時沒有指定 NOT NULL 的字段,實際的內(nèi)容是不是 NULL,如果是 NULL,在這個區(qū)域中對應的位置會設置為 1,如果不是 NULL,則在這個區(qū)域中對應的位置會設置為 0,每個字段的 NULL 標記占用 1 bit。
這個字段在 record[0] 的開頭處,所以它的 Offset = 0,由于示例表中,有 10 個字段都沒有指定 NOT NULL,所以總共需要 10 bit 來存儲 NULL 標記,共占用 2 字節(jié)。
存儲引擎讀取每個字段時,如果該字段在字段 NULL 值標記區(qū)域有一席之地,就會把它對應的位置設置個值(0 或者 1)。
id
id 字段的類型是 int,定長字段,占用 4 字節(jié),Offset = 字段 NULL 值標記區(qū)域占用字節(jié)數(shù) = 2,ptr 屬性指向 Offset 2。
存儲引擎讀取到 id 字段內(nèi)容,經(jīng)過大小端存儲模式轉(zhuǎn)換之后,把內(nèi)容寫入到 ptr 屬性指向的內(nèi)存空間。
由于 InnoDB 中,內(nèi)容是按大端模式存儲的(內(nèi)容高位在前,低位在后),而 server 層是按照小端模式讀取的,所以在寫入整數(shù)字段內(nèi)容到 record[0] 之前會進行大小端存儲模式的轉(zhuǎn)換。
i1
i1 字段的類型是 int,定長字段,占用 4 字節(jié),Offset = id Offset(2) + id 長度(4) = 6,ptr 屬性指向 Offset 6。
存儲引擎讀取到 i1 字段內(nèi)容,經(jīng)過大小端存儲模式轉(zhuǎn)換之后,把內(nèi)容寫入到 ptr 屬性指向的內(nèi)存空間。
str1
str1 字段的類型是 varchar,變長字段,Offset = i1 Offset(6) + i1 長度(4) = 10,ptr 屬性指向 Offset 10。
str1 字段定義時指定要存儲 32 個字符,表的字符集是 utf8,每個字符最多會占用 3 字節(jié),32 個字符最多會占用 96 字節(jié),96 < 255,只需要 1 字節(jié)就夠存儲 str1 內(nèi)容的長度了,所以 str1 len 區(qū)域占用 1 字節(jié)。
str1 字段內(nèi)容緊挨著 str1 len 之后,由于 str1 len 占用 1 字節(jié),所以 str1 內(nèi)容的 Offset = 10 + 1 = 11。
存儲引擎讀取 str1 字段的內(nèi)容時,也會讀取到 str1 的內(nèi)容長度,會先把內(nèi)容長度寫入 ptr 屬性指向的內(nèi)存空間,然后緊挨著寫入 str1 的內(nèi)容。
str2
str2 字段的類型也是 varchar,變長字段,Offset = str1 Offset(10) + str1 內(nèi)容長度占用字節(jié)數(shù)(1) + 內(nèi)容最大占用字節(jié)數(shù)(96) = 107,ptr 屬性指向 Offset 107。
str2 字段定義時指定要存儲 255 個字符,最多會占用 255 * 3 = 765 字節(jié),需要 2 字節(jié)才能存儲 str2 的內(nèi)容長度,所以 str2 len 區(qū)域占用 2 字節(jié)。
str2 字段內(nèi)容緊挨著 str2 len 之后存儲,由于 str2 len 占用 2 字節(jié),所以 str2 內(nèi)容的 Offset = 107 + 2 = 109。
存儲引擎讀取 str2 字段內(nèi)容后,會先把內(nèi)容長度寫入 ptr 屬性指向的內(nèi)存空間,然后緊挨著寫入 str2 的內(nèi)容。
c1
c1 字段的類型是 char,定長字段,Offset = str2 Offset(107) + str2 內(nèi)容長度占用字節(jié)數(shù)(2) + 內(nèi)容最大占用字節(jié)數(shù)(765) = 874,ptr 屬性指向 Offset 874。
c1 字段定義時指定要存儲 11 個字符,最多會占用 11 * 3 = 33 字節(jié)。
存儲引擎讀取 c1 字段內(nèi)容后,會把內(nèi)容寫入 ptr 屬性指向的內(nèi)存空間。如果 c1 字段的實際內(nèi)容長度比字段內(nèi)容最大字節(jié)數(shù)小,會挨著剛剛寫入的內(nèi)容,再寫入一定數(shù)量的空格。
比如:實際內(nèi)容長度為 11 字節(jié),而字段內(nèi)容最大字節(jié)數(shù)為 33,則會在實際內(nèi)容之后再寫入 22 個空格。
e1
e1 字段類型是 enum,定長字段,只有 4 個選項,占用 1 字節(jié),Offset = c1 Offset(874) + 內(nèi)容最大長度占用字節(jié)數(shù)(33) = 907。
enum 類型在存儲引擎中是用整數(shù)存儲的,存儲引擎讀取 e1 字段內(nèi)容后,會對內(nèi)容進行大小端轉(zhuǎn)換,把轉(zhuǎn)換后的內(nèi)容寫入 ptr 屬性指向的內(nèi)在空間。
s1
s1 字段類型是 set,定長字段,只有 4 個選項,占用 1 字節(jié),Offset = e1 Offset(907) + e1 長度(1) = 908。
set 類型在存儲引擎中也是按照整數(shù)存儲的,存儲引擎讀取 s1 字段內(nèi)容后,也需要對內(nèi)容進行大小端轉(zhuǎn)換,把轉(zhuǎn)換后的內(nèi)容寫入 ptr 屬性指向的內(nèi)存空間。
set 字段是用 enum 來實現(xiàn)的,最多占用 8 字節(jié),共 64 bit,每個選項用 1 bit 表示,所以 1 個 set 字段總共可以有 64 個選項。
enum、set 字段的需要長度說明一下,如果創(chuàng)建表時定義的選項數(shù)量不一樣,字段的長度也可能會不一樣(1 ~ 8 字節(jié)),但是字段長度在創(chuàng)建表時就已經(jīng)是確定的了,所以它們也是定長字段。
bit1
bit1 字段的類型是 bit,定長字段,創(chuàng)建表時定義的長度表示的是 bit,不是字節(jié)數(shù),Offset = s1 Offset(908) + s1 長度(1) = 909。
bit1 字段定義時指定的是 bit(8),表示該字段長度為 8 bit,也就是 1 字節(jié)。
bit 類型的字段在存儲引擎中是按 char 存儲的,存儲引擎讀取 bit1 字段的內(nèi)容后,把內(nèi)容寫入到 ptr 屬性指向的內(nèi)存空間。
這里的 char 是指的 C/C++ 里的 char,不是指的 MySQL 的 char 類型。
bit2
bit2 字段的類型也是 bit,定長字段,創(chuàng)建表時定義的是 bit(17),占用 3 字節(jié),Offset = bit1 Offset(909) + bit1 長度(1) = 910。
bit 類型的字段,如果創(chuàng)建表時指定的 bit 數(shù)不是 8 的整數(shù)倍,存儲引擎在插入數(shù)據(jù)到磁盤或者內(nèi)存時,就會在前面補充 0,比如 bit(17),占用 3 字節(jié),內(nèi)容為 00010000010010011 時,會在前面再補充 7 個 0 變成 000000000010000010010011,讀出來的時候也還是這樣的內(nèi)容。
之所以定義 2 個 bit 字段,是為了測試 bit 類型的字段,定義的 bit 位數(shù)不是 8 的整數(shù)倍時,是不是會把多出來的那些 bit 存儲到 字段值 NULL 標記區(qū)域中,后來發(fā)現(xiàn),只有 MyISAM、NDB 存儲引擎才會這樣處理,InnoDB 中 bit 字段是按 char 存儲的,bit 位數(shù)不是 8 的整數(shù)倍時,多出來的 bit 還需要占用 1 字節(jié),比如:bit(17) 需要占用 3 字節(jié)。
blob1 len
blob1 字段的類型是 blob,變長字段,Offset = bit2 Offset(910) + bit2 長度(3) = 913。
blob 類型的字段,最多可以存儲 2 ^ 16 = 65536 字節(jié) = 64K。
存儲引擎讀取 blob1 字段內(nèi)容之后,會分配一塊能夠容納 blob1 字段內(nèi)容的內(nèi)存空間,把讀取出來的內(nèi)容寫入該內(nèi)存空間中。然后把 blob1 字段的內(nèi)容長度 寫入 ptr 屬性指向的內(nèi)存空間處,占用 2 字節(jié),然后緊挨著寫入剛剛分配的那塊內(nèi)存空間的首地址,占用 8 字節(jié)。
注意:只是把 blob1 字段的內(nèi)容首地址,而不是 blob1 字段的完整內(nèi)容寫入 record[0]。
示例中只使用了 blob 類型的字段,實際 blob 類型分為 4 種:tinyblob、blob、mediumblob、longblob,這 4 種類型的內(nèi)容長度分別占用 1 ~ 4 字節(jié)。
另外,還需要說明的一點是:tinytext、text、mediumtext、longtext 也是用上面相應的 blob 類型實現(xiàn)的,json 類型是用 longblob 類型實現(xiàn)的。
d1
d1 字段的類型是 decimial,定長字段,Offset = blob1 Offset(913) + blob1 長度占用字節(jié)數(shù)(2) + blob1 內(nèi)容首地址占用字節(jié)數(shù)(8) = 923。
decimal 類型的字段,在存儲引擎中是用二進制存儲的,在創(chuàng)建表的時候,就計算出來了需要用幾字節(jié)來存儲。
存儲引擎讀取 d1 字段的內(nèi)容之后,把內(nèi)容寫入 ptr 屬性指向的內(nèi)存空間。
本文轉(zhuǎn)載自微信公眾號「一樹一溪」,可以通過以下二維碼關注。轉(zhuǎn)載本文請聯(lián)系一樹一溪公眾號。