MYSQL专题-MVCC多版本并发控制
MVCC,全稱Multi-Version Concurrency Control,即多版本并發控制。MVCC是一種并發控制的方法,一般在數據庫管理系統中,實現對數據庫的并發訪問,在編程語言中實現事務內存。MVCC在MySQL InnoDB中的實現主要是為了提高數據庫并發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發讀。
基礎概述
數據庫并發場景大致分為三種:
- 讀-讀:不存在任何問題,也不需要并發控制
- 讀-寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀
- 寫-寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失
多版本并發控制(MVCC)是一種用來解決讀-寫沖突的無鎖并發控制,也就是為事務分配單向增長的時間戳,為每個修改保存一個版本,版本與事務時間戳關聯,讀操作只讀該事務開始前的數據庫的快照。 MVCC可以為數據庫解決以下問題:
- 在并發讀寫數據庫時,可以做到在讀操作時不用阻塞寫操作,寫操作也不用阻塞讀操作,提高了數據庫并發讀寫的性能
- 可以解決臟讀,幻讀,不可重復讀等事務隔離問題,但不能解決更新丟失問題
既然MVCC可以解決數據庫的并發的相關問題,那對于其原理的理解就很重要。不過在學習MVCC多版本并發控制之前,我們必須先了解一下,什么是MySQL InnoDB下的當前讀和快照讀。
-
當前讀
- 像select lock in share mode(共享鎖), select for update, update, insert ,delete(排他鎖)這些操作都是一種當前讀,
- 它讀取的是記錄的最新版本,所以叫當前讀。讀取時還要保證其他并發事務不能修改當前記錄,會對讀取的記錄進行加鎖。
-
快照讀
- 像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;
- 快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀;
- 出現快照讀的原因,是基于提高并發性能的考慮,快照讀的實現是基于多版本并發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;
- 因為基于多版本,即快照讀可能讀到的并不一定是數據的最新版本,而有可能是之前的歷史版本。
說白了MVCC就是為了實現讀-寫沖突不加鎖,而這個讀指的就是快照讀, 而非當前讀,當前讀實際上是一種加鎖的操作,是悲觀鎖的實現。那么當前讀,快照讀和MVCC的到底有什么關系呢?準確的說,MVCC多版本并發控制指的是 “維持一個數據的多個版本,使得讀寫操作沒有沖突” 這么一個概念。而在MySQL中,實現這么一個MVCC概念,我們就需要MySQL提供具體的功能去實現它,而快照讀就是MySQL為我們實現MVCC理想模型的其中一個具體非阻塞讀功能。而相對而言,當前讀就是悲觀鎖的具體功能實現。要說的再細致一些,快照讀本身也是一個抽象概念,再深入研究。MVCC模型在MySQL中的具體實現則是由 3個隱式字段,undo日志 ,Read View 等去完成的,這個會在下面的MVCC實現原理中具體講解。
有了MVCC,我們可以形成兩個組合:
- MVCC + 悲觀鎖
- MVCC解決讀寫沖突,悲觀鎖解決寫寫沖突
- MVCC + 樂觀鎖
- MVCC解決讀寫沖突,樂觀鎖解決寫寫沖突。這種組合的方式就可以最大程度的提高數據庫并發性能,并解決讀寫沖突,和寫寫沖突導致的問題。
MVCC的實現原理
MVCC的目的就是多版本并發控制,在數據庫中的實現,就是為了解決讀寫沖突,它的實現原理主要是依賴記錄中的 3個隱式字段,undo日志 ,Read View 來實現的。
隱式字段
每行記錄其實除了我們在數據庫中定義的列之外,每一行中還包含了幾個數據庫隱藏列,分別是DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID。假設有一張person表,里面包含name和age兩個字段,插入一條記錄如下圖,DB_ROW_ID是數據庫默認為該行記錄生成的唯一隱式主鍵,DB_TRX_ID是當前操作該記錄的事務ID,而DB_ROLL_PTR是一個回滾指針,用于配合undo日志,指向上一個舊版本,這三個字段在實際數據庫中是看不到的。
-
DB_TRX_ID
- 6byte,一個事務對某個表執行了增、刪、改操作,分配這條記錄的事務ID(最近修改(修改/插入)事務ID);
- 對于只讀事務來說,只有在它第一次對某個用戶創建的「臨時表執行增、刪、改操作」時才會為這個事務分配一個事務id,否則的話是不分配事務id的;
- 對于讀寫事務來說,只有在它「第一次對某個表(包括用戶創建的臨時表)執行增、刪、改操作」時才會為這個事務分配一個事務id,否則的話也是不分配事務id的;
- 有的時候雖然我們開啟了一個讀寫事務,但是在這個事務中全是查詢語句,并沒有執行增、刪、改的語句,那也就意味著這個事務并不會被分配一個事務id。
-
DB_ROLL_PTR
- 7byte,回滾指針,指向這條記錄的上一個版本(存儲于rollback segment里),即指向該記錄對應的undo log。
-
DB_ROW_ID
- DB_ROW_ID是6byte,行記錄的唯一標志,這一列不是必須的;
- MySQL會優先使用用戶自定義主鍵作為主鍵,如果用戶沒有定義主鍵,則選取一個Unique鍵作為主鍵,如果表中連Unique鍵都沒有定義的話,則InnoDB會為表默認添加一個名為DB_ROW_ID的隱藏列作為主鍵;
- 只有在表中既沒有定義主鍵,也沒有申明唯一索引的情況MySQL才會添加這個隱藏列。
-
實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除并不代表真的刪除,而是刪除flag變了
undo日志
對于undo日志的具體介紹之前寫過文章MYSQL專題-MySQL三大日志binlog、redo log和undo log,大家想要更好的了解可以去看看,這里再做一下簡單介紹。undo log主要分為兩種,insert undo log和update undo log。
-
insert undo log
- 事務在insert新記錄時產生的undo log;
- 只在事務回滾時需要,并且在事務提交后可以被立即丟棄。
-
update undo log
- 事務在進行update或delete時產生的undo log;
- 不僅在事務回滾時需要,在快照讀時也需要,不能隨便刪除。只有在快速讀或事務回滾不涉及該日志時,對應的日志才會被purge線程統一清除。
前面提到,還有一個刪除flag隱藏字段。為了實現InnoDB的MVCC機制,更新或者刪除操作都只是設置一下老記錄的deleted_bit,并不真正將過時的記錄刪除:
- 為了節省磁盤空間,InnoDB有專門的purge線程來清理deleted_bit為true的記錄;
- 為了不影響MVCC的正常工作,purge線程自己也維護了一個read view(這個read view相當于系統中最老活躍事務的read view);
- 如果某個記錄的deleted_bit為true,并且DB_TRX_ID相對于purge線程的read view可見,那么這條記錄一定是可以被安全清除的。
我們以實際例子來看一下它的執行流程。比如有個事務往person表插入一條新記錄,記錄如下,name為Jack, age為25歲,隱式主鍵是1,我們假設事務ID為0,和回滾指針為NULL:
現在又來了一個事務對該記錄的name做出了修改,改為Jim,則它過程大致如下:
則此時的對應關系如下圖所示:
又來了個事務修改person表的同一個記錄,將age修改為30歲,執行過程類似上一步:
則此時的對應關系如下圖所示:
我們可以看出,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的undo log成為一條記錄版本線性表,既鏈表,undo log的鏈首就是最新的舊記錄,鏈尾就是最早的舊記錄。
Read View(讀視圖)
- 對于使用READ UNCOMMITTED隔離級別的事務來說,由于可以讀到未提交事務修改過的記錄,所以直接讀取記錄的最新版本就好了;
- 對于使用SERIALIZABLE隔離級別的事務來說,MySQL規定使用加鎖的方式來訪問記錄;
- 對于使用READ COMMITTED和REPEATABLE READ隔離級別的事務來說,都必須保證讀到已經提交了的事務修改過的記錄,也就是說假如另一個事務已經修改了記錄但是尚未提交,是不能直接讀取最新版本的記錄的,核心問題就是需要判斷一下版本鏈中的哪個版本是當前事務可見的。
為了解決哪個版本是當前事務可見的,MySQL提出了一個ReadView(快照)的概念,在Select操作前會為當前事務生成一個快照,然后根據快照中記錄的信息來判斷當前記錄是否對事務是可見的,如果不可見那么沿著版本鏈繼續往上找,直至找到一個可見的記錄。
說白了Read View就是事務進行快照讀操作的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄并維護系統當前活躍事務的ID(當每個事務開啟時,都會被分配一個ID, 這個ID是遞增的,所以最新的事務,ID值越大)。
ReadView(快照)中包含了下面幾個關鍵屬性:
注意max_trx_id并不是m_ids中的最大值,事務id是遞增分配的。比方說現在有id為1,2,3這三個事務,之后id為3的事務提交了。那么一個新的讀事務在生成ReadView時,m_ids就包括1和2,min_trx_id的值就是1,max_trx_id的值就是4,creator_trx_id就是3。我們前邊說過,只有在對表中的記錄做改動時(執行INSERT、DELETE、UPDATE這些語句時)才會為事務分配事務id,否則在一個只讀事務中的事務id值都默認為0,即creator_trx_id為0。
根據當前數據庫中運行中的讀寫事務id,會去生成一個ReadView。然后根據要讀取的數據記錄中的事務id(方便區別,記為r_trx_id)跟ReadView中保存的幾個屬性做如下判斷:
如果某個版本的數據對當前事務不可見的話,那就順著版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。
整體過程
介紹完隱式字段,undo log, 以及Read View的概念之后,我們來模擬一下整體的流程。假設現在又四個事務,其對應的狀態如下表所示:
| 事務開始 | 事務開始 | 事務開始 | 事務開始 |
| … | … | … | 修改且已提交 |
| 進行中 | 快照讀 | 進行中 | |
| … | … | … |
根據之前的描述,當事務2對某行數據執行了快照讀,數據庫為該行數據生成一個Read View讀視圖,假設當前事務ID為2,此時還有事務1和事務3在活躍中,事務4在事務2快照讀前一刻提交更新了,所以Read View記錄了系統當前活躍事務1,3的ID,維護列表上m_ids,當前系統中活躍的讀寫事務中最小的事務id即min_trx_id為1,系統中應該分配給下一個事務的id即max_trx_id為5,該ReadView的事務的事務id即creator_trx_id為2。
因為只有事務4修改過該行記錄,并在事務2執行快照讀前,就提交了事務,所以當前該行當前數據的undo log如下圖所示:
快照讀的過程是這樣的:
所以事務4修改后提交的最新結果對事務2快照讀時是可見的,所以事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本。正是Read View生成時機的不同,從而造成RC,RR級別下快照讀的結果的不同:
- 在RC隔離級別下,是每個快照讀都會生成并獲取最新的Read View(即每次select都會生成一個快照);
- 在RR隔離級別下,則是同一個事務中的第一個快照讀才會創建Read View, 之后的快照讀獲取的都是同一個Read View(即只有在第一次會生成一個快照)。
猜你感興趣:
MYSQL專題-絕對實用的MYSQL優化總結
MYSQL專題-MySQL事務實現原理
MYSQL專題-使用Binlog日志恢復MySQL數據
MYSQL專題-MySQL三大日志binlog、redo log和undo log
更多文章請點擊:更多…
總結
以上是生活随笔為你收集整理的MYSQL专题-MVCC多版本并发控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MYSQL专题-MySQL事务实现原理
- 下一篇: Java的TheadLocal使用