MVCC 多版本並發控制


引言

    MVCC全稱為Multiversion concurrency control多版本並發控制,我們清楚Mysql的默認隔離級別是可重復讀,而Mysql實現可重復讀就是使用的MVCC多版本並發控制,通過每條數據的版本號(也可以叫做事務id)來實現不同事務之間的並發訪問,有點類似樂觀鎖,並不需要對每條數據都加鎖,而是通過版本號去控制。


臟讀、不可重復讀、幻讀

    在數據庫事務的並發訪問中,可能就會帶來臟讀、不可重復讀、幻讀等問題。

    臟讀

        臟讀就是指讀到別人未提交的事務,也就是別人修改了數據,但是沒有進行事務提交,而自己能夠讀到別人修改過的數據

        舉個例子:假設有兩個人A,B,

        1.首先B修改了A的賬戶余額由1000元修改成2000元,但是並沒有提交

        2.此時A去讀取自己的賬戶余額,發現變成了2000元,非常高興

        3.之后B發現自己修改的余額有問題,又將事務進行回滾,那么A賬戶的余額又變成1000元。

        以上,步驟2中A讀取的2000元就是臟數據,讀到了別人還沒有提交的數據


    不可重復讀

        不可重復讀是指在同一個事務中,多次讀取同一數據的結果不一樣。

        舉個例子:

        1.在事務1中,A賬戶讀取到自己的賬戶余額為1000元

        2.在事務2中,B將A賬戶的余額由1000元修改成2000元並提交事務

        3.還是在事務1中,A賬戶再次去讀取自己的賬戶余額變成了2000元

        以上,在同一個事務中,A賬戶兩次去讀同一批數據,發現讀到的結果不一樣,這就發生了不可重復讀


    幻讀

        幻讀是指一個事務對表中的數據進行了修改,同時,第二個事務新增了一條數據。那么此時,第一個事務去查發現自己表中還有沒有修改的數據行,也就是第二個事務新增的那條數據沒有被修改,出現了幻覺一樣

        舉個例子:

        1.在事務1中,讀取到所有員工的工資為1000元,有10條數據

        2.事務1,並對10條數據進行修改,修改成2000元。此時,事務2新增一個員工信息,工資為1000元並提交

        3.此時事務1中,讀到所有員工的工資,會發現有10條工資為2000元的,一條工資還是1000元的

        以上,明明自己修改了所有的數據,但是發現新增一條沒有被修改,出現幻覺一樣

    有人會覺得不可重復讀和幻讀有點類似,但是不可重復讀強調的是修改操作,也就是別人修改了數據,導致自己兩次讀出來的結果不一樣,而幻讀則強調的是數據的新增或刪除操作,也就是第 1 次和第 2 次讀出來的記錄數不一樣


事務隔離級別

    為了解決上面所提到的臟讀、不可重復讀、幻讀問題,Mysql提供了四種隔離級別:讀未提交(Read uncommit), 讀已提交(Read commit)、可重復讀(Repeatable Read)、串行化(Serializable)

    讀未提交(Read uncommit)

        該隔離級別,並沒有解決任何問題,是指即使事務修改了數據,沒有進行提交,其他事務也能查看得到,這樣的話就會讀到臟數據,

    讀已提交(Read commit)

        該隔離級別,能保證我們讀到的數據都是已經提交的數據,未提交的數據讀不到,但是這樣雖然能解決臟讀問題,但是會引起不可重復讀。在同一個事務中,第一次去讀同一數據,過一會兒,其他事務修改這批數據並提交事務,這時候我們再去讀同一數據,發現結果已經不一樣了,就是兩次讀取的結果不同,這種現象稱之為不可重復讀

    可重復讀(Repeatable Read)

        可重復讀能保證在同一個事務中,多次查詢同一數據結果一樣。也就是解決了不可重復讀,但是無法解決幻讀。后面再詳細講解Mysql是怎么實現可重復讀的,怎樣解決不可重復讀,為什么不能解決幻讀??

    串行化(Serializable)

        串行化跟它的名字一樣,每個事務將會被串行執行,也就不存在在自己的事務中,數據被其他事務所影響,因為同一時刻只有一個事務在執行,雖然這樣,臟讀、不可重復讀、幻讀問題都能得到解決,但是會嚴重降低執行效率,所以這種隔離級別很少別采用

    我們可以看到,四個隔離級別在解決問題是依次遞進的,而Mysql默認采用的隔離級別是可重復讀,下一節來重點分析下Mysql是怎樣利用MVCC機制實現可重復讀的


MVCC 多版本並發控制

    通常我們在並發的場景中,有時候讀操作會讀到寫操作還沒完全寫完的數據,也就會出現數據不一致問題,碰到這種場景,我們通常會想到通過加鎖操作來解決,來讀操作必須等待寫操作完全寫完后再能讀取數據,通過加鎖操作確實能夠得到解決,但是加鎖操作所帶來的效率問題將不是很高。而MVCC機制使用了一種不同的手段,通過給每條記錄加上版本號,來控制讀寫的並發執行。

    首先看一下圖中有一條記錄,后面三列是InnoDB的內部實現中為每一行數據增加了三個隱藏列用於實現MVCC,其中跟MVCC版本控制有關的是DB_TRX_ID和DB_ROLL_PTR兩列

        DB_TRX_ID:數據行的創建版本號,也可以叫做系統事務編號

        DB_ROLL_PTR:數據行的刪除版本號,里面包括回滾指針,需要通過該指針找到歷史數據

        DB_ROW_ID:行標識(隱藏單調自增id)

    下面通過幾個事務中的sql來講解MVCC是怎么通過版本號來實現可重復讀的。注意一點:begin/start transaction命令並不是事務的起點,這時候還不會向Mysql申請事務id,只有執行了第一個sql語句,才會向mysql申請事務id,mysql內部也是按照事務的啟動順序來分配事務id的

    假設數據庫的初始數據是這條,后面兩個null依次為創建版本號,刪除版本號(回滾指針)

    當執行查詢sql時會生成一致性視圖read-view(可以成為快照),這個視圖由所有查詢時已經開啟了事務但是沒有進行提交(未提交)的事務id數組(數組里最小的taxId為min_id)和已經創建的最大事務taxId(max_id)組成,有了這個視圖我們可以再根據undo日志中的數據跟視圖做比對得到符合條件的數據,最后得出來的結果就是我們sql查詢的結果

    每次執行sql查詢就會生成如上圖一樣的一致性視圖,

    假設在當前事務sql執行查詢時,前面已經開啟了2個事務,2個事務都未進行提交,並且它們的編號分別是100,200. 當前事務編號為300,這時min_id為100,因為min_id=100(未提交的事務id中的最小的一個事務id),而max_id=300(已經創建的事務中最大的一個事務id,不管是已經提交過的還是未提交的都算,最大的事務id),那么生成的read-view視圖為:[100, 200, 300] ,[100, 200, 300]所組成的數組所表示的是圖中紅色的部分

    假設還是兩個事務,編號不變,其中編號為100的事務已經提交,而200的事務未提交,那么min_id=200(因為min_id是未提交的最小事務id),max_id=300(還是不變),這樣生成的read-view:100 [200, 300],100所表示的就是綠色的部分,已提交的事務,因為小於min_id,而[200, 300]所組成的數組所表示的中間紅色的部分

    假設還是兩個事務,編號不變,而現在是編號為200的事務已經提交,而100的事務未提交,那么min_id=100,max_id=300,所組成的read-view: [100, 300] 200,

[100, 300]所組成的數組是未提交的事務,而200是已提交的事務,[100, 300] 200所表示的就是中間紅色的部分,即包括未提交事務,也包括提交事務


    版本比對規則:

        1. 如果數據的創建版本號tax_id<min_id的話,也就是落在綠色部分,表示這個數據就是已經被提交的數據,那么這個數據是對其它事務是可見的。

        2. 如果落在黃色部分(tax_id>max_id),表示這個版本是未開始事務的,這個是不太可能的,是肯定不可見的

        3. 如果落在紅色部分(min_id<=tax_id<=max_id),那么包括兩種情況

            a: 如果查找的row的tax_id在min_id和max_id組成的數組中,表示這個版本是由還沒提交的事務生成的,對其它事務來說是不可見的,當是對產生這條數據row的事務tax_id是可見的

            b:如果查找的row的tax_id不在數組中,則表示這個row的版本是已經提交的版本,是對其它事務可見的

    版本規則的比對非常重要,當我們掌握了版本的規則比對,那么只要拿着undo中的數據日志,跟一致性視圖去比對,按照規則進行篩選進行

    還需要注意一點的就是:在同一個事務中,后面的sql語句查詢都會復用事務第一次執行的查詢sql所創建的一致性視圖read-view,這樣的話就能保證后面的sql語句跟第一次查詢的sql語句查出來的結果一致,保證可重復讀

    下面通過幾個例子來加深一下:

    假設有四個事務A,B,C,編號分別為100,200,300。

        事務A首先開啟事務,並執行update user set age = 100 where id = 1, 並不進行提交事務,那么事務A產生的undo日志為

        最低下一條是初始數據,而上面一條執行了update語句會復制出一條,創建的版本號為該事務A的事務編號100,刪除版本號中的回滾指針則指向歷史數據,而初始數據的刪除版本號則表示刪除該數據的事務id,也就是100。注意一點:在undo日志中update語句是會復制一條新數據,並不會直接在原數據上做修改,而原數據則表示刪除,具有刪除版本號

        接下來事務B也進行update語句,update user set age = 200 where id = 1,但是跟事務A不同的是,事務B進行事務提交,所產生的undo日志:

        可以看到事務B產生了最新的一條數據,age被修改成了200,並且該row的事務tax_id為200.

        最后,事務C執行select語句,這里有兩種情況,如果事務C執行select語句是在事務B提交之后執行,那么事務C所產生的一致性視圖read-view那么就會是:[100, 300] 200,min_id=100是事務A未進行提交,最小未提交的事務id。max_id=300是所創建的最大事務id,也就是事務C的tax_id。我們看一下事務C執行select * from user where id = 1會找到undo日志中哪一條數據。首先MVCC會從undo中的最新一條記錄開始比對,最新一條數據是age=200,tax_id=200,根據版本比對規則,發現tax_id落在一致性視圖中的[100, 300]區間內,但是tax_id=200並不在min_id和max_id所組建的數組中,[100, 300]不包括200,根據規則3中的b,說明這條數據是對其它事務可見,那么就會返回age=200這條數據

        前面我們分析的是事務C執行select語句是在事務Bcommit提交后,假設一下如果事務C執行select語句是在事務B執行update語句之后,commit之前,那么事務C所產生的一致性視圖就會不同了,read-view: [100, 200, 300],min_id=100, max_id=300, 首先MVCC還是從undo日志中查找到最新的,age=200,tax_id=200的數據row,發現row的tax_id落在了區間[100, 200, 300]中,並且200在該數組中,那么根據規則3中的a,表示這個版本是對其它事務不可見的,后面接下來查找到age=100, tax_id=100。發現還是在區間[100, 200, 300]中,並且100在該數組中,也是不可見的,最后就會找到初始數據,初始數據的tax_id是要小於100的,根據規則1,落在綠色部分,是已提交的數據。如果事務B在事務C執行select語句后,執行了commit操作,也就是事務編號200進行了提交,理論上產生的一致性視圖read-view為[100, 300] 200,但是前面我們講過,在同一個事務中,后續的select語句會復用第一個select語句所產生的read-view,也就是事務C的read-view還是[100, 200, 300]這樣就保證后續執行select語句結果還是一樣,如果read-view變成了[100, 300] 200那么所查詢出來的結果肯定是不一樣的,也就是沒有保證可重復讀

    上面我只是列舉了一個簡單的例子,來講解MVCC通過版本號控制事務中數據的讀寫。網友可以列舉一些復雜的例子來進行驗證,通過上面的版本比對規則,看是否滿足我們的猜想

    MVCC機制只能解決可重復讀問題,不能完全的解決幻讀問題,為什么不能完全解決幻讀問題,首先mysql中分為快照讀和當前讀,快照讀也就是我們所執行的select語句,MVCC機制能夠解決select快照讀的幻讀。當前讀是指我們所執行的update, delete, insert語句,假設要update一條記錄,但是在另一個事務中已經delete掉這條數據並且commit了,如果update就會產生沖突,所以在update的時候需要知道最新的數據。所以update也會把最新數據也更新掉

    我們希望的結果是事務1只需要將后勤部更新為財務部就行,但是結果卻是:研發部也會被我們更新為財務部,兩條數據都被修改了。這種結果告訴我們其實在MySQL可重復讀的隔離級別中並不是完全解決了幻讀的問題,而是解決了讀數據情況下的幻讀問題。而對於修改的操作依舊存在幻讀問題,就是說MVCC對於幻讀的解決時不徹底的。

    如何解決幻讀

        如果需要徹底解決幻讀的話,也有兩個辦法:

                1. 一個是使用串行化隔離級別

                2. MVCC+next-key locks:next-key locks由record locks(索引加鎖) 和 gap locks(間隙鎖,每次鎖住的不光是需要使用的數據,還會鎖住這些數據附近的數據)

    實際上很多的項目中是不會使用到上面的兩種方法的,串行化讀的性能太差,而且其實幻讀很多時候是我們完全可以接受的


總結

    前面大概講述了事務並發執行所帶來的問題,mysql為了解決這些問題所設置的隔離級別,默認隔離級別是可重復讀,重點講解了MVCC機制,怎樣通過版本去實現可重復讀的,一致性快照。最后列舉了一下幻讀


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2025 CODEPRJ.COM