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