一文讲清,MySQL如何解决多事务并发问题
MySQL默認事務隔離級別是repeatable-read(RR),臟讀、不可重復讀、幻讀,都不會發生。它是怎么做到的呢?
這就是由經典的MVCC多版本并發控制機制做到的,MVCC的實現,又是基于undo log版本鏈的。
前面講MySQL一行數據的存儲格式,講到了每行數據有兩個隱藏的字段:trx_id、roll_pointer。trx_id就是最近一次更新這條數據的事務id,roll_pointer指向了你更新這個事務之前生成的undo log。
假設有一個事務A(id = 50),插入了一條數據A,它的數據格式如下:
圖1?undo log版本鏈
接著事務B修改這條數據把值修改為B,事務B的id是58,此時會生成一個undo log記錄之前的值,roll_pointer指向這個undo log日志。
圖2?undo log版本鏈
假設再來了一個事務C,它的事務id是68,把數據值改為了C,此時undo log版本鏈就變成這樣了。
圖3?undo log版本鏈
事務執行的時候,都會更新隱藏的字段trx_id和roll_pointer,同時之前多個數據快照對應的undo log也會通過roll_pointer串聯起來,最終形成一個版本鏈。
基于undo log實現的ReadView
執行一個事務的時候,會生成一個ReadView,里面包含這些東西:
m_ids,此時有哪些事務在MySQL中還沒有提交的事務id;
min_trx_id,m_ids里最小的;
max_trx_id,MySQL下一個要生成的事務id;
creator_trx_id,表示生成該ReadView的事務的事務id。
假設數據庫中有一行數據,值是A,事務id是32,如下圖所示:
圖4 初始情況下,數據庫中有一行數據
此時有兩個事務并發過來執行,事務A(id=45),事務B(id=59),事務A要去讀取這行數據,事務B要去修改這行數據。
事務A開啟一個ReadView,此時它長這樣:
圖5 ReadView
ReadView的m_ids包含事務A和事務B的兩個id,45和49,min_trx_id是45,max_trx_id是60,creator_trx_id就是45,就是事務A自己。
這時候事務A第一次查詢這行數據,會去判斷一下當前這行數據的trx_id是否小于ReadView中的min_trx_id?,F在trx_id = 32,是小于ReadView里的min_trx_id=45的,說明你事務開啟之前,修改這行數據的事務早就提交了,所以此時可以查詢到這行數據。
圖6?事務A讀取數據
接著事務B開始修改這行數據,事務B把值修改為B,然后這行數據的trx_id設置為自己的id,也就是59,同時roll_pointer指向了修改之前生成的undo log。
圖7?事務B修改數據
這時候事務A第二次查詢,發現此時數據行里的trx_id=59,大于ReadView里的min_trx_id=45,同時小于max_trx_id=60,說明更新這條數據的事務,很可能跟自己差不多同時開啟。果然ReadView的m_ids里有45和59兩個事務id,事務B是跟自己并發執行提交的,所以這行數據是不能查詢的。
圖8?事務A第二次讀數據
事務A不能查修改后的值,那怎么辦?順著undo log版本鏈查詢之前的版本!
于是就會查到trx_id=32的數據,trx_id=32是小于ReadView里min_trx_id=45的,可以查出來。
看到這里,大家能不能猜想到多事務并發的時候,MySQL是如何解決那一堆問題的?就是通過undo log版本鏈 + ReadView解決的!
假設事務A執行的過程中,事務C來更新這行數據為C,事務id=78。
圖9 事務C修改數據
此時事務A第三次去查,發現當前數據的trx_id=78,比ReadView中的max_trx_id=60還大,說明這條數據是事務A開啟之后修改的,不應該查到!
于是事務A順著undo log版本鏈往下找,先找到trx_id=59的數據,上面分析過了,這條數據也不能查,于是繼續向undo log版本鏈向下找,最終返回trx_id=32的數據。
通過undo log版本鏈和ReadView,MySQL就可以保證你只能讀取到事務開啟前別的事務更新的值,和自己更新的值。
總的來說,就是一個事務只能讀取到事務id小于等于自己的數據。
讀已提交(RC)如何基于MVCC實現多事務并發控制?
只要你搞明白了上面的undo log版本鏈 + ReadView機制,對于RC、RR如何基于這套機制實現多版本并發控制,就非常好理解了。
首先,有一點非常重要,RC隔離級別下,一個事務每次發起查詢,都會生成一個ReadView。
假設庫里有一行數據,trx_id=50,現在有兩個事務A(id=60),事務B(id=70)并發執行。
事務B修改數據值為B,此時trx_id=70,如圖:
這時候,事務B還沒提交,事務A發起查詢,那么就會生成已給ReadView。
ReadView的m_ids里活躍的事務由60和70,此時事務A是無法查出事務B修改的值B的。于是順著版本鏈向下找,就找到trx_id=50的數據了。
接著,事務B提交了,事務A再次發起查詢,又生成了一個ReadView。
事務A再次基于ReadView查詢,發現這條數據的trx_id雖然在min_trx_id和max_trx_id之間,卻不在m_id里,說明事務B在生成本次ReadView之前已經提交了,那么本次就可以查詢到事務B修改的這個值了。
RC隔離級別如何實現的,級別就講完了,其關鍵在于每次查詢都會生成一個新的ReadView。
可重復讀(RR)如何基于MVCC實現多事務并發控制?
可重復讀隔離級別下,解決了臟讀、不可重復讀、幻讀這些問題,它是如何實現的呢?
假設,數據庫有一條數據trx_id=50,現在有兩個事務A(id=60),事務B(id= 70)并發執行。
事務A發起一個查詢,會生成一個ReadView。
這個事務A基于這個ReadView去查這條數據,會發現trx_id =50,小于ReadView里的min_trx_id,可以直接查出來。
接著事務B修改數據值為B,此時會修改trx_id=70,然后提交事務。
接著事務A第二次去查詢這條數據,要知道它的ReadView沒有變。它會發現此時數據的trx_id=70在min_trx_id和max_trx_id之間,并且在m_ids中。那肯定不能查詢出來。于是順著undo log版本鏈向下找。
找到了trx_id=50的數據,這條數據是事務A開啟查詢之前提交了,可以返回。
所有,RR隔離級別下,事務多次查詢,它的ReadView是不變的,這與RC是不同的,RC隔離級別下,每次查詢都會生成應給ReadView。
RR隔離級別下,就這樣解決了不可重復讀問題。
由于RR隔離級別下,ReadView只會生成一次,那么你可以簡單的理解成,MySQL多事務并發執行時,只能查詢到事務id比小于等于自己的數據。
其實幻讀的解決方法與解決不可重復讀原理是一樣的,筆者這里就不再多贅述,有興趣的同學可以自己整理下思路,在腦子里過一下它內部的運行流程。
有道無術,術可成;有術無道,止于術
歡迎大家關注Java之道公眾號
好文章,我在看??
新人創作打卡挑戰賽發博客就能抽獎!定制產品紅包拿不停!總結
以上是生活随笔為你收集整理的一文讲清,MySQL如何解决多事务并发问题的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: workbench透明设置_ansys
- 下一篇: 如何解决 IDEA 占用大量 CPU 导