關於數據庫咱們知道是經過內存對磁盤進行操做的,也知道數據會落實到磁盤上,可是數據在磁盤上的存儲結構可能你們還不是很清楚。mysql
MySQL服務器上負責對錶中的數據的讀取和寫入的工做的部分是存儲引擎,而關於服務器會支持不一樣類型的服務器,如:InnoDB、MyISAM、Memory......算法
不一樣的存儲引擎都是爲了實現不一樣的特性進行開發的,真實數據的存儲在不一樣的存儲引擎中存放的格式通常是不一樣的,有的存儲引擎好比Memory都不用磁盤來存儲數據,就跟NoSQL同樣,服務器關閉後數據就不見了。InnoDB是MySQL的默認儲存引擎,也是咱們你們經常使用的存儲引擎。sql
Mysql把頁做爲管理存儲空間的基本單位,一個頁的大小通常是16KB,你們知道記錄實際上是被儲存在頁中的,本文將詳細的帶你們看一下InnoDB儲存引擎中頁的結構。數據庫
參考文章:InnoDB數據頁結構bash
InnoDB
是一個將表中的數據存儲到磁盤上的存儲引擎,因此即便關機後重啓咱們的數據仍是存在的。而真正處理數據的過程是發生在內存中的,因此須要把磁盤中的數據加載到內存中,若是是處理寫入或修改請求的話,還須要把內存中的內容刷新到磁盤上。而咱們知道讀寫磁盤的速度很是慢,和內存讀寫之間的差距就再也不多說,因此當咱們想從表中獲取某些記錄時,InnoDB
存儲引擎須要一條一條的把記錄從磁盤上讀出來麼?不,那樣會慢死,InnoDB
採起的方式是:將數據劃分爲若干個頁,以頁做爲磁盤和內存之間交互的基本單位,InnoDB中頁的大小通常爲 16KB。也就是在通常狀況下,一次最少從磁盤中讀取16KB的內容到內存中,一次最少把內存中的16KB內容刷新到磁盤中。服務器
頁的本質介紹一個大小爲16KB大小的存儲空間,頁有不少種類型的,不一樣的類型有不一樣的做用;性能
用於存儲記錄的頁被稱爲數據頁 ,大小也爲16KB,可是這16KB大小的存儲空間被劃分爲多個部分,不一樣的部分固然有着不一樣的功能,結構以下:spa
從上面的圖能夠看到,InnoDB的頁結構分爲七個部分,下面用表格說明一下各個部分對應的做用:設計
名稱 | 中文名 | 佔用空間大小 | 簡單描述 |
---|---|---|---|
File Header | 文件頭 | 38字節 | 描述頁的信息 |
Page Header | 頁頭 | 56字節 | 頁的狀態信息 |
Infimum + SupreMum | 最小記錄和最大記錄 | 26字節 | 兩個虛擬的行記錄(後面會說明) |
User Records | 用戶記錄 | 不肯定 | 實際存儲的行記錄內容 |
Free Space | 空閒空間 | 不肯定 | 頁中還沒有使用的空間 |
Page Directory | 頁目錄 | 不肯定 | 頁中的記錄相對位置 |
File Trailer | 文件結尾 | 8字節 | 結尾信息 |
下面會詳細介紹他們的做用3d
當咱們在存儲數據的時候,記錄會存儲到User Records部分 。可是在一個頁新造成的時候是不存在User Records
這個部分的,每當咱們在插入一條記錄的時候,都會從Free Space中去申請一塊大小符合該記錄大小的空間並劃分到User Records
,當Free Space
的部分空間所有被User Records
部分替換掉以後,就意味着當前頁使用完畢,若是還有新的記錄插入,須要再去申請新的頁,過程以下:
對於User Records中的每一條記錄的管理,MySQL作了不少的處理,究竟作出了什麼處理呢,這須要從每條記錄裏面的記錄的額外信息
部分中的記錄頭信息提及
這是有關行格式的知識,關於行格式(指的就是一條記錄的存儲結構,有多種格式),有興趣的能夠去看一下InnoDB記錄存儲結構 這篇文章。
首先,建立一個表:
mysql> CREATE TABLE page_demo(
-> c1 INT,
-> c2 INT,
-> c3 VARCHAR(10000),
-> PRIMARY KEY (c1)
-> ) CHARSET=ascii ROW_FORMAT=Compact;
Query OK, 0 rows affected (0.03 sec)
mysql>
複製代碼
如上所示,表中有三列,c1和c2用來存儲整數的,c3用來存儲字符串的。由於指定了主鍵爲c1,因此MySQL就不會去建立那個隱藏的 row_id 列。指定了ascii
字符集以及Compact
的行格式,因此裏面的每一條記錄的行格式以下:
先看一下行格式中每一個屬性表明的意思:
名稱 | 大小(單位:bit) | 描述 |
---|---|---|
預留位1 | 1 | 沒有使用 |
預留位2 | 1 | 沒有使用 |
delete_mask | 1 | 標記該記錄是否被刪除 |
min_rec_mask | 1 | 標記該記錄是否爲B+樹的非葉子節點中的最小記錄(索引時用到) |
n_owned | 4 | 表示當前槽管理的記錄數 |
heap_no | 13 | 表示當前記錄在記錄堆的位置信息 |
record_type | 3 | 表示當前記錄的類型,0 表示普通記錄,1 表示B+樹非葉節點記錄,2 表示最小記錄,3 表示最大記錄 |
next_record | 16 | 表示下一條記錄的相對位置 |
因爲這裏只是描述在User Records
中記錄頭的做用,因此下面只會說明一些相關的屬性以及c1
、c2
、c3
列的信息(其餘信息沒畫不表明它們不存在,只是爲了理解上的方便省略了~),簡化後的行格式示意圖就是這樣:
咱們往表中插入幾條數據:
mysql> INSERT INTO page_demo VALUES(1, 100, 'aaaa'), (2, 200, 'bbbb'), (3, 300, 'cccc'), (4, 400, 'dddd');
Query OK, 4 rows affected (0.00 sec)
Records: 4 Duplicates: 0 Warnings: 0
mysql>
複製代碼
下面看看幾條記錄在頁中的User Records
是以何種形式進行體現的,爲了方便理解,下面的圖中把記錄中的頭信息和實際的數據都用的十進制進行的表示(其實都是二進制):
下面說說,記錄頭中的各個部分表明的含義:
這個屬性說的是當前這條記錄是否被刪除,當值爲0的時候表明着沒有被刪除,爲1的時候標誌着被刪除了。
是的,您沒看錯,當您執行刪除一個記錄的操做的時候,被刪除的記錄還存在頁中,您對它進行了刪除,它會把的
記錄頭中的這個屬性設置爲1,只是打了個標記。
緣由
這些被刪除的記錄之因此不當即從磁盤上移除,是由於移除它們以後把其餘的記錄在磁盤上從新排列須要性能消耗,因此只是打個刪除標記而已,並且這部分存儲空間以後還能夠重用,也就是說以後若是有新記錄插入到表中的話,可能把這些被刪除的記錄佔用的存儲空間覆蓋掉。
若是您想完全的從磁盤上移除這些被刪除的記錄,可使用這個語句:
optimize table '表名'; 複製代碼
執行這個命令後服務器會從新規劃表中記錄的存儲方式,把被標記爲刪除的記錄從磁盤上移除。
有關索引的,暫時不說,後面說到索引會說明;
下面會講
這個屬性是表示的當前記錄在當前頁中的位置,上面的一張圖若是您仔細看了的話,會發現它們的位置分別是二、三、四、5,那麼問題來了? 0和1呢?
這是由於在每次建立的一頁裏面會自動的加入兩條記錄,這被稱爲僞記錄
或者 虛擬記錄
(由於不是咱們本身插入的);
這兩條僞記錄一個表明着最小記錄
,一個表明着最大記錄
;
記錄大小的比較是經過主鍵值來比較的。在上面咱們插入的幾條記錄中的從小到大的順序就是:1 < 2 < 3 < 4,
這標誌着這4條記錄的大小依次遞增。
無論咱們插入了什麼數據,頁中的最小記錄
和 最大記錄
都是頁生成時候的那兩條僞記錄。這兩條僞記錄的結構頁相對簡單,以下:
還記得頁結構組成的七部分中一個部分叫Infimum + SupreMum
,這個部分用來存儲最小記錄和最大記錄的,沒錯,就是這兩條僞記錄。
緣由:因爲這兩條記錄不是咱們本身定義的記錄,因此它們並不存放在
頁
的User Records
部分,他們被單獨放在一個稱爲Infimum + Supremum
的部分
由上面的圖能夠看出,最小記錄和最大記錄的heap_no的值分別爲0和1,也就是說它們的位置最靠前。
這個屬性表示當前記錄的類型,一共有4種類型的記錄,0
表示普通記錄,1
表示B+樹非葉節點記錄,2
表示最小記錄,3
表示最大記錄。從圖中咱們也能夠看出來,咱們本身插入的記錄就是普通記錄,它們的record_type
值都是0
,而最小記錄和最大記錄的record_type
值分別爲2
和3
,關於1暫且不說;
這個屬性表示這從當前記錄真實數據到下一條記錄的真實數據的地址偏移量 ;
假若有一條記錄的next_record
的值爲12,就標誌着從這條記錄的真實數據的地址日後找12個字節就是下一條記錄的真實數據(鏈表)。也就是說頁中的數據之間的聯繫是一個根據大小比較後從小指到大的單向鏈表。
規定 最小記錄 的下一條記錄就本頁中主鍵值最小的記錄,而本頁中主鍵值最大的記錄的下一條記錄就是 最大記錄(最大的那條僞記錄) ,爲了更形象的表示一下這個next_record
起到的做用,咱們用箭頭來替代一下next_record
中的地址偏移量:
從上面能夠看出,最大記錄
的 next_record
的值爲0,表明着最大記錄的下一條記錄是不存在的,它也是鏈條中的最後一個節點。
當咱們從頁中刪除一條數據後能夠看看鏈表會發生那些變化:
mysql> DELETE FROM page_demo WHERE c1 = 2;
Query OK, 1 row affected (0.02 sec)
mysql>
複製代碼
刪掉第2條記錄後的示意圖就是:
從上面能夠看到:
當咱們刪除第二條記錄後,鏈表中的變化最明顯的就是各個節點之間的聯繫,它會把被刪除數據的上一條記錄和被刪除數據的下一條數據進行關聯(這條數據仍是存在的,以前說的那個刪除標記別忘了哦)。
- 第2條記錄並無從存儲空間中移除,而是把該條記錄的
delete_mask
值設置爲1
。- 第2條記錄的
next_record
值變爲了0,意味着該記錄沒有下一條記錄了。- 第1條記錄的
next_record
指向了第3條記錄。- 還有一點您可能忽略了,就是
最大記錄
的n_owned
值從5
變成了4
,關於這一點的變化咱們稍後會詳細說明的。因此獲得:不論咱們怎麼對頁中的記錄作增刪改操做,InnoDB始終會維護一條記錄的單鏈表,鏈表中的各個節點是按照主鍵值由小到大的順序鏈接起來的。
下面咱們再作一個操做,把刪除的記錄再次插入:
mysql> INSERT INTO page_demo VALUES(2, 200, 'bbbb');
Query OK, 1 row affected (0.00 sec)
mysql>
複製代碼
咱們來看看發生了什麼變化:
很明顯的能夠看到,InnoDB
並無由於新記錄的插入而爲它申請新的存儲空間,而是直接複用了原來被刪除記錄的存儲空間。
經過上面,咱們知道到了頁中記錄是一個按照大小從下到大連續的單向鏈表,如今來想一想,當咱們根據主鍵查詢一條記錄的時候是怎樣進行的,咱們來看看;
SELECT * FROM page_demo WHERE c1 = 3;
複製代碼
上面是一條查詢語句,咱們想一想它的執行方式多是:
從最小記錄開始,沿着鏈表一直日後找,總有一天會找到(或者找不到),在找的時候還能投機取巧,由於鏈表中各個記錄的值是按照從小到大順序排列的,因此當鏈表的某個節點表明的記錄的主鍵值大於您想要查找的主鍵值時,若是這個時候還沒找到數據的話您就能夠中止查找了(表明找不到),由於該節點後邊的節點的主鍵值都是依次遞增。
上面的方式存在的問題就是,當頁中的存儲的記錄數量比較少的狀況用起來也沒啥問題,可是若是一個頁中存儲了很是多的記錄,這麼查找對性能來講仍是有損耗的,因此這個方式很笨啊。
咱們來看看InnoDB
的處理方式:InnoDB
的處理方式至關於咱們平時看書的時候,想看那一章的時候不會傻到去一頁一頁的找,而是經過目錄去找到對應的頁數,直接就定位過去了。說說InnoDB
這樣處理的步驟吧:
1. 將全部正常的記錄(包括最大和最小記錄,不包括標記爲已刪除的記錄)劃分爲幾個組。
2. 每一個組的最後一條記錄的頭信息中的n_owned
屬性表示該組內共有幾條記錄。
3. 將每一個組的最後一條記錄的地址偏移量按順序存儲起來,每一個地址偏移量也被稱爲一個槽
(英文名:Slot
)。這些地址偏移量都會被存儲到靠近頁
的尾部的地方,頁中存儲地址偏移量的部分也被稱爲Page Directory
。
好比說,如今表中有6條記錄,InnoDB
會把它們分紅兩組,第一組中只有一個最小記錄,第二組中是剩餘的5條記錄,看下邊的示意圖:
從上面的圖中能夠看到:
n_owned
的值爲1,表明着以最小記錄結尾的這個分組中只有1條記錄,就是最小記錄自己;n_owned
的值爲5,表明着以最大記錄結尾的這個分組中只有5條記錄,這5條記錄包括它自己,就是說除了它自己還有其它4條記錄; 咱們用圖來表示一下:
上面的圖中爲了方便理解,暫時沒管各條記錄在存儲設備上的排列方式了,單純從邏輯上看一下這些記錄和頁目錄的關係。真實的Page Directory
是在下面的。
再說說,爲何最小記錄的n_owned
值爲1,而最大記錄的n_owned
值爲5
呢?它們是怎麼分配的?
InnoDB
對每一個分組中的記錄條數是有規定的,對於最小記錄所在的分組只能有 1 條記錄,最大記錄所在的分組擁有的記錄條數只能在 1~8 條之間,剩下的分組中記錄的條數範圍只能在是 4~8 條之間。因此分組是按照下邊的步驟進行的:
咱們一口氣又往表中添加了12條記錄,如今就一共有16條正常的記錄了(包括最小和最大記錄),這些記錄被分紅了5個組,如圖所示:
上圖中,只保留了頭信息中的n_owned
和next_record
屬性,也省略了各個記錄之間的箭頭,沒畫不等於沒有!
由於各個槽表明的記錄的主鍵值都是從小到大排序的,因此咱們可使用二分法
來進行快速查找。4個槽的編號分別是:0
、1
、2
、3
、4
,因此初始狀況下最低的槽就是low=0
,最高的槽就是high=4
。比方說咱們想找主鍵值爲5
的記錄,如今咱們再來看看查找一條記錄的步驟:
1. 首先獲得中間槽的位置:(0 + 4)/2 = 2
,因此獲得槽2,根據槽2的地址偏移量知道它的主鍵值是8,由於8>5,設置high=2
,low
不變;
2. 再次計算中間槽的位置:(0 + 2)/2 = 1
,因此獲得槽1,根據槽1的地址偏移量知道它的主鍵值是4, 由於4<5,設置low=1
,high
不變;
3. 由於high - low
的值爲1,因此肯定主鍵值爲5
的記錄在槽1和槽2之間,接下來就是遍歷鏈表的查找了;
因此在一個數據頁中查找指定主鍵值的記錄的過程分爲兩步:
1. 經過二分法肯定該記錄所在的槽。
2. 經過記錄的next_record屬性組成的鏈表遍歷查找該槽中的各個記錄。
複製代碼
設計InnoDB
的大叔們爲了能獲得一個數據頁中存儲的記錄的狀態信息,好比本頁中已經存儲了多少條記錄,第一條記錄的地址是什麼,Page Directory
中存儲了多少個槽等等,特地在頁中定義了一個叫Page Header
的部分,它是頁
結構的第二部分,這個部分佔用固定的56
個字節,專門存儲各類狀態信息,具體各個字節都是幹嗎的看下錶:
名稱 | 大小(單位:byte) | |
---|---|---|
PAGE_N_DIR_SLOTS | 2 | 在頁目錄中的槽數量 |
PAGE_HEAP_TOP | 2 | 第一個記錄的地址 |
PAGE_N_HEAP | 2 | 本頁中的記錄的數量(包括最小和最大記錄以及標記爲刪除的記錄) |
PAGE_FREE | 2 | 指向可重用空間的地址(就是標記爲刪除的記錄地址) |
PAGE_GARBAGE | 2 | 已刪除的字節數,行記錄結構中delete_flag 爲1的記錄大小總數 |
PAGE_LAST_INSERT | 2 | 最後插入記錄的位置 |
PAGE_DIRECTION | 2 | 最後插入的方向 |
PAGE_N_DIRECTION | 2 | 一個方向連續插入的記錄數量 |
PAGE_N_RECS | 2 | 該頁中記錄的數量(不包括最小和最大記錄以及被標記爲刪除的記錄) |
PAGE_MAX_TRX_ID | 2 | 修改當前頁的最大事務ID,該值僅在二級索引中定義 |
PAGE_LEVEL | 2 | 當前頁在索引樹中的位置,高度 |
PAGE_INDEX_ID | 8 | 索引ID,表示當前頁屬於哪一個索引 |
PAGE_BTR | 10 | 非葉節點所在段的segment header,僅在B+樹的Root頁定義 |
PAGE_LEVEL | 10 | B+樹所在段的segment header,僅在B+樹的Root頁定義 |
若是你們認真看過前邊的文章,那麼大體能看明白這裏頭前邊一半左右的狀態信息的意思,剩下的狀態信息看不明白不要着急,飯要一口一口吃,東西要一點一點學。在這裏想強調如下PAGE_DIRECTION
和PAGE_N_DIRECTION
的意思。
PAGE_DIRECTION
假如新插入的一條記錄的主鍵值比上一條記錄的主鍵值比上一條記錄大,咱們說這條記錄的插入方向是右邊,反之則是左邊。用來表示最後一條記錄插入方向的狀態就是PAGE_DIRECTION
。
PAGE_N_DIRECTION
假設連續幾回插入新記錄的方向都是一致的,InnoDB
會把沿着同一個方向插入記錄的條數記下來,這個條數就用PAGE_N_DIRECTION
這個狀態表示。固然,若是最後一條記錄的插入方向改變了的話,這個狀態的值會被清零從新統計。
若是說Page Header
描述的是頁
內的各類狀態信息,比方說頁裏頭有多少個記錄了呀,有多少個槽了呀,那麼File Header
描述的就是頁
外的各類狀態信息,比方說這個頁的編號是多少,它的上一個頁、下一個頁是誰啦。File Header
是InnoDB
頁的第一部分,這個部分佔用固定的38
個字節,下邊咱們看看這個部分的各個字節都是表明啥意思吧:
名稱 | 大小(單位:byte) | 描述 |
---|---|---|
FIL_PAGE_SPACE_OR_CHKSUM | 4 | 頁的校驗和(checksum值) |
FIL_PAGE_OFFSET | 4 | 頁號 |
FIL_PAGE_PREV | 4 | 上一個頁的頁號 |
FIL_PAGE_NEXT | 4 | 下一個頁的頁號 |
FIL_PAGE_LSN | 8 | 最後被修改的日誌序列位置(英文名是:Log Sequence Number) |
FIL_PAGE_TYPE | 2 | 該頁的類型(以前咱們說的是數據頁) |
FIL_PAGE_FILE_FLUSH_LSN | 8 | 僅在系統表空間的一個頁中定義,表明文件至少被更新到了該LSN值,獨立表空間中都是0 |
FIL_PAGE_ARCH_LOG_NO_OR_SPACE_ID | 4 | 頁屬於哪一個表空間 |
對照着這個表格,咱們看幾個目前比較重要的部分:
FIL_PAGE_SPACE_OR_CHKSUM
這個表明當前頁面的校驗和(checksum)。啥是個校驗和?就是對於一個很長很長的字節串來講,咱們會經過某種算法來計算一個值,這個值就稱爲校驗和
。這樣在比較兩個很長的字節串以前先比較這兩個長字節串的校驗和,若是校驗和都不同兩個長字節串確定是不一樣的(hashCode和equals),因此省去了直接比較兩個比較長的字節串的時間損耗(和後面的File Trailer裏面的那個相對應,看到後面您就明白了)。
FIL_PAGE_OFFSET
每個頁
都有一個單獨的頁號,就跟您的身份證號碼同樣,InnoDB
經過頁號來能夠惟必定位一個頁
。
FIL_PAGE_TYPE
這個表明當前頁
的類型,咱們前邊說過,InnoDB
爲了避免同的目的而把頁分爲不一樣的類型,本集中介紹的其實都是存儲記錄的數據頁
,其實還有不少別的類型的頁:
FIL_PAGE_PREV
和FIL_PAGE_NEXT
一張表中能夠有成千上萬條記錄,一個頁只有16KB
,因此可能須要好多頁來存放數據,FIL_PAGE_PREV
和FIL_PAGE_NEXT
就分別表明本頁的上一個和下一個頁的頁號(雙向鏈表)。
Page Header
的其它屬性就不說了;
對於這個部分,個人理解比較簡單,咱們知道InnoDB
會把數據從內存刷新到磁盤,中間交互的單位是頁 ,可是咱們想一想,假如再刷新到磁盤的時候出現了問題,這樣的話怎麼辦呢?
這就是File Trailer
做用,這個部分由8
個字節組成,能夠分紅2個小部分:
File Header
中的校驗和相對應的。每當一個頁面在內存中修改了,在同步以前就要把它的校驗和算出來,由於File Header
在頁面的前邊,因此校驗和會被首先同步到磁盤,當徹底寫完時,校驗和也會被寫到頁的尾部,若是徹底同步成功,則頁的首部和尾部的校驗和應該是一致的,反之意味着同步中間出了錯;1. InnoDB爲了避免同的目的而設計了不一樣類型的頁,用於存放咱們記錄的頁也叫作`數據頁`。
2. 一個數據頁能夠被分爲7個部分,分別是
- `File Header`,表示文件頭,佔固定的38字節。
- `Page Header`,表示頁裏的一些狀態信息,佔固定的56個字節。
- `Infimum + Supremum`,兩個虛擬的僞記錄,分別表示頁中的最小和最大記錄,佔固定的`26`個字節。
- `User Records`:真實存儲咱們插入的記錄的部分,大小不固定。
- `Free Space`:頁中還沒有使用的部分,大小不肯定。
- `Page Directory`:頁中的記錄相對位置,也就是各個槽在頁面中的地址偏移量,大小不固定,插入的記錄越多,這個部分佔用的空間越多。
複製代碼
next_record
屬性,從而使頁中的全部記錄串聯成一個單向鏈表
。InnoDB
會爲把頁中的記錄劃分爲若干個組,每一個組的最後一個記錄的地址偏移量做爲一個槽
,存放在Page Directory
中,因此在一個頁中根據主鍵查找記錄是很是快的,分爲兩步:
File Header
部分都有上一個和下一個頁的編號,因此全部的數據頁會組成一個雙鏈表
。LSN
值,若是首部和尾部的校驗和和LSN
值校驗不成功的話,就說明同步過程出現了問題。 本文的大部份內容都是參考並使用的原文中的內容,只是在中間加入了一些本身的理解,並但願把它更清楚的表達出來,你們也能夠去看看原文:
若是有地方理解的不對,還望指教。