文章很長,且持續更新,建議收藏起來,慢慢讀!瘋狂創客圈總目錄 博客園版 爲您奉上珍貴的學習資源 :
免費贈送 :《尼恩Java面試寶典》 持續更新+ 史上最全 + 面試必備 2000頁+ 面試必備 + 大廠必備 +漲薪必備
免費贈送 :《尼恩技術聖經+高併發系列PDF》 ,幫你 實現技術自由,完成職業升級, 薪酬猛漲!加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷1)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷2)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 經典圖書:《Java高併發核心編程(卷3)加強版》 面試必備 + 大廠必備 +漲薪必備 加尼恩免費領
免費贈送 資源寶庫: Java 必備 百度網盤資源大合集 價值>10000元 加尼恩領取
MVCC學習聖經:一文穿透MySQL MVCC,吊打面試官
尼恩特別說明: 尼恩的文章,都會在 《技術自由圈》 公號 發佈, 並且維護最新版本。 如果發現圖片 不可見, 請去 《技術自由圈》 公號 查找
尼恩說在前面
在40歲老架構師 尼恩的讀者交流羣(50+)中,很多小夥伴拿到了一線互聯網企業如得物、阿里、滴滴、極兔、有贊、希音、百度、網易、美團的面試機會,遇到很多很重要的面試題:
1.請解釋Mysql MVCC,它的 作用是什麼?
2.在MySQL中,MVCC是如何實現的?請簡述其工作原理。
3.MVCC是如何解決讀-寫和寫-寫衝突的?
4.在併發環境中,當多個事務同時讀取同一行數據時,MVCC是如何保證每個事務看到的數據版本是一致的?
5.MVCC如何幫助提高數據庫的併發性能?
最近有小夥伴在面試阿里,又遇到了MVCC相關的面試題。小夥伴 支支吾吾的說了幾句,沒說清楚,面試掛了。
所以,尼恩給大家做一下系統化、體系化的梳理,使得大家內力猛增,可以充分展示一下大家雄厚的 “技術肌肉”,讓面試官愛到 “不能自已、口水直流”,然後實現”offer直提”。
這裏,尼恩團隊把MVCC 進行了全面的梳理,穿透式的梳理,
梳理爲一個PDF文檔 《MVCC 學習聖經:一次穿透MYSQL MVCC 》, 並且持續迭代。
這個文檔將成爲大家 面試的殺手鐗, 此文當最新PDF版本,可以找40歲老架構師尼恩獲取。
當然,上面的面試題以及參考答案,也會收入咱們的 《尼恩Java面試寶典PDF》V171版本,供後面的小夥伴參考,提升大家的 3高 架構、設計、開發水平。
最新《尼恩 架構筆記》《尼恩高併發三部曲》《尼恩Java面試寶典》的PDF,請關注本公衆號【技術自由圈】獲取,回覆:領電子書
本文作者:
- 第一作者 Moen (負責寫初稿 )
- 第二作者 尼恩 (40歲老架構師, 負責提升此文的 技術高度,讓大家有一種 俯視 技術的感覺)
本文目錄
什麼是MVCC
MVCC
機制的全稱爲Multi-Version Concurrency Control
,即多版本併發控制。
MVCC主要是爲了提升數據庫併發性能而設計的,其中採用更好的方式處理了讀-寫併發衝突,做到即使有讀寫衝突時,可以實現併發執行,從而提升併發能力,確保了任何時刻的讀操作都是非阻塞的。
在衆多的MySQL開源存儲引擎中,幾乎只有InnoDB實現了MVCC機制,其他的存儲引擎如:MyISAM、memory等存儲引擎中並未實現MVCC。
MVCC(Multi-Version Concurrency Control,多版本併發控制)一種併發控制機制,用於解決併發事務訪問數據庫時可能出現的一些問題,如髒讀、不可重複讀和幻讀。
在MVCC機制中,數據庫中的每個數據行都可以存在多個版本,並且每個事務看到的數據版本可能不同。具體來說,MVCC機制通過以下方式實現併發控制:
- 版本控制:每當對數據庫中的數據行進行更新操作時,不是直接覆蓋原始數據,而是創建一個新的數據版本,並將新版本的數據與事務的時間戳相關聯。
- 快照讀取:在MVCC中,讀取操作不會阻塞寫入操作,也不會阻塞其他讀取操作。事務可以讀取數據庫中的數據快照,即某個時間點之前的數據版本,而不會受到其他事務的影響。
- 可見性判斷:在執行讀取操作時,事務只能看到在其開始之前已經提交的數據版本,而看不到其他事務正在修改的數據。這樣可以避免髒讀和不可重複讀問題。
- 回滾操作:當事務回滾時,不會對數據庫中的數據進行物理刪除或修改,而是標記事務所涉及的數據版本爲無效,使得其他事務無法看到該版本。
總的來說,MVCC機制通過維護多個數據版本,實現了事務的隔離性和併發性,保證了數據庫的一致性和可靠性。它是許多現代數據庫系統(如MySQL、PostgreSQL等)中常用的併發控制技術。
MVCC的根本目標:提升併發能力
在併發讀寫數據庫時,讀操作可能會不一致的數據(髒讀)。
爲了避免這種情況,需要實現數據庫的併發訪問控制,最簡單的方式就是加鎖訪問。
由於,加鎖會將讀寫操作串行化,所以不會出現不一致的狀態。
但是,讀操作會被寫操作阻塞,大幅降低讀性能。
事務併發處理的四大場景
首先, 這裏講 事務的併發處理分爲四大場景,分別是
讀-讀
、寫-寫
、讀-寫
、寫-讀
,
這四種情況分別對應併發事務執行時的四種場景,爲了後續分析MVCC
機制時方便理解,因此先將這幾種情況說明。
讀-讀場景:
讀-讀場景即是指多個事務/線程在併發讀取一個相同的數據,比如事務T1
正在讀取ID=16
的行記錄,事務T2
也在讀取這條記錄,兩個事務之間是併發執行的。
MySQL
執行查詢語句,絕對不會對引起數據的任何變化,因此對於這種情況而言,不需要做任何操作,因爲不改變數據就不會引起任何併發問題。
寫-寫場景
寫-寫場景也比較簡單,也就是指多個事務之間一起對同一數據進行寫操作,
比如事務T1
對ID=16
的行記錄做修改操作,事務T2
則對這條數據做刪除操作,事務T1
提交事務後想查詢看一下,結果連這條數據都不見了,這也是所謂的髒寫問題,也被稱爲更新覆蓋問題,
對於這個問題在所有數據庫、所有隔離級別中都是零容忍的存在,最低的隔離級別也要解決這個問題。
讀-寫、寫-讀場景
讀-寫、寫-讀實際上從宏觀角度來看,可以理解成同一種類型的操作,但從微觀角度而言則是兩種不同的情況,
- 讀-寫是指一個事務先開始讀,然後另一個事務則過來執行寫操作,
- 寫-讀則相反,主要是讀、寫發生的前後順序的區別。
併發事務中同時存在讀、寫兩類操作時,這是最容易出問題的場景,髒讀、不可重複讀、幻讀都出自於這種場景中,當有一個事務在做寫操作時,讀的事務中就有可能出現這一系列問題,因此數據庫纔會引入各種機制解決。
各併發事務場景的解決方案
對於寫-寫、讀-寫、寫-讀這三類存在線程安全問題的場景,最爲簡單粗暴的方式,通過 加鎖 的方案確保線程安全。
但是,加鎖會導致部分的串行化、整體串行化,因此效率會下降,而MVCC
機制的誕生則解決了這個問題。
因此MySQL
推出了MVCC
機制,在讀-寫並存(讀-寫、寫-讀)的場景,使用局部無鎖架構,提升性能。
MVCC 機制 在線程安全問題和加鎖串行化之間做了一定取捨,讓兩者之間達到了很好的平衡,即防止了髒讀、不可重複讀及幻讀問題的出現,
又無需對併發讀-寫事務加鎖處理。
無鎖架構:COW思想
Copy-On-Write(COW,寫時複製)是一種常見的併發編程思想。
Copy-On-Write基本思想是,當多個線程需要對共享數據進行修改時,不直接在原始數據上進行操作,而是先將原始數據複製一份(即寫時複製),然後在副本上進行Write。
Copy-On-Write 通過操作寫操作副本,引入局部無鎖架構,解決並且處理之間的數據衝突,提高了併發性能。
Copy-On-Write的實現步驟如下:
- 讀取數據:多個線程同時讀取共享數據時,它們可以直接訪問原始數據,而不需要複製。因爲讀取操作不會修改數據,所以可以安全地共享原始數據。
- 寫入數據:當某個線程需要修改共享數據時,首先會將原始數據進行復制(即寫時複製),然後在副本上進行修改。這樣做的好處是,其他線程仍然可以繼續讀取原始數據,不受寫入線程的影響。
- 更新引用:寫入線程完成修改後,會更新共享數據的引用,使得其他線程後續訪問時可以獲取到最新的數據副本。
Copy-On-Write的優點包括:
- 線程安全:通過複製數據副本並在副本上進行修改,避免了多線程併發修改原始數據時的數據衝突問題,從而提高了線程安全性。
- 減少鎖競爭:由於讀取操作不需要加鎖,所以可以減少鎖競爭,提高了併發性能。
- 節省內存:只有在有寫入操作時纔會進行數據複製,而讀取操作可以共享原始數據,因此可以節省內存空間。
然而,Copy-On-Write也有一些缺點,主要是由於數據複製和更新引用所帶來的額外開銷,可能會導致內存和性能方面的消耗增加。因此,適用場景需要根據具體情況進行評估和選擇。
COW思想寫操作之間是要互斥的,並且每次寫操作都會有一次copy,所以只適合讀大於寫的情況。所以,COW思想 專門用於優化讀的次數遠大於寫次數的場景。比如,Java的 併發容器CopyOnWriteArrayList。
Java中的CopyOnWriteArrayList
CopyOnWriteArrayList 是jdk1.5以後併發包中提供的一種併發容器,寫操作通過創建底層數組的新副本來實現,是一種讀寫分離的併發策略,我們也成爲“寫時複製容器”。
public boolean add(E e) {
//加鎖,對寫操作保證線程安全
final ReentrantLock lock = this.lock;
lock.lock();
try {
Object[] elements = getArray();
int len = elements.length;
//拷貝原容器,長度爲原容器+1
Object[] newElements = Arrays.copyOf(elements, len + 1);
//在新副本執行添加操作
newElements[len] = e;
//底層數組指向新的數組
setArray(newElements);
return true;
} finally {
lock.unlock();
}
}
CopyOnWriteArrayList底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器裏添加這個新的數據,最後把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器裏的數據。
MVCC如何使用Copy-On-Write思想呢?借雞生蛋
一圖勝千言,40歲老架構師用一張圖,給大家總結一如何借雞生蛋,實現 Copy-On-Write思想的。
總體來說, MVCC Copy-On-Write思想, 包括三個組成部分:
事務要實現ACID,其中的原子性、一致性主要使用 undo-log 數據副本實現,undo-log 就是重做日誌,一個事務一個 undo-log 日誌副本。
多個事務的 undo-log 日誌副本 (數據快照),組成了一個 副本鏈,如下下圖:
MVCC 也就借雞生蛋, 複用 這個 undo-log 副本鏈, 實現了自己 Copy-On-Write思想。
MVCC與鎖的關係
再看看 一個核心問題:MVCC與鎖的關係?
還是一圖勝千言,40歲老架構師用一張圖,給大家總結一下MVCC和鎖如何結合使用,提升事務並行能力的:
MVCC(Multi-Version Concurrency Control,多版本併發控制)和鎖是數據庫管理系統中兩種不同的併發控制機制,它們在處理事務併發訪問時起着不同的作用。
- MVCC:
- MVCC通過維護數據的多個版本來實現併發控制,允許事務併發訪問數據庫而不會發生阻塞。
- 在MVCC中,讀取操作不會阻塞寫入操作,也不會阻塞其他讀取操作。每個事務可以看到一個一致性的數據快照,而不受其他事務的影響。
- MVCC主要用於讀取操作的併發控制,可以有效地避免髒讀、不可重複讀和幻讀等併發問題。
- 鎖:
- 鎖是一種悲觀併發控制機制,通過在事務訪問數據時對數據進行加鎖,以防止其他事務對該數據進行修改或讀取。
- 在使用鎖進行併發控制時,可能會出現阻塞和死鎖等問題,特別是在高併發的情況下,鎖的粒度過大或者鎖的競爭過於激烈時,性能可能會受到影響。
MVCC和鎖之間的關係可以總結如下:
- MVCC是一種 樂觀的併發控制機制,通過多副本的版本控制來實現併發訪問,而不需要對數據進行加鎖。
- 鎖是一種 悲觀的併發控制機制,通過對數據進行加鎖來確保事務的隔離性和一致性。
40歲老架構師尼恩提示: 很多時候,MVCC和鎖可以結合使用,以實現更細粒度的併發控制,提高系統的性能和併發能力。
MySQL事務隔離級別與MVCC
什麼是事務
事務(Transaction)是數據庫管理系統執行過程中的一個邏輯單位,它由一個有限的數據庫操作序列構成。
這些操作要麼全部執行,要麼全部不執行,是一個不可分割的工作單位。
事務的目的是確保數據的完整性和一致性,它通過一系列的操作,將數據庫從一個一致性狀態轉換到另一個一致性狀態。
事務的ACID特性
事務通常具有以下四個特性,也被稱爲ACID屬性:
-
原子性(Atomicity):事務作爲一個整體執行,包含在其中的對數據庫的操作要麼全部執行,要麼全部不執行。
-
一致性(Consistency):事務必須使數據庫從一個一致性狀態變換到另一個一致性狀態。也就是說,一個事務的執行不能破壞數據庫數據的完整性和一致性。
-
隔離性(Isolation):事務的執行不受其他事務的干擾,事務執行的中間結果對其他事務是不可見的。
-
持久性(Durability):一旦事務提交,則其結果就是永久性的,即使系統崩潰也不會丟失。
事務的這些特性確保了即使在高併發的環境中,數據庫也能保持數據的完整性和一致性。在數據庫系統中,事務是通過一系列的操作來完成的,包括數據的插入、更新、刪除等。如果事務中的任何操作失敗,或者因爲某種原因被中斷,那麼整個事務都會回滾(Rollback),即撤銷所有已經執行的操作,使數據庫回到事務開始之前的狀態。如果事務中的所有操作都成功完成,那麼事務會提交(Commit),所做的更改會永久保存到數據庫中。
4中事務隔離級別
什麼是事務個隔離級別?事務隔離級別主要定義了事務在併發執行時的行爲,特別是它們如何與其他事務交互以及它們如何看到數據庫中的更改。
ANSI/ISO SQL標準定義了4中事務隔離級別:未提交讀(read uncommitted),提交讀(read committed),重複讀(repeatable read),串行讀(serializable)。
-
Oracle中默認的事務隔離級別是提交讀 (read committed)。
-
對於MySQL的Innodb的默認事務隔離級別是重複讀(repeated read)。
MySQL支持四種不同的事務隔離級別,每種級別都有其特定的行爲和適用場景。以下是MySQL的四種事務隔離級別及其描述:
-
READ UNCOMMITTED(讀取未提交)
- 允許讀取尚未提交的數據變更。
- 這是最低的隔離級別,它可能導致髒讀、不可重複讀和幻讀。
- 在這個級別,一個事務可以讀取到另一個尚未提交事務的修改,這可能導致數據的不一致性。
-
READ COMMITTED(讀取已提交)
- 只允許讀取併發事務已經提交的數據。
- 這個級別可以防止髒讀,但仍可能導致不可重複讀和幻讀。
- 在這個級別,每個事務只能看到它開始時的數據狀態以及它提交時其他事務所做的提交。
-
REPEATABLE READ(可重複讀取)
- 這是MySQL的默認隔離級別。
- 它確保在同一事務中多次讀取同一數據時,看到的是相同的數據版本,即使其他事務在此期間修改了這些數據。
- 儘管可以避免髒讀和不可重複讀,但在這個級別下仍可能出現幻讀(即在一個事務中,兩次相同的查詢可能會返回不同的結果集,因爲其他事務在此期間插入了新的記錄)。
-
SERIALIZABLE(可串行化)
- 這是最高的隔離級別。
- 它通過強制事務串行執行來避免髒讀、不可重複讀和幻讀。
- 在這個級別,每個事務在執行時都會完全鎖定它所訪問的數據,從而確保數據的一致性。但這也可能導致性能下降,因爲併發事務必須等待其他事務完成才能執行。
選擇適當的事務隔離級別需要根據應用的需求和性能考慮進行權衡。在某些情況下,可能需要更高的隔離級別來確保數據的一致性,而在其他情況下,可能需要降低隔離級別以提高性能。同時,也需要注意不同隔離級別可能帶來的併發問題,如髒讀、不可重複讀和幻讀等。
髒讀(Dirty Read):
一個事務讀取到另一個尚未提交事務的修改。不可重複讀(Non-repeatable Read):
在同一個事務內,多次讀取同一數據返回的結果有所不同。幻讀(Phantom Read):
一個事務在執行兩次相同的查詢時,因爲另一個併發事務的插入或刪除操作,導致兩次查詢返回的結果集不同。
隔離級別、併發性、數據一致性的三角之間關係
一圖勝千言,40歲老架構師用一張圖,給大家總結一下 事務隔離級別、併發性、數據一致性的三角之間關係:
事務隔離級別和併發性和數據一致性密切相關。不同的隔離級別提供了不同的併發性和數據一致性保證。
- 併發性:
- 併發性指的是數據庫系統同時處理多個事務的能力。隔離級別越低,允許的併發操作越多,系統的併發性能越高。
- 但是,過高的併發操作可能會導致事務之間的相互干擾,產生一些併發問題,如髒讀、不可重複讀和幻讀。
- 數據一致性:
- 數據一致性指的是事務執行後,數據庫中的數據是否保持一致性。隔離級別越高,數據一致性越好,但對併發操作的限制也越嚴格。
- 高隔離級別可以防止一些併發問題的產生,如髒讀、不可重複讀和幻讀,但會降低系統的併發性能。
參考閱讀一下 尼恩的幾篇相關文章:
注意:RC/RR 適用MVCC
MySQL
中僅在RC
讀已提交級別、RR
可重複讀級別纔會使用MVCC
機制。
1:RU讀未提交級別,不適用MVCC。
既然都允許存在髒讀問題、允許一個事務讀取另一個事務未提交的數據,直接進行當前讀,那自然可以直接讀最新版本的數據,因此無需MVCC
介入。
2:Serializable串行化級別不存在事務併發,不適用MVCC。
如果是Serializable串行化級別,因爲會將所有的併發事務串行化處理,
Serializable串行化級別,不論事務是讀操作,亦或是寫操作,都會被排好隊一個個執行,這都不存在所謂的多線程併發問題了,自然也無需MVCC介入。
MVCC機制的三個核心組件
MVCC
機制主要通過三個組件實現:
隱藏字段
Undo-log
日誌ReadView
。
核心組件1. 隱藏字段
在Innodb存儲引擎中,每一行記錄中都有隱藏字段
- 在有聚簇索引的情況下每一行記錄中都會隱藏3個字段,
- 如果沒有聚簇索引的情況下每一行記錄中都會隱藏4個字段。
在有聚簇索引的情況下每一行記錄中都會隱藏3個字段爲DB_TRX_ID,DB_ROLL_PTR、deleted_bit,
- DB_TRX_ID:記錄創建這條數據上次修改它的事務 ID,
- DB_ROLL_PTR:回滾指針,指向這條記錄的上一個版本
- deleted_bit字段,即記錄被更新或刪除,這裏的刪除並不代表真的刪除,而是將這條記錄的delete flag改爲true
除了上面的3個隱藏字段,沒有聚簇索引還會有DB_ROW_ID這個字段。
40歲老架構師尼恩提是:隱藏字段的細節,稍後詳細介紹。
核心組件2. undo log(回滾日誌)
在事務的ACID特性中,undo log(回滾日誌)主要用於實現事務的原子性、隔離性、一致性的關鍵組件之一。它的主要作用包括:
-
事務的回滾操作:
當一個事務執行過程中發生錯誤或者被用戶顯式回滾時,數據庫系統需要能夠撤銷該事務已經執行的操作,將數據庫恢復到事務開始之前的狀態。這就是回滾操作。
undo log記錄了事務執行過程中所做的所有修改操作的逆操作,通過undo log可以快速回滾事務所做的修改,從而保證事務的原子性。
-
恢復和崩潰恢復:
當數據庫系統發生崩潰或者異常關閉時,可能會導致部分事務未提交的修改操作丟失或者部分已提交的修改操作未持久化到磁盤。
通過undo log,數據庫系統可以在恢復過程中, 將未提交的修改操作回滾,並將已提交但未持久化的修改操作重新應用到數據庫中,從而保證數據庫的一致性和完整性。
總的來說,undo log在數據庫系統中扮演着非常重要的角色,它不僅用於實現事務的回滾操作和併發控制,還用於數據庫系統的恢復和崩潰恢復。通過記錄事務的修改操作和逆操作,undo log確保了數據庫的原子性、隔離性和一致性,是數據庫系統的關鍵組件之一。
尼恩在前面講到, MVCC 實現了自己 Copy-On-Write思想提升併發能力的時候, 也需要數據的副本,這裏既然undo-log 有了那麼多副本,MVCC 就借雞生蛋, 複用 這些數據副本。
所以,undo log 中的副本,可以用於實現多版本併發控制(MVCC),提升事務的併發性能。
核心組件3. read-view
那麼多的數據副本,通過對比時間戳或者版本號,看到自己能看的版本?
undo log保存的是一個版本鏈,也就是使用DB_ROLL_PTR這個字段來連接的。
多個事務的 undo-log 日誌副本 (數據快照),組成了一個 副本鏈,如下圖:
那麼,如果多個事務並行的讀寫操作,每一個事務應該使用那個版本呢?
簡單來說,在MVCC中,每個事務可以有一個特定的時間戳或者版本號,而通過對比事務的時間點所能看到的數據版本的集合。
一般來說,時間戳或者版本號的對比規則包括以下幾個方面:
- 已提交數據:事務只能看到已經提交的數據版本。即如果某個數據版本的提交時間早於當前事務的開始時間,則該數據版本對事務是可見的。
- 未提交數據:事務不應該看到其他事務尚未提交的數據版本。即如果某個數據版本的提交時間晚於事務的開始時間,則該數據版本對事務是不可見的。
- 事務開始時間:事務開始時間是確定事務 read-view 的關鍵因素之一。事務只能看到在它開始時間之前已經提交的數據版本。
- 數據快照:事務讀取數據時,read-view 應該是一個一致的數據快照,即事務開始時刻的數據庫狀態的一個一致性快照。這樣可以確保事務讀取的數據是在一個一致的時間點獲取的。
通過遵循這些對比規則,數據庫系統可以保證事務讀取的數據是一致的、可靠的,並且與其他併發事務的操作相互獨立。
一圖勝千言。
40歲老架構師用一張圖,給大家弄一個例子,展示一個下面的場景。
下面的圖中,對於事務4來說,可以看到的數據版本,是事務1的已經提交的數據:
上圖中,事務2,事務3,事務5的快照版本,事務4的是不可以看到的。
當然, 上面是通過時間比對來的,但是 mysql 的MVCC不是通過對比時間戳來實現的。
MVCC 使用 一個新的組件,read-view + 一組對比規則,來計算 可見版本。
read-view 有一些列的對比規則,這些規則用於確定一個事務在讀取數據時,如何與數據庫中的其他事務的版本號(這裏其實就是事務ID)進行比較,以確定它所能看到的數據版本。
當 執行一個select語句時MVCC 會產生一致性視圖read view
。那麼這個read view 沒有記錄事務的開始時間,和截止時間 , 而是換成另一種方式去記錄開始時間和截止時間,換成什麼方式呢:
- read view 記錄當前活躍事務 id,組成活躍事務id數組 ,這個屬性的作用,哪些事務是當前事務,也是不可見的
- read view 記錄當前最小活躍事務 id,這個屬性的作用,用於判斷哪些事務是已經提交了的
- read view 記錄當前的下一個事務 id,這個屬性的作用,用於判斷哪些事務是未來事務,也是不可見的
注意,上面是尼恩爲大家總結和歸納的,比較清晰好記, mysql 的MVCC 版本的對比規則, 看上去非常、非常複雜。
下面是mysql 的MVCC 的read view 版本對比規則, 確實也是一個非常複雜的對比邏輯, 很多小夥伴傻傻看不懂, 並且背誦了半天還記不住,非常痛苦。
通過 上面的這個複雜的對比流程, read-view 終於確定一個事務在執行時所能看到的數據視圖。
InnoDB表的四個隱藏字段
通常情況下,當你基於InnoDB
引擎建立一張表後,MySQL
除了會構建你顯式聲明的字段外,通常還會構建一些InnoDB
引擎的隱藏字段,
在InnoDB
引擎中,隱藏字段主要有DB_ROW_ID、DB_Deleted_Bit、DB_TRX_ID、DB_ROLL_PTR
這四個。
列名 | 是否必須 | 描述 |
---|---|---|
row_id | 否 | 隱藏主鍵,單調遞增的行ID,不是必需的,佔用6個字節。 |
deleted_bit | 是 | 刪除標識,佔用1個字節。 |
trx_id | 是 | 最近的更新事務Id,記錄操作該行數據事務的事務ID,佔用6個字節。 |
roll_pointer | 是 | 回滾指針,指向當前記錄行的Undo-log日誌中的舊版本數據,佔用7個字節。 |
隱藏的主鍵:row_id
對於InnoDB
引擎的表而言,由於其表數據是按照 聚簇索引的格式存儲,因此通常都會選擇主鍵作爲聚簇索引列,然後基於主鍵字段構建索引樹,
但如若表中未定義主鍵,則會選擇一個具備 唯一非空屬性 的字段,作爲聚簇索引的字段來構建樹。
當兩者都不存在時,InnoDB
就會隱式定義一個順序遞增的列ROW_ID
來作爲聚簇索引列。
所以, 就算你的表中未定義主鍵、索引,其實默認也會存在一個聚簇索引,只不過這個索引在上層無法使用,僅提供給InnoDB
構建樹結構存儲表數據。
隱藏的刪除標識:deleted_bit
在MySQL中,對於InnoDB中一條delete
語句而言,當執行後並不會立馬刪除表的數據,而是將這條數據的Deleted_Bit
刪除標識改爲1/true
,而不是不會對數據庫中的數據進行物理刪除。
後續的查詢SQL
檢索數據時,如果檢索到了這條數據,但看到隱藏字段Deleted_Bit=1
時,就知道該數據已經被其他事務delete
了,因此不會將這條數據納入結果集。
Deleted_Bit
的優勢:主要是能夠有利於聚簇索引,比如當一個事務中刪除一條數據後,後續又執行了回滾操作,假設此時是真正的刪除了表數據,會發生如下兩種情況:
-
①刪除表數據時,有可能會破壞索引樹原本的結構,導致 葉子節點合併的情況。
-
②事務回滾時,又需重新插入這條數據,再次插入時又會破壞前面的結構,導致 葉子節點分裂 的情況。
所以,當執行delete
語句時,只會改變將隱藏字段中的刪除標識(Deleted_Bit
)改爲1/true
,而不去執行物理刪除(不去破壞索引樹),如果後續事務出現回滾動作,直接將其標識再改回0/false
即可,這樣就避免了索引樹的結構調整。
誰來清理過期數據呢?
了防止“已刪除”的數據佔用過多的磁盤空間,同時確保清理數據時不會影響MVCC
的正常工作,Mysql使用 "Purger線程"完成“已刪除”的數據的定期清理。
"Purger線程"用來定期檢查數據庫中的數據,並根據一些預定義的規則或條件來決定哪些數據應該被刪除或清理。
Purger線程的主要職責包括:
- 檢查數據庫中的數據,識別哪些數據應該被清理。
- 根據一些預定義的規則或條件來決定數據的清理方式,比如按時間戳刪除過期數據或者根據某些屬性標記數據爲無效。
- 執行清理操作,刪除或標記需要清理的數據。
- 定期運行,以確保數據庫中的數據保持在一個合理的範圍內,避免存儲空間被不必要的數據佔用。
Purger線程通常在後臺運行,定期執行清理任務,以保持數據庫的健康狀態和良好的性能。
purger
線程自身也會維護一個ReadView
,如果某條數據的Deleted_Bit=true
,並且TRX_ID
對purge
線程的ReadView
可見,那麼這條數據一定是可以被安全清除的(即不會影響MVCC
工作)。
隱藏的最近更新事務ID:trx_id
TRX_ID
全稱爲transaction_id
,即是事務ID
的意思,
MySQL
對於每一個創建的事務,都會爲其分配一個事務ID
,事務ID
同樣遵循順序遞增的特性,即後來的事務ID
絕對會比之前的ID
要大,比如:
此時事務
T1
準備修改表字段的值,MySQL
會爲其分配一個事務ID=1
,當事務T2
準備向表中插入一條數據時,又會爲這個事務分配一個ID=2
......如果是SELECT語句,則分配的事務ID = 0;
表中的隱藏字段TRX_ID
,記錄的就是最近一次改動當前這條數據的事務ID
,這個字段是實現MVCC
機制的核心之一。
隱藏的回滾指針:roll_ptr
ROLL_PTR
全稱爲rollback_pointer
,也就是回滾指針的意思,這個也是表中每條數據都會存在的一個隱藏字段。
當一個事務對一條數據做了改動後,都會將舊版本的數據放到Undo-log
日誌中,而rollback_pointer
就是一個地址指針,指向Undo-log
日誌中舊版本的數據。
當需要回滾事務時,就可以通過這個隱藏列,來找到改動之前的舊版本數據,而MVCC
機制也利用這點,實現了行數據的多版本。
InnoDB引擎的Undo-log日誌
Undo-log可以理解成回滾日誌,它存儲的是老版本數據。
在表記錄修改之前,會先把原始數據拷貝到Undo-log裏,如果事務回滾,即可以通過Undo-log來還原數據。
或者如果當前記錄行不可見,可以順着Undo-log鏈找到滿足其可見性條件的記錄行版本。
在insert/update/delete(本質也是做更新,只是更新一個特殊的刪除位字段)操作時,都會產生Undo-log。
在InnoDB裏,Undo-log分爲如下兩類:
-
insert Undo-log : 事務對insert新記錄時產生的Undo-log, 只在事務回滾時需要, 並且在事務提交後就可以立即丟棄。
-
update Undo-log : 事務對記錄進行delete和update操作時產生的Undo-log,不僅在事務回滾時需要,快照讀也需要,只有當數據庫所使用的快照中不涉及該日誌記錄,對應的回滾日誌纔會被刪除。
Undo-log有什麼用途呢?
1.事務回滾時,保證原子性和一致性。
2.如果當前記錄行不可見,可以順着undo log鏈找到滿足其可見性條件的記錄行版本(用於MVCC快照讀)。
我們來看如下例子,理解一下Undo-log版本鏈。
如上述這段SQL
隸屬於trx_id=1
的T1
事務,其中對同一條數據改動了兩次,那Undo-log
日誌中只會存儲兩條舊版本的數據,如下圖:
從上圖中可明顯看出:
不同的舊版本數據,會以roll_ptr
回滾指針作爲鏈接點,然後將所有的舊版本數據組成一個單向鏈表
。
請注意:最新的舊版本數據,都會插入到鏈表頭中
,而不是追加到鏈表尾部。
細說一下執行上述
update
語句的詳細過程:
1.對ID=1
這條要修改的行數據加上排他鎖。2.將原本的舊數據拷貝到
Undo-log
的rollback Segment
區域。3.對錶數據上的記錄進行修改,修改完成後將隱藏字段中的
trx_id
改爲當前事務ID
。4.將隱藏字段中的
roll_ptr
指向Undo-log
中對應的舊數據,並在提交事務後釋放鎖。
爲什麼Undo-log
日誌要設計出版本鏈呢?
有如下兩個好處:
-
一方面可以實現
事務點回滾
; -
另一方面則可以實現
MVCC
機制。
與之前的刪除標識類似,一條數據被delete
後並提交了,最終會從磁盤移除,而Undo-log
中記錄的舊版本數據,同樣會佔用空間,
因此在事務提交後也會移除,移除的工作同樣由purger
線程負責,purger
線程內部也會維護一個ReadView
,它會以此作爲判斷依據,來決定何時移除Undo
記錄。
快照讀和當前讀
快照讀,就是讀取快照數據,即快照生成的那一刻的數據。
在不加鎖的情況下,我們使常用的 普通的SELECT語句 就是快照讀,如下:
SELECT * FROM USER WHERE ......
當前讀,就是讀取最新的數據,要讀取最新提交的數據版本。
我們在加鎖SELECT語句,或者對數據進行增、刪、改都會進行當前讀。如下:
SELECT * FROM USER LOCK IN SHARE MODE;
SELECT * FROM USER FOR UPDATE;
INSERT INTO USER VALUES ......
DELETE FROM USER WHERE ......
UPDATE USER SET ......
在MySQL中只有在RR和RC
這兩個事務隔離級別下才會使用 快照讀。
在RR中,快照會在事務中第一次SELECT語句執行時生成,只有在本事務中對數據進行更改 纔會更新快照。
在RC中,每次SELECT都會重新生成一個快照,總是讀取最新版本數據。
MVCC核心ReadView
經過前面的分析,對於MVCC多版本併發控制,多版本是通過Undo-log日誌
實現。
先來思考如下的問題:
如果T1
事務要查詢id=1的一條行數據,此時這條行數據正在被T2
事務修改,那也就代表着這條數據可能存在多箇舊版本數據,T1
事務在查詢時,應該讀這條數據的哪個版本呢?
此時就需要用到ReadView
,用它來做多版本的併發控制,根據查詢的時機,來選擇一個當前事務可見的舊版本數據讀取。
什麼是ReadView呢?
當一個事務在嘗試讀取一條數據時,MVCC
基於當前MySQL
的運行狀態生成的快照,也被稱之爲讀視圖,即ReadView
,在這個快照中記錄着當前所有活躍事務的ID
(活躍事務是指還在執行的事務,即未結束(提交/回滾)的事務)。
ReadView是事務在進行快照讀的時候生成的記錄快照, 可以幫助我們解決可見性問題的。
ReadView的核心屬性
當一個事務啓動後,首次執行select
操作時,MVCC
就會生成一個數據庫當前的ReadView
,
通常而言,一個事務與一個ReadView
屬於一對一的關係(不同隔離級別下也會存在細微差異),ReadView
一般包含4個核心屬性:
屬性 | 描述 |
---|---|
creator_trx_id | 代表創建當前這個ReadView 的事務ID 。 |
trx_ids | 表示在生成當前ReadView 時,系統內活躍(未提交)的事務ID 列表,它的數據結構爲一個List。(注意 :這裏的trx_ids中的活躍事務,不包括當前事務自己和已提交的事務,這點非常重要) |
up_limit_id | 活躍的事務列表(trx_ids)中,最小的事務ID ,如果trx_ids爲空,則up_limit_id 爲 low_limit_id。 |
low_limit_id | 表示在生成當前ReadView 時,系統中要給下一個事務分配的ID值 。(注意 :它並不是目前系統中活躍事務的最大ID,因爲MySQL的事務ID是按序遞增的,因此當啓動一個新的事務時,都會爲其分配事務ID,而這個low_limit_id則是整個MySQL中要爲下一個事務分配的ID值。) |
我們假設目前數據庫中共有T1~T6
這6個事務,T1、T2、T4、T6
還在執行,T3
已經回滾,T5
已經提交,
此時當有一條查詢語句執行時,就會利用MVCC
機制生成一個ReadView
,由於在MySQL中單純由一條select
語句組成的事務並不會分配事務ID
,因此默認爲0
,所以目前這個ReadView的信息如下:
ReadView的讀取規則
訪問某條記錄的時候如何判斷該記錄是否可見,具體規則如下:
- 如果被訪問版本的
事務ID = creator_trx_id
,那麼表示當前事務訪問的是自己修改過的記錄,那麼該版本對當前事務可見; - 如果被訪問版本的
事務ID < up_limit_id
,那麼表示生成該版本的事務在當前事務生成 ReadView 前已經提交,所以該版本可以被當前事務訪問。 - 如果被訪問版本的
事務ID > low_limit_id
值,那麼表示生成該版本的事務在當前事務生成 ReadView 後纔開啓,所以該版本不可以被當前事務訪問。 - 如果被訪問版本的
事務ID在 up_limit_id和m_low_limit_id
之間,那就需要判斷一下版本的事務ID是不是在 trx_ids 列表中,如果在,說明創建 ReadView 時生成該版本的事務還是活躍的,該版本不可以被訪問; - 如果不在,說明創建 ReadView 時生成該版本的事務已經被提交,該版本可以被訪問。
上面這種圖,網上有上萬篇文章, 都是抄來抄去, 沒有一篇文章做了總結和簡化。
關於這個對比規則,由於邏輯複雜,導致儘管大家看了那些文章,甚至看了很多視頻,還是不能理解透徹, 迷迷糊糊的,面試的時候 說不清楚,也很容易忘了。
尼恩團隊看不下去,用咱們的雄厚技術實力(洪荒之力), 給大家來總結和簡化。
具體如下:
此圖,是全網的第一張徹底穿透式的解讀 MVCC的對比規則的圖。
通過此文,尼恩團隊 第一次,幫助大家搞清楚複雜的 MVCC的底層原理。
此圖很容易理解,很容易記憶。
大家可收藏起來, 面試之前複習一下,一定能吊打面試官。
不對,是吊死面試官。
尼恩團隊用深厚的架構功力,非常喜歡也非常善於,把複雜的問題做清晰深入的穿透式、起底式的分析:
- 比如Netty的內存池和對象池(那個超級難,很多人窮其一生都搞不懂),
- 比如DDD的建模和落地,
- 比如Caffeine的底層架構,
- 比如高性能葵花寶典
- 比如 Thread Local 學習聖經
- 等等等等。
這個技術難題一旦掌握,大家內力猛漲。 所以,建議大家去看看尼恩的這些核心內容。
而且,尼恩團隊進行一次真正的AI架構穿透,幫助大家穿透AI架構。
扯遠了,言歸正傳。
ReadView的生成規則
在MySQL中只有在RR(可重複讀)和RC(讀已提交)
這兩個事務隔離級別下有效,生成ReadView規則是不同的:
在RR中,
ReadView
會在事務中第一次SELECT
語句執行時生成,只有在本事務中對數據進行更改纔會更新快照。在RC中,每次SELECT都會重新生成一個
ReadView
,總是讀取最新版本數據。讀已提交和可重複讀唯一的區別在於:
1.在RC隔離級別下,是每個select都會創建最新的ReadView;
2.而在RR隔離級別下,則是當事務中的第一個select請求才創建ReadView。
總結:MVCC實現原理
經過前面的分析後已得知:
-
當一個事務嘗試改動某條數據時,會將原本表中的舊數據放入
Undo-log
日誌中。 -
當一個事務嘗試查詢某條數據時,
MVCC
會生成一個ReadView
快照。其中
Undo-log
主要實現數據的多版本,ReadView
則主要實現多版本的併發控制。結合如下例子說明:
-- 事務T1:trx_id=1
UPDATE user_info SET name = "小夏" WHERE id = 1;
UPDATE user_info SET sex = "女" WHERE id = 1;
-- 事務T2:trx_id=2
SELECT * FROM user_info WHERE id = 1;
目前存在T1、T2
兩個併發事務,T1
目前在修改ID=1
的這條數據,而T2
則準備查詢這條數據,那麼T2
在執行時具體過程如下:
-
1.當事務中出現
select
語句時,會先根據MySQL
的當前情況生成一個ReadView
。 -
2.判斷行數據中的隱藏列
trx_id
與ReadView.creator_trx_id
是否相同:- 相同:代表創建
ReadView
和修改行數據的事務是同一個,自然可以讀取最新版數據。 - 不相同:代表目前要查詢的數據,是被其他事務修改過的,繼續往下執行。
- 相同:代表創建
-
3.判斷隱藏列
trx_id
是否小於ReadView.up_limit_id
最小活躍事務ID
:- 小於:代表改動行數據的事務在創建快照前就已結束,可以讀取最新版本的數據。
- 不小於:則代表改動行數據的事務還在執行,因此需要繼續往下判斷。
-
4.判斷隱藏列
trx_id
是否小於ReadView.low_limit_id
這個值:- 大於或等於:代表改動行數據的事務是生成快照後纔開啓的,因此不能訪問最新版數據。
- 小於:表示改動行數據的事務
ID
在up_limit_id、low_limit_id
之間,需要進一步判斷。
-
5.如果隱藏列
trx_id
小於low_limit_id
,繼續判斷trx_id
是否在trx_ids
中:-
在:表示改動行數據的事務目前依舊在執行,不能訪問最新版數據。
-
不在:表示改動行數據的事務已經結束,可以訪問最新版的數據。
然後經過上述一系列判斷後,可以得知:目前查詢數據的事務到底能不能訪問最新版的數據。
如果能,就直接拿到表中的數據並返回,反之,不能則去
Undo-log
日誌中獲取舊版本的數據返回。
-
總結
MVCC
多版本併發控制,其中的多版本主要依賴Undo-log
日誌來實現,而併發控制則通過表的隱藏字段
+ReadView
快照來實現,通過Undo-log
日誌、隱藏字段
、ReadView
快照這3點,就實現了MVCC
機制。
說在最後:有問題找老架構取經
MVCC 相關的面試題,是非常常見的面試題。也是核心面試題。
以上的內容,如果大家能對答如流,如數家珍,基本上 面試官會被你 震驚到、吸引到。最終,讓面試官愛到 “不能自已、口水直流”。offer, 也就來了。
在面試之前,建議大家系統化的刷一波 5000頁《尼恩Java面試寶典》V174,在刷題過程中,如果有啥問題,大家可以來 找 40歲老架構師尼恩交流。
另外,如果沒有面試機會,可以找尼恩來幫扶、領路。尼恩已經指導了大量的就業困難的小夥伴上岸.
前段時間,幫助一個40歲+就業困難小夥伴拿到了一個年薪100W的offer,小夥伴實現了 逆天改命 。
技術自由的實現路徑:
實現你的 架構自由:
《阿里二面:千萬級、億級數據,如何性能優化? 教科書級 答案來了》
《峯值21WQps、億級DAU,小遊戲《羊了個羊》是怎麼架構的?》
… 更多架構文章,正在添加中
實現你的 響應式 自由:
這是老版本 《Flux、Mono、Reactor 實戰(史上最全)》
實現你的 spring cloud 自由:
《Spring cloud Alibaba 學習聖經》 PDF
《分庫分表 Sharding-JDBC 底層原理、核心實戰(史上最全)》
《一文搞定:SpringBoot、SLF4j、Log4j、Logback、Netty之間混亂關係(史上最全)》
實現你的 linux 自由:
實現你的 網絡 自由:
《網絡三張表:ARP表, MAC表, 路由表,實現你的網絡自由!!》
實現你的 分佈式鎖 自由:
實現你的 王者組件 自由:
《隊列之王: Disruptor 原理、架構、源碼 一文穿透》
《緩存之王:Caffeine 源碼、架構、原理(史上最全,10W字 超級長文)》
《Java Agent 探針、字節碼增強 ByteBuddy(史上最全)》