多版本並發控制(MVCC) 是通過保存數據在某個時間點的快照來實現並發控制的。不管事務執行多長時間,事務內部看到的數據是不受其它事務影響的,根據事務開始的時間不同,每個事務對同一張表,同一時刻看到的數據可能是不一樣的。
多版本並發控制 的思想就是保存數據的歷史版本,通過對數據行的多個版本管理來實現數據庫的並發控制。這樣就可以通過比較版本號決定數據是否顯示出來,讀取數據的時候不需要加鎖也可以保證事務的隔離效果。
解決問題
1. 讀寫之間阻塞的問題
通過 MVCC 可以讓讀寫互相不阻塞,即讀不阻塞寫,寫不阻塞讀,就可以提升事務並發處理能力。提高並發的演進思路:
普通鎖,只能串行執行;
讀寫鎖,可以實現讀讀並發;
數據多版本並發控制,可以實現讀寫並發。
2. 降低了死鎖的概率
InnoDB 的 MVCC 采用樂觀鎖的方式,讀取數據時並不需要加鎖,對於寫操作,也只鎖定必要的行。
3. 解決一致性讀的問題
一致性讀也被稱為快照讀,查詢數據庫在某個時間點的快照時,只能看到這個時間點之前事務提交更新的結果,而不能看到這個時間點之后事務提交的更新結果。
快照讀與當前讀
快照讀(SnapShot Read) 是一種一致性不加鎖的讀,是InnoDB並發如此之高的核心原因之一。一致性是指,事務讀取到的數據,要么是事務開始前就已經存在的數據,要么是事務自身插入或者修改過的數據。
不加鎖的簡單的 SELECT 都屬於快照讀,當前讀就是讀取最新數據,而不是歷史版本的數據。加鎖的 SELECT 就屬於當前讀,例如:
SELECT * FROM t WHERE id=1 LOCK IN SHARE MODE;
SELECT * FROM t WHERE id=1 FOR UPDATE;
InnoDB 的 MVCC 是如何工作的
1. InnoDB 是如何存儲記錄的多個版本的
事務版本號:每開啟一個事務,都會從數據庫中獲得一個事務 ID(也就是事務版本號),這個事務 ID 是自增長的,通過 ID 大小,就可以判斷事務的時間順序。
行記錄的隱藏列:InnoDB 的葉子段存儲了數據頁,數據頁中保存了行記錄,而在行記錄中有一些重要的隱藏字段:
DB_ROW_ID
:6-byte,隱藏的行 ID,用來生成默認聚簇索引。如果我們創建數據表的時候沒有指定聚簇索引,這時 InnoDB 就會用這個隱藏 ID 來創建聚集索引。采用聚簇索引的方式可以提升數據的查找效率。DB_TRX_ID
:6-byte,操作這個數據的事務 ID,也就是最后一個對該數據進行插入或更新的事務 ID。DB_ROLL_PTR
:7-byte,回滾指針,也就是指向這個記錄的 Undo Log 信息。
Undo Log
InnoDB 將行記錄快照保存在了 Undo Log 里,可以在回滾段中找到它們,如下圖所示:
從圖中能看到回滾指針將數據行的所有快照記錄都通過鏈表的結構串聯了起來,每個快照的記錄都保存了當時的 db_trx_id,也是那個時間點操作這個數據的事務 ID。這樣如果想要找歷史快照,就可以通過遍歷回滾指針的方式進行查找。
可重復讀(REPEATABLE READ) 隔離級別下, InnoDB 的 MVCC 是如何工作的
查詢(SELECT)
InnoDB 會根據以下兩個條件檢查每行記錄:
- InnoDB只查找版本早於當前事務版本的數據行(也就是,行的系統版本號小於或等於事務的系統版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經存在的,要么是事務自身插入或者修改過的。
- 行的刪除版本要么未定義,要么大於當前事務版本號。可以確保事務讀取到的行,在事務開始之前未被刪除。
只有符合上述兩個條件的記錄,才能返回作為查詢結果。
插入(INSERT)
InnoDB為新插入的每一行保存當前系統版本號作為行版本號。
刪除(DELETE)
InnoDB為刪除的每一行保存當前系統版本號作為行刪除標識。刪除在內部被視為更新,行中的一個特殊位會被設置為已刪除。
更新(UPDATE)
InnoDB為插入一行新記錄,保存當前系統版本號作為行版本號,同時保存當前系統版本號到原來的行作為行刪除標識。
多版本並發控制(MVCC) 在一定程度上實現了讀寫並發,它只在 可重復讀(REPEATABLE READ) 和 提交讀(READ COMMITTED) 兩個隔離級別下工作。其他兩個隔離級別都和 MVCC 不兼容,因為 未提交讀(READ UNCOMMITTED),總是讀取最新的數據行,而不是符合當前事務版本的數據行。而 可串行化(SERIALIZABLE) 則會對所有讀取的行都加鎖。