MySQL / 多版本并发控制
- 前提概要
- 什么是 MVCC
- 什么是當前讀和快照讀?
- 當前讀,快照讀和 MVCC 的關系
- MVCC 實現原理
- 隱式字段
- undo 日志
- Read View(讀視圖)
- 整體流程
- MVCC 相關問題
- RR 是如何在 RC 級的基礎上解決不可重復讀的?
- RC、RR 級別下的 InnoDB 快照讀有什么不同?
一、前提概要
1、什么是 MVCC ?
MVCC,全稱 Multi-Version Concurrency Control,即多版本并發控制。MVCC 是一種并發控制的方法,一般在數據庫管理系統中,實現對數據庫的并發訪問,在編程語言中實現事務內存。
MVCC在 MySQL InnoDB 中的實現主要是為了提高數據庫并發性能,用更好的方式去處理讀-寫沖突,做到即使有讀寫沖突時,也能做到不加鎖,非阻塞并發讀。
2、什么是當前讀和快照讀?
在學習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 實現原理
3、MVCC能解決什么問題,好處是?
數據庫并發場景有三種,分別為:
- 讀 - 讀:不存在任何問題,也不需要并發控制
- 讀 - 寫:有線程安全問題,可能會造成事務隔離性問題,可能遇到臟讀,幻讀,不可重復讀。
- 寫 - 寫:有線程安全問題,可能會存在更新丟失問題,比如第一類更新丟失,第二類更新丟失。
4、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 中舊記錄鏈,它的執行流程如下:
一、 比如一個有個事務插入 persion 表插入了一條新記錄,記錄如下,name為 Jerry,age 為 24 歲,隱式主鍵是 1,事務 ID 和回滾指針,我們假設為 NULL。
?
二、 現在來了一個事務 1 對該記錄的 name 做出了修改,改為 Tom 。
- 在事務 1 修改該行(記錄)數據時,數據庫會先對該行加排他鎖。
- 然后把該行數據拷貝到 undo log 中,作為舊記錄,既在 undo log 中有當前行的拷貝副本。
- 拷貝完畢后,修改該行 name 為 Tom,并且修改隱藏字段的事務 ID 為當前事務 1 的 ID,我們默認從 1 開始,之后遞增,回滾指針指向拷貝到 undo log 的副本記錄,既表示我的上一個版本就是它
- 事務提交后,釋放鎖。
三、 又來了個事務 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 。
?
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:
?
表2:
而在表 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。
轉載于:https://www.jianshu.com/p/8845ddca3b23
(SAW:Game Over!)
總結
以上是生活随笔為你收集整理的MySQL / 多版本并发控制的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: MySQL / 各种锁
- 下一篇: linux cmake编译源码,linu