之前講過(guò)事務(wù)隔離級(jí)別的時(shí)候提到過(guò),如果時(shí)可重復(fù)讀隔離級(jí)別,事務(wù)T啟動(dòng)的時(shí)候會(huì)創(chuàng)建一個(gè)視圖read-view,之后事務(wù)T執(zhí)行期間,即使有其他事務(wù)修改了數(shù)據(jù),事務(wù)T看到的仍然跟在啟動(dòng)時(shí)看到的一樣.也就是說(shuō),一個(gè)在可重復(fù)讀隔離級(jí)別下執(zhí)行的事務(wù),好像與世無(wú)爭(zhēng),不受外界影響.
但是,之前提到的行鎖,一個(gè)事務(wù)要更新一行,如果剛好有另外一個(gè)事務(wù)擁有這一行的行鎖,它又不能這么超然了,會(huì)被鎖住,進(jìn)入等待狀態(tài).問(wèn)題是,既然進(jìn)入了等待狀態(tài),那么等到這個(gè)事務(wù)自己獲取到行鎖要更更新數(shù)據(jù)的時(shí)候,它讀到的值又是什么呢?
舉個(gè)例子把,下面時(shí)一個(gè)只有兩行的表的初始話語(yǔ)句.
ctrate 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);
這里,我們需要注意的是事務(wù)的啟動(dòng)時(shí)機(jī).
begin/start transcation命令并不是一個(gè)事務(wù)的起點(diǎn).在執(zhí)行到他們之后的第一個(gè)操作InnoDB表的語(yǔ)句,事務(wù)才真正啟動(dòng).如果你想要馬上啟動(dòng)一個(gè)事務(wù),可以使用start transaction with consistent snapshot 這個(gè)命令.
第一種啟動(dòng)方式,一致性視圖是在執(zhí)行第一個(gè)快照讀語(yǔ)句時(shí)創(chuàng)建的;
第二種啟動(dòng)方式,一致性視圖是在執(zhí)行start transaction with consistent snapshot時(shí)創(chuàng)建的.
在這個(gè)例子中,事務(wù)C沒(méi)有顯式地使用begin/commit.表示這個(gè)update語(yǔ)句本身就是一個(gè)事務(wù),語(yǔ)句完成的時(shí)候會(huì)自動(dòng)提交.事務(wù)B在更新了行之后查詢;事務(wù)A在一個(gè)只讀事務(wù)中查詢,并且時(shí)間順序上時(shí)在事務(wù)B的查詢之后.
這時(shí),如果我告訴你事務(wù)B查到的K的值是3,而事務(wù)A查到的k的值是1,你是不是感到有點(diǎn)暈?zāi)?
在mysql里,有兩個(gè)"視圖"的概念:
一個(gè)是view.他是一個(gè)用查詢語(yǔ)句定義的虛擬表,在調(diào)用的時(shí)候執(zhí)行查詢語(yǔ)句并生成結(jié)果.創(chuàng)建視圖的語(yǔ)法是create view....., 而它的查詢方法與表一樣.
另一個(gè)是InnoDB在實(shí)現(xiàn)MVCC時(shí)用到一致性讀視圖,即consistent read view,用于支持RC(Read Committed,讀提交)和RR(Repeatable Read,可重復(fù)讀)隔離級(jí)別的實(shí)現(xiàn).
他沒(méi)有物理結(jié)構(gòu),作用時(shí)事務(wù)執(zhí)行期間用來(lái)定義"我能看到什么數(shù)據(jù)".
在"事務(wù)隔離:為什么你改了我還看不見(jiàn)?"中,跟你解釋過(guò)一邊MVCC的實(shí)現(xiàn)邏輯,今天為了說(shuō)明查詢和更新的區(qū)別.換一個(gè)方式來(lái)說(shuō)明,.把read view拆開(kāi),你可以結(jié)合則兩篇文章的說(shuō)明來(lái)更深一步的理解MVCC.
"快照"在MVCC里時(shí)怎么工作的?
在可重復(fù)讀隔離級(jí)別下,事務(wù)在啟動(dòng)的時(shí)候就"拍了個(gè)快照".注意.這個(gè)快照實(shí)際基于整個(gè)庫(kù)的.
這時(shí),你會(huì)說(shuō)看上去不太現(xiàn)實(shí)啊.如果一個(gè)庫(kù)有100G,那么我啟動(dòng)一個(gè)事務(wù),Mysql就要拷貝100G的數(shù)據(jù)出來(lái),這個(gè)過(guò)程得多慢啊,可是,我平時(shí)的事務(wù)執(zhí)行起來(lái)很快啊.
實(shí)際上,我們并不需要拷貝出這100G的數(shù)據(jù),我們先來(lái)看看這個(gè)快照是怎么實(shí)現(xiàn)的.
InnoDB里面每個(gè)事務(wù)有一個(gè)唯一的事務(wù)ID,叫做transaction id,他是在事務(wù)開(kāi)始的時(shí)候向innoDB的事務(wù)系統(tǒng)申請(qǐng)的,是按申請(qǐng)順序嚴(yán)格遞增的.
而每行數(shù)據(jù)也都是有多個(gè)版本的.每次事務(wù)更新數(shù)據(jù)的時(shí)候,都會(huì)生成一個(gè)新的數(shù)據(jù)版本,并且把transacation id 賦值給這個(gè)數(shù)據(jù)版本的事務(wù)id,記為row trx_id. 同時(shí),舊的數(shù)據(jù)版本要保留,并且在新的數(shù)據(jù)版本中,能后有信息可以直接拿到它,.
也就是說(shuō),數(shù)據(jù)表中的一行記錄,其實(shí)可能有多個(gè)版本(row),每個(gè)版本都有自己的row trx_id.
如圖2所示,就是一個(gè)記錄被多個(gè)事務(wù)連續(xù)更新后的狀態(tài).
圖中虛線框里是同一行數(shù)據(jù)的4個(gè)版本,當(dāng)前最新版本是V4,k的值是22,它是被transacation id 為25的事務(wù)更新的,因此它的row trx_id也是25.
你可能會(huì)問(wèn),前面的文章不是說(shuō),語(yǔ)句更新會(huì)生成undo log(回滾日志)嗎?那么.undo log在哪呢?
實(shí)際上,圖2中的三個(gè)虛線箭頭,就是undo log;而V1,V2,V3并不是物理上真實(shí)存在的,而是每次需要的時(shí)候給很具當(dāng)前版本和undo log計(jì)算出來(lái)的,比如,需要V2的時(shí)候,就是通過(guò)V4一次執(zhí)行U3,U2算出來(lái)的.
明白了多版本和row trx_id的概念后,我們?cè)賮?lái)想一下,InnoDB是怎么定于那個(gè)100G快照的.
按照可重復(fù)讀的定義,一個(gè)事務(wù)啟動(dòng)的時(shí)候,能夠看到所有已經(jīng)提交的事務(wù)結(jié)果,但是之后,這個(gè)事務(wù)執(zhí)行期間,.其他事務(wù)的更新對(duì)它不可見(jiàn).
因此,一個(gè)事務(wù)只需要在啟動(dòng)的時(shí)候聲明說(shuō),"以我啟動(dòng)的時(shí)刻為準(zhǔn),如果一個(gè)數(shù)據(jù)版本是我在啟動(dòng)之前生成的,就認(rèn);如果是在我啟動(dòng)后以后才生成的,我就不認(rèn),我必須要找到它的上一個(gè)版本".
當(dāng)然,如果"上一個(gè)版本"也是不可見(jiàn),那就得繼續(xù)往前找.還有,如果是這個(gè)事務(wù)自己更新的數(shù)據(jù),它自己還是要認(rèn)得.
在實(shí)現(xiàn)上,InnoDB為每個(gè)事務(wù)構(gòu)造了一組數(shù)組,用來(lái)保存這個(gè)事務(wù)啟動(dòng)瞬間,當(dāng)前在"活躍"的所有事務(wù)ID,"活躍"指的就是,啟動(dòng)了但還沒(méi)提交.
數(shù)組里面事務(wù)Id的最小值記為低水位,當(dāng)前系統(tǒng)里面已經(jīng)創(chuàng)建過(guò)的事務(wù)id的最大值加1記為高水位.
這個(gè)視圖數(shù)組和高水位,就組成了當(dāng)前事務(wù)的一致性視圖(read-view).
而數(shù)據(jù)版本的可見(jiàn)性規(guī)則,就是基于數(shù)據(jù)的row trx_id和這個(gè)一致性視圖的對(duì)比結(jié)果得到的.
這個(gè)視圖數(shù)組把所有的row trx_id分成了幾種不同的情況.
這樣,對(duì)于當(dāng)前事務(wù)的啟動(dòng)瞬間來(lái)說(shuō),一個(gè)數(shù)據(jù)版本的row trx_id,有一下幾種可能:
1.如果落在綠色部分,表示這個(gè)版本是已提交的事務(wù)或者是當(dāng)前事務(wù)自己生成的.這個(gè)數(shù)據(jù)是可見(jiàn)的.
2.如果落在紅色部分,表示這個(gè)版本是由將來(lái)啟動(dòng)的事務(wù)生成的,是肯定不可見(jiàn)的;
3.如果落在黃色部分,那就包括兩種情況
a.若row trx_id 在數(shù)組中,表示這個(gè)版本是由還沒(méi)提交的事務(wù)生成的,不可見(jiàn).
b.若row trx_id不在數(shù)組中,表示這個(gè)版本是已經(jīng)提交了的事務(wù)生成的.可見(jiàn).
比如,對(duì)于圖2中的數(shù)據(jù)來(lái)說(shuō),如果有一個(gè)事務(wù),它的低水位是18,那么當(dāng)它訪問(wèn)這一行數(shù)據(jù)時(shí),就會(huì)從V4通過(guò)U3計(jì)算出V3,所以在它看來(lái),這一行的值是11.
你看,有了這個(gè)聲明后,系統(tǒng)里面隨后發(fā)生的更新,是不是就跟這個(gè)事務(wù)看到的內(nèi)容無(wú)關(guān)了呢?因?yàn)橹蟮母?生成的版本一定屬于上面的2或者3(a)的情況,.而對(duì)它來(lái)說(shuō),這些新的數(shù)據(jù)版本是不存在的,所以這個(gè)事務(wù)的快照,就是"靜態(tài)"的了.
所以你現(xiàn)在知道了,InnoDB利用了"所有數(shù)據(jù)都有多個(gè)版本"的這個(gè)特性,實(shí)現(xiàn)了"秒級(jí)創(chuàng)建快照"的能力.
接下來(lái),我們繼續(xù)看一下圖1中的三個(gè)事務(wù),分析下事務(wù)A的語(yǔ)句返回的結(jié)果,為什么是k = 1.
這里,我們不妨做如下假設(shè):
1.事務(wù)A開(kāi)始前,系統(tǒng)里面只有一個(gè)活躍事務(wù)ID是99;
2.事務(wù)A,B,C的版本好分別是100,101,102,且當(dāng)前系統(tǒng)里只有四個(gè)事務(wù);
3.三個(gè)事務(wù)開(kāi)始前,(1,1)這一行數(shù)據(jù)的row trx_id是90.
這樣,事務(wù)A的視圖數(shù)組就是[99,100],事務(wù)B的視圖數(shù)組是[99,100,101],事務(wù)C的視圖數(shù)組是[99,100,101,102].
為了簡(jiǎn)化分析,我先把其他干擾語(yǔ)句去掉,只畫(huà)出跟事務(wù)A查詢邏輯有關(guān)的操作:
從圖中可以看到,第一個(gè)有效更新是事務(wù)C,把數(shù)據(jù)從(1,1)改成了(1,2).這時(shí)候,.這個(gè)數(shù)據(jù)的最新版本的row trx_id是102,而90這個(gè)版本已經(jīng)成為了歷史版本.
第二個(gè)有效更新是事務(wù)B,把數(shù)據(jù)從(1,2)改成了(1,3).這時(shí)候,這個(gè)數(shù)據(jù)的最新版本(即row trx_id)是101,而102又成為了歷史版本.
你可能注意到了,.在事務(wù)A查詢的時(shí)候,其實(shí)事務(wù)B還沒(méi)有提交,但是它生成的(1,3)這個(gè)版本已經(jīng)變成當(dāng)前版本了.但這個(gè)版本對(duì)事務(wù)A必須是不可見(jiàn)的.否側(cè)就變成臟讀了.
好,現(xiàn)在事務(wù)A要來(lái)讀數(shù)據(jù)了,它的視圖數(shù)組是[99,100].當(dāng)然了,讀數(shù)據(jù)都是從當(dāng)前版本讀起的.所以,事務(wù)A查詢語(yǔ)句的讀數(shù)據(jù)流程是這樣的:
找到(1,3)的時(shí)候,判斷出row trx_id = 101,比高水位大,處于紅色區(qū)域,不可見(jiàn);
接著,找到上一個(gè)歷史版本,一看row trx_id = 102,比高水位大,處于紅色區(qū)域,不可見(jiàn);
在往前找,終于找到了(1,1),它的row trx_id = 90,比低水位小,處于綠色區(qū)域,可見(jiàn).
這樣執(zhí)行下來(lái),雖然期間這一行數(shù)據(jù)被修改過(guò),但是事務(wù)A不論在什么時(shí)候查詢,看到這行數(shù)據(jù)的結(jié)果都是一致的,所以我們稱之為一致性讀.
這個(gè)判斷規(guī)則是從代碼邏輯直接轉(zhuǎn)譯過(guò)來(lái)的,但是正如你所見(jiàn),用于人肉分析可見(jiàn)性很麻煩.
所以,我來(lái)給你翻譯一下.一個(gè)數(shù)據(jù)版本,對(duì)于一個(gè)事務(wù)視圖來(lái)說(shuō),除了自己的更新總是可見(jiàn)以外,又三種情況:
1.版本未提交,不可見(jiàn);
2.版本已提交,但是是在視圖創(chuàng)建后提交的,不可見(jiàn).
3.版本已提交,而且是在視圖創(chuàng)建前提交的,可見(jiàn).
現(xiàn)在,我們用這個(gè)規(guī)則來(lái)判斷圖4中的查詢結(jié)果,事務(wù)A的查詢語(yǔ)句的視圖數(shù)組是在事務(wù)A啟動(dòng)的時(shí)候生成的,這時(shí)候:
(1,3)還沒(méi)提交,屬于情況1,不可見(jiàn).
(1,2)雖然提交了,但是是在視圖數(shù)組創(chuàng)建之后提交的,屬于情況2,不可見(jiàn);
(1,1)是在視圖數(shù)組創(chuàng)建之前提交的,可見(jiàn).
你看,去掉數(shù)字對(duì)比后,只用時(shí)間先后順序來(lái)判斷,分析起來(lái)是不是輕松多了.所以,后面我們就都用這個(gè)規(guī)則來(lái)分析.
更新邏輯
細(xì)心的同學(xué)可能有疑問(wèn)了:事務(wù)B的update語(yǔ)句,如果按照一致性讀,好像結(jié)果不對(duì)哦?
你看圖5中,事務(wù)B的視圖數(shù)組是先生成的,之后事務(wù)C才提交,不是應(yīng)該看不見(jiàn)(1,2)嗎,怎么能算出(1,3)來(lái)?
是的,如果事務(wù)B在更新之前查詢一次數(shù)據(jù),這個(gè)查詢返回的k的值確實(shí)是1.
但是,當(dāng)他要去更新數(shù)據(jù)的時(shí)候,就不能再在歷史版本上更新了,否則事務(wù)C的更新了,否則事務(wù)C的更新就丟失了.因此,事務(wù)B此時(shí)的set k = k + 1是在(1,2)的基礎(chǔ)上進(jìn)行的操作.
所以,這里就用到了這樣一條規(guī)則:更新數(shù)據(jù)都是先讀后寫的光戈,而這個(gè)讀,只能讀當(dāng)前的值彪薛,稱為“當(dāng)前讀”(current read)寂呛。
因此,在更新的時(shí)候伦籍,當(dāng)前讀拿到的數(shù)據(jù)是 (1,2)蓝晒,更新后生成了新版本的數(shù)據(jù) (1,3),這個(gè)新版本的 row trx_id 是 101帖鸦。
所以芝薇,在執(zhí)行事務(wù) B 查詢語(yǔ)句的時(shí)候,一看自己的版本號(hào)是 101作儿,最新數(shù)據(jù)的版本號(hào)也是101洛二,是自己的更新,可以直接使用,所以查詢得到的 k 的值是 3晾嘶。
這里我們提到了一個(gè)概念妓雾,叫作當(dāng)前讀。其實(shí)变擒,除了 update 語(yǔ)句外君珠,select 語(yǔ)句如果加鎖,也是當(dāng)前讀娇斑。
所以策添,如果把事務(wù) A 的查詢語(yǔ)句 select * from t where id=1 修改一下,加上 lock inshare mode 或 for update毫缆,也都可以讀到版本號(hào)是 101 的數(shù)據(jù)唯竹,返回的 k 的值是 3。下面這兩個(gè) select 語(yǔ)句苦丁,就是分別加了讀鎖(S 鎖浸颓,共享鎖)和寫鎖(X 鎖,排他鎖)旺拉。
select k from t where id? = 1 lock in share mode;
select k from k from t where id = 1 for update;
再往前一步,假設(shè)事務(wù)C不是馬上提交的,而是變成了下面的事務(wù)C`,會(huì)怎么樣呢?
事務(wù)C`的不同是,更新后并沒(méi)有馬上提交,在它提交前,事務(wù)B的更新語(yǔ)句先發(fā)起了.前面說(shuō)過(guò)了,雖然事務(wù)C`還沒(méi)提交,但是(1,2)這個(gè)版本也已經(jīng)生成了,并且是當(dāng)前的最新版本.那么,事務(wù)B的更新語(yǔ)句會(huì)怎么處理呢?
這時(shí)候,我們?cè)谏弦黄恼轮刑峤坏降?兩階段鎖協(xié)議"就要上場(chǎng)了.事務(wù)C`沒(méi)提交,也就是說(shuō)(1,2)這個(gè)版本上的寫鎖還沒(méi)釋放.而事務(wù)B是當(dāng)前讀,必須讀最新版本,而且必須加鎖,因此就被鎖住了,必須等到事務(wù)C`釋放這個(gè)鎖,才能繼續(xù)它的當(dāng)前讀.
到這里,我們把一致性讀,當(dāng)前讀和行鎖就串起來(lái)了.
現(xiàn)在,我們?cè)倩氐轿恼麻_(kāi)頭的問(wèn)題:事務(wù)的可重復(fù)讀的能力是怎么實(shí)現(xiàn)的?
可重復(fù)讀的核心就是一致性讀(consistent read);而事務(wù)更新數(shù)據(jù)的時(shí)候,只能用當(dāng)前讀.如果當(dāng)前的記錄的行鎖被其他事務(wù)占用的話,就需要進(jìn)入鎖等待.
而讀提交的邏輯和可重復(fù)讀的邏輯類似,他們最主要的區(qū)別是:
在可重復(fù)讀隔離級(jí)別下,只需要在事務(wù)開(kāi)始的時(shí)候創(chuàng)建一致性視圖,之后事務(wù)里的其他查詢都共用這個(gè)一致性視圖;
在讀提交隔離級(jí)別下,每一句語(yǔ)句執(zhí)行前都會(huì)重新算出一個(gè)新的視圖.
那么,我們?cè)倏匆幌?再讀提交隔離級(jí)別下,事務(wù)A和事務(wù)B的查詢語(yǔ)句查到的k,分別應(yīng)該是多少呢?
這里需要說(shuō)明一下,"start transaction with consistent snapshot" 的意思是從這個(gè)語(yǔ)句開(kāi)始,創(chuàng)建一個(gè)和持續(xù)整個(gè)事務(wù)的一致性快照.所以,在讀提交隔離級(jí)別下,這個(gè)用法就沒(méi)意義了,等效于普通的start transaction.
下面是讀提交時(shí)的狀態(tài)圖,可以看到這兩個(gè)查詢語(yǔ)句的創(chuàng)建視圖數(shù)組的時(shí)機(jī)發(fā)生了變化,就是圖中的read view框.(注意:這里,我們用的還是事務(wù)C的邏輯直接提交,而不是事務(wù)C`)
這時(shí),事務(wù)A的查詢語(yǔ)句的視圖數(shù)組是在執(zhí)行這個(gè)語(yǔ)句的時(shí)候創(chuàng)建的,時(shí)序上(1,2).(1,3)的生成時(shí)間都在創(chuàng)建這個(gè)視圖數(shù)組的時(shí)刻之前,.但是,在這個(gè)時(shí)刻:
(1,3)還沒(méi)提交,屬于情況1,不可見(jiàn);
(1,2)提交了,屬于情況3,可見(jiàn);
所以,這時(shí)候事務(wù)A查詢語(yǔ)句返回的是k = 2.
顯然地,事務(wù)B查詢結(jié)果k = 3.
小結(jié)
InnoDB的行數(shù)據(jù)有多個(gè)版本,每個(gè)數(shù)據(jù)版本有自己的row trx_id,每個(gè)事務(wù)或者語(yǔ)句有自己的一致性視圖.普通查詢語(yǔ)句是一致性讀.一致性讀根據(jù)roe trx_id和一致性視圖確定數(shù)據(jù)版本的可見(jiàn)性.
對(duì)于可重復(fù)讀,查詢只承認(rèn)在事務(wù)啟動(dòng)前就已經(jīng)提交完成的數(shù)據(jù).
對(duì)于讀提交,查詢只承認(rèn)在語(yǔ)句啟動(dòng)前就已經(jīng)提交完成的數(shù)據(jù);
而當(dāng)前讀,總是讀取已經(jīng)提交完成的最新版本.
你也可以想一下,為甚惡魔表結(jié)構(gòu)不支持可重復(fù)讀?這是因?yàn)楸斫Y(jié)構(gòu)沒(méi)有對(duì)應(yīng)的行數(shù)據(jù),也沒(méi)有row trx_id,因此只能遵循當(dāng)前讀的邏輯.
當(dāng)然,mysql8.0已經(jīng)可以把表結(jié)構(gòu)放在InnoDB字典里了,也許以后會(huì)支持表結(jié)構(gòu)的可重復(fù)讀.