專欄系列文章:MySQL系列專欄web
前面在 事務原子性之UndoLog 這篇文章中屢次提到過MVCC
這個東西了,咱們說執行DELETE
語句或者更新主鍵
的UPDATE
語句並不會當即把對應的記錄直接從頁面中刪除,而是將記錄頭的delete_mask
設置爲1
,作標記刪除,這主要就是爲MVCC
服務的。數據庫
而後在 事務基礎 這篇文章中介紹了併發事務會有 髒寫、髒讀、不可重複讀、幻讀
四個問題,髒寫
能夠經過樂觀鎖或悲觀鎖的方式來解決,髒讀、不可重複讀、幻讀
三個問題須要數據庫提供必定的事務隔離機制來解決,也就是事務的隔離性。markdown
SQL標準的事務隔離級別有四個,分別能解決併發事務的一些問題:併發
髒讀、不可重複讀、幻讀
說的都是併發讀取的問題,最簡單的方式就是給記錄加一把鎖,不論是更新、讀取記錄都須要競爭到這把鎖以後才能操做。但這種方式的併發性能可想而知會有多麼低。post
因而 InnoDB 就設計了MVCC
來解決併發讀取的問題,MVCC
就是多版本併發控制(Multi-Version Concurrency Control
)。在 RC
、RR
這兩種隔離級別下執行SELECT
查詢時,經過訪問記錄的版本鏈
,而不須要加鎖,這樣使得不一樣事務的讀-寫
操做能夠併發執行,從而提高數據庫的性能。性能
前面說 MVCC 是讀取記錄的版本鏈
實現的,這個版本鏈其實就是由undo log
造成的一個版本鏈條。url
以 事務原子性之UndoLog 文章中的這幅圖爲例,很直觀的能夠看到,增刪改產生的 undo log 經過old roll_pointer
連成一個單向鏈表,記錄中的隱藏列roll_pointer
則指向最新的一個undo log
,就是undo版本鏈的頭結點。spa
記錄中始終都是最新更新的值,可能更新這條記錄的事務還未提交:設計
對於使用 RU
隔離級別的事務來講,因爲能夠讀到未提交事務修改過的記錄,因此直接讀取記錄的最新版本就行了。3d
對於使用 SERIALIZABLE
隔離級別的事務來講,InnoDB 使用加鎖的方式來訪問記錄。這個後面再說。
對於使用 RC
、RR
隔離級別的事務來講,都必須保證讀到已提交
事務修改過的記錄,若是另外一個事務修改的記錄還未提交,是不能直接讀取記錄的最新版本的,此時就能夠沿着undo版本鏈查找當前事務可見的版本。
上一節說到在RC、RR
隔離級別下,要保證讀到已提交
事務修改過的記錄,就要在undo版本鏈上找到當前事務可見的版本。那如何判斷版本鏈上的哪一個版本是當前事務可見的呢?
InnoDB 設計了一個 ReadView
,在執行一個事務的時候就會建立一個ReadView
。
ReadView 有四個關鍵屬性:
m_ids
:在生成 ReadView 時當前系統中活躍的事務的事務ID列表。min_trx_id
:生成 ReadView 時當前系統中活躍的事務中最小的事務ID,也就是m_ids
中的最小值。max_trx_id
:生成 ReadView 時系統中分配給下一個事務的ID值,就是全局事務ID(Max Trx Id
),注意並非m_ids
中的最大值。creator_trx_id
:生成該 ReadView 的事務的事務ID。事務中只有在執行了增刪改操做時纔會分配一個事務ID,若是是一個只讀事務,那 creator_trx_id 默認就爲0
。有了ReadView
後,在事務中查詢的時候,就能夠沿着 undo 版本鏈查找當前事務可見的版本。這時 undo log 中的隱藏列 trx_id
就派上用場了,它表示產生這條 undo log 時的事務的事務ID。判斷此版本是否可訪問的依據就是用 undo log 中的 trx_id
屬性值與 ReadView 中的各個屬性作比較。
經過以下步驟來判斷版本是否可被訪問:
① 若是 trx_id
等於 creator_trx_id
,說明當前事務在訪問它本身修改過的記錄,因此該版本能夠被當前事務訪問。
② 若是 trx_id
小於 min_trx_id
,說明生成該版本的事務在當前事務生成 ReadView 前已經提交,因此該版本能夠被當前事務訪問。
③ 若是 trx_id
大於或等於max_trx_id
,說明生成該版本的事務在當前事務生成 ReadView 後纔開啓,因此該版本不能夠被當前事務訪問。
④ 若是 trx_id
在 min_trx_id
和 max_trx_id
之間,此時再判斷一下 trx_id
是否是在 m_ids
列表中,若是在,說明建立 ReadView 時生成該版本的事務仍是活躍的,該版本不能夠被訪問;若是不在,說明建立 ReadView 時生成該版本的事務已經被提交,該版本能夠被訪問。
大體的流程圖以下:
READ COMMITTED
和 REPEATABLE READ
隔離級別的區別就是它們生成ReadView
的時機不一樣。
READ COMMITTED
是每次查詢前都會生成一個獨立的 ReadView。REPEATABLE READ
則只在第一次查詢前生成一個 ReadView,以後的查詢都重複使用這個 ReadView。READ UNCOMMITTED
則不須要生成 ReadView,直接讀取行記錄的數據。仍是以以前的 account
表爲例,下面按照操做發生的時間順序來進行說明。
一、T1
如今 account 表中的初始狀態以下,最後更新這條數據的事務ID爲100
,card 的值爲 AA
。
系統下一個要分配的事務ID爲150
,而後系統有兩個事務正在運行,事務ID分別爲130、135
。
二、T2
此時新開一個事務A
,查詢ID=1的數據,就會生成一個 ReadView 以下圖所示:
此時會先判斷記錄中的 trx_id(100) 與 creator_trx_id(0) 是否相等,這個條件不知足;
接着判斷 trx_id(100) < min_trx_id(130),這個條件知足,那就直接讀取這行數據。
此時在事務A
中查詢 balance=0 的數據,也只會返回1
條數據。
三、T3
接着另外一個事務B
(trx_id=150)更新這條數據,將 AA 更新爲 BB,事務還未提交。
接着在事務A
中再次查詢ID=1的這條數據。
RC
隔離級別下,會生成一個新的 ReadView:先判斷行記錄,發現 trx_id(150) 在 min_trx_id(130) 和 max_trx_id(160) 之間,同時在
m_ids(130,135,150) 列表中,因此記錄行上的數據對本事務不可見;而後繼續對比以前的版本,發現 AA 這條版本的 trx_id(100) < min_trx_id(130),因此就返回 AA 這個版本。因此在 RC
隔離級別下屢次讀取,看不到別的事務未提交
的更新,可避免髒讀
的問題。
RR
隔離級別下,ReadView 不變:先判斷行記錄,發現 trx_id(150) 等於 max_trx_id(150),因此記錄行上的數據對本事務不可見;而後繼續對比以前的版本,發現 AA 這條版本的 trx_id(100) < min_trx_id(130),因此就返回 AA 這個版本。因此在 RR
隔離級別下屢次讀取,看不到別的事務未提交
的更新,可避免髒讀
的問題。
四、T4
接着事務B
提交了更新。
接着在事務A
中再次查詢ID=1的這條數據。
RC
隔離級別下,會生成一個新的 ReadView:先判斷行記錄,發現 trx_id(150) 在 min_trx_id(130) 和 max_trx_id(165) 之間,同時不在
m_ids(130,135) 列表中,因此記錄行上的數據對本事務可見,返回 BB 這個版本。因此在 RC
隔離級別下屢次讀取,是能夠看到別的事務已提交
的更新,會有不可重複讀
的問題。
RR
隔離級別下,ReadView 不變:此時的判斷跟上一步中的判斷是同樣的,最後也是返回 AA 個版本。因此在 RR
隔離級別下屢次讀取,看不到別的事務已提交
的更新,避免了不可重複讀
的問題。
五、T5
接着一個新的事務(trx_id=175)更新ID=1這條數據,將 BB 更新爲 CC,同時還插入了ID=2這條數據,且事務已提交。
這時在事務A
中查詢 balance=0 的數據。
RC
隔離級別下,會生成一個新的 ReadView:查詢ID=1這行記錄時,先判斷行版本,因爲 trx_id(175) 在 min_trx_id(170) 和 max_trx_id(200) 之間,且不在 m_ids(170,180) 列表中,因此返回 CC 這個版本。查詢ID=2這行記錄時,一樣的判斷過程,會返回 MM 這個版本。最終查詢返回2
條數據,而最開始查詢只返回1
條數據,因此在 RC
隔離級別下屢次讀取,會有幻讀
的問題。
RR
隔離級別下,ReadView 不變:查詢ID=1這行記錄時,最終會沿着版本鏈找到 AA 這個版本。查詢ID=2這行記錄時,trx_id(175) > max_trx_id(150),因此ID=2這行記錄不匹配。最終查詢只返回1
條數據,因此在 RR
隔離級別下屢次讀取,不會有幻讀
的問題。
六、T6
接着一個新的事務(trx_id=205)刪除了ID=1這條數據,但刪除的時候並非真正的刪除,只是將delete_mask
標記爲 1
。
接着在事務A
中再次查詢ID=1的這條數據。
因爲行記錄中 delete_mask
標記爲 1
了,是不能被查詢的,因此只能沿着版本鏈查詢以前的版本。以後的匹配過程跟前面的描述是相似的,就不在贅述了。在 RC
隔離級別下,會返回 trx_id=175,值爲 CC 這個版本。在 RR
隔離級別下,會返回 trx_id=100,值爲 AA 這個版本。
從上面示例的演示過程就能夠看出,MVCC 就是經過 undo log 版本鏈 + ReadView 實現的一套併發讀取的機制。
在 READ COMMITTD
隔離級別下,每次查詢都生成一個新的 ReadView,不能讀到別的事務未提交的修改,所以解決了 髒讀
的問題。可是能讀取到別的事務已提交的修改,會有 不可重複讀、幻讀
的問題。
在 REPEATABLE READ
隔離級別下,只在第一次查詢時生成一個 ReadView,以後的查詢都重複使用這個 ReadView。別的事務未提交、已提交、新插入的修改都讀取不到,所以解決了 髒讀、不可重複讀、幻讀
的問題。
前面介紹 undo log 的文章說過,執行 DELETE 語句或者更新主鍵的 UPDATE 語句並不會當即把對應的記錄徹底從頁面中刪除,而是將 delete_mask
設置爲 1,作標記刪除。這時就清楚是爲何了,這主要就是爲MVCC服務的,由於可能有其它併發運行的事務,要經過版本鏈讀取當前事務可見的版本。
有一點須要注意的是,前面的示例中的查詢都是簡單的SELECT
查詢,這種就是讀取undo版本鏈上的一個快照版本,能夠稱爲快照讀
或一致性非鎖定讀
。因爲是讀取的快照,所以在RR
隔離級別下能夠避免幻讀的發生。
但若是是INSERT、DELETE、UPDATE
語句,例以下面的SQL,這個 UPDATE 語句會更新 balance=0 的記錄,這種方式就稱爲當前讀
,讀取的是最新的數據。當前讀
能讀取到別的事務已提交的修改,就可能會產生幻讀的問題。
UPDATE account SET balance=100 WHERE balance = 0;
複製代碼
例如,在默認RR
隔離級別下,按以下順序執行SQL,Session B 兩次普通查詢的結果都是同樣的,沒有幻讀的問題。這是由於 Session B 第二次查詢讀取的是快照版本,即快照讀
,不會讀取到別的事務提交的修改。
Timeline | Session A | Session B | Session C |
---|---|---|---|
t1 | TUNCATE TABLE account; INSERT INTO account(card) VALUES ('AA'); |
||
t2 | BEGIN; | BEGIN; | |
t3 | SELECT * FROM account WHERE balance=0; (返回AA這條記錄) |
||
t4 | INSERT INTO account(card) VALUES ('BB'); | ||
t5 | COMMIT; | ||
t6 | SELECT * FROM account WHERE balance=0; (返回AA這條記錄) |
||
u7 | COMMIT; |
再按照以下順序執行SQL,Session B 第一次查詢 balance=0 的數據只有AA這一條,而後更新 balance=0 的數據,按理來講只更新一條纔對,會發現更新了兩條數據,並且再次查詢返回了AA、BB這兩條數據,此時就產生了幻讀的問題。這是由於中間那次更新是當前讀
,更新時的查詢能夠讀到其它事務提交的更新,此時MVCC是沒法解決這個問題的。
Timeline | Session A | Session B | Session C |
---|---|---|---|
t1 | TUNCATE TABLE account; INSERT INTO account(card) VALUES ('AA'); |
||
t2 | BEGIN; | BEGIN; | |
t3 | SELECT * FROM account WHERE balance=0; (返回AA這條記錄) |
||
t4 | INSERT INTO account(card) VALUES('BB') | ||
t5 | COMMIT; | ||
t6 | UPDATE account SET balance=100 WHERE balance=0; (會看到更新了兩行數據) |
||
t7 | SELECT * FROM account WHERE balance=100; (返回AA、BB這兩條記錄) |
那當前讀這種問題如何解決呢?這就要用到下篇文章中介紹的鎖
了。