什么是MVCC
Multi-Version Concurrency Control 多版本并發(fā)控制,MVCC 是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫管理系統(tǒng)中,實現(xiàn)對數(shù)據(jù)庫的并發(fā)訪問;在編程語言中實現(xiàn)事務內(nèi)存。
MVCC有什么用
如何控制并發(fā)是數(shù)據(jù)庫領域中非常重要的問題之一,不過到今天為止事務并發(fā)的控制已經(jīng)有了很多成熟的解決方案,而這些方案的原理就是這篇文章想要介紹的內(nèi)容,文章中會介紹最為常見的三種并發(fā)控制機制:

分別是悲觀并發(fā)控制、樂觀并發(fā)控制和多版本并發(fā)控制,其中悲觀并發(fā)控制其實是最常見的并發(fā)控制機制,也就是鎖;而樂觀并發(fā)控制其實也有另一個名字:樂觀鎖,樂觀鎖其實并不是一種真實存在的鎖,我們會在文章后面的部分中具體介紹;最后就是多版本并發(fā)控制(MVCC)了,與前兩者對立的命名不同,MVCC 可以與前兩者中的任意一種機制結(jié)合使用,以提高數(shù)據(jù)庫的讀性能。
悲觀并發(fā)控制
控制不同的事務對同一份數(shù)據(jù)的獲取是保證數(shù)據(jù)庫的一致性的最根本方法,如果我們能夠讓事務在同一時間對同一資源有著獨占的能力,那么就可以保證操作同一資源的不同事務不會相互影響。

最簡單的、應用最廣的方法就是使用鎖來解決,當事務需要對資源進行操作時需要先獲得資源對應的鎖,保證其他事務不會訪問該資源后,在對資源進行各種操作;在悲觀并發(fā)控制中,數(shù)據(jù)庫程序?qū)τ跀?shù)據(jù)被修改持悲觀的態(tài)度,在數(shù)據(jù)處理的過程中都會被鎖定,以此來解決競爭的問題。
讀寫鎖
為了最大化數(shù)據(jù)庫事務的并發(fā)能力,數(shù)據(jù)庫中的鎖被設計為兩種模式,分別是共享鎖和互斥鎖。當一個事務獲得共享鎖之后,它只可以進行讀操作,所以共享鎖也叫讀鎖;而當一個事務獲得一行數(shù)據(jù)的互斥鎖時,就可以對該行數(shù)據(jù)進行讀和寫操作,所以互斥鎖也叫寫鎖。

共享鎖和互斥鎖除了限制事務能夠執(zhí)行的讀寫操作之外,它們之間還有『共享』和『互斥』的關系,也就是多個事務可以同時獲得某一行數(shù)據(jù)的共享鎖,但是互斥鎖與共享鎖和其他的互斥鎖并不兼容,我們可以很自然地理解這么設計的原因:多個事務同時寫入同一數(shù)據(jù)難免會發(fā)生各種詭異的問題。

如果當前事務沒有辦法獲取該行數(shù)據(jù)對應的鎖時就會陷入等待的狀態(tài),直到其他事務將當前數(shù)據(jù)對應的鎖釋放才可以獲得鎖并執(zhí)行相應的操作。
樂觀并發(fā)控制
除了悲觀并發(fā)控制機制 - 鎖之外,我們其實還有其他的并發(fā)控制機制,樂觀并發(fā)控制(Optimistic Concurrency Control)。樂觀并發(fā)控制也叫樂觀鎖,但是它并不是真正的鎖,很多人都會誤以為樂觀鎖是一種真正的鎖,然而它只是一種并發(fā)控制的思想。

在這一節(jié)中,我們將會先介紹基于時間戳的并發(fā)控制機制,然后在這個協(xié)議的基礎上進行擴展,實現(xiàn)樂觀的并發(fā)控制機制。
基于時間戳的協(xié)議
鎖協(xié)議按照不同事務對同一數(shù)據(jù)項請求的時間依次執(zhí)行,因為后面執(zhí)行的事務想要獲取的數(shù)據(jù)已將被前面的事務加鎖,只能等待鎖的釋放,所以基于鎖的協(xié)議執(zhí)行事務的順序與獲得鎖的順序有關。在這里想要介紹的基于時間戳的協(xié)議能夠在事務執(zhí)行之前先決定事務的執(zhí)行順序。
每一個事務都會具有一個全局唯一的時間戳,它即可以使用系統(tǒng)的時鐘時間,也可以使用計數(shù)器,只要能夠保證所有的時間戳都是唯一并且是隨時間遞增的就可以。

基于時間戳的協(xié)議能夠保證事務并行執(zhí)行的順序與事務按照時間戳串行執(zhí)行的效果完全相同;每一個數(shù)據(jù)項都有兩個時間戳,讀時間戳和寫時間戳,分別代表了當前成功執(zhí)行對應操作的事務的時間戳。
該協(xié)議能夠保證所有沖突的讀寫操作都能按照時間戳的大小串行執(zhí)行,在執(zhí)行對應的操作時不需要關注其他的事務只需要關心數(shù)據(jù)項對應時間戳的值就可以了:

無論是讀操作還是寫操作都會從左到右依次比較讀寫時間戳的值,如果小于當前值就會直接被拒絕然后回滾,數(shù)據(jù)庫系統(tǒng)會給回滾的事務添加一個新的時間戳并重新執(zhí)行這個事務。
基于驗證的協(xié)議
樂觀并發(fā)控制其實本質(zhì)上就是基于驗證的協(xié)議,因為在多數(shù)的應用中只讀的事務占了絕大多數(shù),事務之間因為寫操作造成沖突的可能非常小,也就是說大多數(shù)的事務在不需要并發(fā)控制機制也能運行的非常好,也可以保證數(shù)據(jù)庫的一致性;而并發(fā)控制機制其實向整個數(shù)據(jù)庫系統(tǒng)添加了很多的開銷,我們其實可以通過別的策略降低這部分開銷。
而驗證協(xié)議就是我們找到的解決辦法,它根據(jù)事務的只讀或者更新將所有事務的執(zhí)行分為兩到三個階段:

在讀階段,數(shù)據(jù)庫會執(zhí)行事務中的全部讀操作和寫操作,并將所有寫后的值存入臨時變量中,并不會真正更新數(shù)據(jù)庫中的內(nèi)容;在這時候會進入下一個階段,數(shù)據(jù)庫程序會檢查當前的改動是否合法,也就是是否有其他事務在 RAED PHASE 期間更新了數(shù)據(jù),如果通過測試那么直接就進入 WRITE PHASE 將所有存在臨時變量中的改動全部寫入數(shù)據(jù)庫,沒有通過測試的事務會直接被終止。
為了保證樂觀并發(fā)控制能夠正常運行,我們需要知道一個事務不同階段的發(fā)生時間,包括事務開始時間、驗證階段的開始時間以及寫階段的結(jié)束時間;通過這三個時間戳,我們可以保證任意沖突的事務不會同時寫入數(shù)據(jù)庫,一旦由一個事務完成了驗證階段就會立即寫入,其他讀取了相同數(shù)據(jù)的事務就會回滾重新執(zhí)行。
作為樂觀的并發(fā)控制機制,它會假定所有的事務在最終都會通過驗證階段并且執(zhí)行成功,而鎖機制和基于時間戳排序的協(xié)議是悲觀的,因為它們會在發(fā)生沖突時強制事務進行等待或者回滾,哪怕有不需要鎖也能夠保證事務之間不會沖突的可能。
多版本并發(fā)控制
到目前為止我們介紹的并發(fā)控制機制其實都是通過延遲或者終止相應的事務來解決事務之間的競爭條件(Race condition)來保證事務的可串行化;雖然前面的兩種并發(fā)控制機制確實能夠從根本上解決并發(fā)事務的可串行化的問題,但是在實際環(huán)境中數(shù)據(jù)庫的事務大都是只讀的,讀請求是寫請求的很多倍,如果寫請求和讀請求之前沒有并發(fā)控制機制,那么最壞的情況也是讀請求讀到了已經(jīng)寫入的數(shù)據(jù),這對很多應用完全是可以接受的。

在這種大前提下,數(shù)據(jù)庫系統(tǒng)引入了另一種并發(fā)控制機制 - 多版本并發(fā)控制(Multiversion Concurrency Control),每一個寫操作都會創(chuàng)建一個新版本的數(shù)據(jù),讀操作會從有限多個版本的數(shù)據(jù)中挑選一個最合適的結(jié)果直接返回;在這時,讀寫操作之間的沖突就不再需要被關注,而管理和快速挑選數(shù)據(jù)的版本就成了 MVCC 需要解決的主要問題。
MVCC 并不是一個與樂觀和悲觀并發(fā)控制對立的東西,它能夠與兩者很好的結(jié)合以增加事務的并發(fā)量,在目前最流行的 SQL 數(shù)據(jù)庫 MySQL 和 PostgreSQL 中都對 MVCC 進行了實現(xiàn);但是由于它們分別實現(xiàn)了悲觀鎖和樂觀鎖,所以 MVCC 實現(xiàn)的方式也不同。
MySQL 與 MVCC
MySQL 中實現(xiàn)的多版本兩階段鎖協(xié)議(Multiversion 2PL)將 MVCC 和 2PL 的優(yōu)點結(jié)合了起來,每一個版本的數(shù)據(jù)行都具有一個唯一的時間戳,當有讀事務請求時,數(shù)據(jù)庫程序會直接從多個版本的數(shù)據(jù)項中具有最大時間戳的返回。

更新操作就稍微有些復雜了,事務會先讀取最新版本的數(shù)據(jù)計算出數(shù)據(jù)更新后的結(jié)果,然后創(chuàng)建一個新版本的數(shù)據(jù),新數(shù)據(jù)的時間戳是目前數(shù)據(jù)行的最大版本 +1:

數(shù)據(jù)版本的刪除也是根據(jù)時間戳來選擇的,MySQL 會將版本最低的數(shù)據(jù)定時從數(shù)據(jù)庫中清除以保證不會出現(xiàn)大量的遺留內(nèi)容。
PostgreSQL 與 MVCC
與 MySQL 中使用悲觀并發(fā)控制不同,PostgreSQL 中都是使用樂觀并發(fā)控制的,這也就導致了 MVCC 在于樂觀鎖結(jié)合時的實現(xiàn)上有一些不同,最終實現(xiàn)的叫做多版本時間戳排序協(xié)議(Multiversion Timestamp Ordering),在這個協(xié)議中,所有的的事務在執(zhí)行之前都會被分配一個唯一的時間戳,每一個數(shù)據(jù)項都有讀寫兩個時間戳:

當 PostgreSQL 的事務發(fā)出了一個讀請求,數(shù)據(jù)庫直接將最新版本的數(shù)據(jù)返回,不會被任何操作阻塞,而寫操作在執(zhí)行時,事務的時間戳一定要大或者等于數(shù)據(jù)行的讀時間戳,否則就會被回滾。
這種 MVCC 的實現(xiàn)保證了讀事務永遠都不會失敗并且不需要等待鎖的釋放,對于讀請求遠遠多于寫請求的應用程序,樂觀鎖加 MVCC 對數(shù)據(jù)庫的性能有著非常大的提升;雖然這種協(xié)議能夠針對一些實際情況做出一些明顯的性能提升,但是也會導致兩個問題,一個是每一次讀操作都會更新讀時間戳造成兩次的磁盤寫入,第二是事務之間的沖突是通過回滾解決的,所以如果沖突的可能性非常高或者回滾代價巨大,數(shù)據(jù)庫的讀寫性能還不如使用傳統(tǒng)的鎖等待方式。
一個例子
MVCC是通過在每行記錄后面保存兩個隱藏的列來實現(xiàn)的。這兩個列,一個保存了行的創(chuàng)建時間,一個保存行的過期時間(或刪除時間)。當然存儲的并不是實際的時間值,而是系統(tǒng)版本號(system version number)。每開始一個新的事務,系統(tǒng)版本號都會自動遞增。事務開始時刻的系統(tǒng)版本號會作為事務的版本號,用來和查詢到的每行記錄的版本號進行比較。
下面看一下在REPEATABLE READ隔離級別下,MVCC具體是如何操作的。
- SELECT
- InnoDB會根據(jù)以下兩個條件檢查每行記錄:
- InnoDB只查找版本早于當前事務版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號小于或等于事務的系統(tǒng)版本號),這樣可以確保事務讀取的行,要么是在事務開始前已經(jīng)存在的,要么是事務自身插入或者修改過的。
- 行的刪除版本要么未定義,要么大于當前事務版本號。這可以確保事務讀取到的行,在事務開始之前未被刪除。
- 只有符合上述兩個條件的記錄,才能返回作為查詢結(jié)果
- INSERT
- InnoDB為新插入的每一行保存當前系統(tǒng)版本號作為行版本號。
- DELETE
- InnoDB為刪除的每一行保存當前系統(tǒng)版本號作為行刪除標識。
- UPDATE
- InnoDB為插入一行新記錄,保存當前系統(tǒng)版本號作為行版本號,同時保存當前系統(tǒng)版本號到原來的行作為行刪除標識。
- 保存這兩個額外系統(tǒng)版本號,使大多數(shù)讀操作都可以不用加鎖。這樣設計使得讀數(shù)據(jù)操作很簡單,性能很好,并且也能保證只會讀取到符合標準的行,不足之處是每行記錄都需要額外的存儲空間,需要做更多的行檢查工作,以及一些額外的維護工作
舉例說明
create table mvcctest(
id int primary key auto_increment,
name varchar(20));
transaction 1:
start transaction; insert into mvcctest values(NULL,'mi'); insert into mvcctest values(NULL,'kong'); commit;
假設系統(tǒng)初始事務ID為1;
IDNAME創(chuàng)建時間過期時間1mi1undefined2kong1undefined
transaction 2:
start transaction; select * from mvcctest; //(1) select * from mvcctest; //(2) commit
SELECT
假設當執(zhí)行事務2的過程中,準備執(zhí)行語句(2)時,開始執(zhí)行事務3:
transaction 3:
start transaction;
insert into mvcctest values(NULL,'qu');
commit;
IDNAME創(chuàng)建時間過期時間1mi1undefined2kong1undefined3qu3undefined
事務3執(zhí)行完畢,開始執(zhí)行事務2 語句2,由于事務2只能查詢創(chuàng)建時間小于等于2的,所以事務3新增的記錄在事務2中是查不出來的,這就通過樂觀鎖的方式避免了幻讀的產(chǎn)生
UPDATE
假設當執(zhí)行事務2的過程中,準備執(zhí)行語句(2)時,開始執(zhí)行事務4:
transaction session 4:
start transaction; update mvcctest set name = 'fan' where id = 2; commit;
InnoDB執(zhí)行UPDATE,實際上是新插入了一行記錄,并保存其創(chuàng)建時間為當前事務的ID,同時保存當前事務ID到要UPDATE的行的刪除時間
IDNAME創(chuàng)建時間過期時間1mi1undefined2kong142fan4undefined
事務4執(zhí)行完畢,開始執(zhí)行事務2 語句2,由于事務2只能查詢創(chuàng)建時間小于等于2的,所以事務修改的記錄在事務2中是查不出來的,這樣就保證了事務在兩次讀取時讀取到的數(shù)據(jù)的狀態(tài)是一致的
DELETE
假設當執(zhí)行事務2的過程中,準備執(zhí)行語句(2)時,開始執(zhí)行事務5:
transaction session 5:
start transaction; delete from mvcctest where id = 2; commit;
IDNAME創(chuàng)建時間過期時間1mi1undefined2kong15
事務5執(zhí)行完畢,開始執(zhí)行事務2 語句2,由于事務2只能查詢創(chuàng)建時間小于等于2、并且過期時間大于等于2,所以id=2的記錄在事務2 語句2中,也是可以查出來的,這樣就保證了事務在兩次讀取時讀取到的數(shù)據(jù)的狀態(tài)是一致的