如何實現(xiàn)事務(wù)原子性?PolarDB 原子性深度剖析
一 前言
在巍峨的數(shù)據(jù)庫大廈體系中,查詢優(yōu)化器和事務(wù)體系是兩堵重要的承重墻,二者是如此重要以至于整個數(shù)據(jù)庫體系結(jié)構(gòu)設(shè)計中大量的數(shù)據(jù)結(jié)構(gòu)、機制和特性都是圍繞著二者搭建起來的。他們一個負(fù)責(zé)如何更快的查詢到數(shù)據(jù),更有效的組織起底層數(shù)據(jù)體系;一個負(fù)責(zé)安全、穩(wěn)定、持久的存儲數(shù)據(jù),為用戶的讀寫并發(fā)提供邏輯實現(xiàn)。我們今天探索的主題是事務(wù)體系,然而事務(wù)體系太過龐大,我們需要分成若干次的內(nèi)容。本文就針對PolarDB事務(wù)體系中的原子性進行剖析。
二 問題
在閱讀本文之前,首先提出幾個重要的問題,這幾個問題或許在接觸數(shù)據(jù)庫之前你也曾經(jīng)疑惑過。但是曾經(jīng)這些問題的答案可能只是簡單的被諸如“預(yù)寫日志”,“崩潰恢復(fù)機制”等簡單的答案回答過了,本文希望能夠更深一步的討論這些機制的實現(xiàn)及內(nèi)在原理。
數(shù)據(jù)庫原子性到底是如何保證的?使用了哪些特殊的數(shù)據(jù)結(jié)構(gòu)?為什么要用?
為什么我寫入成功的數(shù)據(jù)能夠被保證不丟失?
為什么數(shù)據(jù)庫崩潰后可以完整的恢復(fù)出來邏輯上我已經(jīng)提交的數(shù)據(jù)?
更進一步,什么是邏輯上已提交的數(shù)據(jù)?哪一個步驟才算是真正的提交?
三 背景
1 原子性在ACID中的位置
大名鼎鼎的ACID特性被提出后這個概念不斷的被引用(最初被寫入SQL92標(biāo)準(zhǔn)),這四種特性可以大概概括出人們心中對于數(shù)據(jù)庫最核心的訴求。本文要講的原子性便是其中第一個特性,我們先關(guān)注原子性在事務(wù)ACID中的位置。
這是個人對于數(shù)據(jù)庫ACID特性關(guān)系的理解,我認(rèn)為數(shù)據(jù)庫ACID特性其實可以分為兩個視角去定義,其中AID(原子、持久、隔離)特性是從事務(wù)本身的視角去定義,而C(一致)特性是從用戶的視角去定義。下面我會分別談下自己的理解。
原子性:我們還是從這些特性的概念出發(fā)去討論,原子性的概念是一個事務(wù)要么執(zhí)行成功,要么執(zhí)行失敗,即All or nothing。這種特質(zhì)我們可以用一個最小的事務(wù)模型去定義出來,我們假設(shè)有一個事務(wù),我們通過一套機制能夠?qū)崿F(xiàn)它真正的提交或回滾,這個目的就達成了,用戶只是通過我們的系統(tǒng)進行了一次提交,而原子性的重心不在于事務(wù)成功或失敗本身;而是保證了事務(wù)體系只接受成功或失敗兩種狀態(tài),而且有相應(yīng)的策略來保證成功或失敗的物理結(jié)果和邏輯結(jié)果是一致的。原子性可以通過最小事務(wù)單元的特性定義出來,是整個事務(wù)體系的基石。
持久性:而持久性指的是事務(wù)一旦提交后就可以永久的保存在數(shù)據(jù)庫中。持久性的范圍與視角幾乎與原子性是一致的,其實也導(dǎo)致了二者在概念和實現(xiàn)上也是緊密相連的。二者都一定意義上保證了數(shù)據(jù)的一致和可恢復(fù)性,而界限便是事務(wù)提交的時刻。舉例來說,一個數(shù)據(jù)目前的狀態(tài)是T,如果某個事務(wù)A試圖將狀態(tài)更新到T+1,如果這個事務(wù)A失敗了,那么數(shù)據(jù)庫狀態(tài)回到T,這是原子性保證的;如果事務(wù)A提交成功了,那么事務(wù)狀態(tài)變成T+1的那一刻,這個是原子性保證的;而一旦事務(wù)狀態(tài)變成T+1且事務(wù)成功提交,事務(wù)已經(jīng)結(jié)束不再存在原子性,這個T+1的狀態(tài)就是由持久性負(fù)責(zé)保證。從這個角度可以推斷原子性保證了事務(wù)提交前數(shù)據(jù)的崩潰恢復(fù),而持久性保證了事務(wù)提交后的崩潰恢復(fù)。
隔離性:隔離性同樣是定義在事務(wù)層面的一個機制,給事務(wù)并發(fā)提供了某種程度的隔離保證。隔離性的本質(zhì)是防止事務(wù)并發(fā)會導(dǎo)致不一致的狀態(tài)。由于不是本文的重點這里不做詳述。
一致性:相較于其他幾個特性很特殊,一致性的概念是數(shù)據(jù)庫在經(jīng)過一個或多個事務(wù)后,數(shù)據(jù)庫必須保持在一致性的狀態(tài)。如果從事務(wù)的角度去理解,保證了AID就可以保證事務(wù)是可串行、可恢復(fù)、原子性的,但是這種事務(wù)狀態(tài)的一致性就是真正的一致性嗎?破壞了AID就一定破壞C,但是反之AID都保證了C一定會被保證嗎?如果答案是是的話那這個概念就會失去它的意義。我們可以保證AID來保證事務(wù)是一致的,但是是否能夠證明事務(wù)的一致一定保證數(shù)據(jù)的一致呢?另外數(shù)據(jù)一致這個概念通過事務(wù)很難去準(zhǔn)確定義,而如果通過用戶層面就很好定義。數(shù)據(jù)一致就是用戶認(rèn)為數(shù)據(jù)庫中數(shù)據(jù)任何時候的狀態(tài)是滿足其業(yè)務(wù)邏輯的。比如銀行存款不能是負(fù)數(shù),所以用戶定義了一個非負(fù)約束。我認(rèn)為這是概念設(shè)計者的一個留白,傾向于將一致性視為一種高階目標(biāo)。
本文主要還是圍繞原子性進行,而中間涉及到崩潰恢復(fù)的話題可能會涉及到持久性。隔離性和一致性本文不討論,在可見性的部分我們默認(rèn)數(shù)據(jù)庫具有完成的隔離性,即可串行化的隔離級別。
2 原子性的內(nèi)在要求
上面講了很多對于數(shù)據(jù)庫事務(wù)特性的理解,下面進入我們的主題原子性。我們還是需要拿剛才的例子來繼續(xù)闡述原子性。目前數(shù)據(jù)庫的狀態(tài)是T,現(xiàn)在希望通過一個事務(wù)A將數(shù)據(jù)狀態(tài)升級為T+1。我們討論這個過程的原子性。
如果我們要保證這個事務(wù)是原子的,那么我們可以定義三個要求,只有滿足了下者,才可以說這個事務(wù)是原子性的:
數(shù)據(jù)庫存在一個事務(wù)真正成功提交的時間點。
在這個時間點之前開啟的事務(wù)(或者獲取的快照)只應(yīng)該看到T狀態(tài),這個時間點之后開啟的事務(wù)(或者獲取的快照),只應(yīng)該看到T+1狀態(tài)。
在這個時間點之前任何時候的崩潰,數(shù)據(jù)庫都應(yīng)該能夠回到T狀態(tài);在這個時間點之后任何時候崩潰,數(shù)據(jù)庫都應(yīng)該能回到T+1狀態(tài)。
注意這個時間點我們并沒有定義出來,甚至我們都不能確定2/3中的這個時間點是不是同一個時間點。我們能確定的是這個時間點一定存在,否則就沒辦法說事務(wù)是原子性的,原子性確定了提交/回滾必須有一個確定的時間點。另外根據(jù)我們剛才的描述,可以推測出2中的時間點,我們可以定義為原子性位點。由于原子性位點之前的提交我們不可見,之后可見,那么這個原子性位點對于數(shù)據(jù)庫中其他事務(wù)來說就是該事務(wù)提交的時間點;而3中的位點可以定位為持久性位點,由于這符合持久性對于崩潰恢復(fù)的定義。即對于持久性來說,3這個位點后事務(wù)已經(jīng)提交了。
四 原子性方案討論
1 從兩種簡單的方案說起
首先我們從兩個簡單的方案來談起原子性,這一步的目的是試圖說明為什么我們接下來每一步介紹的數(shù)據(jù)結(jié)構(gòu)都是為了實現(xiàn)原子性必不可少的。
簡單Direct IO
設(shè)想我們存在這樣一個數(shù)據(jù)庫,每次用戶操作都會把數(shù)據(jù)寫到磁盤中。我們把這種方式叫做簡單Direct IO,簡單的意思是指我們沒有記錄任何數(shù)據(jù)日志而只記錄了數(shù)據(jù)本身。假設(shè)初始的數(shù)據(jù)版本是T,這樣當(dāng)我們插入了一些數(shù)據(jù)之后如果發(fā)生了數(shù)據(jù)崩潰,磁盤上會寫著一個T+0.5版本的數(shù)據(jù)頁,并且我們沒有任何辦法去回滾或繼續(xù)進行后續(xù)的操作。這樣失敗的CASE無疑打破了原子性,因為目前的狀態(tài)既不是提交也不是回滾而是一個介于中間的狀態(tài),所以這是一次失敗的嘗試。
簡單Buffer IO
接下來我們有了一種新的方案,這種方案叫做簡單Buffer IO。同樣我們沒有日志,但是我們加入了一個新的數(shù)據(jù)結(jié)構(gòu)叫做“共享緩存池”。這樣當(dāng)我們每次寫數(shù)據(jù)頁的時候并不是直接把數(shù)據(jù)寫到數(shù)據(jù)庫上,而是寫到了shared buffer pool 中;這樣會有顯而易見的優(yōu)勢,首先讀寫效率會大大的提高,我們每次寫都不必等待數(shù)據(jù)頁真實的寫入磁盤,而可以異步的進行;其次如果數(shù)據(jù)庫在事務(wù)未提交前回滾或者崩潰掉了,我們只需要丟棄掉shared buffer pool中的數(shù)據(jù),只有當(dāng)數(shù)據(jù)庫成功提交時,它才可以真正的把數(shù)據(jù)刷到磁盤上,這樣從可見性和崩潰恢復(fù)性上看,我們看似已經(jīng)滿足了要求。
但是上述方案還是有一個難以解決的問題,即數(shù)據(jù)落盤這件事并不像我們想象的這么簡單。比如shared buffer pool中有10個臟頁,我們可以通過存儲技術(shù)來保證單個頁面的刷盤是原子的,但是在這10個頁面的中間任何時候數(shù)據(jù)庫都可能崩潰。繼而不論我們何時決定數(shù)據(jù)落盤,只要數(shù)據(jù)落盤的過程中機器發(fā)生了崩潰,這個數(shù)據(jù)都可能在磁盤上產(chǎn)生一個T+0.5的版本,并且我們在重啟后還是沒辦法去重做或者回滾。
上面兩個例子的闡述似乎注定了數(shù)據(jù)庫沒有辦法通過不依賴其他結(jié)構(gòu)的情況下保證數(shù)據(jù)的一致性(還有一種流行的方案是SQLite數(shù)據(jù)庫的Shadow Paging技術(shù),這里不討論),所以如果想解決這些問題,我們需要引入下一個重要的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)日志。
2 預(yù)寫日志 + Buffer IO方案
方案總覽
我們在Buffer IO的基礎(chǔ)上引入了數(shù)據(jù)日志這樣的數(shù)據(jù)結(jié)構(gòu),用來解決數(shù)據(jù)不一致的問題。
在數(shù)據(jù)緩存的部分與之前的想法一樣,不同的是我們在寫數(shù)據(jù)之前會額外記錄一個xlog buffer。這些xlog buffer是一個有序列的日志,他的序列號被稱為lsn,我們會把這個數(shù)據(jù)對應(yīng)的日志lsn記錄在數(shù)據(jù)頁面上。每一個數(shù)據(jù)頁頁面都記錄了更新它最新的日志序號。這一特性是為了保證日志與數(shù)據(jù)的一致性。
設(shè)想一下,如果我們能夠引入的日志與數(shù)據(jù)版本是完全一致的,并且保證數(shù)據(jù)日志先于日志持久化,那么不論何時數(shù)據(jù)崩潰我們都可以通過這個一致的日志頁恢復(fù)出來。這樣就可以解決之前說的數(shù)據(jù)崩潰問題。不論事務(wù)提交前或者提交后崩潰,我們都可以通過回放日志的方案來回放出正確的數(shù)據(jù)版本,這樣就可以實現(xiàn)崩潰恢復(fù)的原子性。另外關(guān)于可見性的部分我們可以通過多版本快照的方式實現(xiàn)。保證數(shù)據(jù)日志和數(shù)據(jù)一致并不容易,下面我們詳細(xì)講下如何保證,還有崩潰時數(shù)據(jù)如何恢復(fù)。
事務(wù)提交與控制刷臟
WAL日志被設(shè)計出來的目的是為了保證數(shù)據(jù)的可恢復(fù)性,而為了保證WAL日志與數(shù)據(jù)的一致性,當(dāng)數(shù)據(jù)緩存被持久化到磁盤時,持久化的數(shù)據(jù)頁對應(yīng)的WAL日志必須先一步被持久化到磁盤中,這句話闡述了控制刷臟的本質(zhì)含義。
數(shù)據(jù)庫后臺存在這樣一個進程叫做checkpoint進程,其周期性的進行checkpoint操作。當(dāng)checkpoint發(fā)生的時候,它會向xlog日志中寫入一條checkpoint日志,這條checkpoint日志包含了當(dāng)前的REDO位點。checkpoint保證了當(dāng)前所有臟數(shù)據(jù)已經(jīng)被刷到了磁盤當(dāng)中。
進行第一次插入操作,此時共享內(nèi)存找不到這個頁面,它會把這個頁面從磁盤加載到共享內(nèi)存中,之后寫入本次插入的輸入,并且插入一條寫數(shù)據(jù)的xlog到xlog buffer中,將這個表的日志標(biāo)記從LSN0升級到LSN1。
在事務(wù)提交的時刻,事務(wù)會寫入一條事務(wù)提交日志,之后wal buffer pool上所有本次事務(wù)提交的WAL日志會一并被刷到磁盤上。
之后插入第二條數(shù)據(jù)B,他會插入一條寫數(shù)據(jù)的xlog到xlog buffer中,將這個表的日志標(biāo)記從LSN1升級到LSN2。
同3一樣的操作。
之后如果數(shù)據(jù)庫正常運行,接下來的bgwriter/checkpoint進程會把數(shù)據(jù)頁異步的刷到磁盤上;而一旦數(shù)據(jù)庫發(fā)生崩潰,由于A、B兩條日志對應(yīng)的數(shù)據(jù)日志與事務(wù)提交日志都已經(jīng)被刷到了磁盤上,所以可以通過日志回放在shared buffer pool中重新回放出這些數(shù)據(jù),之后異步寫入磁盤。
fullpage機制保證可恢復(fù)性
WAL日志的恢復(fù)似乎是完美無缺的,但不幸的是剛才的方案還是存在一些瑕疵。設(shè)想當(dāng)一個bgwriter進程在異步的寫數(shù)據(jù)時遇到了數(shù)據(jù)庫的CRASH,這時一部分臟頁寫到了磁盤上,磁盤上可能存在壞頁。(PolarDB數(shù)據(jù)頁是8k,極端情況下磁盤的4k寫是有可能寫出壞頁面的)然而WAL日志是沒辦法在壞頁上回放數(shù)據(jù)的。這時就需要用到另外一個機制來保證極端情況下數(shù)據(jù)庫能夠找到原始數(shù)據(jù),這就涉及到了一個重要的機制fullpage機制。
在每一個checkpoin動作之后的第一次修改數(shù)據(jù)時,PolarDB會將這條修改的數(shù)據(jù)連同整個數(shù)據(jù)頁寫入到wal buffer中之后再刷入磁盤,這種包含整個數(shù)據(jù)頁的WAL日志被稱為備份塊。備份塊的存在使得在任何情況下WAL日志都可以將完整的數(shù)據(jù)頁給回放出來。下面是一個完整的過程。
checkpoint動作
進行第一次插入操作,此時共享內(nèi)存找不到這個頁面,它會把這個頁面從磁盤加載到共享內(nèi)存中,之后寫入本次插入的輸入。這時不同于上一節(jié)的操作,PolarDB序號為LSN1的這條WAL日志會把從磁盤上讀上來標(biāo)記為LSN 0的整個頁面寫入到wal buffer pool中。
事務(wù)提交,此時整個WAL日志被強制刷入磁盤上的WAL段中。
同上節(jié)
這時如果數(shù)據(jù)庫發(fā)生了崩潰,在數(shù)據(jù)庫重新拉起恢復(fù)時,一旦它遇到了壞掉的頁面,便可以通過最初的WAL日志中記錄的最初版本的頁面一步一步的把正確的數(shù)據(jù)給回放出來。
基于WAL日志的崩潰恢復(fù)機制
有了前兩節(jié)的基礎(chǔ)上,我們可以繼續(xù)演示如果數(shù)據(jù)庫崩潰后,數(shù)據(jù)是如何被回放出來的。我們演示一種數(shù)據(jù)頁被寫壞的回放。
當(dāng)數(shù)據(jù)庫回放到寫入數(shù)據(jù)A的這條WAL日志時,它會從磁盤中讀出TABLE A這個頁面。這里的這條WAL日志是一條備份日志,這是由于CHECKPOINT后,每個回放頁面的第一條WAL日志都是備份日志。
當(dāng)這條日志被回放時,備份日志有特殊的回放規(guī)則:它總是將自己頁面覆蓋掉原來的頁面,并將原來頁面的LSN升級為這個頁面的LSN。(為了保證數(shù)據(jù)一致性,正?;胤彭撁嬷粫胤糯笥谧约篖SN號碼的WAL日志)。在這個例子中,由于備份塊的存在,寫壞的頁面被成功恢復(fù)了出來。
接下來PolarDB會按照正常的回放規(guī)則去回放后續(xù)的日志。
最后數(shù)據(jù)回放成功后,shared buffer pool中的數(shù)據(jù)便可以異步的被刷到磁盤上去替換之前損壞的數(shù)據(jù)。
我們花了很大的篇幅來說明數(shù)據(jù)庫是如何通過預(yù)寫日志而進行崩潰恢復(fù)的,這似乎可以解釋持久性位點的含義;下面我我們還需要再解釋可見性的問題。
3 可見性機制
由于我們對于原子性的說明中會涉及可見性的概念,這個概念在PolarDB中由一套復(fù)雜的MVCC機制來實現(xiàn),且大多屬于隔離性的范疇。這里會對可見性進行一個簡單的說明,而更詳細(xì)的說明會放到隔離性的文章中繼續(xù)闡述。
事務(wù)元組
第一個要說到的是事務(wù)元組。他是一條數(shù)據(jù)的最小單元,真正存放了數(shù)據(jù),這里我們只關(guān)注幾個字段就好了。
t_xmin:生成該數(shù)據(jù)的事務(wù)ID
t_xmax:修改該事務(wù)數(shù)據(jù)的事務(wù)ID(刪除或鎖定數(shù)據(jù)的事務(wù)ID)
t_cid:同一事務(wù)中對該元組操作的一個序號
t_ctid:一個由段號/偏移量組成的指針,指向最新版本的數(shù)據(jù)
快照
第二個要說到的是快照??煺沼涗浟四骋粋€時間點數(shù)據(jù)庫中事務(wù)的狀態(tài)。
關(guān)于快照我們依舊不展開,我們知道通過快照可以從procArray中獲取到某一個時間點數(shù)據(jù)庫中所有可能事務(wù)的狀態(tài)即可。
當(dāng)前事務(wù)狀態(tài)
第三點要說的到的是當(dāng)前事務(wù)狀態(tài),事務(wù)狀態(tài)是指數(shù)據(jù)庫中決定事務(wù)運行狀態(tài)的的機制。在并發(fā)的環(huán)境中,決定看到的事務(wù)狀態(tài)是非常重要的一件事。
在查看一個tuple中的事務(wù)狀態(tài)時,可能會涉及到三個數(shù)據(jù)結(jié)構(gòu)t_infomask、procArray、clog:
infomask:位于tuple頭部的緩存標(biāo)志位,標(biāo)志了該元組xmin/xmax兩個事務(wù)的運行狀態(tài),這個狀態(tài)可以看作是clog的一層異步緩存,用來加速事務(wù)狀態(tài)的獲??;其狀態(tài)設(shè)置是異步設(shè)置,在事務(wù)提交時并不將所有事務(wù)相關(guān)的元組都立即升級,而是等待當(dāng)?shù)谝粋€足夠新的能夠看到本次更新的快照設(shè)置時再去設(shè)置。
procArray快照:快照中的事務(wù)狀態(tài),快照的獲取實際上就是在procArray中拿到這一瞬間數(shù)據(jù)庫中所有事務(wù)的狀態(tài),快照一旦獲取狀態(tài)恒定,除非再次獲?。ㄍ皇聞?wù)中獲取內(nèi)容是否改變?nèi)Q于事務(wù)隔離級別)。
clog:事務(wù)的實際狀態(tài),分為clog buffer和clog文件兩部分。clog buffer中實時的記錄了所有的事務(wù)狀態(tài)。
在一個可見性判斷過程中,三者訪問的順序是[infomask -> 快照,clog],而三者的決定性順序是[快照 -> clog -> infomask] 。
infomask是最容易獲取的信息,就記錄在元組的頭部,在部分條件下通過infomask就可以明確當(dāng)前事務(wù)的可見性,不需要涉及到后面的數(shù)據(jù)結(jié)構(gòu);快照擁有最高級的決定權(quán),最終決定xmin/xmax事務(wù)的狀態(tài)是運行/未運行;而clog用來輔助可見性的判斷,并且輔助設(shè)置infomask的值。舉例而言,如果這個判斷xmin事務(wù)可見性時發(fā)現(xiàn)在快照/clog中都已經(jīng)提交,那么會把t_infomask置為已提交;而如果xmin事務(wù)可見性時發(fā)現(xiàn)在快照提交,而clog未提交,則系統(tǒng)判斷發(fā)生了崩潰或回滾,將infomask設(shè)置為事務(wù)非法。
事務(wù)快照可見性
在介紹元組和快照后,我們就可以繼續(xù)討論快照可見性的話題。PolarDB的可見性有一套復(fù)雜的定義體系,需要通過許多信息組合定義出來,但是其中最直接的就是快照和元組頭。下面通過一個數(shù)據(jù)插入和更新的示例來說明元組頭和快照的可見性。
本文不討論隔離性,我們假設(shè)隔離級別是可串行化:
Snapshot1時刻:此時事務(wù)1184/1187都未開始,元組中也沒有記錄,student表是一張空表;通過Snapshot1快照可以得到的數(shù)據(jù)是空,我們把這個版本記做T。
Snapshot1 - Snapshot2時,此刻我們獲取快照那么拿到的還是Snapshot1,那么他看到的數(shù)據(jù)應(yīng)該還是T。
Snapshot2時刻:此時事務(wù)1184已經(jīng)結(jié)束,1187還未開始。所以1184的修改對用戶可見,1187仍舊不可見。具體到元組中可以看到 (1184/0) 這樣的元組頭,所以看到的是數(shù)據(jù)版本Tom,我們把這個版本記做T+1。
Snapshot2 - Snapshot3時,此刻我們獲取快照那么拿到的還是Snapshot2,那么他看到的數(shù)據(jù)應(yīng)該還是T+1。
Snapshot3時刻:此刻事務(wù)1184/1187都已經(jīng)結(jié)束,二者都可見,所以我們可以看到元組中(1184,1187)和(1187,1187)二者都不可見,而(1187,0)即Susan是可見的。我們把這個版本記做T+2。
通過上述分析我們可以得到一個簡單的結(jié)論,數(shù)據(jù)庫的可見性取決于快照的時機。我們原子性中所謂的可見性版本不同其實是指拿到的快照不同,快照決定了一個正在執(zhí)行中的事務(wù)是否已經(jīng)提交。這種提交與事務(wù)標(biāo)記提交狀態(tài)甚至是記錄clog提交都沒有關(guān)系,我們可以通過這種方法來使得我們拿到的快照與事務(wù)提交具有一致性。
事務(wù)原子性中的可見性
上文中我們已經(jīng)簡述了PolarDB快照可見性的問題,這里補充下事務(wù)提交時的具體實現(xiàn)問題。
我們設(shè)計可見性機制的核心思想是:“事務(wù)只應(yīng)該看到它應(yīng)該看到的數(shù)據(jù)版本”。如何定義應(yīng)該看到,這里只舉一個簡單的例子,如果一個元組的xmin事務(wù)沒有提交,其他事務(wù)大概率是看不到的;而如果一個元組的xmin事務(wù)已經(jīng)提交,其他事務(wù)就可能會看到。如何知道這個xmin有沒有提交,上文已經(jīng)提到了我們通過快照來決定,所以我們事務(wù)提交時的關(guān)鍵機制就是新快照的更新機制。
可見性在事務(wù)提交時涉及到兩個重要的數(shù)據(jù)結(jié)構(gòu)clog buffer和procArray 。二者的關(guān)系在上文已經(jīng)給出了解釋,他們在判斷事務(wù)可見性時發(fā)揮一定的作用,當(dāng)然procArray起到了決定性的作用。這是因為快照的獲取實際上就是一個遍歷ProcArray的過程。
在實際第三步會將本事務(wù)提交的信息寫入clog buffer,此時事務(wù)標(biāo)記clog是已提交,但實際上仍舊沒有提交。之后事務(wù)標(biāo)記ProcArray已提交,這一步事務(wù)完成了真正的提交,這個時間點之后重新獲取的快照會更新數(shù)據(jù)版本。
五 PolarDB 中原子性的實現(xiàn)
在完成了PolarDB崩潰恢復(fù)及可見性理論的說明之后,我們可以知道PolarDB可以通過這樣一套預(yù)寫日志+BufferIO的方案來保證事務(wù)的崩潰恢復(fù)和可見一致性,從而實現(xiàn)原子性。下面我們將針對事務(wù)提交中最重要的環(huán)節(jié)進行探究,找出我們最初提到的原子性位點到底指什么。
1 事務(wù)崩潰恢復(fù)一致——持久性位點
簡單來說事務(wù)提交中有這樣四個操作對于事務(wù)的原子性來說是最為核心和重要的。本節(jié)我們先考慮前兩個操作。
提交事務(wù)的Commit日志(即Commit 的WAL日志)。
將本次事務(wù)所有的提交的WAL日志全部強制刷盤,持久化到存儲。
我們標(biāo)記這個xlog(WAL日志)落盤的位點,我們設(shè)想兩種情況:
如果在這個位點前事務(wù)崩潰或者回滾了,那么不管數(shù)據(jù)日志有沒有刷盤,Commit日志一定沒有刷盤,由于WAL日志具有順序性,Commit日志一定是最后一個持久化到磁盤中。此時如果我們對數(shù)據(jù)進行回放,我們發(fā)現(xiàn)缺少Commit日志的事務(wù)無法被標(biāo)記為已提交狀態(tài),而根據(jù)可見性這種狀態(tài)相關(guān)的數(shù)據(jù)一定是不可見。這些數(shù)據(jù)之后會被視為臟數(shù)據(jù)給清理掉。所以我們可以得出結(jié)論,在這個節(jié)點前崩潰,事務(wù)實際上就是沒有提交。數(shù)據(jù)庫實質(zhì)上是恢復(fù)到了狀態(tài)T。
如果在這個位點后崩潰或回滾了,此時我們不論它在哪一步崩潰或回滾,我們都可以確定Commit日志一定刷到了磁盤上。而一旦Commit日志被刷到了磁盤上,那么這個事務(wù)所寫的數(shù)據(jù)一定可以被回放出來且標(biāo)記為已提交。那么這個數(shù)據(jù)就是可見的。這個事務(wù)實際上已經(jīng)提交了,數(shù)據(jù)庫被恢復(fù)到了T+1。
這個現(xiàn)象表明,2號位點似乎就是崩潰恢復(fù)的臨界點,它標(biāo)注了數(shù)據(jù)庫崩潰恢復(fù)可以回到T或者T+1狀態(tài)。那么我們?nèi)绾畏Q呼這個位點?回想持久性的概念:事務(wù)一旦提交,該事務(wù)對于數(shù)據(jù)庫的修改就永久的保留在了數(shù)據(jù)庫中。二者實際上是吻合的。所以我們將這個2號位點稱為持久性位點。
另外關(guān)于xlog刷盤還有一點需要說明的是xlog刷盤和回放具有單個文件的原子性;WAL日志頭部的CRC校驗提供了單個WAL日志文件的合法性校驗,如果WAL日志寫磁盤損壞,這條WAL日志的內(nèi)容無效,確保不會出現(xiàn)數(shù)據(jù)的部分回放。
2 事務(wù)的可見性一致——原子性位點
接下來我們繼續(xù)看3、4號操作:
將本次事務(wù)提交寫入到Clog buffer中。
將本次事務(wù)提交的結(jié)果寫入到ProcArray中。
3號操作是在Clog buffer中記錄了事務(wù)的當(dāng)前狀態(tài),可以看作是一層日志緩存。4號操作將提交操作寫入到了ProcArray中,這是非常重要的一步操作,通過剛才的說明我們知道快照判斷事務(wù)狀態(tài)是通過ProcArray進行的。即這一步?jīng)Q定了其他事務(wù)看到的該事務(wù)狀態(tài)。
如果在4號操作前事務(wù)崩潰或回滾,那么數(shù)據(jù)庫中所有其他事務(wù)看到的數(shù)據(jù)版本都是T,相當(dāng)于事務(wù)沒有真正的提交。這個判斷即通過可見性 -> 快照 -> Procarray這個順序決定的。
而當(dāng)4號操作后,針對所有觀察者來說這個事務(wù)已經(jīng)提交了,因為所有在這個時間點之后拿到的快照數(shù)據(jù)版本都是T+1。
從這一點考慮,4號操作完全切合原子性操作的含義。因為4號操作的進行與否影響了事務(wù)能否成功提交。4號操作前事務(wù)總是允許回滾的,因為沒有其他事務(wù)看到該事務(wù)的T+1狀態(tài);但是4號操作過后,事務(wù)便不允許回滾,不然一旦存在讀到T+1版本的其他事務(wù)就會造成數(shù)據(jù)的不一致。而原子性的概念即是,事務(wù)成功提交或失敗回滾。由于4號操作后不允許回滾,那4號操作就完全可以作為事務(wù)成功提交的標(biāo)志。
綜上所述,我們可以將4號操作定義為事務(wù)的原子性位點。
3 持久性位點與原子性位點
原子性與持久性的要求
再次給出原子性與持久性的概念:
原子性:一個事務(wù)要么執(zhí)行成功,要么執(zhí)行失敗。
持久性:一個事務(wù)一旦執(zhí)行成功,就可以永久的保存在數(shù)據(jù)庫中。
我們把4號操作標(biāo)記為原子性位點,是因為在4號操作的時刻,客觀上所有的觀察者都認(rèn)為這個事務(wù)已經(jīng)提交了,快照的版本從T升級為T+1,事務(wù)不再可回滾。那么事務(wù)一旦提交,原子性是否就不生效了,我認(rèn)為是的,原子性至多只保證事務(wù)成功提交那一刻的數(shù)據(jù)一致性,事務(wù)已經(jīng)結(jié)束了我們就沒辦法再說原子性。所以原子性在原子性位點前保證了事務(wù)的可見、可恢復(fù)。
我們把2號位點標(biāo)記為持久性位點,是因為持久性認(rèn)為事務(wù)成功后就可以永久的保留。根據(jù)上述的推測,這個位點無疑就是2號這個持久性位點。所以從2號位點開始后的所有時間我們都應(yīng)該保證持久性。
如何理解兩個位點
在解釋完2、4號兩個位點之后,我們最終可以把事務(wù)提交時涉及到的兩個最重要概念定義出來,我們現(xiàn)在可以回答第一個問題,到底在哪個時刻事務(wù)真正的提交?答案是持久性位點后事務(wù)可以被完整的恢復(fù)出來;而原子性位點后事務(wù)真正的被其他事務(wù)視作提交。但是二者卻并不是分離性的,這如何理解呢?
我認(rèn)為這其實是原子性實現(xiàn)的一種妥協(xié),因為我們沒有必要把二者統(tǒng)一,我們只需要保證關(guān)鍵性的一點,只要兩個位點的順序能夠使得在不同狀態(tài)下的數(shù)據(jù)具有一致性,那么就可以認(rèn)為它符合我們原子性的定義。
在持久性位點前崩潰或回滾,此時事務(wù)失敗,崩潰前或恢復(fù)后數(shù)據(jù)版本都是T。
在持久性位點后原子性位點間崩潰或回滾,此時事務(wù)的可見性版本是T,也就是說對于數(shù)據(jù)庫中的所有事務(wù)來說,我們看到的都是T?;貪L后,數(shù)據(jù)被重新回放到了T+1;而此時數(shù)據(jù)庫重啟后會發(fā)現(xiàn),在數(shù)據(jù)庫崩潰
前的事務(wù)拿到快照看到的數(shù)據(jù)版本是T,崩潰后重啟拿到快照看到的數(shù)據(jù)版本是T+1,仿佛事務(wù)被隱式的提交了。但是這并不違背數(shù)據(jù)的一致性。
在原子位點后崩潰。這個事務(wù)已經(jīng)提交了,崩潰前崩潰后事務(wù)看到的都是T+1版本的數(shù)據(jù)。
最后我們考慮兩個位點為什么沒有選擇合并。持久性位點的操作是WAL日志的刷盤,這個涉及到了磁盤IO的問題;另一方面原子性位點做的事情是寫ProcArray,這就要拿到ProcArray上的一把爭搶很嚴(yán)重的大鎖,可以認(rèn)為是一次高頻的共享內(nèi)存寫行為;二者本身都關(guān)乎數(shù)據(jù)庫事務(wù)的效率,如果綁定了二者成為一個原子操作,無疑會使得二者等待相當(dāng)嚴(yán)重,可能會對事務(wù)的運行效率造成較大影響。從這個角度來說二者的行為分離是一個效率上的考慮。
二者順序是否可以顛倒?
顯然不可以,通過上述的示意圖我們可以看到中間這一段時間可能出現(xiàn)既不滿足原子性要求,也不滿足持久性要求的區(qū)域。
具體而言,如果先進行原子性位點,再進行持久性位點,則設(shè)想二者中間崩潰的事務(wù)情形。其他事務(wù)在崩潰前會看到T+1版本的數(shù)據(jù),崩潰后看到了T版本的數(shù)據(jù),這樣看到未來數(shù)據(jù)的行為顯然是不被允許的。
如何定義真正的提交
真正的提交就是原子性位點提交。
還是最基本的道理,真正提交的標(biāo)志就是數(shù)據(jù)版本從T升級為T+1。這個位點就是原子性位點。在這個點之前,其他事務(wù)看到的數(shù)據(jù)版本都是T,說真正的提交是不恰當(dāng)?shù)?;在這個點之后事務(wù)無法被回滾。這足以說明這就是事務(wù)真正的提交點。
其他操作
我們最后關(guān)注1/3號操作:
1號操作是寫wal commit日志到xlog buffer,這個寫日志對于事務(wù)提交來說并不關(guān)鍵;因為如果它寫入了沒有刷到磁盤上,那么它其實還是毫無作用。
3號操作是在clog buffer 中標(biāo)記本事務(wù)為已提交狀態(tài);這個操作對事務(wù)提交來說也不關(guān)鍵。因為如果數(shù)據(jù)庫運行正常,它不影響本事務(wù)快照的可見性;如果數(shù)據(jù)庫崩潰,這個clog狀態(tài)不論是否已經(jīng)持久化,事務(wù)狀態(tài)都可以被xlog中的 Commmit/Abort日志給回放出來。
六 PolarDB的原子性過程
1 事務(wù)提交
本節(jié)我們回到事務(wù)提交函數(shù)中,看到這幾個操作在函數(shù)調(diào)用棧中的位置。
事務(wù)提交流程是帶有事務(wù)ID的事務(wù),不帶事務(wù)ID的事務(wù)沒有這個過程。由于不帶事務(wù)ID的事務(wù)大概率是只讀操作,不會對數(shù)據(jù)庫中數(shù)據(jù)一致性造成任何影響。
提交xlog前會開啟嚴(yán)格模式,這個模式下任何錯誤都會是致命錯誤,數(shù)據(jù)庫直接崩潰重啟。
xlog刷盤和CLOG寫內(nèi)存的順序是在同步模式下進行的,異步模式下不保證xlog刷盤,所以可能會崩潰后丟失數(shù)據(jù)。
3/4中間有一步關(guān)鍵的操作,Replication等待。實際上此時數(shù)據(jù)xlog已經(jīng)刷盤,但是還沒有真正的提交,在同步模式下主庫會等待被庫將刷到磁盤上的xlog應(yīng)用完畢,之后再進行下一步。
寫ProcArray本事務(wù)提交,事務(wù)真正提交完成,事務(wù)不再可回滾。
清理資源狀態(tài),此時工作已和本事務(wù)沒有任何關(guān)系。
2 事務(wù)回滾
沒有事務(wù)ID的事務(wù)回滾會直接跳過。
回滾前會首先判斷事務(wù)是否已提交,這個判斷是基于CLOG進行的。一個事務(wù)怎么能又提交又回滾呢?這就是我們之前討論的3-4之間的狀態(tài),如果CLOG記錄了提交,那么遇到回滾命令數(shù)據(jù)庫直接發(fā)生致命故障崩潰重啟。
回滾中也會相應(yīng)的寫入xlog回滾日志,不過是異步刷到磁盤??梢栽O(shè)想其實回滾日志即使不寫入,數(shù)據(jù)也是不可見的。
當(dāng)事務(wù)在ProcArray中寫入回滾日志后,事務(wù)在進程中真正的回滾了(其實這個狀態(tài)對其他事務(wù)沒有影響,之前后拿到的數(shù)據(jù)版本都是T)。
七 總結(jié)與展望
最后對全文做一個總結(jié),本文主要圍繞著“如何實現(xiàn)事務(wù)原子性”這個話題展開,分別從數(shù)據(jù)庫的崩潰恢復(fù)特性和事務(wù)可見性來說明了PolarDB數(shù)據(jù)庫實現(xiàn)原子性的底層原理。在介紹預(yù)寫日志+buffer IO原理的過程中還談到了shared buffer、WAL日志、clog、ProcArray、這些對原子性來說重要的數(shù)據(jù)結(jié)構(gòu)。在事務(wù)這個整體下數(shù)據(jù)庫的各個模塊巧妙的搭接起來,充分利用磁盤、緩存、IO這些計算機資源組成了一套完整的數(shù)據(jù)庫系統(tǒng)。
聯(lián)想到計算機科學(xué)其他的模型,如ISO網(wǎng)絡(luò)模型中傳輸層TCP協(xié)議在一個不可靠的信道上提供可靠的通信服務(wù)。數(shù)據(jù)庫事務(wù)實現(xiàn)了類似的思想,即在一個不可靠的操作系統(tǒng)(隨時可能崩潰)和磁盤存儲(無法大量數(shù)據(jù)的原子寫)上可靠的存儲數(shù)據(jù)。這一簡單而重要的思想可謂是數(shù)據(jù)庫系統(tǒng)的基石,它如此重要以至于整個數(shù)據(jù)庫中最核心的數(shù)據(jù)結(jié)構(gòu)大多其有關(guān)?;蛟S隨著數(shù)據(jù)庫的發(fā)展未來技術(shù)更迭出更先進的數(shù)據(jù)庫架構(gòu)體系,但是我們不能忘記是原子性、持久性仍舊應(yīng)當(dāng)是數(shù)據(jù)庫設(shè)計的核心。
八 思考
到這里事務(wù)原子性的重點就結(jié)束了,最后針對本文提到的觀點留下幾個問題供大家思考。
如何理解事務(wù)提交的原子性和持久性位點?
思考單個事務(wù)原子性和多個事務(wù)原子性的關(guān)系?崩潰恢復(fù)和可見性是否是一體的?
PolarDB中存在異步提交的概念,即不要求事務(wù)提交時不要求xlog日志落盤。請思考在這個模式下可能違背事務(wù)的哪些特性?是否違背原子性和持久性?