日韩性视频-久久久蜜桃-www中文字幕-在线中文字幕av-亚洲欧美一区二区三区四区-撸久久-香蕉视频一区-久久无码精品丰满人妻-国产高潮av-激情福利社-日韩av网址大全-国产精品久久999-日本五十路在线-性欧美在线-久久99精品波多结衣一区-男女午夜免费视频-黑人极品ⅴideos精品欧美棵-人人妻人人澡人人爽精品欧美一区-日韩一区在线看-欧美a级在线免费观看

歡迎訪問 生活随笔!

生活随笔

當前位置: 首頁 > 运维知识 > 数据库 >内容正文

数据库

MYSQL专题-MVCC多版本并发控制

發布時間:2025/3/20 数据库 65 豆豆
生活随笔 收集整理的這篇文章主要介紹了 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,則它過程大致如下:

  • 事務修改該行(記錄)數據時,數據庫會先對該行加排他鎖;
  • 把該行數據拷貝到undo log中,作為舊記錄,既在undo log中有當前行的拷貝副本;
  • 拷貝完畢后,修改該行name為Jim,并且修改隱藏字段的事務ID為當前事務1的ID, 我們默認從1開始,之后遞增,回滾指針指向拷貝到undo log的副本記錄,既表示我的上一個版本就是它;
  • 事務提交后,釋放鎖。
  • 則此時的對應關系如下圖所示:

    又來了個事務修改person表的同一個記錄,將age修改為30歲,執行過程類似上一步:

  • 事務修改該行數據時,數據庫也先為該行加鎖;
  • 然后把該行數據拷貝到undo log中,作為舊記錄,發現該行記錄已經有undo log了,那么最新的舊數據作為鏈表的表頭,插在該行記錄的undo log最前面;
  • 修改該行age為30歲,并且修改隱藏字段的事務ID為當前事務的ID, 那就是2,回滾指針指向剛剛拷貝到undo log的副本記錄;
  • 事務提交,釋放鎖。
  • 則此時的對應關系如下圖所示:

    我們可以看出,不同事務或者相同事務的對同一記錄的修改,會導致該記錄的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(快照)中包含了下面幾個關鍵屬性:

    m_ids:生成ReadView時當前系統中活躍的讀寫事務的事務id列表 min_trx_id:生成ReadView時當前系統中活躍的讀寫事務中最小的事務id,也就是m_ids中的最小值。 max_trx_id:生成ReadView時系統中應該分配給下一個事務的id值 creator_trx_id:生成該ReadView的事務的事務id

    注意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中保存的幾個屬性做如下判斷:

  • 如果被訪問版本的r_trx_id屬性值與ReadView中的creator_trx_id值相同,意味著當前事務在訪問它自己修改過的記錄,所以該版本可以被當前事務訪問;
  • 如果被訪問版本的r_trx_id屬性值小于ReadView中的min_trx_id值,表明生成該版本的事務在當前事務生成ReadView前已經提交,所以該版本可以被當前事務訪問;
  • 如果被訪問版本的r_trx_id屬性值大于或等于ReadView中的max_trx_id值,表明生成該版本的事務在當前事務生成ReadView后才開啟,所以該版本不可以被當前事務訪問;
  • 如果被訪問版本的r_trx_id屬性值在ReadView的min_trx_id和max_trx_id之間,那就需要判斷一下r_trx_id屬性值是不是在m_ids列表中,如果在,說明創建ReadView時生成該版本的事務還是活躍的,該版本不可以被訪問;如果不在,說明創建ReadView時生成該版本的事務已經被提交,該版本可以被訪問。
  • 如果某個版本的數據對當前事務不可見的話,那就順著版本鏈找到下一個版本的數據,繼續按照上邊的步驟判斷可見性,依此類推,直到版本鏈中的最后一個版本。如果最后一個版本也不可見的話,那么就意味著該條記錄對該事務完全不可見,查詢結果就不包含該記錄。

    整體過程

    介紹完隱式字段,undo log, 以及Read View的概念之后,我們來模擬一下整體的流程。假設現在又四個事務,其對應的狀態如下表所示:

    事務1事務2事務3事務4
    事務開始事務開始事務開始事務開始
    修改且已提交
    進行中快照讀進行中

    根據之前的描述,當事務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如下圖所示:

    快照讀的過程是這樣的:

  • 先拿該記錄DB_TRX_ID字段記錄的事務ID 4去跟Read View的creator_trx_id(2)比較,看是否相等,顯示不相等;
  • 繼續跟min_trx_id比較,看4是否小于min_trx_id(1),所以不符合條件;
  • 繼續判斷 4 是否大于等于 max_trx_id(5),也不符合條件;
  • 最后判斷4是否處于m_ids(1,3)中的活躍事務, 最后發現事務ID為4的事務不在當前活躍事務列表中, 符合可見性條件。
  • 所以事務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多版本并发控制的全部內容,希望文章能夠幫你解決所遇到的問題。

    如果覺得生活随笔網站內容還不錯,歡迎將生活随笔推薦給好友。