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

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

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

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

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

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

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

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

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

圖9 事務(wù)C修改數(shù)據(jù)
此時事務(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ù)更新的值,和自己更新的值。
總的來說,就是一個事務(wù)只能讀取到事務(wù)id小于等于自己的數(shù)據(jù)。
讀已提交(RC)如何基于MVCC實現(xiàn)多事務(wù)并發(fā)控制?
只要你搞明白了上面的undo log版本鏈 + ReadView機(jī)制,對于RC、RR如何基于這套機(jī)制實現(xiàn)多版本并發(fā)控制,就非常好理解了。
首先,有一點非常重要,RC隔離級別下,一個事務(wù)每次發(fā)起查詢,都會生成一個ReadView。
假設(shè)庫里有一行數(shù)據(jù),trx_id=50,現(xiàn)在有兩個事務(wù)A(id=60),事務(wù)B(id=70)并發(fā)執(zhí)行。
事務(wù)B修改數(shù)據(jù)值為B,此時trx_id=70,如圖:

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

ReadView的m_ids里活躍的事務(wù)由60和70,此時事務(wù)A是無法查出事務(wù)B修改的值B的。于是順著版本鏈向下找,就找到trx_id=50的數(shù)據(jù)了。
接著,事務(wù)B提交了,事務(wù)A再次發(fā)起查詢,又生成了一個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修改的這個值了。
RC隔離級別如何實現(xiàn)的,級別就講完了,其關(guān)鍵在于每次查詢都會生成一個新的ReadView。
可重復(fù)讀(RR)如何基于MVCC實現(xiàn)多事務(wù)并發(fā)控制?
可重復(fù)讀隔離級別下,解決了臟讀、不可重復(fù)讀、幻讀這些問題,它是如何實現(xiàn)的呢?
假設(shè),數(shù)據(jù)庫有一條數(shù)據(jù)trx_id=50,現(xiàn)在有兩個事務(wù)A(id=60),事務(wù)B(id= 70)并發(fā)執(zhí)行。
事務(wù)A發(fā)起一個查詢,會生成一個ReadView。

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

接著事務(wù)A第二次去查詢這條數(shù)據(jù),要知道它的ReadView沒有變。它會發(fā)現(xiàn)此時數(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隔離級別下,事務(wù)多次查詢,它的ReadView是不變的,這與RC是不同的,RC隔離級別下,每次查詢都會生成應(yīng)給ReadView。
RR隔離級別下,就這樣解決了不可重復(fù)讀問題。
由于RR隔離級別下,ReadView只會生成一次,那么你可以簡單的理解成,MySQL多事務(wù)并發(fā)執(zhí)行時,只能查詢到事務(wù)id比小于等于自己的數(shù)據(jù)。
其實幻讀的解決方法與解決不可重復(fù)讀原理是一樣的,筆者這里就不再多贅述,有興趣的同學(xué)可以自己整理下思路,在腦子里過一下它內(nèi)部的運行流程。
有道無術(shù),術(shù)可成;有術(shù)無道,止于術(shù)
歡迎大家關(guān)注Java之道公眾號
好文章,我在看??
