頁的數據結構:
數據頁結構,頁是innodb存儲引擎管理數據的最小磁盤單位,而B-TREE節點就是實際存放表數據的節點,一個innodb頁有七個部分組成:
每一個頁中包含了兩對 header/trailer:內部的 Page Header/Page Directory 關心的是頁的狀態信息,而 Fil Header/Fil Trailer 關心的是記錄頁的頭信息。
在頁的頭部和尾部之間就是用戶記錄和空閒空間了,每一個數據頁中都包含 Infimum 和 Supremum 這兩個虛擬的記錄(可以理解爲佔位符),Infimum 記錄是比該頁中任何主鍵值都要小的值,Supremum 是該頁中的最大值:
User Records 就是整個頁面中真正用於存放行記錄的部分,而 Free Space 就是空餘空間了,它是一個鏈表的數據結構,爲了保證插入和刪除的效率,整個頁面並不會按照主鍵順序對所有記錄進行排序,它會自動從左側向右尋找空白節點進行插入,行記錄在物理存儲上並不是按照順序的,它們之間的順序是由 next_record
這一指針控制的。
B+ 樹在查找對應的記錄時,並不會直接從樹中找出對應的行記錄,它只能獲取記錄所在的頁,將整個頁加載到內存中,再通過 Page Directory 中存儲的稀疏索引和 n_owned
、next_record
屬性取出對應的記錄,不過因爲這一操作是在內存中進行的,所以通常會忽略這部分查找的耗時。
關於MySQL的索引:
索引是數據庫中非常非常重要的概念,它是存儲引擎能夠快速定位記錄的祕密武器,對於提升數據庫的性能、減輕數據庫服務器的負擔有着非常重要的作用;索引優化是對查詢性能優化的最有效手段,它能夠輕鬆地將查詢的性能提高几個數量級。需要注意的是查詢記錄時每次只能使用一個索引,因爲和全表掃描和只是用一個索引的速度比起來,去分析兩個索引二叉樹更耗費時間。
那麼索引是如何存儲的呢?
InnoDB 存儲引擎在絕大多數情況下使用 B+ 樹建立索引,這是關係型數據庫中查找最爲常用和有效的索引,但是 B+ 樹索引並不能找到一個給定鍵對應的具體值,它只能找到數據行對應的頁,然後正如上一節所提到的,數據庫把整個頁讀入到內存中,並在內存中查找具體的數據行。
B+ 樹是平衡樹,它查找任意節點所耗費的時間都是完全相同的,比較的次數就是 B+ 樹的高度;在這裏,我們並不會深入分析或者動手實現一個 B+ 樹,只是對它的特性進行簡單的介紹。
數據庫中的 B+ 樹索引可以分爲聚集索引(clustered index)和輔助索引(secondary index),它們之間的最大區別就是,聚集索引中存放着一條行記錄的全部信息,而輔助索引中只包含索引列和一個用於查找對應行記錄的『書籤』,在mysql中,可以把主鍵理解成聚集索引,如果沒有創建,系統會自動創建一個隱含列爲表的聚集索引。
MySQL的併發控制:
三種常見的併發控制機制:分別是悲觀併發控制、樂觀併發控制和多版本併發控制,其中悲觀併發控制其實是最常見的併發控制機制,也就是鎖;而樂觀併發控制其實也有另一個名字:樂觀鎖,樂觀鎖其實並不是一種真實存在的鎖,我們會在文章後面的部分中具體介紹;最後就是多版本併發控制(MVCC)了,與前兩者對立的命名不同,MVCC 可以與前兩者中的任意一種機制結合使用,以提高數據庫的讀性能。
- 樂觀鎖是一種思想,它其實並不是一種真正的『鎖』,它會先嚐試對資源進行修改,在寫回時判斷資源是否進行了改變,如果沒有發生改變就會寫回,否則就會進行重試,在整個的執行過程中其實都沒有對數據庫進行加鎖;
- 悲觀鎖就是一種真正的鎖了,它會在獲取資源前對資源進行加鎖,確保同一時刻只有有限的線程能夠訪問該資源,其他想要嘗試獲取資源的操作都會進入等待狀態,直到該線程完成了對資源的操作並且釋放了鎖後,其他線程才能重新操作資源;
樂觀鎖不會存在死鎖的問題,但是由於更新後驗證,所以當衝突頻率和重試成本較高時更推薦使用悲觀鎖,而需要非常高的響應速度並且併發量非常大的時候使用樂觀鎖就能較好的解決問題,在這時使用悲觀鎖就可能出現嚴重的性能問題;在選擇併發控制機制時,需要綜合考慮上面的四個方面(衝突頻率、重試成本、響應速度和併發量)進行選擇。
共享鎖和排它鎖是悲觀鎖的不同的實現,共享鎖和排它鎖實現了標準的行級鎖,它倆都屬於悲觀鎖的範疇。
鎖的算法:
- 共享鎖(讀鎖):允許事務對一條行數據進行讀取;
- 互斥鎖(寫鎖):允許事務對一條行數據進行刪除或更新;
Record Lock
記錄鎖(Record Lock)是加到索引記錄上的鎖,假設我們存在下面的一張表 users
:
CREATE TABLE users(
id INT NOT NULL AUTO_INCREMENT,
last_name VARCHAR(255) NOT NULL,
first_name VARCHAR(255),
age INT,
PRIMARY KEY(id),
KEY(last_name),
KEY(age)
);
如果我們使用 id
或者 last_name
作爲 SQL 中 WHERE
語句的過濾條件,那麼 InnoDB 就可以通過索引建立的 B+ 樹找到行記錄並添加索引,但是如果使用 first_name
作爲過濾條件時,由於 InnoDB 不知道待修改的記錄具體存放的位置,也無法對將要修改哪條記錄提前做出判斷就會鎖定整個表。
Gap Lock
記錄鎖是在存儲引擎中最爲常見的鎖,除了記錄鎖之外,InnoDB 中還存在間隙鎖(Gap Lock),間隙鎖是對索引記錄中的一段連續區域的鎖;當使用類似 SELECT * FROM users WHERE id BETWEEN 10 AND 20 FOR UPDATE;
的 SQL 語句時,就會阻止其他事務向表中插入 id = 15
的記錄,因爲整個範圍都被間隙鎖鎖定了。
間隙鎖是存儲引擎對於性能和併發做出的權衡,並且只用於某些事務隔離級別。
雖然間隙鎖中也分爲共享鎖和互斥鎖,不過它們之間並不是互斥的,也就是不同的事務可以同時持有一段相同範圍的共享鎖和互斥鎖,它唯一阻止的就是其他事務向這個範圍中添加新的記錄。
Next-Key Lock
Next-Key 鎖相比前兩者就稍微有一些複雜,它是記錄鎖和記錄前的間隙鎖的結合,在 users
表中有以下記錄:
+------|-------------|--------------|-------+
| id | last_name | first_name | age |
|------|-------------|--------------|-------|
| 4 | stark | tony | 21 |
| 1 | tom | hiddleston | 30 |
| 3 | morgan | freeman | 40 |
| 5 | jeff | dean | 50 |
| 2 | donald | trump | 80 |
+------|-------------|--------------|-------+
如果使用 Next-Key 鎖,那麼 Next-Key 鎖就可以在需要的時候鎖定以下的範圍:
(-∞, 21]
(21, 30]
(30, 40]
(40, 50]
(50, 80]
(80, ∞)
既然叫 Next-Key 鎖,鎖定的應該是當前值和後面的範圍,但是實際上卻不是,Next-Key 鎖鎖定的是當前值和前面的範圍。
當我們更新一條記錄,比如 SELECT * FROM users WHERE age = 30 FOR UPDATE;
,InnoDB 不僅會在範圍 (21, 30]
上加 Next-Key 鎖,還會在這條記錄後面的範圍 (30, 40]
加間隙鎖,所以插入 (21, 40]
範圍內的記錄都會被鎖定。
事務:
在介紹了鎖之後,我們再來談談數據庫中一個非常重要的概念 —— 事務;相信只要是一個合格的軟件工程師就對事務的特性有所瞭解,其中被人經常提起的就是事務的原子性,在數據提交工作時,要麼保證所有的修改都能夠提交,要麼就所有的修改全部回滾。
但是事務還遵循包括原子性在內的 ACID 四大特性:原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和持久性(Durability);文章不會對這四大特性全部展開進行介紹,相信你能夠通過 Google 和數據庫相關的書籍輕鬆獲得有關它們的概念,本文最後要介紹的就是事務的四種隔離級別。
幾種隔離級別
事務的隔離性是數據庫處理數據的幾大基礎之一,而隔離級別其實就是提供給用戶用於在性能和可靠性做出選擇和權衡的配置項。
ISO 和 ANIS SQL 標準制定了四種事務隔離級別,而 InnoDB 遵循了 SQL:1992 標準中的四種隔離級別:READ UNCOMMITED
、READ COMMITED
、REPEATABLE READ
和 SERIALIZABLE
;每個事務的隔離級別其實都比上一級多解決了一個問題:
RAED UNCOMMITED
:使用查詢語句不會加鎖,可能會讀到未提交的行(Dirty Read);READ COMMITED
:只對記錄加記錄鎖,而不會在記錄之間加間隙鎖,所以允許新的記錄插入到被鎖定記錄的附近,所以再多次使用查詢語句時,可能得到不同的結果(Non-Repeatable Read);REPEATABLE READ
:多次讀取同一範圍的數據會返回第一次查詢的快照,不會返回不同的數據行,但是可能發生幻讀(Phantom Read);SERIALIZABLE
:InnoDB 隱式地將全部的查詢語句加上共享鎖,解決了幻讀的問題;
MySQL 中默認的事務隔離級別就是 REPEATABLE READ
,但是它通過 Next-Key 鎖也能夠在某種程度上解決幻讀的問題。
總結:
MySQL的Innodb存儲引擎使用B+Tree來建立索引,索引可分爲兩大類,聚集索引和輔助索引,它們主要的區別是聚集索引(可以理解爲主鍵)中存儲了行記錄的全部信息,在使用聚集索引檢索數據時,可以直接查詢到,而輔助索引值存儲了索引中標明的列和一個用於查找對應行記錄的書籤(主鍵),如果需要查詢完整的行記錄,就要先通過輔助索引查找到主鍵,然後在聚集索引中使用主鍵查找完整的行記錄。
MySQL的併發控制有三種常見的機制:悲觀控制、樂觀控制和多版本控制,其中悲觀併發控制其實是最常見的併發控制機制,也就是鎖,分爲行級鎖和表鎖,行級鎖又分爲共享鎖和排它鎖,事務其實是併發控制的基本單位,事務的隔離性是數據庫處理數據的幾大基礎之一,分爲髒讀、不可重複讀、可重複讀、串行化,MySQL通過隔離性和鎖來控制事務併發的可靠性。