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