數(shù)據(jù)庫(kù)的事務(wù)包含原子性、一致性、隔離性、持久性四個(gè)特性。隔離性與一致性緊密相連,它們也容易讓人迷惑。SQL標(biāo)準(zhǔn)定義了4個(gè)隔離級(jí)別,但由于定義使用的是自然語(yǔ)言,而非形式化語(yǔ)言,導(dǎo)致人們對(duì)隔離級(jí)別的理解有所差異,各個(gè)數(shù)據(jù)庫(kù)系統(tǒng)的實(shí)現(xiàn)方式也有所不同。然而在分布式的場(chǎng)景下,又面臨新的問(wèn)題。
探索前沿研究,聚焦技術(shù)創(chuàng)新。本期由騰訊云數(shù)據(jù)庫(kù)高級(jí)工程師孟慶鐘為大家介紹數(shù)據(jù)庫(kù)事務(wù)一致性的實(shí)現(xiàn),內(nèi)容包括事務(wù)的基本概念以及特性、主要的隔離級(jí)別及實(shí)現(xiàn)、TDSQL事務(wù)一致性的實(shí)現(xiàn)。
事務(wù)的基本概念及特性
(資料圖)
1.1 事務(wù)的基本概念及特性
事務(wù)是用戶定義的一個(gè)數(shù)據(jù)庫(kù)操作序列,這些操作要么全做,要么全不做,是一個(gè)不可分割的工作單位。
事務(wù)具有四個(gè)特性即ACID:
原子性(A):事務(wù)中包括的操作要么都做,要么都不做;
一致性(C):事務(wù)執(zhí)行的結(jié)果必須使數(shù)據(jù)庫(kù)從一個(gè)一致性狀態(tài)轉(zhuǎn)移到另一個(gè)一致性狀態(tài);
隔離性(I ):并發(fā)執(zhí)行的事務(wù)之間不能互相干擾;
持久性(D):事務(wù)一旦提交,它對(duì)數(shù)據(jù)庫(kù)的改變應(yīng)該是永久性的。
原子性和持久性比較直觀易懂,但是一致性和隔離性則較為復(fù)雜,不同人有不同的理解。
1.2 一致性的理解
一致性是偏應(yīng)用角度的特性,每個(gè)應(yīng)用程序需要自己保證現(xiàn)實(shí)意義上一致。
數(shù)據(jù)庫(kù)在一致性方面對(duì)應(yīng)用程序能作出的保證是:只要事務(wù)執(zhí)行成功,都不會(huì)違反用戶定義的完整性約束。在執(zhí)行事務(wù)的過(guò)程中,只要沒(méi)有違反約束,那么數(shù)據(jù)庫(kù)內(nèi)核就認(rèn)為是一致的。
常見(jiàn)的完整性約束有主鍵約束、外鍵約束、唯一約束、Not-NULL約束、Check約束。只要定義了這些約束,數(shù)據(jù)庫(kù)系統(tǒng)在運(yùn)行時(shí)就不會(huì)違反;只要沒(méi)有違反,數(shù)據(jù)庫(kù)內(nèi)核就認(rèn)為數(shù)據(jù)庫(kù)是一致的。至于現(xiàn)實(shí)意義上是否一致,需要由應(yīng)用程序自行判斷。
1.3 導(dǎo)致不一致的原因
為什么數(shù)據(jù)庫(kù)可能會(huì)不一致呢?其實(shí)是由沖突所導(dǎo)致的。應(yīng)用程序?qū)?shù)據(jù)的讀寫操作,最終體現(xiàn)為數(shù)據(jù)庫(kù)內(nèi)核中的事務(wù)對(duì)數(shù)據(jù)庫(kù)對(duì)象的讀寫操作。如果不同事務(wù)對(duì)相同的數(shù)據(jù)進(jìn)行操作,并且其中一個(gè)操作是寫操作,則這兩個(gè)操作就會(huì)出現(xiàn)沖突。如果不能正確處理這些沖突,就會(huì)出現(xiàn)某些異常。常見(jiàn)的異常主要有臟寫、臟讀、不可重復(fù)讀、幻讀等。
并發(fā)執(zhí)行的事務(wù)產(chǎn)生沖突,其實(shí)可以理解為科幻小說(shuō)里兩個(gè)不相容的物體進(jìn)入了同一時(shí)空。因?yàn)槭窃跁r(shí)空上產(chǎn)生沖突,所以我們可以從時(shí)間和空間兩個(gè)維度解決:
時(shí)間維度:把兩個(gè)操作從時(shí)間維度隔開(kāi),禁止同時(shí)訪問(wèn)。這其實(shí)是基于鎖實(shí)現(xiàn)的并發(fā)控制思想。
空間維度:把這兩個(gè)操作從空間維度隔開(kāi),禁止訪問(wèn)同一份數(shù)據(jù)。這其實(shí)是基于多版本實(shí)現(xiàn)的并發(fā)控制思想。
1.4 隔離性的理解
有些應(yīng)用程序的執(zhí)行邏輯,永遠(yuǎn)不會(huì)導(dǎo)致某些異常的產(chǎn)生。在這種前提下,即使數(shù)據(jù)庫(kù)允許某些異常,實(shí)際上永遠(yuǎn)也不會(huì)產(chǎn)生這些異常,數(shù)據(jù)庫(kù)仍然是一致的。
我們用一個(gè)簡(jiǎn)單的比喻來(lái)進(jìn)行理解。底層模塊看作是數(shù)據(jù)庫(kù),上層模塊看作是應(yīng)用軟件,當(dāng)上層軟件模塊調(diào)用底層模塊時(shí),即使底層模塊有BUG,但如果不踩這個(gè)坑就永遠(yuǎn)不會(huì)觸發(fā)BUG,則應(yīng)用軟件和數(shù)據(jù)庫(kù)組成的成體看起來(lái)并沒(méi)有BUG,數(shù)據(jù)庫(kù)則會(huì)一致。
隔離性是指并發(fā)執(zhí)行的事務(wù)之間不能互相干擾。為了提高系統(tǒng)運(yùn)行效率,SQL標(biāo)準(zhǔn)允許數(shù)據(jù)庫(kù)在隔離性上進(jìn)行妥協(xié),即允許數(shù)據(jù)庫(kù)產(chǎn)生某些異常。那到底需要隔離到什么程度呢?這需要由隔離級(jí)別來(lái)確定。根據(jù)需求的不同,我們可以選擇不同的隔離級(jí)別。
主要的隔離級(jí)別及實(shí)現(xiàn)
2.1 SQL標(biāo)準(zhǔn)定義的隔離級(jí)別
我們所理解的隔離級(jí)別是指并發(fā)執(zhí)行的事務(wù)能看到對(duì)方的多少。下圖是SQL標(biāo)準(zhǔn)給出的定義,根據(jù)數(shù)據(jù)庫(kù)禁止哪些異常來(lái)進(jìn)行劃分。SQL標(biāo)準(zhǔn)定義了四種隔離級(jí)別:Read Uncommitted、Read Committed、Repeatable Read、Serializable。
Read Uncommitted為讀未提交,所以三種異常都有可能發(fā)生。比Read Uncommitted更嚴(yán)格一級(jí)的是Read Committed,即不能讀未提交的事務(wù),但可以出現(xiàn)不可重復(fù)讀和幻讀。更嚴(yán)格的是Repeatable Read,只允許出現(xiàn)一種異常。最嚴(yán)格的是Serializable,這三種異常都不允許發(fā)生。
但該定義也有不足。一方面,上述定義來(lái)源于鎖實(shí)現(xiàn)的并發(fā)控制,但是又想擺脫對(duì)鎖實(shí)現(xiàn)的依賴,所以根據(jù)數(shù)據(jù)庫(kù)不允許哪些異常來(lái)定義數(shù)據(jù)庫(kù)的隔離級(jí)別?;阪i實(shí)現(xiàn)的并發(fā)控制可以完美匹配上面的定義,但是其它實(shí)現(xiàn)方式不一定匹配這個(gè)定義。比如常見(jiàn)的快照隔離(Serializable Isolation),不會(huì)出現(xiàn)這三種異常,按定義屬Serializable,但它可能出現(xiàn)其它異常(寫傾斜),所以快照隔離并非真正的Serializable。另一方面,該定義使用的是自然語(yǔ)言,而非形式化的語(yǔ)言,導(dǎo)致人們理解有差異,有些系統(tǒng)因此直接把快照隔離稱為可串行化。
2.2 基于鎖實(shí)現(xiàn)的并發(fā)控制
鎖可以分為多種類型,包括讀鎖、寫鎖和謂詞鎖。讀鎖、寫鎖鎖單個(gè)數(shù)據(jù)對(duì)象,謂詞鎖鎖一個(gè)范圍。持鎖的時(shí)長(zhǎng)也不相同,可以操作完數(shù)據(jù)直接放鎖,也可以等事務(wù)結(jié)束才放鎖,比如讀數(shù)據(jù)或?qū)憯?shù)據(jù)前先把鎖拿到手,一直持著不放直到事務(wù)結(jié)束。下圖中的Well-formed Reads/Writes是指讀/寫數(shù)據(jù)之前都要加鎖,非Well-formed Reads/Writes就是不加鎖而直接對(duì)數(shù)據(jù)進(jìn)行讀/寫。
基于上述三個(gè)前提,我們可以看到視圖中的隔離級(jí)別,關(guān)于寫的操作基本相同,都是寫數(shù)據(jù)前要先拿到寫鎖,寫鎖等事務(wù)結(jié)束后再放,主要區(qū)別在于讀鎖。
Read Uncommitted的讀不需要加鎖。事務(wù)寫數(shù)據(jù)要加寫鎖,事務(wù)結(jié)束后才放鎖。雖然讀鎖和寫鎖互斥,寫加寫鎖,但讀時(shí)不加鎖就可以直接讀到。
比Read Uncommitted更嚴(yán)格一級(jí)的是Read Committed,讀時(shí)需要加讀鎖。只要能加讀鎖就代表沒(méi)有其它事務(wù)在寫,寫該數(shù)據(jù)的事務(wù)一定已經(jīng)提交。因?yàn)槲刺峤坏膶懯聞?wù)其寫鎖還沒(méi)有放,讀鎖和寫鎖互斥,讀事務(wù)無(wú)法加讀鎖,因此用該隔離級(jí)別讀到的都已提交事務(wù)的數(shù)據(jù)。讀事務(wù)的讀鎖在讀完后就放鎖,下次再讀該數(shù)據(jù)時(shí)要重新加讀鎖。放鎖后再加鎖,該數(shù)據(jù)可能在讀事務(wù)未持讀鎖的期間被其它事務(wù)修改,因此讀到的數(shù)據(jù)可能有變化。
在Serializable下,讀要加讀鎖,到事務(wù)提交時(shí)才放。這就保證了數(shù)據(jù)不會(huì)在讀事務(wù)執(zhí)行期間被修改。因?yàn)槿绻渌聞?wù)要改就需要加寫鎖,寫鎖讀鎖互斥,因此其它事務(wù)的寫鎖加不上。括號(hào)中的Both指的是謂詞鎖和一般的讀鎖都到最后才放。
Repeatable Read(RR)比Read Committed(RC)強(qiáng),比Serializable弱。RR比RC強(qiáng)在讀鎖,RR比RC多一個(gè)讀鎖最后放。RR比Serializable弱在謂詞鎖,RR比Serializable少一個(gè)謂詞鎖最后放,所以RR可能出現(xiàn)幻讀。
2.3 基于多版本實(shí)現(xiàn)的并發(fā)控制
一個(gè)數(shù)據(jù)庫(kù)對(duì)象有多個(gè)不同的版本,每個(gè)版本關(guān)聯(lián)一個(gè)時(shí)間戳。事務(wù)在訪問(wèn)數(shù)據(jù)時(shí),會(huì)使用一個(gè)快照選出合適的版本。這就是多版本并發(fā)控制(MVCC),好處是讀寫互不堵塞,讀時(shí)可在多版本中讀合適的版本,寫時(shí)追加一個(gè)版本。
時(shí)間戳的選擇有兩種主流的方式:
使用事務(wù)的開(kāi)始時(shí)間:PostgreSQL屬于這類系統(tǒng)。大多數(shù)情況下,事務(wù)開(kāi)始的時(shí)間越晚,則產(chǎn)生的版本越新,但是存在特例。為了排除這些特例,PostgreSQL的快照中有一個(gè)活躍事務(wù)列表,列表中的事務(wù)對(duì)快照不可見(jiàn)。
使用事務(wù)的結(jié)束時(shí)間:TDSQL屬于這類系統(tǒng),事務(wù)結(jié)束的早,就可以對(duì)后續(xù)事務(wù)可見(jiàn),所以這類系統(tǒng)的快照只需要一個(gè)時(shí)間戳,通常只是一個(gè)整數(shù)。
典型的隔離級(jí)別有三種,Read Committed(RC)、Snapshot lsolation(SI)以及 Serializable Snapshot Isolation(SSI)。
2.3.1 Read committed的實(shí)現(xiàn)
理論上,Read Committed有三種實(shí)現(xiàn)方式:
對(duì)每行數(shù)據(jù)來(lái)說(shuō),隨意讀取一個(gè)已經(jīng)提交的版本;
對(duì)每行數(shù)據(jù)來(lái)說(shuō),讀取數(shù)據(jù)最后提交的版本;
每條SQL語(yǔ)句開(kāi)始時(shí),獲取一個(gè)最新的快照,使用這個(gè)快照選擇數(shù)據(jù)合適的版本,修改數(shù)據(jù)時(shí)修改最新版本。
第一種實(shí)現(xiàn)方式滿足定義,但可能因?yàn)樽x取的數(shù)據(jù)太老,導(dǎo)致現(xiàn)實(shí)中無(wú)意義,因此實(shí)際系統(tǒng)里基本不用這種實(shí)現(xiàn)方式。
第二種實(shí)現(xiàn)方式也滿足RC定義,但會(huì)存在讀偏序問(wèn)題。讀偏序是指只讀取到某個(gè)事務(wù)的部分結(jié)果,比如T1更新了兩行數(shù)據(jù),但是T2只讀到其中一行的更新。如果對(duì)每行數(shù)據(jù)都只讀最新提交的版本,就會(huì)存在讀偏序問(wèn)題,實(shí)際系統(tǒng)中也較少使用這種實(shí)現(xiàn)方式。
第三種則是實(shí)際系統(tǒng)里較為常用的實(shí)現(xiàn)方式。具體實(shí)現(xiàn)方式為:每條SQL語(yǔ)句開(kāi)始時(shí)都會(huì)獲取一個(gè)最新的快照,用該快照選擇合適的版本,用同一快照選擇就不存在讀偏序問(wèn)題。
2.3.2 Snapshot lsolation的實(shí)現(xiàn)
第二種比較典型的隔離級(jí)別是Snapshot lsolation(SI),它并不在SQL標(biāo)準(zhǔn)定義的四種隔離級(jí)別中。其實(shí)現(xiàn)方式是:事務(wù)開(kāi)始時(shí)獲取一個(gè)最新的快照,事務(wù)的整個(gè)執(zhí)行過(guò)程中使用同一個(gè)快照,保證可重復(fù)讀。修改數(shù)據(jù)時(shí),如果發(fā)現(xiàn)數(shù)據(jù)已被其它事務(wù)修改,則abort。
在SI中,上述提及的三個(gè)異常即臟讀、不可重復(fù)讀、幻讀都不存在,但存在寫偏序問(wèn)題。如果兩個(gè)事務(wù)讀取了相同的數(shù)據(jù),但是修改了這些數(shù)據(jù)中的不同部分,就可能導(dǎo)致異常,這種異常叫寫偏序。
2.3.3 Serializable Snapshot Isolation的實(shí)現(xiàn)
第三種比較典型的隔離級(jí)別是Serializable Snapshot Isolation(SSI),其實(shí)現(xiàn)方式為:事務(wù)開(kāi)始時(shí),獲取一個(gè)快照(通常是最新的。為了降低事務(wù)abort的概率,某些只讀事務(wù)可能拿到非最新快照)。修改數(shù)據(jù)時(shí),如果發(fā)現(xiàn)數(shù)據(jù)已經(jīng)被其它事務(wù)修改,則abort。
SSI跟SI的不同在于:在讀數(shù)據(jù)時(shí),SSI記錄事務(wù)讀取數(shù)據(jù)的集合,再使用算法進(jìn)行檢測(cè),如果檢測(cè)到可能會(huì)有不可串行化的發(fā)生,則abort。這種算法可能會(huì)有誤判,但不會(huì)有遺漏,因此SSI不存在寫偏序問(wèn)題。
SSI是真正的Serializable隔離級(jí)別。
2.3.4 寫寫沖突的處理
對(duì)于寫寫沖突的處理,基于多版本實(shí)現(xiàn)有兩種實(shí)現(xiàn)方式:
First commit win,誰(shuí)先提交誰(shuí)贏。誰(shuí)最先把數(shù)據(jù)提交到數(shù)據(jù)庫(kù)里誰(shuí)就勝出。
First write win,誰(shuí)先寫誰(shuí)贏。誰(shuí)先往數(shù)據(jù)庫(kù)里寫(不一定提交),就會(huì)阻塞后面的寫事務(wù),從而更有可能贏。
2.4 MySQL的隔離級(jí)別
在分析MySQL的隔離級(jí)別之前,我們需要先了解兩個(gè)概念:當(dāng)前讀和快照讀。當(dāng)前讀是指讀取數(shù)據(jù)的最新版本,讀寫時(shí)都需要加鎖。快照讀是指使用快照讀取合適的版本,快照讀不加鎖。MySQL支持SQL標(biāo)準(zhǔn)定義的四種隔離級(jí)別,具體實(shí)現(xiàn)方式如下:
MySQL在Read Uncommitted直接讀,不加鎖,可能出現(xiàn)臟讀。
MySQL在Read ?Committed下,對(duì)于select(非for update、 非in share mode)使用快照讀,每個(gè)SQL語(yǔ)句獲取一個(gè)快照;對(duì)于insert、update、delete、select for update、select in share mode則使用當(dāng)前讀,讀寫都加鎖,但不使用gap鎖,讀鎖用完就釋放,寫鎖等到事務(wù)提交再釋放。
MySQL的Serializable屬于純兩階段鎖實(shí)現(xiàn),所有DML都使用當(dāng)前讀,都讀最新版本,讀寫都加鎖,使用gap鎖,鎖都到最后再放。
MySQL的Repeatable Read在Serializable的基礎(chǔ)上,放松了對(duì)select的限制,select(非for update、非in share mode)使用快照讀,其它場(chǎng)景則與Serializable相同。MySQL在Repeatable Read下,混用了當(dāng)前讀和快照讀,可能會(huì)導(dǎo)致看起來(lái)比較奇怪的現(xiàn)象,但只要理解它的實(shí)現(xiàn)方式,就可以對(duì)這些行為做出分析,這些都是可解釋的。
2.5 PostgreSQL的隔離級(jí)別
MySQL更像是基于鎖和多版本的結(jié)合。而PostgreSQL則是基于多版本的實(shí)現(xiàn),寫時(shí)有行鎖。PostgreSQL支持三種隔離級(jí)別,即Read ?Committed、SI、SSI,PostgreSQL里沒(méi)有當(dāng)前讀,都是快照讀,三種隔離級(jí)別的實(shí)現(xiàn)方式如下:
在Read ?Committed中,每條SQL語(yǔ)句都會(huì)使用一個(gè)最新的快照。對(duì)于update,如果發(fā)現(xiàn)本事務(wù)將要修改的行已經(jīng)被其它事務(wù)修改了,則使用數(shù)據(jù)最新的版本重新跑一遍SQL語(yǔ)句,重新計(jì)算過(guò)濾條件、計(jì)算投影結(jié)果等,再嘗試更新最新的行,如果不滿足過(guò)濾條件則直接放棄更新。這個(gè)過(guò)程在PostgreSQL中被稱為EPQ(EvalPlanQual)。
在SI中,整個(gè)事務(wù)使用同一個(gè)快照,更新時(shí)如果發(fā)現(xiàn)數(shù)據(jù)已經(jīng)被其他事務(wù)修改,則直接abort。這在PostgreSQL代碼里有較為直觀的呈現(xiàn),發(fā)現(xiàn)數(shù)據(jù)被改后,判斷當(dāng)前隔離級(jí)別是否大于等于SI,如果是則直接abort,如果小于則會(huì)跑EPQ。
SSI在SI的基礎(chǔ)上,使用SIREAD鎖(PG內(nèi)部也稱為Predicate Locking),記錄本事務(wù)的讀集合。如果檢測(cè)到可能導(dǎo)致非可串行化,則將事務(wù)abort。實(shí)際加鎖的數(shù)據(jù)范圍,與執(zhí)行計(jì)劃相關(guān),比如seqscan對(duì)整個(gè)表加鎖,index scan只需要對(duì)部分行加鎖。需要注意的是,SIREAD鎖是一套獨(dú)立于PG讀寫鎖之外的機(jī)制,與PG中讀寫鎖均不沖突,它只是起到記錄作用,用于寫傾斜的檢測(cè)。
PostgreSQL里面關(guān)于寫寫沖突的處理方式是誰(shuí)先寫誰(shuí)勝出,具體實(shí)現(xiàn)機(jī)制為給行加上行鎖,這時(shí)其它事務(wù)就無(wú)法修改。PG的行鎖幾乎不占內(nèi)存,本文不詳細(xì)展開(kāi)。
下面介紹下PostgreSQL中SSI的檢測(cè)原理。SSI檢測(cè)是為了消除SI里的寫偏序異常,主要檢測(cè)系統(tǒng)里是否存在下圖中的危險(xiǎn)結(jié)構(gòu)。
PostgreSQL把下圖中的結(jié)構(gòu)稱為危險(xiǎn)結(jié)構(gòu),具體如圖所示。T1是第一個(gè)事務(wù),T1讀取數(shù)據(jù)后,T2對(duì)讀取的數(shù)據(jù)進(jìn)行修改,這就形成了一個(gè)讀寫依賴。同理,T2和T3也形成了讀寫依賴關(guān)系。在所有可能導(dǎo)致不可串行化的調(diào)度里,必定會(huì)存在該結(jié)構(gòu),PostgreSQL通過(guò)檢測(cè)這種危險(xiǎn)結(jié)構(gòu)從而避免寫偏序。
TDSQL事務(wù)一致性的實(shí)現(xiàn)
3.1 TDSQL的架構(gòu)
TDSQL的目標(biāo)是讓業(yè)務(wù)能夠像使用單機(jī)數(shù)據(jù)庫(kù)一樣使用分布式數(shù)據(jù)庫(kù)。其功能特性主要有MySQL完全兼容、全局一致性、擴(kuò)縮容業(yè)務(wù)無(wú)感知、完全原生的在線表結(jié)構(gòu)變更,其存儲(chǔ)引擎為分布式的KV系統(tǒng),提供事務(wù)和自動(dòng)擴(kuò)縮容能力。
TDSQL是存儲(chǔ)計(jì)算分離架構(gòu),有三個(gè)組件,分別是計(jì)算層SQLEngine,分布式存儲(chǔ)層TDStore、元數(shù)據(jù)管理層TDMetaCluster。計(jì)算層完全無(wú)狀態(tài),所有數(shù)據(jù)都存儲(chǔ)在存儲(chǔ)層。業(yè)務(wù)通過(guò)MySQL協(xié)議接到SQLEngine,SQLEngine在計(jì)算時(shí)如果需要數(shù)據(jù)就從存儲(chǔ)層里讀取。存儲(chǔ)層是分布式的KV系統(tǒng),用Region進(jìn)行劃分,一個(gè)Region代表一個(gè)Key的范圍。以下圖為例,圖中有四個(gè)Region,分別是Region1、Region2、Region3、Region4,每個(gè)Region都有三個(gè)副本,副本之間用Raft協(xié)議保證一致性。
3.2 多副本的一致性
TDSQL使用Raft協(xié)議保證高可用以及多副本間的一致性。在Raft協(xié)議中,比較重要的內(nèi)容主要是選主、日志復(fù)制、安全和配置變更。
選主。Raft協(xié)議是一個(gè)強(qiáng)主的協(xié)議,集群中必須要有一個(gè)leader,系統(tǒng)才能對(duì)外提供服務(wù),要保證選出來(lái)的leader唯一。
日志復(fù)制。日志只會(huì)從leader到follower單向復(fù)制,日志沒(méi)有其它流動(dòng)方向。
安全。保證選出來(lái)的leader包含最多的日志,避免leader因?yàn)槿罩静蝗枰狡渌?jié)點(diǎn)上拉取日志。如果日志不夠多,就不可能成為leader。
配置變更。Raft協(xié)議中,系統(tǒng)只能有唯一的leader,不能產(chǎn)生雙主。為了避免產(chǎn)生雙主,增減節(jié)點(diǎn)時(shí)使用兩階段完成。整體流程為:先是舊配置生效,再到新配置和舊配置同時(shí)生效,最后新配置生效。
3.3 TDSQL的并發(fā)控制
TDSQL的并發(fā)控制是基于時(shí)間戳的多版本變化控制。通過(guò)提供全局時(shí)間戳服務(wù)的TDMetaCluster,保證時(shí)間戳全局單調(diào)遞增。
每個(gè)事務(wù)開(kāi)始時(shí)會(huì)拿一個(gè)start-ts,即快照。讀數(shù)據(jù)時(shí),因?yàn)閿?shù)據(jù)項(xiàng)上有關(guān)聯(lián)時(shí)間戳,我們就讀取數(shù)據(jù)所有版本中關(guān)聯(lián)時(shí)間戳小于等于start-ts且最大的那個(gè)版本。start-ts是事務(wù)開(kāi)始時(shí)拿的時(shí)間戳,只要數(shù)據(jù)版本關(guān)聯(lián)的時(shí)間戳比start-ts大,就代表該版本是后面事務(wù)產(chǎn)生的,不應(yīng)該可見(jiàn)。
TDSQL使用write on commit。事務(wù)對(duì)數(shù)據(jù)的修改,會(huì)先緩存到本地的事務(wù)空間,在事務(wù)運(yùn)行過(guò)程中只要不提交,就不會(huì)往存儲(chǔ)上面寫。事務(wù)的commit-ts寫到數(shù)據(jù)項(xiàng)上,這是關(guān)聯(lián)數(shù)據(jù)項(xiàng)的時(shí)間戳。
我們以u(píng)pdate A=A+5為例。事務(wù)開(kāi)始后先拿時(shí)間戳為4,再選擇應(yīng)該讀取哪一行。這個(gè)例子中有兩個(gè)key但有三個(gè)版本,A有兩個(gè)版本,時(shí)間戳分別為1和3。我們用start-ts=4的時(shí)間戳去取,因?yàn)橐x最新版本的值,1為舊版本,所以讀取到的是時(shí)間戳為3的版本即A=10。再進(jìn)行計(jì)算10+5=15,所以A=15。
update A=A+5事務(wù)對(duì)數(shù)據(jù)的修改,會(huì)先緩存到本地的事務(wù)空間而非存到公共存儲(chǔ),等到提交后拿到時(shí)間戳,關(guān)聯(lián)新產(chǎn)生的版本后再進(jìn)行存儲(chǔ)。
我們以下圖中的例子來(lái)說(shuō)明TDSQL的并發(fā)控制。左邊事務(wù)讀到時(shí)間戳為3的版本即A=10,通過(guò)計(jì)算A=A+5得到15,緩存到本地事務(wù)空間再開(kāi)始提交。右邊事務(wù)在完成后準(zhǔn)備提交,會(huì)先到存儲(chǔ)里檢查是否有其它事務(wù)先于自己往里面插入時(shí)間戳大于4的版本,讀取后發(fā)現(xiàn)最新版本關(guān)聯(lián)的時(shí)間戳為3,因?yàn)?
TDSQL的并發(fā)控制采用樂(lè)觀事務(wù)模型,在提交前需要做沖突檢測(cè)。這個(gè)過(guò)程不需要逐個(gè)比對(duì)最新數(shù)據(jù)與已讀取的數(shù)據(jù),耗時(shí)較短,它將之前讀到的所有key的時(shí)間戳與start-ts比較,如果都小于start-ts則允許提交,否則就不允許提交。如果兩個(gè)事務(wù)同時(shí)進(jìn)行檢測(cè),只有1個(gè)事務(wù)可以檢測(cè)成功,并不存在都檢測(cè)成功的情況。
這種方式其實(shí)采用的是快照隔離。事務(wù)開(kāi)始時(shí)先獲取唯一快照,提交時(shí)做檢查,誰(shuí)先提交誰(shuí)就勝出。事務(wù)的寫,最初都是寫在事務(wù)的本地空間中,沒(méi)有寫到公共的存儲(chǔ)上,對(duì)其它事務(wù)不可見(jiàn)。TDSQL采用誰(shuí)先提交誰(shuí)勝出的策略,誰(shuí)最先提交到系統(tǒng)里就勝出。
3.4 TDSQL的分布式提交協(xié)議
分布式事務(wù)有多節(jié)點(diǎn)參與,為了保證事務(wù)的原子性,最后提交時(shí)要么全部提交,要么都不提交。TDSQL使用兩階段提交(2PC)來(lái)完成,兩階段提交是共識(shí)算法的特例,它對(duì)環(huán)境做了下面的假設(shè):
每個(gè)節(jié)點(diǎn)都有帶預(yù)寫式日志的持久化儲(chǔ)存;
沒(méi)有節(jié)點(diǎn)發(fā)生永久故障;
預(yù)寫式日志永遠(yuǎn)不會(huì)丟失;
任何節(jié)點(diǎn)之間都可以相互通信。
2PC是一個(gè)阻塞性的協(xié)議,其中的角色分為協(xié)調(diào)者和參與者,協(xié)調(diào)者負(fù)責(zé)整個(gè)事務(wù)狀態(tài)的推進(jìn)。如果協(xié)調(diào)者發(fā)生永久性故障,分布式事務(wù)則無(wú)法繼續(xù)推進(jìn)。事務(wù)持有的其它資源也無(wú)法釋放。
TDSQL中的計(jì)算層SQLEngine完全無(wú)狀態(tài)。在TDSQL把協(xié)調(diào)者下沉到存儲(chǔ)層TDStore上、SQLEngine發(fā)出commit命令之后,提交工作全部由TDStore自行完成。因此在協(xié)調(diào)者發(fā)生故障后,TDStore會(huì)自動(dòng)讓第一個(gè)參與者成為協(xié)調(diào)者。協(xié)調(diào)者一定是某個(gè)Raft組的leader,如果協(xié)調(diào)者故障,Raft會(huì)選出新的leader,新leader通過(guò)回放日志,構(gòu)造出舊leader上事務(wù)的上下文,繼續(xù)推進(jìn)舊協(xié)調(diào)者上未完成的事務(wù),根據(jù)具體場(chǎng)景執(zhí)行commit或abort。
3.5 TDSQL的一致性讀
大部分使用2PC提交的系統(tǒng),都會(huì)先讓所有節(jié)點(diǎn)完成prepare,再獲取提交時(shí)間戳,TDSQL也不例外。為了保證一致性讀(不發(fā)生讀偏序),讀取數(shù)據(jù)時(shí),如果遇到prepare成功(但還沒(méi)有commit)的事務(wù),則需要等待已經(jīng)prepare的事務(wù)結(jié)束(commit或abort)之后,才能確定這條記錄是否對(duì)本事務(wù)可見(jiàn)。TDSQL在這一環(huán)節(jié)進(jìn)行了優(yōu)化。為了減少等待,TDSQL在prepare時(shí),會(huì)給事務(wù)分配一個(gè)臨時(shí)的時(shí)間戳即prepare_ts,等到提交時(shí)再分配一個(gè)commit_ts。因?yàn)樗鼈兌际菑耐恍蛄兄挟a(chǎn)生,因此commit_ts一定大于等于prepare_ts。這樣設(shè)計(jì)之后,其它事務(wù)遇到剛剛完成prepare的事務(wù)產(chǎn)生的記錄時(shí),如果事務(wù)start_ts小于prepare_ts,則無(wú)需等待事務(wù)結(jié)束,直接不可見(jiàn)這條記錄即可,從而避免不必要的等待。
孟慶鐘,騰訊云數(shù)據(jù)庫(kù)高級(jí)工程師,中國(guó)人民大學(xué)博士,深耕存儲(chǔ)和優(yōu)化器設(shè)計(jì)研發(fā)領(lǐng)域多年,熟悉PostgreSQL源代碼、Linux操作系統(tǒng)內(nèi)核代碼,目前在騰訊從事TDSQL存儲(chǔ)引擎的開(kāi)發(fā)。研究領(lǐng)域包含數(shù)據(jù)庫(kù)存儲(chǔ)引擎、查詢優(yōu)化等。
參考文獻(xiàn)
[1]《數(shù)據(jù)庫(kù)系統(tǒng)概論》王珊, 薩師煊.
[2]《PostgreSQL技術(shù)內(nèi)幕:事務(wù)處理深度探索》張樹(shù)杰
[3] Berenson H, Bernstein P, Gray J, et al. A critique of ANSI SQL isolation levels[J]. ACM SIGMOD Record, 1995, 24(2): 1-10.
[4] Cahill M J, R?hm U, Fekete A D. Serializable isolation for snapshot databases[J]. ACM Transactions on Database Systems (TODS), 2009, 34(4): 1-42.
[5] Ports D R K, Grittner K. Serializable snapshot isolation in PostgreSQL[J]. arXiv preprint arXiv:1208.4179, 2012.
[6] PostgreSQL中與EvalPlanQual相關(guān)的代碼
[7] MySQL中與加鎖相關(guān)的代碼
[8] MySQL中與隔離級(jí)別、鎖相關(guān)的文檔
[9] Gray J, Lamport L. Consensus on transaction commit[J]. ACM Transactions on Database Systems (TODS), 2006, 31(1): 133-160.
[10] https://en.wikipedia.org/wiki/Two-phase_commit_protocol
關(guān)鍵詞: 數(shù)據(jù)庫(kù)事務(wù)一致性實(shí)現(xiàn)上的各種細(xì)節(jié) 你注意到了嗎|DB