Innodb的默認隔離級別是可重複讀,會出現幻讀的問題,通過兩種方式來解決幻讀。
1、MVCC
多版本併發控制(Multi-Version Concurrency Control, MVCC)是 MySQL 的 InnoDB 存儲引擎實現隔離級別的一種具體方式,用於實現提交讀和可重複讀這兩種隔離級別。而未提交讀隔離級別總是讀取最新的數據行,要求很低,無需使用 MVCC。可串行化隔離級別需要對所有讀取的行都加鎖,單純使用 MVCC 無法實現。
基本思想
-
在封鎖一節中提到,加鎖能解決多個事務同時執行時出現的併發一致性問題。在實際場景中讀操作往往多於寫操作,因此又引入了讀寫鎖來避免不必要的加鎖操作,例如讀和讀沒有互斥關係。讀寫鎖中讀和寫操作仍然是互斥的,而 MVCC 利用了多版本的思想,寫操作更新最新的版本快照,而讀操作去讀舊版本快照,沒有互斥關係,這一點和 CopyOnWrite 類似。
-
在 MVCC 中事務的修改操作(DELETE、INSERT、UPDATE)會爲數據行新增一個版本快照。
-
髒讀和不可重複讀最根本的原因是事務讀取到其它事務未提交的修改。在事務進行讀取操作時,爲了解決髒讀和不可重複讀問題,MVCC 規定只能讀取已經提交的快照。當然一個事務可以讀取自身未提交的快照,這不算是髒讀。
版本號
1)系統版本號 SYS_ID:是一個遞增的數字,每開始一個新的事務,系統版本號就會自動遞增。
2)事務版本號 TRX_ID :事務開始時的系統版本號。
Undo 日誌
MVCC 的多版本指的是多個版本的快照,快照存儲在 Undo 日誌中,該日誌通過回滾指針 ROLL_PTR 把一個數據行的所有快照連接起來。
例如在 MySQL 創建一個表 t,包含主鍵 id 和一個字段 x。我們先插入一個數據行,然後對該數據行執行兩次更新操作。
INSERT INTO t(id, x) VALUES(1, "a");
UPDATE t SET x="b" WHERE id=1;
UPDATE t SET x="c" WHERE id=1;
因爲沒有使用 START TRANSACTION 將上面的操作當成一個事務來執行,根據 MySQL 的 AUTOCOMMIT 機制,每個操作都會被當成一個事務來執行,所以上面的操作總共涉及到三個事務。快照中除了記錄事務版本號 TRX_ID 和操作之外,還記錄了一個 bit 的 DEL 字段,用於標記是否被刪除。
INSERT、UPDATE、DELETE 操作會創建一個日誌,並將事務版本號 TRX_ID 寫入。DELETE 可以看成是一個特殊的 UPDATE,還會額外將 DEL 字段設置爲 1。
ReadView
MVCC 維護了一個 ReadView 結構,主要包含了當前系統未提交的事務列表 TRX_IDs {TRX_ID_1, TRX_ID_2, …},還有該列表的最小值 TRX_ID_MIN 和 TRX_ID_MAX。
在進行 SELECT 操作時,根據數據行快照的 TRX_ID 與 TRX_ID_MIN 和 TRX_ID_MAX 之間的關係,從而判斷數據行快照是否可以使用:
-
TRX_ID < TRX_ID_MIN,表示該數據行快照時在當前所有未提交事務之前進行更改的,因此可以使用。
-
TRX_ID > TRX_ID_MAX,表示該數據行快照是在事務啓動之後被更改的,因此不可使用。
-
TRX_ID_MIN <= TRX_ID <= TRX_ID_MAX,需要根據隔離級別再進行判斷:
- 提交讀:如果 TRX_ID 在 TRX_IDs 列表中,表示該數據行快照對應的事務還未提交,則該快照不可使用。否則表示已經提交,可以使用。
- 可重複讀:都不可以使用。因爲如果可以使用的話,那麼其它事務也可以讀到這個數據行快照並進行修改,那麼當前事務再去讀這個數據行得到的值就會發生改變,也就是出現了不可重複讀問題。
在數據行快照不可使用的情況下,需要沿着 Undo Log 的回滾指針 ROLL_PTR 找到下一個快照,再進行上面的判斷。
快照讀與當前讀
1. 快照讀
MVCC 的 SELECT 操作是快照中的數據,不需要進行加鎖操作。
SELECT * FROM table ...;
2. 當前讀
MVCC 其它會對數據庫進行修改的操作(INSERT、UPDATE、DELETE)需要進行加鎖操作,從而讀取最新的數據。可以看到 MVCC 並不是完全不用加鎖,而只是避免了 SELECT 的加鎖操作。
INSERT;
UPDATE;
DELETE;
在進行 SELECT 操作時,可以強制指定進行加鎖操作。以下第一個語句需要加 S 鎖,第二個需要加 X 鎖。
SELECT * FROM table WHERE ? lock in share mode;
SELECT * FROM table WHERE ? for update;
2、Next-Key Locks
Next-Key Locks 是 MySQL 的 InnoDB 存儲引擎的一種鎖實現。
MVCC 不能解決幻影讀問題,Next-Key Locks 就是爲了解決這個問題而存在的。在可重複讀(REPEATABLE READ)隔離級別下,使用 MVCC + Next-Key Locks 可以解決幻讀問題。
Record Locks
鎖定一個記錄上的索引,而不是記錄本身。
如果表沒有設置索引,InnoDB 會自動在主鍵上創建隱藏的聚簇索引,因此 Record Locks 依然可以使用。
Gap Locks
鎖定索引之間的間隙,但是不包含索引本身。例如當一個事務執行以下語句,其它事務就不能在 t.c 中插入 15。
SELECT c FROM t WHERE c BETWEEN 10 and 20 FOR UPDATE;
Next-Key Locks
它是 Record Locks 和 Gap Locks 的結合,不僅鎖定一個記錄上的索引,也鎖定索引之間的間隙。它鎖定一個前開後閉區間,例如一個索引包含以下值:10, 11, 13, and 20,那麼就需要鎖定以下區間:
(-∞, 10]
(10, 11]
(11, 13]
(13, 20]
(20, +∞)