什么是MVCC
Multi-Version Concurrency Control 多版本并發(fā)控制,MVCC 是一種并發(fā)控制的方法,一般在數(shù)據(jù)庫(kù)管理系統(tǒng)中,實(shí)現(xiàn)對(duì)數(shù)據(jù)庫(kù)的并發(fā)訪問;在編程語(yǔ)言中實(shí)現(xiàn)事務(wù)內(nèi)存。
MVCC有什么用
如何控制并發(fā)是數(shù)據(jù)庫(kù)領(lǐng)域中非常重要的問題之一,不過到今天為止事務(wù)并發(fā)的控制已經(jīng)有了很多成熟的解決方案,而這些方案的原理就是這篇文章想要介紹的內(nèi)容,文章中會(huì)介紹最為常見的三種并發(fā)控制機(jī)制:
分別是悲觀并發(fā)控制、樂觀并發(fā)控制和多版本并發(fā)控制,其中悲觀并發(fā)控制其實(shí)是最常見的并發(fā)控制機(jī)制,也就是鎖;而樂觀并發(fā)控制其實(shí)也有另一個(gè)名字:樂觀鎖,樂觀鎖其實(shí)并不是一種真實(shí)存在的鎖,我們會(huì)在文章后面的部分中具體介紹;最后就是多版本并發(fā)控制(MVCC)了,與前兩者對(duì)立的命名不同,MVCC 可以與前兩者中的任意一種機(jī)制結(jié)合使用,以提高數(shù)據(jù)庫(kù)的讀性能。
悲觀并發(fā)控制
控制不同的事務(wù)對(duì)同一份數(shù)據(jù)的獲取是保證數(shù)據(jù)庫(kù)的一致性的最根本方法,如果我們能夠讓事務(wù)在同一時(shí)間對(duì)同一資源有著獨(dú)占的能力,那么就可以保證操作同一資源的不同事務(wù)不會(huì)相互影響。
最簡(jiǎn)單的、應(yīng)用最廣的方法就是使用鎖來解決,當(dāng)事務(wù)需要對(duì)資源進(jìn)行操作時(shí)需要先獲得資源對(duì)應(yīng)的鎖,保證其他事務(wù)不會(huì)訪問該資源后,在對(duì)資源進(jìn)行各種操作;在悲觀并發(fā)控制中,數(shù)據(jù)庫(kù)程序?qū)τ跀?shù)據(jù)被修改持悲觀的態(tài)度,在數(shù)據(jù)處理的過程中都會(huì)被鎖定,以此來解決競(jìng)爭(zhēng)的問題。
讀寫鎖
為了最大化數(shù)據(jù)庫(kù)事務(wù)的并發(fā)能力,數(shù)據(jù)庫(kù)中的鎖被設(shè)計(jì)為兩種模式,分別是共享鎖和互斥鎖。當(dāng)一個(gè)事務(wù)獲得共享鎖之后,它只可以進(jìn)行讀操作,所以共享鎖也叫讀鎖;而當(dāng)一個(gè)事務(wù)獲得一行數(shù)據(jù)的互斥鎖時(shí),就可以對(duì)該行數(shù)據(jù)進(jìn)行讀和寫操作,所以互斥鎖也叫寫鎖。
共享鎖和互斥鎖除了限制事務(wù)能夠執(zhí)行的讀寫操作之外,它們之間還有『共享』和『互斥』的關(guān)系,也就是多個(gè)事務(wù)可以同時(shí)獲得某一行數(shù)據(jù)的共享鎖,但是互斥鎖與共享鎖和其他的互斥鎖并不兼容,我們可以很自然地理解這么設(shè)計(jì)的原因:多個(gè)事務(wù)同時(shí)寫入同一數(shù)據(jù)難免會(huì)發(fā)生各種詭異的問題。
如果當(dāng)前事務(wù)沒有辦法獲取該行數(shù)據(jù)對(duì)應(yīng)的鎖時(shí)就會(huì)陷入等待的狀態(tài),直到其他事務(wù)將當(dāng)前數(shù)據(jù)對(duì)應(yīng)的鎖釋放才可以獲得鎖并執(zhí)行相應(yīng)的操作。
樂觀并發(fā)控制
除了悲觀并發(fā)控制機(jī)制 - 鎖之外,我們其實(shí)還有其他的并發(fā)控制機(jī)制,樂觀并發(fā)控制(Optimistic Concurrency Control)。樂觀并發(fā)控制也叫樂觀鎖,但是它并不是真正的鎖,很多人都會(huì)誤以為樂觀鎖是一種真正的鎖,然而它只是一種并發(fā)控制的思想。
在這一節(jié)中,我們將會(huì)先介紹基于時(shí)間戳的并發(fā)控制機(jī)制,然后在這個(gè)協(xié)議的基礎(chǔ)上進(jìn)行擴(kuò)展,實(shí)現(xiàn)樂觀的并發(fā)控制機(jī)制。
基于時(shí)間戳的協(xié)議
鎖協(xié)議按照不同事務(wù)對(duì)同一數(shù)據(jù)項(xiàng)請(qǐng)求的時(shí)間依次執(zhí)行,因?yàn)楹竺鎴?zhí)行的事務(wù)想要獲取的數(shù)據(jù)已將被前面的事務(wù)加鎖,只能等待鎖的釋放,所以基于鎖的協(xié)議執(zhí)行事務(wù)的順序與獲得鎖的順序有關(guān)。在這里想要介紹的基于時(shí)間戳的協(xié)議能夠在事務(wù)執(zhí)行之前先決定事務(wù)的執(zhí)行順序。
每一個(gè)事務(wù)都會(huì)具有一個(gè)全局唯一的時(shí)間戳,它即可以使用系統(tǒng)的時(shí)鐘時(shí)間,也可以使用計(jì)數(shù)器,只要能夠保證所有的時(shí)間戳都是唯一并且是隨時(shí)間遞增的就可以。
基于時(shí)間戳的協(xié)議能夠保證事務(wù)并行執(zhí)行的順序與事務(wù)按照時(shí)間戳串行執(zhí)行的效果完全相同;每一個(gè)數(shù)據(jù)項(xiàng)都有兩個(gè)時(shí)間戳,讀時(shí)間戳和寫時(shí)間戳,分別代表了當(dāng)前成功執(zhí)行對(duì)應(yīng)操作的事務(wù)的時(shí)間戳。
該協(xié)議能夠保證所有沖突的讀寫操作都能按照時(shí)間戳的大小串行執(zhí)行,在執(zhí)行對(duì)應(yīng)的操作時(shí)不需要關(guān)注其他的事務(wù)只需要關(guān)心數(shù)據(jù)項(xiàng)對(duì)應(yīng)時(shí)間戳的值就可以了:
無論是讀操作還是寫操作都會(huì)從左到右依次比較讀寫時(shí)間戳的值,如果小于當(dāng)前值就會(huì)直接被拒絕然后回滾,數(shù)據(jù)庫(kù)系統(tǒng)會(huì)給回滾的事務(wù)添加一個(gè)新的時(shí)間戳并重新執(zhí)行這個(gè)事務(wù)。
基于驗(yàn)證的協(xié)議
樂觀并發(fā)控制其實(shí)本質(zhì)上就是基于驗(yàn)證的協(xié)議,因?yàn)樵诙鄶?shù)的應(yīng)用中只讀的事務(wù)占了絕大多數(shù),事務(wù)之間因?yàn)閷懖僮髟斐蓻_突的可能非常小,也就是說大多數(shù)的事務(wù)在不需要并發(fā)控制機(jī)制也能運(yùn)行的非常好,也可以保證數(shù)據(jù)庫(kù)的一致性;而并發(fā)控制機(jī)制其實(shí)向整個(gè)數(shù)據(jù)庫(kù)系統(tǒng)添加了很多的開銷,我們其實(shí)可以通過別的策略降低這部分開銷。
而驗(yàn)證協(xié)議就是我們找到的解決辦法,它根據(jù)事務(wù)的只讀或者更新將所有事務(wù)的執(zhí)行分為兩到三個(gè)階段:
在讀階段,數(shù)據(jù)庫(kù)會(huì)執(zhí)行事務(wù)中的全部讀操作和寫操作,并將所有寫后的值存入臨時(shí)變量中,并不會(huì)真正更新數(shù)據(jù)庫(kù)中的內(nèi)容;在這時(shí)候會(huì)進(jìn)入下一個(gè)階段,數(shù)據(jù)庫(kù)程序會(huì)檢查當(dāng)前的改動(dòng)是否合法,也就是是否有其他事務(wù)在 RAED PHASE 期間更新了數(shù)據(jù),如果通過測(cè)試那么直接就進(jìn)入 WRITE PHASE 將所有存在臨時(shí)變量中的改動(dòng)全部寫入數(shù)據(jù)庫(kù),沒有通過測(cè)試的事務(wù)會(huì)直接被終止。
為了保證樂觀并發(fā)控制能夠正常運(yùn)行,我們需要知道一個(gè)事務(wù)不同階段的發(fā)生時(shí)間,包括事務(wù)開始時(shí)間、驗(yàn)證階段的開始時(shí)間以及寫階段的結(jié)束時(shí)間;通過這三個(gè)時(shí)間戳,我們可以保證任意沖突的事務(wù)不會(huì)同時(shí)寫入數(shù)據(jù)庫(kù),一旦由一個(gè)事務(wù)完成了驗(yàn)證階段就會(huì)立即寫入,其他讀取了相同數(shù)據(jù)的事務(wù)就會(huì)回滾重新執(zhí)行。
作為樂觀的并發(fā)控制機(jī)制,它會(huì)假定所有的事務(wù)在最終都會(huì)通過驗(yàn)證階段并且執(zhí)行成功,而鎖機(jī)制和基于時(shí)間戳排序的協(xié)議是悲觀的,因?yàn)樗鼈儠?huì)在發(fā)生沖突時(shí)強(qiáng)制事務(wù)進(jìn)行等待或者回滾,哪怕有不需要鎖也能夠保證事務(wù)之間不會(huì)沖突的可能。
多版本并發(fā)控制
到目前為止我們介紹的并發(fā)控制機(jī)制其實(shí)都是通過延遲或者終止相應(yīng)的事務(wù)來解決事務(wù)之間的競(jìng)爭(zhēng)條件(Race condition)來保證事務(wù)的可串行化;雖然前面的兩種并發(fā)控制機(jī)制確實(shí)能夠從根本上解決并發(fā)事務(wù)的可串行化的問題,但是在實(shí)際環(huán)境中數(shù)據(jù)庫(kù)的事務(wù)大都是只讀的,讀請(qǐng)求是寫請(qǐng)求的很多倍,如果寫請(qǐng)求和讀請(qǐng)求之前沒有并發(fā)控制機(jī)制,那么最壞的情況也是讀請(qǐng)求讀到了已經(jīng)寫入的數(shù)據(jù),這對(duì)很多應(yīng)用完全是可以接受的。
在這種大前提下,數(shù)據(jù)庫(kù)系統(tǒng)引入了另一種并發(fā)控制機(jī)制 - 多版本并發(fā)控制(Multiversion Concurrency Control),每一個(gè)寫操作都會(huì)創(chuàng)建一個(gè)新版本的數(shù)據(jù),讀操作會(huì)從有限多個(gè)版本的數(shù)據(jù)中挑選一個(gè)最合適的結(jié)果直接返回;在這時(shí),讀寫操作之間的沖突就不再需要被關(guān)注,而管理和快速挑選數(shù)據(jù)的版本就成了 MVCC 需要解決的主要問題。
MVCC 并不是一個(gè)與樂觀和悲觀并發(fā)控制對(duì)立的東西,它能夠與兩者很好的結(jié)合以增加事務(wù)的并發(fā)量,在目前最流行的 SQL 數(shù)據(jù)庫(kù) MySQL 和 PostgreSQL 中都對(duì) MVCC 進(jìn)行了實(shí)現(xiàn);但是由于它們分別實(shí)現(xiàn)了悲觀鎖和樂觀鎖,所以 MVCC 實(shí)現(xiàn)的方式也不同。
MySQL 與 MVCC
MySQL 中實(shí)現(xiàn)的多版本兩階段鎖協(xié)議(Multiversion 2PL)將 MVCC 和 2PL 的優(yōu)點(diǎn)結(jié)合了起來,每一個(gè)版本的數(shù)據(jù)行都具有一個(gè)唯一的時(shí)間戳,當(dāng)有讀事務(wù)請(qǐng)求時(shí),數(shù)據(jù)庫(kù)程序會(huì)直接從多個(gè)版本的數(shù)據(jù)項(xiàng)中具有最大時(shí)間戳的返回。
更新操作就稍微有些復(fù)雜了,事務(wù)會(huì)先讀取最新版本的數(shù)據(jù)計(jì)算出數(shù)據(jù)更新后的結(jié)果,然后創(chuàng)建一個(gè)新版本的數(shù)據(jù),新數(shù)據(jù)的時(shí)間戳是目前數(shù)據(jù)行的最大版本 +1:
數(shù)據(jù)版本的刪除也是根據(jù)時(shí)間戳來選擇的,MySQL 會(huì)將版本最低的數(shù)據(jù)定時(shí)從數(shù)據(jù)庫(kù)中清除以保證不會(huì)出現(xiàn)大量的遺留內(nèi)容。
PostgreSQL 與 MVCC
與 MySQL 中使用悲觀并發(fā)控制不同,PostgreSQL 中都是使用樂觀并發(fā)控制的,這也就導(dǎo)致了 MVCC 在于樂觀鎖結(jié)合時(shí)的實(shí)現(xiàn)上有一些不同,最終實(shí)現(xiàn)的叫做多版本時(shí)間戳排序協(xié)議(Multiversion Timestamp Ordering),在這個(gè)協(xié)議中,所有的的事務(wù)在執(zhí)行之前都會(huì)被分配一個(gè)唯一的時(shí)間戳,每一個(gè)數(shù)據(jù)項(xiàng)都有讀寫兩個(gè)時(shí)間戳:
當(dāng) PostgreSQL 的事務(wù)發(fā)出了一個(gè)讀請(qǐng)求,數(shù)據(jù)庫(kù)直接將最新版本的數(shù)據(jù)返回,不會(huì)被任何操作阻塞,而寫操作在執(zhí)行時(shí),事務(wù)的時(shí)間戳一定要大或者等于數(shù)據(jù)行的讀時(shí)間戳,否則就會(huì)被回滾。
這種 MVCC 的實(shí)現(xiàn)保證了讀事務(wù)永遠(yuǎn)都不會(huì)失敗并且不需要等待鎖的釋放,對(duì)于讀請(qǐng)求遠(yuǎn)遠(yuǎn)多于寫請(qǐng)求的應(yīng)用程序,樂觀鎖加 MVCC 對(duì)數(shù)據(jù)庫(kù)的性能有著非常大的提升;雖然這種協(xié)議能夠針對(duì)一些實(shí)際情況做出一些明顯的性能提升,但是也會(huì)導(dǎo)致兩個(gè)問題,一個(gè)是每一次讀操作都會(huì)更新讀時(shí)間戳造成兩次的磁盤寫入,第二是事務(wù)之間的沖突是通過回滾解決的,所以如果沖突的可能性非常高或者回滾代價(jià)巨大,數(shù)據(jù)庫(kù)的讀寫性能還不如使用傳統(tǒng)的鎖等待方式。
一個(gè)例子
MVCC是通過在每行記錄后面保存兩個(gè)隱藏的列來實(shí)現(xiàn)的。這兩個(gè)列,一個(gè)保存了行的創(chuàng)建時(shí)間,一個(gè)保存行的過期時(shí)間(或刪除時(shí)間)。當(dāng)然存儲(chǔ)的并不是實(shí)際的時(shí)間值,而是系統(tǒng)版本號(hào)(system version number)。每開始一個(gè)新的事務(wù),系統(tǒng)版本號(hào)都會(huì)自動(dòng)遞增。事務(wù)開始時(shí)刻的系統(tǒng)版本號(hào)會(huì)作為事務(wù)的版本號(hào),用來和查詢到的每行記錄的版本號(hào)進(jìn)行比較。
下面看一下在REPEATABLE READ隔離級(jí)別下,MVCC具體是如何操作的。
- SELECT
- InnoDB會(huì)根據(jù)以下兩個(gè)條件檢查每行記錄:
- InnoDB只查找版本早于當(dāng)前事務(wù)版本的數(shù)據(jù)行(也就是,行的系統(tǒng)版本號(hào)小于或等于事務(wù)的系統(tǒng)版本號(hào)),這樣可以確保事務(wù)讀取的行,要么是在事務(wù)開始前已經(jīng)存在的,要么是事務(wù)自身插入或者修改過的。
- 行的刪除版本要么未定義,要么大于當(dāng)前事務(wù)版本號(hào)。這可以確保事務(wù)讀取到的行,在事務(wù)開始之前未被刪除。
- 只有符合上述兩個(gè)條件的記錄,才能返回作為查詢結(jié)果
- INSERT
- InnoDB為新插入的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào)。
- DELETE
- InnoDB為刪除的每一行保存當(dāng)前系統(tǒng)版本號(hào)作為行刪除標(biāo)識(shí)。
- UPDATE
- InnoDB為插入一行新記錄,保存當(dāng)前系統(tǒng)版本號(hào)作為行版本號(hào),同時(shí)保存當(dāng)前系統(tǒng)版本號(hào)到原來的行作為行刪除標(biāo)識(shí)。
- 保存這兩個(gè)額外系統(tǒng)版本號(hào),使大多數(shù)讀操作都可以不用加鎖。這樣設(shè)計(jì)使得讀數(shù)據(jù)操作很簡(jiǎn)單,性能很好,并且也能保證只會(huì)讀取到符合標(biāo)準(zhǔn)的行,不足之處是每行記錄都需要額外的存儲(chǔ)空間,需要做更多的行檢查工作,以及一些額外的維護(hù)工作
舉例說明
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;
假設(shè)系統(tǒng)初始事務(wù)ID為1;
IDNAME創(chuàng)建時(shí)間過期時(shí)間1mi1undefined2kong1undefined
transaction 2:
start transaction; select * from mvcctest; //(1) select * from mvcctest; //(2) commit
SELECT
假設(shè)當(dāng)執(zhí)行事務(wù)2的過程中,準(zhǔn)備執(zhí)行語(yǔ)句(2)時(shí),開始執(zhí)行事務(wù)3:
transaction 3:
start transaction;
insert into mvcctest values(NULL,'qu');
commit;
IDNAME創(chuàng)建時(shí)間過期時(shí)間1mi1undefined2kong1undefined3qu3undefined
事務(wù)3執(zhí)行完畢,開始執(zhí)行事務(wù)2 語(yǔ)句2,由于事務(wù)2只能查詢創(chuàng)建時(shí)間小于等于2的,所以事務(wù)3新增的記錄在事務(wù)2中是查不出來的,這就通過樂觀鎖的方式避免了幻讀的產(chǎn)生
UPDATE
假設(shè)當(dāng)執(zhí)行事務(wù)2的過程中,準(zhǔn)備執(zhí)行語(yǔ)句(2)時(shí),開始執(zhí)行事務(wù)4:
transaction session 4:
start transaction; update mvcctest set name = 'fan' where id = 2; commit;
InnoDB執(zhí)行UPDATE,實(shí)際上是新插入了一行記錄,并保存其創(chuàng)建時(shí)間為當(dāng)前事務(wù)的ID,同時(shí)保存當(dāng)前事務(wù)ID到要UPDATE的行的刪除時(shí)間
IDNAME創(chuàng)建時(shí)間過期時(shí)間1mi1undefined2kong142fan4undefined
事務(wù)4執(zhí)行完畢,開始執(zhí)行事務(wù)2 語(yǔ)句2,由于事務(wù)2只能查詢創(chuàng)建時(shí)間小于等于2的,所以事務(wù)修改的記錄在事務(wù)2中是查不出來的,這樣就保證了事務(wù)在兩次讀取時(shí)讀取到的數(shù)據(jù)的狀態(tài)是一致的
DELETE
假設(shè)當(dāng)執(zhí)行事務(wù)2的過程中,準(zhǔn)備執(zhí)行語(yǔ)句(2)時(shí),開始執(zhí)行事務(wù)5:
transaction session 5:
start transaction; delete from mvcctest where id = 2; commit;
IDNAME創(chuàng)建時(shí)間過期時(shí)間1mi1undefined2kong15
事務(wù)5執(zhí)行完畢,開始執(zhí)行事務(wù)2 語(yǔ)句2,由于事務(wù)2只能查詢創(chuàng)建時(shí)間小于等于2、并且過期時(shí)間大于等于2,所以id=2的記錄在事務(wù)2 語(yǔ)句2中,也是可以查出來的,這樣就保證了事務(wù)在兩次讀取時(shí)讀取到的數(shù)據(jù)的狀態(tài)是一致的