一文講清,MySQL如何解決多事務(wù)并發(fā)問題
MySQL默認(rèn)事務(wù)隔離級(jí)別是repeatable-read(RR),臟讀、不可重復(fù)讀、幻讀,都不會(huì)發(fā)生。它是怎么做到的呢?
這就是由經(jīng)典的MVCC多版本并發(fā)控制機(jī)制做到的,MVCC的實(shí)現(xiàn),又是基于undo log版本鏈的。
前面講MySQL一行數(shù)據(jù)的存儲(chǔ)格式,講到了每行數(shù)據(jù)有兩個(gè)隱藏的字段:trx_id、roll_pointer。trx_id就是最近一次更新這條數(shù)據(jù)的事務(wù)id,roll_pointer指向了你更新這個(gè)事務(wù)之前生成的undo log。
假設(shè)有一個(gè)事務(wù)A(id = 50),插入了一條數(shù)據(jù)A,它的數(shù)據(jù)格式如下:

圖1?undo log版本鏈
接著事務(wù)B修改這條數(shù)據(jù)把值修改為B,事務(wù)B的id是58,此時(shí)會(huì)生成一個(gè)undo log記錄之前的值,roll_pointer指向這個(gè)undo log日志。

圖2?undo log版本鏈
假設(shè)再來了一個(gè)事務(wù)C,它的事務(wù)id是68,把數(shù)據(jù)值改為了C,此時(shí)undo log版本鏈就變成這樣了。

圖3?undo log版本鏈
事務(wù)執(zhí)行的時(shí)候,都會(huì)更新隱藏的字段trx_id和roll_pointer,同時(shí)之前多個(gè)數(shù)據(jù)快照對(duì)應(yīng)的undo log也會(huì)通過roll_pointer串聯(lián)起來,最終形成一個(gè)版本鏈。
基于undo log實(shí)現(xiàn)的ReadView
執(zhí)行一個(gè)事務(wù)的時(shí)候,會(huì)生成一個(gè)ReadView,里面包含這些東西:
m_ids,此時(shí)有哪些事務(wù)在MySQL中還沒有提交的事務(wù)id;
min_trx_id,m_ids里最小的;
max_trx_id,MySQL下一個(gè)要生成的事務(wù)id;
creator_trx_id,表示生成該ReadView的事務(wù)的事務(wù)id。
假設(shè)數(shù)據(jù)庫(kù)中有一行數(shù)據(jù),值是A,事務(wù)id是32,如下圖所示:

圖4 初始情況下,數(shù)據(jù)庫(kù)中有一行數(shù)據(jù)
此時(shí)有兩個(gè)事務(wù)并發(fā)過來執(zhí)行,事務(wù)A(id=45),事務(wù)B(id=59),事務(wù)A要去讀取這行數(shù)據(jù),事務(wù)B要去修改這行數(shù)據(jù)。
事務(wù)A開啟一個(gè)ReadView,此時(shí)它長(zhǎng)這樣:

圖5 ReadView
ReadView的m_ids包含事務(wù)A和事務(wù)B的兩個(gè)id,45和49,min_trx_id是45,max_trx_id是60,creator_trx_id就是45,就是事務(wù)A自己。
這時(shí)候事務(wù)A第一次查詢這行數(shù)據(jù),會(huì)去判斷一下當(dāng)前這行數(shù)據(jù)的trx_id是否小于ReadView中的min_trx_id。現(xiàn)在trx_id = 32,是小于ReadView里的min_trx_id=45的,說明你事務(wù)開啟之前,修改這行數(shù)據(jù)的事務(wù)早就提交了,所以此時(shí)可以查詢到這行數(shù)據(jù)。

圖6?事務(wù)A讀取數(shù)據(jù)
接著事務(wù)B開始修改這行數(shù)據(jù),事務(wù)B把值修改為B,然后這行數(shù)據(jù)的trx_id設(shè)置為自己的id,也就是59,同時(shí)roll_pointer指向了修改之前生成的undo log。

圖7?事務(wù)B修改數(shù)據(jù)
這時(shí)候事務(wù)A第二次查詢,發(fā)現(xiàn)此時(shí)數(shù)據(jù)行里的trx_id=59,大于ReadView里的min_trx_id=45,同時(shí)小于max_trx_id=60,說明更新這條數(shù)據(jù)的事務(wù),很可能跟自己差不多同時(shí)開啟。果然ReadView的m_ids里有45和59兩個(gè)事務(wù)id,事務(wù)B是跟自己并發(fā)執(zhí)行提交的,所以這行數(shù)據(jù)是不能查詢的。

圖8?事務(wù)A第二次讀數(shù)據(jù)
事務(wù)A不能查修改后的值,那怎么辦?順著undo log版本鏈查詢之前的版本!
于是就會(huì)查到trx_id=32的數(shù)據(jù),trx_id=32是小于ReadView里min_trx_id=45的,可以查出來。
看到這里,大家能不能猜想到多事務(wù)并發(fā)的時(shí)候,MySQL是如何解決那一堆問題的?就是通過undo log版本鏈 + ReadView解決的!
假設(shè)事務(wù)A執(zhí)行的過程中,事務(wù)C來更新這行數(shù)據(jù)為C,事務(wù)id=78。

圖9 事務(wù)C修改數(shù)據(jù)
此時(shí)事務(wù)A第三次去查,發(fā)現(xiàn)當(dāng)前數(shù)據(jù)的trx_id=78,比ReadView中的max_trx_id=60還大,說明這條數(shù)據(jù)是事務(wù)A開啟之后修改的,不應(yīng)該查到!
于是事務(wù)A順著undo log版本鏈往下找,先找到trx_id=59的數(shù)據(jù),上面分析過了,這條數(shù)據(jù)也不能查,于是繼續(xù)向undo log版本鏈向下找,最終返回trx_id=32的數(shù)據(jù)。
通過undo log版本鏈和ReadView,MySQL就可以保證你只能讀取到事務(wù)開啟前別的事務(wù)更新的值,和自己更新的值。
總的來說,就是一個(gè)事務(wù)只能讀取到事務(wù)id小于等于自己的數(shù)據(jù)。
讀已提交(RC)如何基于MVCC實(shí)現(xiàn)多事務(wù)并發(fā)控制?
只要你搞明白了上面的undo log版本鏈 + ReadView機(jī)制,對(duì)于RC、RR如何基于這套機(jī)制實(shí)現(xiàn)多版本并發(fā)控制,就非常好理解了。
首先,有一點(diǎn)非常重要,RC隔離級(jí)別下,一個(gè)事務(wù)每次發(fā)起查詢,都會(huì)生成一個(gè)ReadView。
假設(shè)庫(kù)里有一行數(shù)據(jù),trx_id=50,現(xiàn)在有兩個(gè)事務(wù)A(id=60),事務(wù)B(id=70)并發(fā)執(zhí)行。
事務(wù)B修改數(shù)據(jù)值為B,此時(shí)trx_id=70,如圖:

這時(shí)候,事務(wù)B還沒提交,事務(wù)A發(fā)起查詢,那么就會(huì)生成已給ReadView。

ReadView的m_ids里活躍的事務(wù)由60和70,此時(shí)事務(wù)A是無法查出事務(wù)B修改的值B的。于是順著版本鏈向下找,就找到trx_id=50的數(shù)據(jù)了。
接著,事務(wù)B提交了,事務(wù)A再次發(fā)起查詢,又生成了一個(gè)ReadView。

事務(wù)A再次基于ReadView查詢,發(fā)現(xiàn)這條數(shù)據(jù)的trx_id雖然在min_trx_id和max_trx_id之間,卻不在m_id里,說明事務(wù)B在生成本次ReadView之前已經(jīng)提交了,那么本次就可以查詢到事務(wù)B修改的這個(gè)值了。
RC隔離級(jí)別如何實(shí)現(xiàn)的,級(jí)別就講完了,其關(guān)鍵在于每次查詢都會(huì)生成一個(gè)新的ReadView。
可重復(fù)讀(RR)如何基于MVCC實(shí)現(xiàn)多事務(wù)并發(fā)控制?
可重復(fù)讀隔離級(jí)別下,解決了臟讀、不可重復(fù)讀、幻讀這些問題,它是如何實(shí)現(xiàn)的呢?
假設(shè),數(shù)據(jù)庫(kù)有一條數(shù)據(jù)trx_id=50,現(xiàn)在有兩個(gè)事務(wù)A(id=60),事務(wù)B(id= 70)并發(fā)執(zhí)行。
事務(wù)A發(fā)起一個(gè)查詢,會(huì)生成一個(gè)ReadView。

這個(gè)事務(wù)A基于這個(gè)ReadView去查這條數(shù)據(jù),會(huì)發(fā)現(xiàn)trx_id =50,小于ReadView里的min_trx_id,可以直接查出來。
接著事務(wù)B修改數(shù)據(jù)值為B,此時(shí)會(huì)修改trx_id=70,然后提交事務(wù)。

接著事務(wù)A第二次去查詢這條數(shù)據(jù),要知道它的ReadView沒有變。它會(huì)發(fā)現(xiàn)此時(shí)數(shù)據(jù)的trx_id=70在min_trx_id和max_trx_id之間,并且在m_ids中。那肯定不能查詢出來。于是順著undo log版本鏈向下找。
找到了trx_id=50的數(shù)據(jù),這條數(shù)據(jù)是事務(wù)A開啟查詢之前提交了,可以返回。
所有,RR隔離級(jí)別下,事務(wù)多次查詢,它的ReadView是不變的,這與RC是不同的,RC隔離級(jí)別下,每次查詢都會(huì)生成應(yīng)給ReadView。
RR隔離級(jí)別下,就這樣解決了不可重復(fù)讀問題。
由于RR隔離級(jí)別下,ReadView只會(huì)生成一次,那么你可以簡(jiǎn)單的理解成,MySQL多事務(wù)并發(fā)執(zhí)行時(shí),只能查詢到事務(wù)id比小于等于自己的數(shù)據(jù)。
其實(shí)幻讀的解決方法與解決不可重復(fù)讀原理是一樣的,筆者這里就不再多贅述,有興趣的同學(xué)可以自己整理下思路,在腦子里過一下它內(nèi)部的運(yùn)行流程。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號(hào)
好文章,我在看??
