之前在講 MySQL 事務隔離性提到過,對於寫操作給讀操作的影響這種情形下發生的髒讀、不可重複讀、虛讀問題。是通過MVCC 機制來進行解決的,那麼MVCC到底是如何實現的,其內部原理是怎樣的呢?我們要抓住三個方面:記錄中的4個隱藏欄位、undo log 和 read view。
一、MVCC 定義和解決的讀問題
1. 事務併發一致性的讀問題
髒讀(Dirty Read)
髒讀也就是當前事務讀取到了其他事務還未提交的資料。我們舉個例子來看看:
Time | session A | session B |
---|---|---|
1 | -設定當前會話事務隔離級別為:讀未提交 set session transaction isolation level read uncommitted; |
|
2 | -設定當前會話事務隔離級別為:讀未提交 set session transaction isolation level read uncommitted; |
|
3 | start transaction; select * from account; |
|
4 | start transaction; select * from account; update account set user_name = '孫七' where id = 6; |
|
5 | select * from account; 查詢到了session B 中還沒有提交的資料 |
不可重複讀(Non-Repeatable Read)
不可重複讀是兩次讀取的結果不相同,和髒讀的區別就是不可重複讀讀到了其他事務提交後的資料。
舉個例項來看看:
Time | session A | session B |
---|---|---|
1 | -設定當前會話事務隔離級別為:讀已提交 set session transaction isolation level read committed; |
|
2 | -設定當前會話事務隔離級別為:讀已提交 set session transaction isolation level read committed; |
|
3 | start transaction; select * from account; |
|
4 | start transaction; select * from account; update account set user_name='趙趙' where id = 1; -此時已經發生修改 select * from account; |
|
5 | select * from account; |
|
6 | commit; |
|
7 | select * from account; 對於未提交的事務,查詢不到。相對於前一個隔離級別,杜絕了未提交事務修改對另外會話的影響。一旦另外的會話提交後,在進行查詢時,會查出相應的修改。即在一個完整會話中,前後查詢不同。 |
虛讀(Phantom)
所謂虛讀,也就是根據某些搜尋條件先後查詢資料庫,發現兩次查詢結果條數不同。和不可重複讀的區別就是不可重複讀的條數沒有變化,虛讀條數因為修改操作造成了條數變化。
下面舉個例項來說明:
Time | session A | session B |
---|---|---|
1 | -設定當前會話事務隔離級別為:可重複讀 set session transaction isolation level repeatable read; select @@transaction_isolation; |
|
2 | -設定當前會話事務隔離級別為:可重複讀 set session transaction isolation level repeatable read; select @@transaction_isolation; |
|
3 | start transaction; select * from account; |
|
4 | start transaction; select * from account; insert into account values(7,'劉八',100); -此時已經發生修改 select * from account; |
|
5 | select * from account; |
|
6 | commit; |
|
7 | select * from account; insert into account values(7,'劉八',100); 雖然此時查詢全表沒有發現新的資料,但是這個時候插入和session B 中相同的插入語句卻提示存在一條 key = 7 的語句,說明 session B 的操作確實影響到了 session A 。 這就是虛讀 |
|
2.MVCC的定義
全稱叫 Multi-Version Concurrency Control 的多版本併發控制。也就是指“維持一個資料的多個版本,使得讀寫操作沒有衝突”。
在說明 MVCC 原理前,先了解一下 InnoDB 的當前讀和快照讀:
當前讀
當前讀,也就是它讀取的是記錄的最新版本,而且還要保證其他併發事務不能修改當前記錄,實現方式是對讀取記錄進行加鎖。比如下面給出的都是當前讀
#共享鎖
select lock in share mode;
select for update;
#排他鎖
update
insert
delete
快照讀
快照讀是一種基於多版本併發控制(MVCC)的不加鎖讀取形式,由於多版本控制,使得快照讀讀到的可能不是資料的最新版本。比如不加鎖的select
操作就是快照讀。
二、MVCC 實現原理
1. 記錄的三個隱藏欄位
對於InnoDB
儲存引擎來說,它的每條聚簇索引記錄中都包含有以下三個隱藏欄位:
row_id
:隱藏主鍵。如果該資料表中沒有設定主鍵,就會自動生成一個6位元組的row_idroll_pointer
:回滾指標。 指向舊版本的 undo 日誌trx_id
:最近修改記錄的事務ID。記錄建立這條記錄或者最後一次修改該記錄的事務ID
如圖所示,row_id
表示該記錄生成的唯一隱式主鍵;trx_id
表示當前操作該記錄的事務ID;roll ptr
是指向上一版本的 undo 日誌的地址。
2. undo 日誌
undo log 就是回滾日誌,之前在事務的原子性中介紹過,它是保證事務原子性的機制。undo 日誌儲存的只有 insert
、delete
和 update
這些修改記錄的操作。下面舉個例子來幫助理解 undo log 的執行流程:
-
1.有一個事務編號為1 的事務向資料表中插入一條記錄,此時事務的狀態是:
- row_id:隱藏主鍵為1
- trx_id:建立該記錄的事務ID
- roll ptr:其上個版本的 undo 日誌為空
-
2.第二個事務編號為2的事務對該記錄進行修改,將name 欄位的 ethan 改為 bob。此時的操作有:
- 修改資料時,資料庫會對該行加排他鎖
- 把該行資料拷貝一份到 undo log 中
- 拷貝完成後,再修改該記錄name 欄位的 ethan 為 bob、修改隱藏欄位的事務ID 為2,回滾指標指向拷貝到 undo log 的記錄。
- 事務提交後釋放排他鎖
-
3.若第三個事務ID 為 3 對記錄的age 欄位進行了修改,將 20 修改為 18,則會出現:
- 事務3修改記錄時,資料庫對該行加排他鎖
- 資料庫將該行資料拷貝到 undo log 中
- 拷貝完畢後將該記錄欄位的 age 改成 18。修改隱藏事務ID 為 3,回滾指標指向上個版本的地址
- 事務提交後釋放鎖
從第二次我們會發現,undo log 中會出現多個版本的日誌。這就是版本鏈。鏈首是最新的舊記錄,鏈尾是最早的舊記錄。
3. ReadView(讀檢視)
ReadView 定義
ReadView 是事務進行快照讀那一刻,生成的一個資料系統當前的快照,記錄並維護當前活躍事務的id,並且這個 ID 值是遞增的。ReadView 的作用就是用來做可見性判斷,記錄當前事務執行快照讀時,建立的ReadView 能夠看到哪些版本的資料。
那麼是ReadView 是怎麼判斷的呢?
ReadView 版本可見性判斷規則
在ReadView 檢視中主要有四個重要的屬性:
trx_list
: 一個數值列表,當前系統活躍的讀寫事務的事務id 列表min_trx_id
:trx_list
中最小的事務id,trx_list
中的最小值max_trx_id
: 不是trx_list
的最大值,它是指系統應該分配給下一事務的事務id 值- 比如現在
trx_list
中有id 為1、2、3、4的事務,那麼max_trx_id
的值就是5
- 比如現在
creator_trx_id
:生成該 ReadView 事務的事務ID
在訪問某條記錄時,只需要按照下面的步驟來判斷記錄的某個版本是否可見:
- 1.(
trx_id
==creator_trx_id
)若被訪問版本的trx_id
值與當前 ReadView 中的creator_trx_id
相同,也就是說當前事務在訪問它自己修改過的記錄,該版本可以被當前事務訪問。 - 2.(
trx_id
<min_trx_id
)若被訪問版本的trx_id
值小於 ReadView 的min_trx_id
值,表明生成該版本的事務在當前事務生成ReadView 以前已經提交,該版本可以被當前事務訪問。 - 3.(
trx_id
>=max_trx_id
)若被訪問版本的trx_id
值大於或等於 ReadView 中的max_trx_id
,表明生成該版本的事務在當前事務生成 ReadView 後才開啟,該版本可以被當前事務訪問。 - 4.(
min_trx_id
<trx_id
<max_trx_id
)若被訪問版本的trx_id
值介於 ReadView 的min_trx_id
和max_trx_id
值之間,需要判斷trx_id
屬性值是否存在trx_list
中- 如果存在,說明建立 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問
- 如果不存在,說明建立 ReadView 時生成該版本的事務已經被提交,因此該版本可以被訪問
如果某個版本的資料對當前事務是不可見的,那就順著版本鏈找到下一個版本資料,繼續執行上面的步驟來判斷記錄的可見性,依次類推。知道版本中的最後一個版本。如果記錄的最後一個版本也不可見,意味著該條記錄對當前事務完全不可見,查詢結果就不包含該記錄。
舉例
下面讓我們來看看 MVCC 實現的具體流程是怎樣的,如下表是事務ID 為2 的事務對某行資料執行了快照讀,其中的列表如下:
事務1 | 事務2 | 事務3 | 事務4 |
事務開始 | 事務開始 | 事務開始 | 事務開始 |
… | … | … | 修改且已提交 |
進行中 | 快照讀 | 進行中 | |
… | … | … |
那麼此時ReadView 的引數值為:
- trx_list:事務1、2、3
- min_trx_id:事務1
- max_trx_id:事務5
- creator_trx_id:事務2
以事務4 版本為例,我們經過上述規則來比較看當前ReadView 能否看見事務4版本的資料:
- 經比較,只有第四條規則滿足。此時
trx_id
的值是介於min_trx_id
和max_trx_id
之間,但是不在 trx_list 中,因此經判斷該事務已經提交。所以該版本可以被訪問。
其實這個規則很好理解,在活躍事務列表裡面的,意味還沒有提交,除了建立ReadView 的當前事務,其他的事務都不可見。不在列表裡面的說明都已經提交,自然可以看見。如下圖除了黃色和紅色不可見,其他的版本都可見。
三、MVCC 如何解決髒讀、不可重複讀和虛讀
首先回顧一下MySQL的事務隔離級別中的檢視
- 讀未提交(RU):它是直接返回記錄的最新值,沒有檢視
- 讀已提交(RC):每次查詢都會建立一個ReadView
- 可重複讀(RR):這個ReadView是在事務啟動時建立,整個事務存在期間都用這個ReadView
- 序列化(serializable):直接用加鎖的方式來避免並行訪問
1.MVCC 解決髒讀
在讀已提交的MVCC 中,每次查詢都會建立一個 ReadView 。由於版本控制的可見性規則,使得當前事務只看的到已經提交的資料,所以這樣就避免了看見未提交的資料,從而解決了髒讀。
2.MVCC 解決不可重複讀
因為RC 級別每次查詢都會建立一個 ReadView ,所以對於已提交的事務,由於不能共用一個ReadView ,還是會造成兩次讀取過程中的不可重複讀。所以RR 級別通過使用從啟動到結束使用一個 ReadView, 來解決提交兩次查詢讀取不一致的現象。
3.MVCC 到底能不能解決虛讀?
先說結論:MVCC可以解決“快照讀”,無法解決“當前讀”
MVCC 可以解決“快照讀”
MVCC 可以解決如不加鎖的select
。原理就是MVCC 使用快照來控制版本資料讀取的範圍,從而在 RR 級別避免了虛讀。在我上面講虛讀的舉例就說明了,在select
快照讀時,沒有發現新的資料。但是新插入同樣的資料卻報錯,說明MVCC 無法徹底解決虛讀。
MVCC 無法解決“當前讀”
如果在select
上加鎖,使用“當前讀”,虛讀還是會出現。所以真正要解決虛讀,還是得用加鎖的形式來解決。所以一般而言,也只有序列化級別才能真正解決虛讀。
參考資料
https://www.cnblogs.com/kismetv/p/10331633.html
https://pdai.tech/md/db/sql-mysql/sql-mysql-mvcc.html
https://time.geekbang.org/column/article/68963
https://blog.csdn.net/qq_35590091/article/details/107734005
《MySQL是怎樣執行的-從根兒上理解MySQL》