MVCC是什么
MySQL的MVCC機制,全稱為多版本并發控制(Multi-VersionConcurrency Control),是一種提高數據庫并發性能的技術。MVCC的主要目的是在保證數據一致性的同時,提高數據庫的并發性能。
它通過為每個讀操作創建數據的快照來實現這一點,這樣即使在數據被其他事務修改的同時,讀操作也能夠看到一致的數據視圖。這種機制避免了同一個數據在不同事務之間的競爭,從而提高了系統的并發性能。
MVCC在MySQL中的實現主要是為了解決讀寫沖突問題,使得即使在有讀寫沖突的情況下,也能做到不加鎖,非阻塞并發讀。MVCC通過維護數據的不同版本來實現這一點,每個事務都可以看到適合自己版本的數據,而不會被其他事務的修改所影響。
MVCC適用范圍
在MySQL中,MVCC只在讀取已提交(Read Committed)和可重復讀(Repeatable Read)兩個事務級別下有效。底層通過Undolog日志中的版本鏈和ReadView一致性視圖來實現的。MVCC就是在多個事務同時存在時,SELECT語句找尋到具體是版本鏈上的哪個版本,然后在找到的版本上返回其中所記錄的數據的過程。
當前讀:總是讀取當前最新的數據。
像select?lock?in?share?mode(共享鎖),?select?for?update?;?update,?insert?,delete(排他鎖)這些操作都是一種當前讀
快照讀:像不加鎖的select操作就是快照讀,即不加鎖的非阻塞讀;快照讀的前提是隔離級別不是串行級別,串行級別下的快照讀會退化成當前讀;之所以出現快照讀的情況,是基于提高并發性能的考慮,快照讀的實現是基于多版本并發控制,即MVCC,可以認為MVCC是行鎖的一個變種,但它在很多情況下,避免了加鎖操作,降低了開銷;既然是基于多版本,即快照讀可能讀到的并不一定是數據的最新版本,而有可能是之前的歷史版本
update delete一定是當前讀
表隱藏字段?
-
DB_ROW_ID:隱藏主鍵,MySQL的B+樹索引特性要求每個表必須要有一個主鍵。如果沒有設置的話,會自動尋找第一個不包含NULL的唯一索引列作為主鍵。如果還是找不到,就會在這個DB_ROW_ID上自動生成一個唯一值,以此來當作主鍵(該列和MVCC的關系不大);
-
DB_TRX_ID:最后一次事務ID,記錄的是當前事務在做INSERT或UPDATE語句操作時的事務ID(DELETE語句被當做是UPDATE語句的特殊情況,后面會進行說明);
-
DB_ROLL_PTR:回滾指針,通過它可以將不同的版本串聯起來,形成版本鏈。相當于鏈表的next指針。這個指針實際就是指向undolog的快照對應版本的數據。
MVCC的工作流程?
1. 事務開始時,獲取一個唯一的事務ID。
2. 當進行讀取操作時,數據庫會創建一個Read View,其中包含了當前系統中活躍事務的信息。
3. 讀取數據時,數據庫會根據Read View中的信息來確定哪個版本的數據是可見的。
4. 如果當前版本的數據不可見,數據庫會通過undo日志找到合適的歷史版本。
MVCC與鎖機制的比較
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;//拷貝原容器,長度為原容器+1Object[] newElements = Arrays.copyOf(elements, len + 1);//在新副本執行添加操作newElements[len] = e;//底層數組指向新的數組setArray(newElements);return true;} finally {lock.unlock();}
}
CopyOnWriteArrayList底層實現添加的原理是先copy出一個容器(可以簡稱副本),再往新的容器里添加這個新的數據,最后把新的容器的引用地址賦值給了之前那個舊的的容器地址,但是在添加這個數據的期間,其他線程如果要去讀取數據,仍然是讀取到舊的容器里的數據。
MVCC核心ReadView
先來思考如下的問題:
如果T1
事務要查詢id=1的一條行數據,此時這條行數據正在被T2
事務修改,那也就代表著這條數據可能存在多個舊版本數據,T1
事務在查詢時,應該讀這條數據的哪個版本呢?
此時就需要用到ReadView
,用它來做多版本的并發控制,根據查詢的時機,來選擇一個當前事務可見的舊版本數據讀取。
什么是ReadView呢??
當一個事務在嘗試讀取一條數據時,MVCC
基于當前MySQL
的運行狀態生成的快照,也被稱之為讀視圖,即ReadView
,在這個快照中記錄著當前所有活躍事務的ID
(活躍事務是指還在執行的事務,即未結束(提交/回滾)的事務)。
ReadView是事務在進行快照讀的時候生成的記錄快照, 可以幫助我們解決可見性問題的。
ReadView的核心屬性
當一個事務啟動后,首次執行select
操作時,MVCC
就會生成一個數據庫當前的ReadView
,
通常而言,一個事務與一個ReadView
屬于一對一的關系(不同隔離級別下也會存在細微差異),ReadView
一般包含4個核心屬性:
我們假設目前數據庫中共有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 時生成該版本的事務已經被提交,該版本可以被訪問。
上面這種圖,網上有上萬篇文章, 都是抄來抄去, 沒有一篇文章做了總結和簡化。
關于這個對比規則,由于邏輯復雜,導致盡管大家看了那些文章,甚至看了很多視頻,還是不能理解透徹, 迷迷糊糊的,面試的時候 說不清楚,也很容易忘了。
尼恩團隊看不下去,用咱們的雄厚技術實力(洪荒之力), 給大家來總結和簡化。
具體如下:
?
?
?