MySQL 服務器上負責對錶中數據的讀取和寫入工做的部分是存儲引擎,好比 InnoDB、MyISAM、Memory 等等,不一樣的存儲引擎通常是由不一樣的人爲實現不一樣的特性而開發的,目前OLTP業務的表若是是使用 MySQL 通常都會使用 InnoDB 引擎,這也是默認的表引擎。mysql
爲了能說明 InnoDB 引擎的原理,咱們必須先搞清楚 InnoDB 的存儲結構,經過這些存儲結構才能實現 InnoDB 的事務特性。sql
首先咱們來看看 InnoDB 表的一行數據是如何存儲的。InnoDB是一個持久化的存儲引擎,也就是數據都是保存在磁盤上面的。可是讀寫數據,對數據處理,這些是發生在內存中。也就是數據須要從磁盤讀取到內存。那麼這個讀取是如何讀取呢?若是處理哪條數據,就讀取哪一條到內存中,這樣效率也過低了。由於每條數據都是一個硬盤尋址讀取,咱們要減小這個硬盤尋址讀取的次數,能夠考慮一塊一塊的讀取數據,這樣,咱們極可能下次請求須要的數據就已經在內存中了,就省去了從硬盤讀取。基於這個思想,InnoDB 將一個表的數據劃分紅了若干頁(pages),這些頁經過 B-Tree 索引聯繫起來。每一頁大小默認爲 16384 Bytes 也就是 16KB(配置爲 innodb_page_size
)。json
同時,這個 B-Tree 索引就是咱們常常聽到的聚簇索引(Clustered Index),若是表有主鍵,那麼主鍵索引就是這個聚簇索引。經過上面的描述,這個索引的節點是包含全部行全部列數據的(就是剛剛咱們提到的頁)。其餘的二級索引的節點只是有指向主鍵的指針。bash
對於比較大的字段,例如 Text 類型的字段,若是也存在於這個聚簇索引上,那這個節點數據就會過大,會一會兒讀取不少頁出來,這樣讀取效率會下降(例如在咱們沒有想讀取這個 Text 列的請求狀況下)。因此,InnoDB 對於變長字段,通常傾向於將他們存儲在其餘地方。至於怎麼存儲,這個還和 InnoDB **行格式(InnoDB Row Format)**有關。行格式一共有四種:Compact、Redundant、Dynamic和Compressed。服務器
咱們能夠在建立或修改表的語句中指定行格式:編輯器
CREATE TABLE 表 (
)ROW_FORMAT=行格式;
ALTER TABLE 表 ROW_FORMAT=行格式;
複製代碼
咱們來建立一個包含幾乎全部基本數據類型的表,其餘的例如 geometry,timestamp 等等,也是基於 double 還有 bigint 而來的, text、json、blob等類型,通常不與行數據一塊兒存儲,咱們以後再說:ide
create table record_test_1 (
id bigint,
score double,
name char(4),
content varchar(8),
extra varchar(16)
)row_format=compact;
複製代碼
插入以下幾條記錄:測試
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (1, 78.5, 'hash', 'wodetian', 'nidetiantadetian');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (65536, 17983.9812, 'zhx', 'shin', 'nosuke');
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (NULL, -669.996, 'aa', NULL, NULL);
INSERT INTO `record_test_1`(`id`, `score`, `name`, `content`, `extra`) VALUES (2048, NULL, NULL, 'c', 'jun');
複製代碼
目前表結構:ui
+-------+------------+------+----------+------------------+
| id | score | name | content | extra |
+-------+------------+------+----------+------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian |
| 65536 | 17983.9812 | zhx | shin | nosuke |
| NULL | -669.996 | aa | NULL | NULL |
| 2048 | NULL | NULL | c | jun |
+-------+------------+------+----------+------------------+
複製代碼
查看底層存儲文件:record_test_1.ibd
,用16進制編輯器打開,我這裏使用的是Notepad++
和他的HEX-Editor
插件。能夠找到以下的數據域(可能會有其中 mysql 生成的行數據不同,可是咱們建立的行數據內容應該是同樣的,並且數據長度應該是一摸同樣的,能夠搜索其中的字符找到這些數據):編碼
咱們這裏先直接給出這些數據表明的意義,讓你們直觀感覺下:
變長字段長度列表:10 08
Null值列表:00
記錄頭信息:00 00 10 00 47
隱藏列DB_ROW_ID:00 00 00 00 08 0c
隱藏列DB_TRX_ID:00 00 00 03 c9 4d
隱藏列DB_ROLL_PTR:b9 00 00 01 2d 01 10
列數據id(1):80 00 00 00 00 00 00 01
列數據score(78.5):00 00 00 00 00 a0 53 40
列數據name(hash):68 61 73 68
列數據content(wodetian):77 6f 64 65 74 69 61 6e
列數據extra(nidetiantadetian):6e 69 64 65 74 69 61 6e 74 61 64 65 74 69 61 6e
變長字段長度列表:06 04
Null值列表:00
記錄頭信息:00 00 18 00 37
隱藏列DB_ROW_ID:00 00 00 00 08 0d
隱藏列DB_TRX_ID:00 00 00 03 c9 4e
隱藏列DB_ROLL_PTR:ba 00 00 01 2f 01 10
列數據id(65536):80 00 00 00 00 01 00 00
列數據score(17983.9812):b5 15 fb cb fe 8f d1 40
列數據name(zhx):7a 68 78 20
列數據content(shin):73 68 69 6e
列數據extra(nosuke):6e 6f 73 75 6b 65
Null值列表:19
記錄頭信息:00 00 00 00 27
隱藏列DB_ROW_ID:00 00 00 00 08 0e
隱藏列DB_TRX_ID:00 00 00 03 c9 51
隱藏列DB_ROLL_PTR:bc 00 00 01 33 01 10
列數據score(-669.996):87 16 d9 ce f7 ef 84 c0
列數據name(aa):61 61 20 20
變長字段長度列表:03 01
Null值列表:06
記錄頭信息:00 00 28 ff 4b
隱藏列DB_ROW_ID:00 00 00 00 08 0f
隱藏列DB_TRX_ID:00 00 00 03 c9 54
隱藏列DB_ROLL_PTR:be 00 00 01 3d 01 10
列數據id(2048):80 00 00 00 00 00 08 00
列數據content(c):63
列數據extra(jun):6a 75 6e
複製代碼
能夠看出,在 Compact 行記錄格式下,一條 InnoDB 記錄,其結構以下圖所示:
對於像 varchar, varbinary,text,blob,json以及他們的各類類型的可變長度字段,須要將他們到底佔用多少字節存儲起來,這樣就省去了列數據之間的邊界定義,MySQL 就能夠分清楚哪些數據屬於這一列,那些不屬於。Compact行格式存儲,開頭就是變長字段長度列表,這個列表包括數據不爲NULL的每一個可變長度字段的長度,並按照列的順序逆序排列。
例如上面的第一條數據:
+-------+------------+------+----------+------------------+
| id | score | name | content | extra |
+-------+------------+------+----------+------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian |
+-------+------------+------+----------+------------------+
複製代碼
有兩個數據不爲NULL的字段content
和extra
,長度分別是 8 和 16,轉換爲 16 進制分別是:0x08,0x10。倒序的順序排列就是10 08
這是對於長度比較短的狀況,用一字節表示長度便可。若是變長列的內容佔用的字節數比較多,可能就須要用2個字節來表示。那麼何時用一個字節,何時用兩個字節呢?
咱們給這張表加一列來測試下:
alter table `record_test_1`
add column `large_content` varchar(1024) null after `extra`;
複製代碼
這時候行數據部分並無變化。
large_content
就是1024) < 255,那麼就用一個字節表示。這裏對於large_content
,已經超過了255.問題一:那麼爲何用 128 做爲分界線呢? 一個字節能夠最多表示255,可是 MySQL 設計長度表示時,爲了區分是不是一個字節表示長度,規定,若是最高位爲1,那麼就是兩個字節表示長度,不然就是一個字節。例如,01111111,這個就表明長度爲 127,而若是長度是 128,就須要兩個字節,就是 10000000 10000000,首個字節的最高位爲1,那麼這就是兩個字節表示長度的開頭,第二個字節能夠用全部位表示長度,而且須要注意的是,MySQL採起 Little Endian 的計數方式,低位在前,高位在後,因此 129 就是 10000001 10000000。同時,這種標識方式,最大長度就是 2^15 - 1 = 32767,也就是32 KB。
問題二:若是兩個字節也不夠表示的長度,該怎麼辦? innoDB 頁大小默認爲 16KB,對於一些佔用字節數很是多的字段,比方說某個字段長度大於了16KB,那麼若是該記錄在單個頁面中沒法存儲時,InnoDB會把一部分數據存放到所謂的溢出頁中,在變長字段長度列表處只存儲留在本頁面中的長度,因此使用兩個字節也能夠存放下來。這個溢出頁機制,咱們後面和Text字段一塊兒再說。
而後對第一行數據填充large_content
字段,對於第二行,將新字段更新爲空字符串。
update `record_test_1` set `large_content` = 'abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz' where id = 1;
update `record_test_1` set `large_content` = '' where id = 1;
複製代碼
查看數據:
發現COMPACT
行記錄格式下,對於變長字段的更新,會使原有數據失效,產生一條新的數據在末尾。
第一行數據原有的被廢棄,記錄頭髮生變化,主要是打上了刪除標記,這個稍後咱們就會提到。第一行新數據:
變長字段長度列表:82 80 10 08
Null值列表:00
記錄頭信息:00 00 30 01 04
隱藏列DB_ROW_ID:00 00 00 00 08 0c
隱藏列DB_TRX_ID:00 00 00 03 c9 6e
隱藏列DB_ROLL_PTR:4f 00 00 01 89 1c 51
列數據id(1):80 00 00 00 00 00 00 01
列數據score(78.5):00 00 00 00 00 a0 53 40
列數據name(hash):68 61 73 68
列數據content(wodetian):77 6f 64 65 74 69 61 6e
列數據extra(nidetiantadetian):6e 69 64 65 74 69 61 6e 74 61 64 65 74 69 61 6e
列數據large_content(abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz):61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a 61 62 63 64 65 66 67 68 69 6a 6b 6c 6d 6e 6f 70 71 72 73 74 75 76 77 78 79 7a
複製代碼
能夠看到,變長字段長度列表變成了82 80 10 08
,這裏的large_content
字符編碼最大字節大小爲1,字段字符最大個數爲1024,這裏第一行記錄這個字段字符數量是130,因此應該用兩個字節。130*1轉換成16進製爲 0x82 也就是 0x02 + 0x80,最高位標識1以後,就是 0x82 + 0x80,對應我們的變長字段長度列表的開頭。
而新的第二行,變長字段長度列表變成了00 06 04
,由於實際large_content
佔用了0個字節。
某些字段可能能夠爲 NULL,若是對於 NULL 還單獨存儲,是一種浪費空間的行爲,和 Compact 行格式存儲的理念相悖。採用 BitMap 的思想,標記這些字段,能夠節省空間。Null值列表就是這樣的一個 BitMap。
NULL 值列表僅僅針對能夠爲 NULL 的字段,若是一個字段標記了not null
,那麼這個字段不會進入這個 NUll 值列表的 BitMap 中。
NULL值列表佔用幾個字節呢?每一個不爲 NULL 的字段,佔用一位,每超過八個字段,就是 8 位,就多一個字節,不足一個字節,高位補0。假如一個表全部字段都是not null
,那麼就沒有NULL 值列表,也就佔用 0 個字節。而且,每一個字段在這個 bitmap 中,相似於變長字段長度列表,是逆序排列的。
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
| id | score | name | content | extra | large_content |
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
| 1 | 78.5 | hash | wodetian | nidetiantadetian | abcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyzabcdefghijklmnopqrstuvwxyz |
| 65536 | 17983.9812 | zhx | shin | nosuke | lex |
| NULL | -669.996 | aa | NULL | NULL | NULL |
| 2048 | NULL | NULL | c | jun | NULL |
+-------+------------+------+----------+------------------+------------------------------------------------------------------------------------------------------------------------------------+
複製代碼
針對第一第二行記錄,因爲沒有爲 NULL 的字段,因此他們的 NULL 值列表爲00. 針對第三行記錄,他的 NULL 字段分別是 id
,content
,extra
,large_content
,分別是第一,第四,第五,第六列,那麼 NULL 值列表爲:00111001,也就是 0x39。在加入新字段以前NULL 字段分別是 id
,content
,extra
,分別是第一,第四,第五列,那麼 NULL 值列表爲:00011001,也就是 0x19 針對第四行記錄,他的 NULL 字段分別是score
,name
,large_content
,分別是第二,第三,第六列,那麼 NULL 值列表爲:00100110,也就是 0x26。在加入新字段以前NULL 字段分別是score
,name
,分別是第二,第三列,那麼 NULL 值列表爲:00000110,也就是 0x06。
對於Compact 行格式存儲,記錄頭固定爲5字節大小:
名稱 | 大小(bits) | 描述 |
---|---|---|
無用位 | 2 | 目前沒用到 |
deleted_flag | 1 | 記錄是否被刪除 |
min_rec_flag | 1 | B+樹中非葉子節點最小記錄標記 |
n_owned | 4 | 該記錄對應槽所擁有記錄數量 |
heap_no | 13 | 該記錄在堆中的序號,也能夠理解爲在堆中的位置信息 |
record_type | 3 | 記錄類型,普通數據記錄爲000,節點指針類型爲001,僞記錄首記錄 infimum 行爲010,僞記錄最後一個記錄 supremum 行爲011,1xx的爲保留的 |
next_record pointer | 16 | 頁中下一條記錄的相對位置 |
對於更新前的第一行和第二行:
第一行記錄頭信息:00 00 10 00 47
轉換爲2進制:00000000 00000000 00010000 00000000 01000111
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
第二行記錄頭信息:00 00 18 00 37
轉換爲2進制:00000000 00000000 00011000 00000000 00110111
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
複製代碼
對於更新後的原始第一行和第二行:
第一行記錄頭信息:20 00 10 00 47
轉換爲2進制:00010000 00000000 00010000 00000000 01000111
無用位:00,deleted_flag:1,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
第二行記錄頭信息:20 00 18 00 37
轉換爲2進制:00010000 00000000 00011000 00000000 00110111
無用位:00,deleted_flag:1,min_rec_flag:0,n_owned:0000,heap_no:0000000000010,record_type:000,next_record:00000000 01000111
複製代碼
能夠看出,原有的數據 deleted_flag 變成 1,表明數據被刪除。
對於更新後的新的第一行和第二行:
第一行記錄頭信息:00 00 30 00 ca
轉換爲2進制:00000000 00000000 00110000 00000000 11001010
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000011,record_type:000,next_record:00000000 11001010
第二行記錄頭信息:00 00 38 fe e6
轉換爲2進制:00000000 00000000 00111000 11111110 11100110
無用位:00,deleted_flag:0,min_rec_flag:0,n_owned:0000,heap_no:0000000000111,record_type:000,next_record:11111110 11100110
複製代碼
這些信息的其餘字段,在咱們以後用到的時候,會詳細說明。
隱藏列包含三個:
列名 | 大小(字節) | 描述 |
---|---|---|
DB_ROW_ID | 6 | 主鍵ID,這個列不必定會生成。優先使用用戶自定義主鍵做爲主鍵,若是用戶沒有定義主鍵,則選取一個 Unique 鍵做爲主鍵,若是表中連 Unique 鍵都沒有定義的話,則會爲表默認添加一個名爲 DB_ROW_ID 的隱藏列做爲主鍵 |
DB_TRX_ID | 6 | 產生當前記錄項的事務id,每開始一個新的事務時,系統版本號會自動遞增,而事務開始時刻的系統版本號會做爲事務id,事務 commit 的話,就會更新這裏的 DB_TRX_ID |
DB_ROLL_PTR | 7 | undo log 指針,指向當前記錄項的 undo log,找以前版本的數據需經過此指針。若是事務回滾的話,則從 undo Log 中把原始值讀取出來再放到記錄中去 |
這裏咱們先不詳細展開這些列的說明,只是先知道這些列便可,只會會在聚簇索引說明以及多版本控制分析的章節中詳細說明。
對於 bigint 類型,若是不爲 NULL,則佔用8字節,首位爲符號位,剩餘位存儲數字,數字範圍是 -2^63 ~ 2^63 - 1 = -9223372036854775808 ~ 9223372036854775807。若是爲 NULL,則不佔用任何存儲空間。
存儲時,若是爲正數,則首位 bit 爲1,若是爲負數,則首位爲 0 並用補碼的形式存儲。
對於咱們的四行數據:
第一行列數據id(1):80 00 00 00 00 00 00 01
第二行列數據id(65536):80 00 00 00 00 01 00 00
第三行行列數據id(NULL):空
第四行列數據id(2048):80 00 00 00 00 00 08 00
複製代碼
其餘的相似的整數存儲,tinyint(1字節),smallint(2字節),mediumint(3字節),int(4字節)等,只是字節長度上面有區別。對應的無符號類型,tinyint unsigned,smallint unsigned, mediumint unsigned,int unsigned,bigint unsigned等等,僅僅是是否有符號位的區別。
同時,這裏提一下 bigint(20)
裏面這個 20 的做用。他只是限制顯示,和底層存儲沒有任何關係。整型字段有個 zerofill 屬性,設置後(例如 bigint(20) zerofill
),在數字長度不夠 20 的數據前面填充0,以達到設定的長度。這個 20 就是顯示長度的設定。
double 的存儲對於非 NULL 的列,符合 IEEE 754 floating-point "double format" bit layout 這個統一標準:
同時,Innodb存儲在數據文件上的格式爲 Little Edian,須要進行反轉後,才能取得字段的真實值。 一樣的,若是爲 NULL, 則不佔用空間。
例如:
第一行列數據score(78.5):00 00 00 00 00 a0 53 40
翻轉: 40 53 a0 00 00 00 00 00
二進制: 01000000 01010011 10100000 00000000 00000000 00000000 00000000 00000000
符號位:0,指數位10000000101 = 1029,減去階數 1023 = 實際指數 6,小數部分0.0011101000000000000000000000000000000000000000000000,轉換爲十進制爲0.125 + 0.0625 + 0.03125 + 0.0078125 = 0.2265625, 加上隱含數字 1 爲 1.2265625, 以後乘以 2 的 6 次方就是 1.2265625 * 64 = 78.5
複製代碼
計算過程較爲複雜,能夠利用 Java 的 Double.longBitsToDouble()
轉換:
public static void main(String[] args) {
System.out.println(Double.longBitsToDouble(0x4053a00000000000L));
}
複製代碼
輸出爲 78.5
相似的類型,float,也是相同的格式,只是長度減半。
對於定長字段,不須要存長度信息直接存儲數據便可,若是不足設定的長度則補充。對於char類型,補充 0x20, 對應的就是空格。
例如:
第一行列數據name(hash):68 61 73 68
第二行列數據name(zhx):7a 68 78 20
第三行列數據name(aa):61 61 20 20
第四行列數據name(NULL):空
複製代碼
對於相似的 binary 類型,補充 0x00。
由於數據開頭有可變長度字段長度列表,因此 varchar 只須要保存實際的數據便可,不須要填充額外的數據。
正是因爲這個特性,對於可變長度字段的更新,通常都是將老記錄標記爲刪除,在記錄末尾添加新的一條記錄填充更新後的記錄。這樣提升了更新速度,可是增長了存儲碎片。