正確的理解MySQL的MVCC及實現原理

MVCC多版本併發控制
!首先聲明,MySQL的測試環境是5.7算法

前提概要
什麼是MVCC
什麼是當前讀和快照讀?
當前讀,快照讀和MVCC的關係
MVCC實現原理
隱式字段
undo日誌
Read View(讀視圖)
總體流程
MVCC相關問題
RR是如何在RC級的基礎上解決不可重複讀的?
RC,RR級別下的InnoDB快照讀有什麼不一樣?數據庫

前提概要
什麼是MVCC?
MVCC
MVCC,全稱Multi-Version Concurrency Control,即多版本併發控制。MVCC是一種併發控制的方法,通常在數據庫管理系統中,實現對數據庫的併發訪問,在編程語言中實現事務內存。
mvcc - @百度百科編程

MVCC在MySQL InnoDB中的實現主要是爲了提升數據庫併發性能,用更好的方式去處理讀-寫衝突,作到即便有讀寫衝突時,也能作到不加鎖,非阻塞併發讀安全

什麼是當前讀和快照讀?
在學習MVCC多版本併發控制以前,咱們必須先了解一下,什麼是MySQL InnoDB下的當前讀和快照讀?併發

當前讀
像select lock in share mode(共享鎖), select for update ; update, insert ,delete(排他鎖)這些操做都是一種當前讀,爲何叫當前讀?就是它讀取的是記錄的最新版本,讀取時還要保證其餘併發事務不能修改當前記錄,會對讀取的記錄進行加鎖mvc

快照讀
像不加鎖的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 + 悲觀鎖
MVCC解決讀寫衝突,悲觀鎖解決寫寫衝突
MVCC + 樂觀鎖
MVCC解決讀寫衝突,樂觀鎖解決寫寫衝突
這種組合的方式就能夠最大程度的提升數據庫併發性能,並解決讀寫衝突,和寫寫衝突致使的問題


MVCC的實現原理
MVCC的目的就是多版本併發控制,在數據庫中的實現,就是爲了解決讀寫衝突,它的實現原理主要是依賴記錄中的 3個隱式字段,undo日誌 ,Read View 來實現的。因此咱們先來看看這個三個point的概念

隱式字段
每行記錄除了咱們自定義的字段外,還有數據庫隱式定義的DB_TRX_ID,DB_ROLL_PTR,DB_ROW_ID等字段

DB_TRX_ID
6byte,最近修改(修改/插入)事務ID:記錄建立這條記錄/最後一次修改該記錄的事務ID
DB_ROLL_PTR
7byte,回滾指針,指向這條記錄的上一個版本(存儲於rollback segment裏)
DB_ROW_ID
6byte,隱含的自增ID(隱藏主鍵),若是數據表沒有主鍵,InnoDB會自動以DB_ROW_ID產生一個聚簇索引
實際還有一個刪除flag隱藏字段, 既記錄被更新或刪除並不表明真的刪除,而是刪除flag變了

如上圖,DB_ROW_ID是數據庫默認爲該行記錄生成的惟一隱式主鍵,DB_TRX_ID是當前操做該記錄的事務ID,而DB_ROLL_PTR是一個回滾指針,用於配合undo日誌,指向上一個舊版本

undo日誌
undo log主要分爲兩種:

insert undo log
表明事務在insert新記錄時產生的undo log, 只在事務回滾時須要,而且在事務提交後能夠被當即丟棄
update undo log
事務在進行update或delete時產生的undo log; 不只在事務回滾時須要,在快照讀時也須要;因此不能隨便刪除,只有在快速讀或事務回滾不涉及該日誌時,對應的日誌纔會被purge線程統一清除
purge

從前面的分析能夠看出,爲了實現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可見,那麼這條記錄必定是能夠被安全清除的。

對MVCC有幫助的實質是update undo log ,undo log實際上就是存在rollback segment中舊記錄鏈,它的執行流程以下:

1、 好比一個有個事務插入persion表插入了一條新記錄,記錄以下,name爲Jerry, age爲24歲,隱式主鍵是1,事務ID和回滾指針,咱們假設爲NULL


2、 如今來了一個事務1對該記錄的name作出了修改,改成Tom

在事務1修改該行(記錄)數據時,數據庫會先對該行加排他鎖
而後把該行數據拷貝到undo log中,做爲舊記錄,既在undo log中有當前行的拷貝副本
拷貝完畢後,修改該行name爲Tom,而且修改隱藏字段的事務ID爲當前事務1的ID, 咱們默認從1開始,以後遞增,回滾指針指向拷貝到undo log的副本記錄,既表示個人上一個版本就是它
事務提交後,釋放鎖

3、 又來了個事務2修改person表的同一個記錄,將age修改成30歲

在事務2修改該行數據時,數據庫也先爲該行加鎖
而後把該行數據拷貝到undo log中,做爲舊記錄,發現該行記錄已經有undo log了,那麼最新的舊數據做爲鏈表的表頭,插在該行記錄的undo log最前面
修改該行age爲30歲,而且修改隱藏字段的事務ID爲當前事務2的ID, 那就是2,回滾指針指向剛剛拷貝到undo log的副本記錄
事務提交,釋放鎖

從上面,咱們就能夠看出,不一樣事務或者相同事務的對同一記錄的修改,會致使該記錄的undo log成爲一條記錄版本線性表,既鏈表,undo log的鏈首就是最新的舊記錄,鏈尾就是最先的舊記錄(固然就像以前說的該undo log的節點多是會purge線程清除掉,向圖中的第一條insert undo log,其實在事務提交以後可能就被刪除丟失了,不過這裏爲了演示,因此還放在這裏)

Read View(讀視圖)
什麼是Read View?

什麼是Read View,說白了Read View就是事務進行快照讀操做的時候生產的讀視圖(Read View),在該事務執行的快照讀的那一刻,會生成數據庫系統當前的一個快照,記錄並維護系統當前活躍事務的ID(當每一個事務開啓時,都會被分配一個ID, 這個ID是遞增的,因此最新的事務,ID值越大)

因此咱們知道 Read View主要是用來作可見性判斷的, 即當咱們某個事務執行快照讀的時候,對該記錄建立一個Read View讀視圖,把它比做條件用來判斷當前事務可以看到哪一個版本的數據,既多是當前最新的數據,也有多是該行記錄的undo log裏面的某個版本的數據。

Read View遵循一個可見性算法,主要是將要被修改的數據的最新記錄中的DB_TRX_ID(即當前事務ID)取出來,與系統當前其餘活躍事務的ID去對比(由Read View維護),若是DB_TRX_ID跟Read View的屬性作了某些比較,不符合可見性,那就經過DB_ROLL_PTR回滾指針去取出Undo Log中的DB_TRX_ID再比較,即遍歷鏈表的DB_TRX_ID(從鏈首到鏈尾,即從最近的一次修改查起),直到找到知足特定條件的DB_TRX_ID, 那麼這個DB_TRX_ID所在的舊記錄就是當前事務能看見的最新老版本

那麼這個判斷條件是什麼呢?

咱們這裏盜竊@呵呵一笑百媚生一張源碼圖,如上,它是一段MySQL判斷可見性的一段源碼,即changes_visible方法(不徹底哈,但能看出大體邏輯),該方法展現了咱們拿DB_TRX_ID去跟Read View某些屬性進行怎麼樣的比較

在展現以前,我先簡化一下Read View,咱們能夠把Read View簡單的理解成有三個全局屬性

trx_list(名字我隨便取的)
一個數值列表,用來維護Read View生成時刻系統正活躍的事務ID
up_limit_id
記錄trx_list列表中事務ID最小的ID
low_limit_id
ReadView生成時刻系統還沒有分配的下一個事務ID,也就是目前已出現過的事務ID的最大值+1
首先比較DB_TRX_ID < up_limit_id, 若是小於,則當前事務能看到DB_TRX_ID 所在的記錄,若是大於等於進入下一個判斷
接下來判斷 DB_TRX_ID 大於等於 low_limit_id , 若是大於等於則表明DB_TRX_ID 所在的記錄在Read View生成後纔出現的,那對當前事務確定不可見,若是小於則進入下一個判斷
判斷DB_TRX_ID 是否在活躍事務之中,trx_list.contains(DB_TRX_ID),若是在,則表明我Read View生成時刻,你這個事務還在活躍,尚未Commit,你修改的數據,我當前事務也是看不見的;若是不在,則說明,你這個事務在Read View生成以前就已經Commit了,你修改的結果,我當前事務是能看見的
總體流程
咱們在瞭解了隱式字段,undo log, 以及Read View的概念以後,就能夠來看看MVCC實現的總體流程是怎麼樣了

總體的流程是怎麼樣的呢?咱們能夠模擬一下

當事務2對某行數據執行了快照讀,數據庫爲該行數據生成一個Read View讀視圖,假設當前事務ID爲2,此時還有事務1和事務3在活躍中,事務4在事務2快照讀前一刻提交更新了,因此Read View記錄了系統當前活躍事務1,3的ID,維護在一個列表上,假設咱們稱爲trx_list
事務1 事務2 事務3 事務4
事務開始 事務開始 事務開始 事務開始
… … … 修改且已提交
進行中 快照讀 進行中
… … …
Read View不只僅會經過一個列表trx_list來維護事務2執行快照讀那刻系統正活躍的事務ID,還會有兩個屬性up_limit_id(記錄trx_list列表中事務ID最小的ID),low_limit_id(記錄trx_list列表中事務ID最大的ID,也有人說快照讀那刻系統還沒有分配的下一個事務ID也就是目前已出現過的事務ID的最大值+1,我更傾向於後者 >>>資料傳送門 | 呵呵一笑百媚生的回答) ;因此在這裏例子中up_limit_id就是1,low_limit_id就是4 + 1 = 5,trx_list集合的值是1,3,Read View以下圖

咱們的例子中,只有事務4修改過該行記錄,並在事務2執行快照讀前,就提交了事務,因此當前該行當前數據的undo log以下圖所示;咱們的事務2在快照讀該行記錄的時候,就會拿該行記錄的DB_TRX_ID去跟up_limit_id,low_limit_id和活躍事務ID列表(trx_list)進行比較,判斷當前事務2能看到該記錄的版本是哪一個。

因此先拿該記錄DB_TRX_ID字段記錄的事務ID 4去跟Read View的的up_limit_id比較,看4是否小於up_limit_id(1),因此不符合條件,繼續判斷 4 是否大於等於 low_limit_id(5),也不符合條件,最後判斷4是否處於trx_list中的活躍事務, 最後發現事務ID爲4的事務不在當前活躍事務列表中, 符合可見性條件,因此事務4修改後提交的最新結果對事務2快照讀時是可見的,因此事務2能讀到的最新數據記錄是事務4所提交的版本,而事務4提交的版本也是全局角度上最新的版本


也正是Read View生成時機的不一樣,從而形成RC,RR級別下快照讀的結果的不一樣

MVCC相關問題
RR是如何在RC級的基礎上解決不可重複讀的?
當前讀和快照讀在RR級別下的區別:
表1:

事務A 事務B
開啓事務 開啓事務
快照讀(無影響)查詢金額爲500 快照讀查詢金額爲500
更新金額爲400
提交事務
select 快照讀金額爲500
select lock in share mode當前讀金額爲400
在上表的順序下,事務B的在事務A提交修改後的快照讀是舊版本數據,而當前讀是實時新數據400

表2:

事務A 事務B
開啓事務 開啓事務
快照讀(無影響)查詢金額爲500
更新金額爲400
提交事務
select 快照讀金額爲400
select lock in share mode當前讀金額爲400
而在表2這裏的順序中,事務B在事務A提交後的快照讀和當前讀都是實時的新數據400,這是爲何呢?

這裏與上表的惟一區別僅僅是表1的事務B在事務A修改金額前快照讀過一次金額數據,而表2的事務B在事務A修改金額前沒有進行過快照讀。
因此咱們知道事務中快照讀的結果是很是依賴該事務首次出現快照讀的地方,即某個事務中首次出現快照讀的地方很是關鍵,它有決定該事務後續快照讀結果的能力

咱們這裏測試的是更新,同時刪除和更新也是同樣的,若是事務B的快照讀是在事務A操做以後進行的,事務B的快照讀也是能讀取到最新的數據的

RC,RR級別下的InnoDB快照讀有什麼不一樣?
正是Read View生成時機的不一樣,從而形成RC,RR級別下快照讀的結果的不一樣

在RR級別下的某個事務的對某條記錄的第一次快照讀會建立一個快照及Read View, 將當前系統活躍的其餘事務記錄起來,此後在調用快照讀的時候,仍是使用的是同一個Read View,因此只要當前事務在其餘事務提交更新以前使用過快照讀,那麼以後的快照讀使用的都是同一個Read View,因此對以後的修改不可見;即RR級別下,快照讀生成Read View時,Read View會記錄此時全部其餘活動事務的快照,這些事務的修改對於當前事務都是不可見的。而早於Read View建立的事務所作的修改均是可見而在RC級別下的,事務中,每次快照讀都會新生成一個快照和Read View, 這就是咱們在RC級別下的事務中能夠看到別的事務提交的更新的緣由總之在RC隔離級別下,是每一個快照讀都會生成並獲取最新的Read View;而在RR隔離級別下,則是同一個事務中的第一個快照讀纔會建立Read View, 以後的快照讀獲取的都是同一個Read View。---------------------

相關文章
相關標籤/搜索