MySQL 的默認隔離級別是 RR - 可重復讀,可以通過命令來查看 MySQL 中的默認隔離級別。
RR - 可重復讀是基于多版本并發控制(Multi-Version Concurrency Control,MVCC )實現的。MVCC,在讀取數據時通過一種類似快照的方式將數據保存下來,不同事務的 session 會看到自己特定版本的數據,這樣讀鎖和寫鎖就不沖突了。
在 InnoDB 存儲引擎里,在有聚簇索引的情況下,每行數據都包含兩個必要的隱藏列:
- DB_TRX_ID:記錄某條數據的上次修改它的事務ID(trx_id)
- DB_ROLL_PTR:回滾指針,指向這條記錄的上一個版本。我們每次對聚簇索引行進行修改時,都會把老版本寫入到undo日志里,這個指針就指向了老版本的位置,當需要進行回滾操作時,事務就通過回滾指針以獲取上一個版本的數據(注意:插入操作的undo日志沒有回滾指針,因為它是新增的數據,沒有老版本;而已刪除的信息會在undo日志記錄的頭信息中存一個delete flag標記,當該標記為true時,表示已刪除,則不返回數據)。
下圖就是一個簡潔的版本鏈概念,InnoDB 中的 undo 日志保存的就是一個版本鏈:
除了版本鏈,我們在實現 MVCC 還用到了另一個概念:read-view,一致性試圖。我們在查詢數據,當使用 select 語句時,InnoDB 會自動生成一個當前活動的(即未提交的)事務 ID 數組,這個 read-view 就是由查詢時所有未提交事務 ID 組成的數組。數組中最小的事務 ID 為 min_id 和已創建的最大事務 ID 為 max_id 組成,查詢的數據結果需要跟 read-view 做比較從而得到快照結果。
我們做查詢時,會查詢出當前 session 的 trx_id,通過和 read-view 比對:
- 若 trx_id 比 read-view 中的 min_id ?小,則該版本是已經提交的事務生成,一定可見;
- 若 trx_id 比 read view 中的 max_id 大,則該版本是還未提交的事務生成,一定不可見;
- 當 trx_id 在 read-view 列表中,即 min_id <= trx_id <= max_id時,如果 trx_id 在 read-view 的數組中,則還未提交,不可見,但是當前事務是可見的;如果 trx_id 不在數組中,表明是已經提交的事務,則該版本可見。
當版本不可見時,需要通過 DB_ROLL_PTR 獲取上一版本的 trx_id,再次比對,直到版本數據可見時,返回結果。
就以上比對的三種情況,用圖示說明下:
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set age = 18 where id = 2 | ||
select name from use where id=1 |
1)select 語句執行時,上次更新的 trx_id 為 100,read-view 中未提交的事務為 [101]。此時 read-view 的 min_id 為 101,trx_id 比它小,則該版本是已經提交的事務生成,所以返回 zhangsan。
2)假設當前 select 的 trx_id 為 102,read-view 中未提交的事務為 [101],則需要通過 DB_ROLL_PTR 獲取上一版本的 trx_id 100,注意 trx_id 為 101 的事務是改變了另一張表的數據,所以 undo 日志里版本鏈指向的上一條數據 trx_id 為 100,還是會返回 zhangsan。
3)當 trx_id 在 read-view 中間時:
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set name='wangwu' where id = 1 | ||
select name from use where id=1 |
此時 trx_id 為 101,read-view 為 [101],當前事務 ID 在數組中,所以不可見。需要用 DB_ROLL_PTR 找到上一條版本的位置 trx_id 為 100,還是會返回 zhangsan。
RC 隔離級別在查詢時,同一個事務多次查詢,每次會生成獨立的 read-view。而 RR - 可重復讀只在第一次查詢時生成統一的 read view,之后的讀取都復用之前的 read view。而 RU - 讀未提交是可以讀取還沒提交的數據,沒有 undo 版本的概念;可串行化隔離級別在每次讀取時都需要加鎖控制,沒法并發,所以通過版本的概念去控制并發也就沒有意義。
transaction 100 | transaction 101 | select |
---|---|---|
update user set name = 'zhangsan' where id = 1 | ||
commit | ||
update test set age = 18 where id = 2 | ||
select name from use where id=1 | ||
update user set name = '666' where id = 1 | ||
select name from use where id=1 |
當使用 RC 級別時,兩次 select 的 read-view 不一樣,第一次查詢時是 [101],第二次是 [100, 101]。而用 RR 級別時,會復用第一次查詢的 read-view,故多次查詢的結果是一樣的。這也是 MySQL 的隔離級別默認用 RR - 可重復讀的原因之一,不用重復生成 read-view,提升數據庫的操作性能。
總結
每次 select 數據時生成 read view 列表,再配合 undo 日志中的版本鏈,讓不同的事務讀-寫,寫-讀操作可以并發執行,進而實現 MVCC。