# MySQL server 層和儲存引擎層是怎麼互動資料的?

ITPUB社群 發表於 2022-11-29
MySQL

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(10unsigned NOT NULL AUTO_INCREMENT,
  `i1` int(10unsigned DEFAULT '0',
  `str1` varchar(32DEFAULT '',
  `str2` varchar(255DEFAULT '',
  `c1` char(11DEFAULT '',
  `e1` enum('北京','上海','廣州','深圳'DEFAULT '北京',
  `s1` set('吃','喝','玩','樂'DEFAULT '',
  `bit1` bit(8DEFAULT b'0',
  `bit2` bit(17DEFAULT b'0',
  `blob1` blob,
  `d1` decimal(10,2DEFAULT NULL,
  PRIMARY KEY (`id`)
ENGINE=InnoDB AUTO_INCREMENT=3 DEFAULT CHARSET=utf8;

這是 record[0] 的記憶體佈局:

# MySQL server 層和儲存引擎層是怎麼互動資料的?

示例表和記憶體佈局圖都有了,接下來我們對照著圖來分析一下各個欄位對應的記憶體空間的情況:

欄位 NULL 值標記區域

這個區域是標記一條具體的記錄中,定義表結構時沒有指定 NOT NULL 的欄位,實際的內容是不是 NULL,如果是 NULL,在這個區域中對應的位置會設定為 1,如果不是 NULL,則在這個區域中對應的位置會設定為 0,每個欄位的 NULL 標記佔用 1 bit

這個欄位在 record[0] 的開頭處,所以它的 Offset = 0,由於示例表中,有 10 個欄位都沒有指定 NOT NULL,所以總共需要 10 bit 來儲存 NULL 標記,共佔用 2 位元組

儲存引擎讀取每個欄位時,如果該欄位在欄位 NULL 值標記區域有一席之地,就會把它對應的位置設定個值(0 或者 1)。

id

id 欄位的型別是 int,定長欄位,佔用 4 位元組,Offset = 欄位 NULL 值標記區域佔用位元組數 = 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 變成 000000000010000010010011,讀出來的時候也還是這樣的內容。

之所以定義 2 個 bit 欄位,是為了測試 bit 型別的欄位,定義的 bit 位數不是 8 的整數倍時,是不是會把多出來的那些 bit 儲存到 欄位值 NULL 標記區域中,後來發現,只有 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

d1 欄位的型別是 decimial,定長欄位,Offset = blob1 Offset(913) + blob1 長度佔用位元組數(2) + blob1 內容首地址佔用位元組數(8) = 923。

decimal 型別的欄位,在儲存引擎中是用二進位制儲存的,在建立表的時候,就計算出來了需要用幾位元組來儲存。

儲存引擎讀取 d1 欄位的內容之後,把內容寫入 ptr 屬性指向的記憶體空間。

來自 “ ITPUB部落格 ” ,連結:http://blog.itpub.net/70024420/viewspace-2925718/,如需轉載,請註明出處,否則將追究法律責任。