我在第 3 篇文章和你講事務隔離級別的時候提到過,若是是可重複讀隔離級別,事務 T 啓動的時候會建立一個視圖 read-view,以後事務 T 執行期間,即便有其餘事務修改了數
據,事務 T 看到的仍然跟在啓動時看到的同樣。也就是說,一個在可重複讀隔離級別下執行的事務,好像與世無爭,不受外界影響。mysql
可是,我在上一篇文章中,和你分享行鎖的時候又提到,一個事務要更新一行,若是恰好有另一個事務擁有這一行的行鎖,它又不能這麼超然了,會被鎖住,進入等待狀態。問
題是,既然進入了等待狀態,那麼等到這個事務本身獲取到行鎖要更新數據的時候,它讀到的值又是什麼呢?
我給你舉一個例子吧。下面是一個只有兩行的表的初始化語句。sql
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `k` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, k) values(1,1),(2,2);
圖 1 事務 A、B、C 的執行流程數組
這裏,咱們須要注意的是事務的啓動時機。bash
begin/start transaction 命令並非一個事務的起點,在執行到它們以後的第一個操做InnoDB 表的語句,事務才真正啓動。若是你想要立刻啓動一個事務,可使用 start
transaction with consistent snapshot 這個命令。spa
第一種啓動方式,一致性視圖是在第執行第一個快照讀語句時建立的; 第二種啓動方式,一致性視圖是在執行 start transaction with consistentsnapshot 時建立的。
還須要注意的是,在整個專欄裏面,咱們的例子中若是沒有特別說明,都是默認autocommit=1。翻譯
在這個例子中,事務 C 沒有顯式地使用 begin/commit,表示這個 update 語句自己就是一個事務,語句完成的時候會自動提交。事務 B 在更新了行以後查詢 ; 事務 A 在一個只讀3d
事務中查詢,而且時間順序上是在事務 B 的查詢以後。
這時,若是我告訴你事務 B 查到的 k 的值是 3,而事務 A 查到的 k 的值是 1,你是否是感受有點暈呢?日誌
因此,今天這篇文章,我其實就是想和你說明白這個問題,但願藉由把這個疑惑解開的過程,可以幫助你對 InnoDB 的事務和鎖有更進一步的理解。blog
在 MySQL 裏,有兩個「視圖」的概念:事務
一個是 view。它是一個用查詢語句定義的虛擬表,在調用的時候執行查詢語句並生成結果。建立視圖的語法是 create view … ,而它的查詢方法與表同樣。 另外一個是 InnoDB 在實現 MVCC 時用到的一致性讀視圖,即 consistent read view,用於支持 RC(Read Committed,讀提交)和 RR(Repeatable Read,可重複讀)隔離級別的實現。
它沒有物理結構,做用是事務執行期間用來定義「我能看到什麼數據」。
在第 3 篇文章《事務隔離:爲何你改了我還看不見?》中,我跟你解釋過一遍 MVCC的實現邏輯。今天爲了說明查詢和更新的區別,我換一個方式來講明,把 read view 拆
開。你能夠結合這兩篇文章的說明來更深一步地理解 MVCC。
在可重複讀隔離級別下,事務在啓動的時候就「拍了個快照」。注意,這個快照是基於整庫的。
這時,你會說這看上去不太現實啊。若是一個庫有 100G,那麼我啓動一個事務,MySQL就要拷貝 100G 的數據出來,這個過程得多慢啊。但是,我平時的事務執行起來很快啊。
實際上,咱們並不須要拷貝出這 100G 的數據。咱們先來看看這個快照是怎麼實現的。InnoDB 裏面每一個事務有一個惟一的事務 ID,叫做 transaction id。它是在事務開始的時
候向 InnoDB 的事務系統申請的,是按申請順序嚴格遞增的。
而每行數據也都是有多個版本的。每次事務更新數據的時候,都會生成一個新的數據版本,而且把 transaction id 賦值給這個數據版本的事務 ID,記爲 row trx_id。同時,舊的
數據版本要保留,而且在新的數據版本中,可以有信息能夠直接拿到它。
也就是說,數據表中的一行記錄,其實可能有多個版本 (row),每一個版本有本身的 rowtrx_id。
如圖 2 所示,就是一個記錄被多個事務連續更新後的狀態。
圖 2 行狀態變動圖
圖中虛線框裏是同一行數據的 4 個版本,當前最新版本是 V4,k 的值是 22,它是被transaction id 爲 25 的事務更新的,所以它的 row trx_id 也是 25。
一、undo log在哪呢?
你可能會問,前面的文章不是說,語句更新會生成 undo log(回滾日誌)嗎?那麼,undo log 在哪呢?
實際上,圖 2 中的三個虛線箭頭,就是 undo log;而 V一、V二、V3 並非物理上真實存在的,而是每次須要的時候根據當前版本和 undo log 計算出來的。好比,須要 V2 的時
候,就是經過 V4 依次執行 U三、U2 算出來。
明白了多版本和 row trx_id 的概念後,咱們再來想一下,InnoDB 是怎麼定義那個「100G」的快照的。
按照可重複讀的定義,一個事務啓動的時候,可以看到全部已經提交的事務結果。可是以後,這個事務執行期間,其餘事務的更新對它不可見。
所以,一個事務只須要在啓動的時候聲明說,
一、以我啓動的時刻爲準,若是一個數據版本是在我啓動以前生成的,就認;
二、若是是我啓動之後才生成的,我就不認,我必需要找到它的上一個版本」。
固然,若是「上一個版本」也不可見,那就得繼續往前找。還有,若是是這個事務本身更新的數據,它本身仍是要認的。
二、活躍的指的是?
在實現上, InnoDB 爲每一個事務構造了一個數組,用來保存這個事務啓動瞬間,當前正在「活躍」的全部事務 ID。「活躍」指的就是,啓動了但還沒提交。
數組裏面事務 ID 的最小值記爲低水位,當前系統裏面已經建立過的事務 ID 的最大值加 1記爲高水位。
這個視圖數組和高水位,就組成了當前事務的一致性視圖(read-view)。
而數據版本的可見性規則,就是基於數據的 row trx_id 和這個一致性視圖的對比結果獲得的。
這個視圖數組把全部的 row trx_id 分紅了幾種不一樣的狀況。
圖 3 數據版本可見性規則
這樣,對於當前事務的啓動瞬間來講,一個數據版本的 row trx_id,有如下幾種可能:
1. 若是落在綠色部分,表示這個版本是已提交的事務或者是當前事務本身生成的,這個數據是可見的;
2. 若是落在紅色部分,表示這個版本是由未來啓動的事務生成的,是確定不可見的;
3. 若是落在黃色部分,那就包括兩種狀況
a. 若 row trx_id 在數組中,表示這個版本是由還沒提交的事務生成的,不可見; b. 若 row trx_id 不在數組中,表示這個版本是已經提交了的事務生成的,可見。
好比,對於圖 2 中的數據來講,若是有一個事務,它的低水位是 18,那麼當它訪問這一行數據時,就會從 V4 經過 U3 計算出 V3,因此在它看來,這一行的值是 11。
你看,有了這個聲明後,系統裏面隨後發生的更新,是否是就跟這個事務看到的內容無關了呢?由於以後的更新,生成的版本必定屬於上面的 2 或者 3(a) 的狀況,而對它來講,這
些新的數據版本是不存在的,因此這個事務的快照,就是「靜態」的了。
因此你如今知道了,InnoDB 利用了「全部數據都有多個版本」的這個特性,實現了「秒級建立快照」的能力。
接下來,咱們繼續看一下圖 1 中的三個事務,分析下事務 A 的語句返回的結果,爲何是k=1。
這裏,咱們不妨作以下假設:
1. 事務 A 開始前,系統裏面只有一個活躍事務 ID 是 99;
2. 事務 A、B、C 的版本號分別是 100、10一、102,且當前系統裏只有這四個事務;
3. 三個事務開始前,(1,1)這一行數據的 row trx_id 是 90。
這樣,事務 A 的視圖數組就是 [99,100], 事務 B 的視圖數組是 [99,100,101], 事務 C 的視圖數組是 [99,100,101,102]。
爲了簡化分析,我先把其餘干擾語句去掉,只畫出跟事務 A 查詢邏輯有關的操做:
圖 4 事務 A 查詢數據邏輯圖
從圖中能夠看到,第一個有效更新是事務 C,把數據從 (1,1) 改爲了 (1,2)。這時候,這個數據的最新版本的 row trx_id 是 102,而 90 這個版本已經成爲了歷史版本。
第二個有效更新是事務 B,把數據從 (1,2) 改爲了 (1,3)。這時候,這個數據的最新版本(即 row trx_id)是 101,而 102 又成爲了歷史版本。
你可能注意到了,在事務 A 查詢的時候,其實事務 B 尚未提交,可是它生成的 (1,3) 這個版本已經變成當前版本了。但這個版本對事務 A 必須是不可見的,不然就變成髒讀了。
好,如今事務 A 要來讀數據了,它的視圖數組是 [99,100]。固然了,讀數據都是從當前版本讀起的。因此,事務 A 查詢語句的讀數據流程是這樣的:
這樣執行下來,雖然期間這一行數據被修改過,可是事務 A 不論在何時查詢,看到這行數據的結果都是一致的,因此咱們稱之爲一致性讀。
這個判斷規則是從代碼邏輯直接轉譯過來的,可是正如你所見,用於人肉分析可見性很麻煩。
因此,我來給你翻譯一下。一個數據版本,對於一個事務視圖來講,除了本身的更新老是可見之外,有三種狀況:
1. 版本未提交,不可見;
2. 版本已提交,可是是在視圖建立後提交的,不可見;
3. 版本已提交,並且是在視圖建立前提交的,可見。
如今,咱們用這個規則來判斷圖 4 中的查詢結果,事務 A 的查詢語句的視圖數組是在事務A 啓動的時候生成的,這時候:
(1,3) 還沒提交,屬於狀況 1,不可見;
(1,2) 雖然提交了,可是是在視圖數組建立以後提交的,屬於狀況 2,不可見;
(1,1) 是在視圖數組建立以前提交的,可見。
你看,去掉數字對比後,只用時間前後順序來判斷,分析起來是否是輕鬆多了。因此,後面咱們就都用這個規則來分析。
細心的同窗可能有疑問了:事務 B 的 update 語句,若是按照一致性讀,好像結果不對哦?
你看圖 5 中,事務 B 的視圖數組是先生成的,以後事務 C 才提交,不是應該看不見 (1,2)嗎,怎麼能算出 (1,3) 來?
圖 5 事務 B 更新邏輯圖
是的,若是事務 B 在更新以前查詢一次數據,這個查詢返回的 k 的值確實是 1。
可是,當它要去更新數據的時候,就不能再在歷史版本上更新了,不然事務 C 的更新就丟失了。所以,事務 B 此時的 set k=k+1 是在(1,2)的基礎上進行的操做。
因此,這裏就用到了這樣一條規則:更新數據都是先讀後寫的,而這個讀,只能讀當前的值,稱爲「當前讀」(current read)。
所以,在更新的時候,當前讀拿到的數據是 (1,2),更新後生成了新版本的數據 (1,3),這個新版本的 row trx_id 是 101。
因此,在執行事務 B 查詢語句的時候,一看本身的版本號是 101,最新數據的版本號也是101,是本身的更新,能夠直接使用,因此查詢獲得的 k 的值是 3。
這裏咱們提到了一個概念,叫做當前讀。其實,除了 update 語句外,select 語句若是加鎖,也是當前讀。
因此,若是把事務 A 的查詢語句 select * from t where id=1 修改一下,加上 lock inshare mode 或 for update,也均可以讀到版本號是 101 的數據,返回的 k 的值是 3。下
面這兩個 select 語句,就是分別加了讀鎖(S 鎖,共享鎖)和寫鎖(X 鎖,排他鎖)。
再往前一步,假設事務 C 不是立刻提交的,而是變成了下面的事務 C’,會怎麼樣呢?
圖 6 事務 A、B、C'的執行流程
事務 C’的不一樣是,更新後並無立刻提交,在它提交前,事務 B 的更新語句先發起了。前面說過了,雖然事務 C’還沒提交,可是 (1,2) 這個版本也已經生成了,而且是當前的
最新版本。那麼,事務 B 的更新語句會怎麼處理呢?
這時候,咱們在上一篇文章中提到的「兩階段鎖協議」就要上場了。事務 C’沒提交,也就是說 (1,2) 這個版本上的寫鎖還沒釋放。而事務 B 是當前讀,必需要讀最新版本,並且
必須加鎖,所以就被鎖住了,必須等到事務 C’釋放這個鎖,才能繼續它的當前讀。
圖 7 事務 B 更新邏輯圖(配合事務 C')
到這裏,咱們把一致性讀、當前讀和行鎖就串起來了。
如今,咱們再回到文章開頭的問題:
事務的可重複讀的能力是怎麼實現的?
可重複讀的核心就是一致性讀(consistent read);而事務更新數據的時候,只能用當前讀。若是當前的記錄的行鎖被其餘事務佔用的話,就須要進入鎖等待。
而讀提交的邏輯和可重複讀的邏輯相似,它們最主要的區別是:
在可重複讀隔離級別下,只須要在事務開始的時候建立一致性視圖,以後事務裏的其餘查詢都共用這個一致性視圖; 在讀提交隔離級別下,每個語句執行前都會從新算出一個新的視圖。
那麼,咱們再看一下,在讀提交隔離級別下,事務 A 和事務 B 的查詢語句查到的 k,分別應該是多少呢?
這裏須要說明一下,「start transaction with consistent snapshot; 」的意思是從這個語句開始,建立一個持續整個事務的一致性快照。因此,在讀提交隔離級別下,這個用法就
沒意義了,等效於普通的 start transaction。
下面是讀提交時的狀態圖,能夠看到這兩個查詢語句的建立視圖數組的時機發生了變化,就是圖中的 read view 框。(注意:這裏,咱們用的仍是事務 C 的邏輯直接提交,而不是事務 C’)
圖 8 讀提交隔離級別下的事務狀態圖
這時,事務 A 的查詢語句的視圖數組是在執行這個語句的時候建立的,時序上 (1,2)、(1,3) 的生成時間都在建立這個視圖數組的時刻以前。可是,在這個時刻:
(1,3) 還沒提交,屬於狀況 1,不可見; (1,2) 提交了,屬於狀況 3,可見
因此,這時候事務 A 查詢語句返回的是 k=2。顯然地,事務 B 查詢結果 k=3。
InnoDB 的行數據有多個版本,每一個數據版本有本身的 row trx_id,每一個事務或者語句有本身的一致性視圖。普通查詢語句是一致性讀,一致性讀會根據 row trx_id 和一致性視圖
肯定數據版本的可見性。
對於可重複讀,查詢只認可在事務啓動前就已經提交完成的數據; 對於讀提交,查詢只認可在語句啓動前就已經提交完成的數據;
而當前讀,老是讀取已經提交完成的最新版本。
你也能夠想一下,爲何表結構不支持「可重複讀」?這是由於表結構沒有對應的行數據,也沒有 row trx_id,所以只能遵循當前讀的邏輯。
固然,MySQL 8.0 已經能夠把表結構放在 InnoDB 字典裏了,也許之後會支持表結構的可重複讀。
又到思考題時間了。我用下面的表結構和初始化語句做爲試驗環境,事務隔離級別是可重複讀。如今,我要把全部「字段 c 和 id 值相等的行」的 c 值清零,可是卻發現了一
個「詭異」的、改不掉的狀況。請你構造出這種狀況,並說明其原理。
mysql> CREATE TABLE `t` ( `id` int(11) NOT NULL, `c` int(11) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB; insert into t(id, c) values(1,1),(2,2),(3,3),(4,4);
復現出來之後,請你再思考一下,在實際的業務開發中有沒有可能碰到這種狀況?你的應用代碼會不會掉進這個「坑」裏,你又是怎麼解決的呢?
你能夠把你的思考和觀點寫在留言區裏,我會在下一篇文章的末尾和你討論這個問題。感謝你的收聽,也歡迎你把這篇文章分享給更多的朋友一塊兒閱讀。
我在上一篇文章最後,留給你的問題是:怎麼刪除表的前 10000 行。比較多的留言都選擇了第二種方式,即:在一個鏈接中循環執行 20 次 delete from T limit 500。
確實是這樣的,第二種方式是相對較好的。
第一種方式(即:直接執行 delete from T limit 10000)裏面,單個語句佔用時間長,鎖的時間也比較長;並且大事務還會致使主從延遲。
第三種方式(即:在 20 個鏈接中同時執行 delete from T limit 500),會人爲形成鎖衝突。
這篇理論知識很豐富,須要先總結下:
1.innodb支持RC和RR隔離級別實現是用的一致性視圖(consistent read view)
2.事務在啓動時會拍一個快照,這個快照是基於整個庫的.
基於整個庫的意思就是說一個事務內,整個庫的修改對於該事務都是不可見的(對於快照讀的狀況)
若是在事務內select t表,另外的事務執行了DDL t表,根據發生時間,要嘛鎖住要嘛報錯(參考第六章)
3.事務是如何實現的MVCC呢?
(1)每一個事務都有一個事務ID,叫作transaction id(嚴格遞增)
(2)事務在啓動時,找到已提交的最大事務ID記爲up_limit_id。
(3)事務在更新一條語句時,好比id=1改成了id=2.會把id=1和該行以前的row trx_id寫到undo log裏,而且在數據頁上把id的值改成2,而且把修改這條語句的transaction id記在該行行頭
(4)再定一個規矩,一個事務要查看一條數據時,必須先用該事務的up_limit_id與該行的transaction id作比對,
若是up_limit_id>=transaction id,那麼能夠看.若是up_limit_id<transaction id,則只能去undo log裏去取。去undo log查找數據的時候,也須要作比對,必須up_limit_id>transaction id,才返回數據
4.什麼是當前讀,因爲當前讀都是先讀後寫,只能讀當前的值,因此爲當前讀.會更新事務內的up_limit_id爲該事務的transaction id
5.爲何rr能實現可重複讀而rc不能,分兩種狀況
(1)快照讀的狀況下,rr不能更新事務內的up_limit_id,而rc每次會把up_limit_id更新爲快照讀以前最新已提交事務的transaction id,則rc不能可重複讀(2)當前讀的狀況下,rr是利用record lock+gap lock來實現的,而rc沒有gap,因此rc不能可重複讀