自拍偷在线精品自拍偷,亚洲欧美中文日韩v在线观看不卡

如何實(shí)現(xiàn)事務(wù)原子性?PolarDB原子性深度剖析

開發(fā) 開發(fā)工具
在巍峨的數(shù)據(jù)庫大廈體系中,查詢優(yōu)化器和事務(wù)體系是兩堵重要的承重墻,二者是如此重要以至于整個(gè)數(shù)據(jù)庫體系結(jié)構(gòu)設(shè)計(jì)中大量的數(shù)據(jù)結(jié)構(gòu)、機(jī)制和特性都是圍繞著二者搭建起來的。

[[403352]]

 一、前言

在巍峨的數(shù)據(jù)庫大廈體系中,查詢優(yōu)化器和事務(wù)體系是兩堵重要的承重墻,二者是如此重要以至于整個(gè)數(shù)據(jù)庫體系結(jié)構(gòu)設(shè)計(jì)中大量的數(shù)據(jù)結(jié)構(gòu)、機(jī)制和特性都是圍繞著二者搭建起來的。他們一個(gè)負(fù)責(zé)如何更快的查詢到數(shù)據(jù),更有效的組織起底層數(shù)據(jù)體系;一個(gè)負(fù)責(zé)安全、穩(wěn)定、持久的存儲(chǔ)數(shù)據(jù),為用戶的讀寫并發(fā)提供邏輯實(shí)現(xiàn)。我們今天探索的主題是事務(wù)體系,然而事務(wù)體系太過龐大,我們需要分成若干次的內(nèi)容。本文就針對(duì)PolarDB事務(wù)體系中的原子性進(jìn)行剖析。

二、問題

在閱讀本文之前,首先提出幾個(gè)重要的問題,這幾個(gè)問題或許在接觸數(shù)據(jù)庫之前你也曾經(jīng)疑惑過。但是曾經(jīng)這些問題的答案可能只是簡(jiǎn)單的被諸如“預(yù)寫日志”,“崩潰恢復(fù)機(jī)制”等簡(jiǎn)單的答案回答過了,本文希望能夠更深一步的討論這些機(jī)制的實(shí)現(xiàn)及內(nèi)在原理。

數(shù)據(jù)庫原子性到底是如何保證的?使用了哪些特殊的數(shù)據(jù)結(jié)構(gòu)?為什么要用?

為什么我寫入成功的數(shù)據(jù)能夠被保證不丟失?

為什么數(shù)據(jù)庫崩潰后可以完整的恢復(fù)出來邏輯上我已經(jīng)提交的數(shù)據(jù)?

更進(jìn)一步,什么是邏輯上已提交的數(shù)據(jù)?哪一個(gè)步驟才算是真正的提交?

三、背景

1.原子性在ACID中的位置

大名鼎鼎的ACID特性被提出后這個(gè)概念不斷的被引用(最初被寫入SQL92標(biāo)準(zhǔn)),這四種特性可以大概概括出人們心中對(duì)于數(shù)據(jù)庫最核心的訴求。本文要講的原子性便是其中第一個(gè)特性,我們先關(guān)注原子性在事務(wù)ACID中的位置。

這是個(gè)人對(duì)于數(shù)據(jù)庫ACID特性關(guān)系的理解,我認(rèn)為數(shù)據(jù)庫ACID特性其實(shí)可以分為兩個(gè)視角去定義,其中AID(原子、持久、隔離)特性是從事務(wù)本身的視角去定義,而C(一致)特性是從用戶的視角去定義。下面我會(huì)分別談下自己的理解。

  • 原子性:我們還是從這些特性的概念出發(fā)去討論,原子性的概念是一個(gè)事務(wù)要么執(zhí)行成功,要么執(zhí)行失敗,即All or nothing。這種特質(zhì)我們可以用一個(gè)最小的事務(wù)模型去定義出來,我們假設(shè)有一個(gè)事務(wù),我們通過一套機(jī)制能夠?qū)崿F(xiàn)它真正的提交或回滾,這個(gè)目的就達(dá)成了,用戶只是通過我們的系統(tǒng)進(jìn)行了一次提交,而原子性的重心不在于事務(wù)成功或失敗本身;而是保證了事務(wù)體系只接受成功或失敗兩種狀態(tài),而且有相應(yīng)的策略來保證成功或失敗的物理結(jié)果和邏輯結(jié)果是一致的。原子性可以通過最小事務(wù)單元的特性定義出來,是整個(gè)事務(wù)體系的基石。
  • 持久性:而持久性指的是事務(wù)一旦提交后就可以永久的保存在數(shù)據(jù)庫中。持久性的范圍與視角幾乎與原子性是一致的,其實(shí)也導(dǎo)致了二者在概念和實(shí)現(xiàn)上也是緊密相連的。二者都一定意義上保證了數(shù)據(jù)的一致和可恢復(fù)性,而界限便是事務(wù)提交的時(shí)刻。舉例來說,一個(gè)數(shù)據(jù)目前的狀態(tài)是T,如果某個(gè)事務(wù)A試圖將狀態(tài)更新到T+1,如果這個(gè)事務(wù)A失敗了,那么數(shù)據(jù)庫狀態(tài)回到T,這是原子性保證的;如果事務(wù)A提交成功了,那么事務(wù)狀態(tài)變成T+1的那一刻,這個(gè)是原子性保證的;而一旦事務(wù)狀態(tài)變成T+1且事務(wù)成功提交,事務(wù)已經(jīng)結(jié)束不再存在原子性,這個(gè)T+1的狀態(tài)就是由持久性負(fù)責(zé)保證。從這個(gè)角度可以推斷原子性保證了事務(wù)提交前數(shù)據(jù)的崩潰恢復(fù),而持久性保證了事務(wù)提交后的崩潰恢復(fù)。
  • 隔離性:隔離性同樣是定義在事務(wù)層面的一個(gè)機(jī)制,給事務(wù)并發(fā)提供了某種程度的隔離保證。隔離性的本質(zhì)是防止事務(wù)并發(fā)會(huì)導(dǎo)致不一致的狀態(tài)。由于不是本文的重點(diǎn)這里不做詳述。
  • 一致性:相較于其他幾個(gè)特性很特殊,一致性的概念是數(shù)據(jù)庫在經(jīng)過一個(gè)或多個(gè)事務(wù)后,數(shù)據(jù)庫必須保持在一致性的狀態(tài)。如果從事務(wù)的角度去理解,保證了AID就可以保證事務(wù)是可串行、可恢復(fù)、原子性的,但是這種事務(wù)狀態(tài)的一致性就是真正的一致性嗎?破壞了AID就一定破壞C,但是反之AID都保證了C一定會(huì)被保證嗎?如果答案是是的話那這個(gè)概念就會(huì)失去它的意義。我們可以保證AID來保證事務(wù)是一致的,但是是否能夠證明事務(wù)的一致一定保證數(shù)據(jù)的一致呢?另外數(shù)據(jù)一致這個(gè)概念通過事務(wù)很難去準(zhǔn)確定義,而如果通過用戶層面就很好定義。數(shù)據(jù)一致就是用戶認(rèn)為數(shù)據(jù)庫中數(shù)據(jù)任何時(shí)候的狀態(tài)是滿足其業(yè)務(wù)邏輯的。比如銀行存款不能是負(fù)數(shù),所以用戶定義了一個(gè)非負(fù)約束。我認(rèn)為這是概念設(shè)計(jì)者的一個(gè)留白,傾向于將一致性視為一種高階目標(biāo)。

本文主要還是圍繞原子性進(jìn)行,而中間涉及到崩潰恢復(fù)的話題可能會(huì)涉及到持久性。隔離性和一致性本文不討論,在可見性的部分我們默認(rèn)數(shù)據(jù)庫具有完成的隔離性,即可串行化的隔離級(jí)別。

2.原子性的內(nèi)在要求

上面講了很多對(duì)于數(shù)據(jù)庫事務(wù)特性的理解,下面進(jìn)入我們的主題原子性。我們還是需要拿剛才的例子來繼續(xù)闡述原子性。目前數(shù)據(jù)庫的狀態(tài)是T,現(xiàn)在希望通過一個(gè)事務(wù)A將數(shù)據(jù)狀態(tài)升級(jí)為T+1。我們討論這個(gè)過程的原子性。

如果我們要保證這個(gè)事務(wù)是原子的,那么我們可以定義三個(gè)要求,只有滿足了下者,才可以說這個(gè)事務(wù)是原子性的:

  • 數(shù)據(jù)庫存在一個(gè)事務(wù)真正成功提交的時(shí)間點(diǎn)。
  • 在這個(gè)時(shí)間點(diǎn)之前開啟的事務(wù)(或者獲取的快照)只應(yīng)該看到T狀態(tài),這個(gè)時(shí)間點(diǎn)之后開啟的事務(wù)(或者獲取的快照),只應(yīng)該看到T+1狀態(tài)。
  • 在這個(gè)時(shí)間點(diǎn)之前任何時(shí)候的崩潰,數(shù)據(jù)庫都應(yīng)該能夠回到T狀態(tài);在這個(gè)時(shí)間點(diǎn)之后任何時(shí)候崩潰,數(shù)據(jù)庫都應(yīng)該能回到T+1狀態(tài)。

注意這個(gè)時(shí)間點(diǎn)我們并沒有定義出來,甚至我們都不能確定2/3中的這個(gè)時(shí)間點(diǎn)是不是同一個(gè)時(shí)間點(diǎn)。我們能確定的是這個(gè)時(shí)間點(diǎn)一定存在,否則就沒辦法說事務(wù)是原子性的,原子性確定了提交/回滾必須有一個(gè)確定的時(shí)間點(diǎn)。另外根據(jù)我們剛才的描述,可以推測(cè)出2中的時(shí)間點(diǎn),我們可以定義為原子性位點(diǎn)。由于原子性位點(diǎn)之前的提交我們不可見,之后可見,那么這個(gè)原子性位點(diǎn)對(duì)于數(shù)據(jù)庫中其他事務(wù)來說就是該事務(wù)提交的時(shí)間點(diǎn);而3中的位點(diǎn)可以定位為持久性位點(diǎn),由于這符合持久性對(duì)于崩潰恢復(fù)的定義。即對(duì)于持久性來說,3這個(gè)位點(diǎn)后事務(wù)已經(jīng)提交了。

四、原子性方案討論

1.從兩種簡(jiǎn)單的方案說起

首先我們從兩個(gè)簡(jiǎn)單的方案來談起原子性,這一步的目的是試圖說明為什么我們接下來每一步介紹的數(shù)據(jù)結(jié)構(gòu)都是為了實(shí)現(xiàn)原子性必不可少的。

簡(jiǎn)單Direct IO

設(shè)想我們存在這樣一個(gè)數(shù)據(jù)庫,每次用戶操作都會(huì)把數(shù)據(jù)寫到磁盤中。我們把這種方式叫做簡(jiǎn)單Direct IO,簡(jiǎn)單的意思是指我們沒有記錄任何數(shù)據(jù)日志而只記錄了數(shù)據(jù)本身。假設(shè)初始的數(shù)據(jù)版本是T,這樣當(dāng)我們插入了一些數(shù)據(jù)之后如果發(fā)生了數(shù)據(jù)崩潰,磁盤上會(huì)寫著一個(gè)T+0.5版本的數(shù)據(jù)頁,并且我們沒有任何辦法去回滾或繼續(xù)進(jìn)行后續(xù)的操作。這樣失敗的CASE無疑打破了原子性,因?yàn)槟壳暗臓顟B(tài)既不是提交也不是回滾而是一個(gè)介于中間的狀態(tài),所以這是一次失敗的嘗試。

簡(jiǎn)單Buffer IO

接下來我們有了一種新的方案,這種方案叫做簡(jiǎn)單Buffer IO。同樣我們沒有日志,但是我們加入了一個(gè)新的數(shù)據(jù)結(jié)構(gòu)叫做“共享緩存池”。這樣當(dāng)我們每次寫數(shù)據(jù)頁的時(shí)候并不是直接把數(shù)據(jù)寫到數(shù)據(jù)庫上,而是寫到了shared buffer pool 中;這樣會(huì)有顯而易見的優(yōu)勢(shì),首先讀寫效率會(huì)大大的提高,我們每次寫都不必等待數(shù)據(jù)頁真實(shí)的寫入磁盤,而可以異步的進(jìn)行;其次如果數(shù)據(jù)庫在事務(wù)未提交前回滾或者崩潰掉了,我們只需要丟棄掉shared buffer pool中的數(shù)據(jù),只有當(dāng)數(shù)據(jù)庫成功提交時(shí),它才可以真正的把數(shù)據(jù)刷到磁盤上,這樣從可見性和崩潰恢復(fù)性上看,我們看似已經(jīng)滿足了要求。

但是上述方案還是有一個(gè)難以解決的問題,即數(shù)據(jù)落盤這件事并不像我們想象的這么簡(jiǎn)單。比如shared buffer pool中有10個(gè)臟頁,我們可以通過存儲(chǔ)技術(shù)來保證單個(gè)頁面的刷盤是原子的,但是在這10個(gè)頁面的中間任何時(shí)候數(shù)據(jù)庫都可能崩潰。繼而不論我們何時(shí)決定數(shù)據(jù)落盤,只要數(shù)據(jù)落盤的過程中機(jī)器發(fā)生了崩潰,這個(gè)數(shù)據(jù)都可能在磁盤上產(chǎn)生一個(gè)T+0.5的版本,并且我們?cè)谥貑⒑筮€是沒辦法去重做或者回滾。

上面兩個(gè)例子的闡述似乎注定了數(shù)據(jù)庫沒有辦法通過不依賴其他結(jié)構(gòu)的情況下保證數(shù)據(jù)的一致性(還有一種流行的方案是SQLite數(shù)據(jù)庫的Shadow Paging技術(shù),這里不討論),所以如果想解決這些問題,我們需要引入下一個(gè)重要的數(shù)據(jù)結(jié)構(gòu),數(shù)據(jù)日志。

2.預(yù)寫日志 + Buffer IO方案

方案總覽

我們?cè)贐uffer IO的基礎(chǔ)上引入了數(shù)據(jù)日志這樣的數(shù)據(jù)結(jié)構(gòu),用來解決數(shù)據(jù)不一致的問題。

在數(shù)據(jù)緩存的部分與之前的想法一樣,不同的是我們?cè)趯憯?shù)據(jù)之前會(huì)額外記錄一個(gè)xlog buffer。這些xlog buffer是一個(gè)有序列的日志,他的序列號(hào)被稱為lsn,我們會(huì)把這個(gè)數(shù)據(jù)對(duì)應(yīng)的日志lsn記錄在數(shù)據(jù)頁面上。每一個(gè)數(shù)據(jù)頁頁面都記錄了更新它最新的日志序號(hào)。這一特性是為了保證日志與數(shù)據(jù)的一致性。

設(shè)想一下,如果我們能夠引入的日志與數(shù)據(jù)版本是完全一致的,并且保證數(shù)據(jù)日志先于日志持久化,那么不論何時(shí)數(shù)據(jù)崩潰我們都可以通過這個(gè)一致的日志頁恢復(fù)出來。這樣就可以解決之前說的數(shù)據(jù)崩潰問題。不論事務(wù)提交前或者提交后崩潰,我們都可以通過回放日志的方案來回放出正確的數(shù)據(jù)版本,這樣就可以實(shí)現(xiàn)崩潰恢復(fù)的原子性。另外關(guān)于可見性的部分我們可以通過多版本快照的方式實(shí)現(xiàn)。保證數(shù)據(jù)日志和數(shù)據(jù)一致并不容易,下面我們?cè)敿?xì)講下如何保證,還有崩潰時(shí)數(shù)據(jù)如何恢復(fù)。

事務(wù)提交與控制刷臟

WAL日志被設(shè)計(jì)出來的目的是為了保證數(shù)據(jù)的可恢復(fù)性,而為了保證WAL日志與數(shù)據(jù)的一致性,當(dāng)數(shù)據(jù)緩存被持久化到磁盤時(shí),持久化的數(shù)據(jù)頁對(duì)應(yīng)的WAL日志必須先一步被持久化到磁盤中,這句話闡述了控制刷臟的本質(zhì)含義。

  1. 數(shù)據(jù)庫后臺(tái)存在這樣一個(gè)進(jìn)程叫做checkpoint進(jìn)程,其周期性的進(jìn)行checkpoint操作。當(dāng)checkpoint發(fā)生的時(shí)候,它會(huì)向xlog日志中寫入一條checkpoint日志,這條checkpoint日志包含了當(dāng)前的REDO位點(diǎn)。checkpoint保證了當(dāng)前所有臟數(shù)據(jù)已經(jīng)被刷到了磁盤當(dāng)中。
  2. 進(jìn)行第一次插入操作,此時(shí)共享內(nèi)存找不到這個(gè)頁面,它會(huì)把這個(gè)頁面從磁盤加載到共享內(nèi)存中,之后寫入本次插入的輸入,并且插入一條寫數(shù)據(jù)的xlog到xlog buffer中,將這個(gè)表的日志標(biāo)記從LSN0升級(jí)到LSN1。
  3. 在事務(wù)提交的時(shí)刻,事務(wù)會(huì)寫入一條事務(wù)提交日志,之后wal buffer pool上所有本次事務(wù)提交的WAL日志會(huì)一并被刷到磁盤上。
  4. 之后插入第二條數(shù)據(jù)B,他會(huì)插入一條寫數(shù)據(jù)的xlog到xlog buffer中,將這個(gè)表的日志標(biāo)記從LSN1升級(jí)到LSN2。
  5. 同3一樣的操作。

之后如果數(shù)據(jù)庫正常運(yùn)行,接下來的bgwriter/checkpoint進(jìn)程會(huì)把數(shù)據(jù)頁異步的刷到磁盤上;而一旦數(shù)據(jù)庫發(fā)生崩潰,由于A、B兩條日志對(duì)應(yīng)的數(shù)據(jù)日志與事務(wù)提交日志都已經(jīng)被刷到了磁盤上,所以可以通過日志回放在shared buffer pool中重新回放出這些數(shù)據(jù),之后異步寫入磁盤。

fullpage機(jī)制保證可恢復(fù)性

WAL日志的恢復(fù)似乎是完美無缺的,但不幸的是剛才的方案還是存在一些瑕疵。設(shè)想當(dāng)一個(gè)bgwriter進(jìn)程在異步的寫數(shù)據(jù)時(shí)遇到了數(shù)據(jù)庫的CRASH,這時(shí)一部分臟頁寫到了磁盤上,磁盤上可能存在壞頁。(PolarDB數(shù)據(jù)頁是8k,極端情況下磁盤的4k寫是有可能寫出壞頁面的)然而WAL日志是沒辦法在壞頁上回放數(shù)據(jù)的。這時(shí)就需要用到另外一個(gè)機(jī)制來保證極端情況下數(shù)據(jù)庫能夠找到原始數(shù)據(jù),這就涉及到了一個(gè)重要的機(jī)制fullpage機(jī)制。

在每一個(gè)checkpoin動(dòng)作之后的第一次修改數(shù)據(jù)時(shí),PolarDB會(huì)將這條修改的數(shù)據(jù)連同整個(gè)數(shù)據(jù)頁寫入到wal buffer中之后再刷入磁盤,這種包含整個(gè)數(shù)據(jù)頁的WAL日志被稱為備份塊。備份塊的存在使得在任何情況下WAL日志都可以將完整的數(shù)據(jù)頁給回放出來。下面是一個(gè)完整的過程。

  1. checkpoint動(dòng)作
  2. 進(jìn)行第一次插入操作,此時(shí)共享內(nèi)存找不到這個(gè)頁面,它會(huì)把這個(gè)頁面從磁盤加載到共享內(nèi)存中,之后寫入本次插入的輸入。這時(shí)不同于上一節(jié)的操作,PolarDB序號(hào)為LSN1的這條WAL日志會(huì)把從磁盤上讀上來標(biāo)記為LSN 0的整個(gè)頁面寫入到wal buffer pool中。
  3. 事務(wù)提交,此時(shí)整個(gè)WAL日志被強(qiáng)制刷入磁盤上的WAL段中。
  4. 同上節(jié)
  5. 同上節(jié)

這時(shí)如果數(shù)據(jù)庫發(fā)生了崩潰,在數(shù)據(jù)庫重新拉起恢復(fù)時(shí),一旦它遇到了壞掉的頁面,便可以通過最初的WAL日志中記錄的最初版本的頁面一步一步的把正確的數(shù)據(jù)給回放出來。

基于WAL日志的崩潰恢復(fù)機(jī)制

有了前兩節(jié)的基礎(chǔ)上,我們可以繼續(xù)演示如果數(shù)據(jù)庫崩潰后,數(shù)據(jù)是如何被回放出來的。我們演示一種數(shù)據(jù)頁被寫壞的回放。

  1. 當(dāng)數(shù)據(jù)庫回放到寫入數(shù)據(jù)A的這條WAL日志時(shí),它會(huì)從磁盤中讀出TABLE A這個(gè)頁面。這里的這條WAL日志是一條備份日志,這是由于CHECKPOINT后,每個(gè)回放頁面的第一條WAL日志都是備份日志。
  2. 當(dāng)這條日志被回放時(shí),備份日志有特殊的回放規(guī)則:它總是將自己頁面覆蓋掉原來的頁面,并將原來頁面的LSN升級(jí)為這個(gè)頁面的LSN。(為了保證數(shù)據(jù)一致性,正?;胤彭撁嬷粫?huì)回放大于自己LSN號(hào)碼的WAL日志)。在這個(gè)例子中,由于備份塊的存在,寫壞的頁面被成功恢復(fù)了出來。
  3. 接下來PolarDB會(huì)按照正常的回放規(guī)則去回放后續(xù)的日志。

最后數(shù)據(jù)回放成功后,shared buffer pool中的數(shù)據(jù)便可以異步的被刷到磁盤上去替換之前損壞的數(shù)據(jù)。

我們花了很大的篇幅來說明數(shù)據(jù)庫是如何通過預(yù)寫日志而進(jìn)行崩潰恢復(fù)的,這似乎可以解釋持久性位點(diǎn)的含義;下面我我們還需要再解釋可見性的問題。

3.可見性機(jī)制

由于我們對(duì)于原子性的說明中會(huì)涉及可見性的概念,這個(gè)概念在PolarDB中由一套復(fù)雜的MVCC機(jī)制來實(shí)現(xiàn),且大多屬于隔離性的范疇。這里會(huì)對(duì)可見性進(jìn)行一個(gè)簡(jiǎn)單的說明,而更詳細(xì)的說明會(huì)放到隔離性的文章中繼續(xù)闡述。

事務(wù)元組

第一個(gè)要說到的是事務(wù)元組。他是一條數(shù)據(jù)的最小單元,真正存放了數(shù)據(jù),這里我們只關(guān)注幾個(gè)字段就好了。

  • t_xmin:生成該數(shù)據(jù)的事務(wù)ID
  • t_xmax:修改該事務(wù)數(shù)據(jù)的事務(wù)ID(刪除或鎖定數(shù)據(jù)的事務(wù)ID)
  • t_cid:同一事務(wù)中對(duì)該元組操作的一個(gè)序號(hào)
  • t_ctid:一個(gè)由段號(hào)/偏移量組成的指針,指向最新版本的數(shù)據(jù)

快照

第二個(gè)要說到的是快照。快照記錄了某一個(gè)時(shí)間點(diǎn)數(shù)據(jù)庫中事務(wù)的狀態(tài)。

關(guān)于快照我們依舊不展開,我們知道通過快照可以從procArray中獲取到某一個(gè)時(shí)間點(diǎn)數(shù)據(jù)庫中所有可能事務(wù)的狀態(tài)即可。

當(dāng)前事務(wù)狀態(tài)

第三點(diǎn)要說的到的是當(dāng)前事務(wù)狀態(tài),事務(wù)狀態(tài)是指數(shù)據(jù)庫中決定事務(wù)運(yùn)行狀態(tài)的的機(jī)制。在并發(fā)的環(huán)境中,決定看到的事務(wù)狀態(tài)是非常重要的一件事。

在查看一個(gè)tuple中的事務(wù)狀態(tài)時(shí),可能會(huì)涉及到三個(gè)數(shù)據(jù)結(jié)構(gòu)t_infomask、procArray、clog:

  • infomask:位于tuple頭部的緩存標(biāo)志位,標(biāo)志了該元組xmin/xmax兩個(gè)事務(wù)的運(yùn)行狀態(tài),這個(gè)狀態(tài)可以看作是clog的一層異步緩存,用來加速事務(wù)狀態(tài)的獲取;其狀態(tài)設(shè)置是異步設(shè)置,在事務(wù)提交時(shí)并不將所有事務(wù)相關(guān)的元組都立即升級(jí),而是等待當(dāng)?shù)谝粋€(gè)足夠新的能夠看到本次更新的快照設(shè)置時(shí)再去設(shè)置。
  • procArray快照:快照中的事務(wù)狀態(tài),快照的獲取實(shí)際上就是在procArray中拿到這一瞬間數(shù)據(jù)庫中所有事務(wù)的狀態(tài),快照一旦獲取狀態(tài)恒定,除非再次獲取(同一事務(wù)中獲取內(nèi)容是否改變?nèi)Q于事務(wù)隔離級(jí)別)。
  • clog:事務(wù)的實(shí)際狀態(tài),分為clog buffer和clog文件兩部分。clog buffer中實(shí)時(shí)的記錄了所有的事務(wù)狀態(tài)。

在一個(gè)可見性判斷過程中,三者訪問的順序是[infomask -> 快照,clog],而三者的決定性順序是[快照 -> clog -> infomask] 。

infomask是最容易獲取的信息,就記錄在元組的頭部,在部分條件下通過infomask就可以明確當(dāng)前事務(wù)的可見性,不需要涉及到后面的數(shù)據(jù)結(jié)構(gòu);快照擁有最高級(jí)的決定權(quán),最終決定xmin/xmax事務(wù)的狀態(tài)是運(yùn)行/未運(yùn)行;而clog用來輔助可見性的判斷,并且輔助設(shè)置infomask的值。舉例而言,如果這個(gè)判斷xmin事務(wù)可見性時(shí)發(fā)現(xiàn)在快照/clog中都已經(jīng)提交,那么會(huì)把t_infomask置為已提交;而如果xmin事務(wù)可見性時(shí)發(fā)現(xiàn)在快照提交,而clog未提交,則系統(tǒng)判斷發(fā)生了崩潰或回滾,將infomask設(shè)置為事務(wù)非法。

事務(wù)快照可見性

在介紹元組和快照后,我們就可以繼續(xù)討論快照可見性的話題。PolarDB的可見性有一套復(fù)雜的定義體系,需要通過許多信息組合定義出來,但是其中最直接的就是快照和元組頭。下面通過一個(gè)數(shù)據(jù)插入和更新的示例來說明元組頭和快照的可見性。

本文不討論隔離性,我們假設(shè)隔離級(jí)別是可串行化:

  • Snapshot1時(shí)刻:此時(shí)事務(wù)1184/1187都未開始,元組中也沒有記錄,student表是一張空表;通過Snapshot1快照可以得到的數(shù)據(jù)是空,我們把這個(gè)版本記做T。
  • Snapshot1 - Snapshot2時(shí),此刻我們獲取快照那么拿到的還是Snapshot1,那么他看到的數(shù)據(jù)應(yīng)該還是T。
  • Snapshot2時(shí)刻:此時(shí)事務(wù)1184已經(jīng)結(jié)束,1187還未開始。所以1184的修改對(duì)用戶可見,1187仍舊不可見。具體到元組中可以看到 (1184/0) 這樣的元組頭,所以看到的是數(shù)據(jù)版本Tom,我們把這個(gè)版本記做T+1。
  • Snapshot2 - Snapshot3時(shí),此刻我們獲取快照那么拿到的還是Snapshot2,那么他看到的數(shù)據(jù)應(yīng)該還是T+1。
  • Snapshot3時(shí)刻:此刻事務(wù)1184/1187都已經(jīng)結(jié)束,二者都可見,所以我們可以看到元組中(1184,1187)和(1187,1187)二者都不可見,而(1187,0)即Susan是可見的。我們把這個(gè)版本記做T+2。

通過上述分析我們可以得到一個(gè)簡(jiǎn)單的結(jié)論,數(shù)據(jù)庫的可見性取決于快照的時(shí)機(jī)。我們?cè)有灾兴^的可見性版本不同其實(shí)是指拿到的快照不同,快照決定了一個(gè)正在執(zhí)行中的事務(wù)是否已經(jīng)提交。這種提交與事務(wù)標(biāo)記提交狀態(tài)甚至是記錄clog提交都沒有關(guān)系,我們可以通過這種方法來使得我們拿到的快照與事務(wù)提交具有一致性。

事務(wù)原子性中的可見性

上文中我們已經(jīng)簡(jiǎn)述了PolarDB快照可見性的問題,這里補(bǔ)充下事務(wù)提交時(shí)的具體實(shí)現(xiàn)問題。

我們?cè)O(shè)計(jì)可見性機(jī)制的核心思想是:“事務(wù)只應(yīng)該看到它應(yīng)該看到的數(shù)據(jù)版本”。如何定義應(yīng)該看到,這里只舉一個(gè)簡(jiǎn)單的例子,如果一個(gè)元組的xmin事務(wù)沒有提交,其他事務(wù)大概率是看不到的;而如果一個(gè)元組的xmin事務(wù)已經(jīng)提交,其他事務(wù)就可能會(huì)看到。如何知道這個(gè)xmin有沒有提交,上文已經(jīng)提到了我們通過快照來決定,所以我們事務(wù)提交時(shí)的關(guān)鍵機(jī)制就是新快照的更新機(jī)制。

可見性在事務(wù)提交時(shí)涉及到兩個(gè)重要的數(shù)據(jù)結(jié)構(gòu)clog buffer和procArray 。二者的關(guān)系在上文已經(jīng)給出了解釋,他們?cè)谂袛嗍聞?wù)可見性時(shí)發(fā)揮一定的作用,當(dāng)然procArray起到了決定性的作用。這是因?yàn)榭煺盏墨@取實(shí)際上就是一個(gè)遍歷ProcArray的過程。

在實(shí)際第三步會(huì)將本事務(wù)提交的信息寫入clog buffer,此時(shí)事務(wù)標(biāo)記clog是已提交,但實(shí)際上仍舊沒有提交。之后事務(wù)標(biāo)記ProcArray已提交,這一步事務(wù)完成了真正的提交,這個(gè)時(shí)間點(diǎn)之后重新獲取的快照會(huì)更新數(shù)據(jù)版本。

五、PolarDB 中原子性的實(shí)現(xiàn)

在完成了PolarDB崩潰恢復(fù)及可見性理論的說明之后,我們可以知道PolarDB可以通過這樣一套預(yù)寫日志+BufferIO的方案來保證事務(wù)的崩潰恢復(fù)和可見一致性,從而實(shí)現(xiàn)原子性。下面我們將針對(duì)事務(wù)提交中最重要的環(huán)節(jié)進(jìn)行探究,找出我們最初提到的原子性位點(diǎn)到底指什么。

1.事務(wù)崩潰恢復(fù)一致——持久性位點(diǎn)

簡(jiǎn)單來說事務(wù)提交中有這樣四個(gè)操作對(duì)于事務(wù)的原子性來說是最為核心和重要的。本節(jié)我們先考慮前兩個(gè)操作。

  • 提交事務(wù)的Commit日志(即Commit 的WAL日志)。
  • 將本次事務(wù)所有的提交的WAL日志全部強(qiáng)制刷盤,持久化到存儲(chǔ)。

我們標(biāo)記這個(gè)xlog(WAL日志)落盤的位點(diǎn),我們?cè)O(shè)想兩種情況:

  • 如果在這個(gè)位點(diǎn)前事務(wù)崩潰或者回滾了,那么不管數(shù)據(jù)日志有沒有刷盤,Commit日志一定沒有刷盤,由于WAL日志具有順序性,Commit日志一定是最后一個(gè)持久化到磁盤中。此時(shí)如果我們對(duì)數(shù)據(jù)進(jìn)行回放,我們發(fā)現(xiàn)缺少Commit日志的事務(wù)無法被標(biāo)記為已提交狀態(tài),而根據(jù)可見性這種狀態(tài)相關(guān)的數(shù)據(jù)一定是不可見。這些數(shù)據(jù)之后會(huì)被視為臟數(shù)據(jù)給清理掉。所以我們可以得出結(jié)論,在這個(gè)節(jié)點(diǎn)前崩潰,事務(wù)實(shí)際上就是沒有提交。數(shù)據(jù)庫實(shí)質(zhì)上是恢復(fù)到了狀態(tài)T。
  • 如果在這個(gè)位點(diǎn)后崩潰或回滾了,此時(shí)我們不論它在哪一步崩潰或回滾,我們都可以確定Commit日志一定刷到了磁盤上。而一旦Commit日志被刷到了磁盤上,那么這個(gè)事務(wù)所寫的數(shù)據(jù)一定可以被回放出來且標(biāo)記為已提交。那么這個(gè)數(shù)據(jù)就是可見的。這個(gè)事務(wù)實(shí)際上已經(jīng)提交了,數(shù)據(jù)庫被恢復(fù)到了T+1。

這個(gè)現(xiàn)象表明,2號(hào)位點(diǎn)似乎就是崩潰恢復(fù)的臨界點(diǎn),它標(biāo)注了數(shù)據(jù)庫崩潰恢復(fù)可以回到T或者T+1狀態(tài)。那么我們?nèi)绾畏Q呼這個(gè)位點(diǎn)?回想持久性的概念:事務(wù)一旦提交,該事務(wù)對(duì)于數(shù)據(jù)庫的修改就永久的保留在了數(shù)據(jù)庫中。二者實(shí)際上是吻合的。所以我們將這個(gè)2號(hào)位點(diǎn)稱為持久性位點(diǎn)。

另外關(guān)于xlog刷盤還有一點(diǎn)需要說明的是xlog刷盤和回放具有單個(gè)文件的原子性;WAL日志頭部的CRC校驗(yàn)提供了單個(gè)WAL日志文件的合法性校驗(yàn),如果WAL日志寫磁盤損壞,這條WAL日志的內(nèi)容無效,確保不會(huì)出現(xiàn)數(shù)據(jù)的部分回放。

2.事務(wù)的可見性一致——原子性位點(diǎn)

接下來我們繼續(xù)看3、4號(hào)操作:

  • 將本次事務(wù)提交寫入到Clog buffer中。
  • 將本次事務(wù)提交的結(jié)果寫入到ProcArray中。

3號(hào)操作是在Clog buffer中記錄了事務(wù)的當(dāng)前狀態(tài),可以看作是一層日志緩存。4號(hào)操作將提交操作寫入到了ProcArray中,這是非常重要的一步操作,通過剛才的說明我們知道快照判斷事務(wù)狀態(tài)是通過ProcArray進(jìn)行的。即這一步?jīng)Q定了其他事務(wù)看到的該事務(wù)狀態(tài)。

如果在4號(hào)操作前事務(wù)崩潰或回滾,那么數(shù)據(jù)庫中所有其他事務(wù)看到的數(shù)據(jù)版本都是T,相當(dāng)于事務(wù)沒有真正的提交。這個(gè)判斷即通過可見性 -> 快照 -> Procarray這個(gè)順序決定的。

而當(dāng)4號(hào)操作后,針對(duì)所有觀察者來說這個(gè)事務(wù)已經(jīng)提交了,因?yàn)樗性谶@個(gè)時(shí)間點(diǎn)之后拿到的快照數(shù)據(jù)版本都是T+1。

從這一點(diǎn)考慮,4號(hào)操作完全切合原子性操作的含義。因?yàn)?號(hào)操作的進(jìn)行與否影響了事務(wù)能否成功提交。4號(hào)操作前事務(wù)總是允許回滾的,因?yàn)闆]有其他事務(wù)看到該事務(wù)的T+1狀態(tài);但是4號(hào)操作過后,事務(wù)便不允許回滾,不然一旦存在讀到T+1版本的其他事務(wù)就會(huì)造成數(shù)據(jù)的不一致。而原子性的概念即是,事務(wù)成功提交或失敗回滾。由于4號(hào)操作后不允許回滾,那4號(hào)操作就完全可以作為事務(wù)成功提交的標(biāo)志。

綜上所述,我們可以將4號(hào)操作定義為事務(wù)的原子性位點(diǎn)。

3.持久性位點(diǎn)與原子性位點(diǎn)

原子性與持久性的要求

再次給出原子性與持久性的概念:

  • 原子性:一個(gè)事務(wù)要么執(zhí)行成功,要么執(zhí)行失敗。
  • 持久性:一個(gè)事務(wù)一旦執(zhí)行成功,就可以永久的保存在數(shù)據(jù)庫中。

我們把4號(hào)操作標(biāo)記為原子性位點(diǎn),是因?yàn)樵?號(hào)操作的時(shí)刻,客觀上所有的觀察者都認(rèn)為這個(gè)事務(wù)已經(jīng)提交了,快照的版本從T升級(jí)為T+1,事務(wù)不再可回滾。那么事務(wù)一旦提交,原子性是否就不生效了,我認(rèn)為是的,原子性至多只保證事務(wù)成功提交那一刻的數(shù)據(jù)一致性,事務(wù)已經(jīng)結(jié)束了我們就沒辦法再說原子性。所以原子性在原子性位點(diǎn)前保證了事務(wù)的可見、可恢復(fù)。

我們把2號(hào)位點(diǎn)標(biāo)記為持久性位點(diǎn),是因?yàn)槌志眯哉J(rèn)為事務(wù)成功后就可以永久的保留。根據(jù)上述的推測(cè),這個(gè)位點(diǎn)無疑就是2號(hào)這個(gè)持久性位點(diǎn)。所以從2號(hào)位點(diǎn)開始后的所有時(shí)間我們都應(yīng)該保證持久性。

如何理解兩個(gè)位點(diǎn)

在解釋完2、4號(hào)兩個(gè)位點(diǎn)之后,我們最終可以把事務(wù)提交時(shí)涉及到的兩個(gè)最重要概念定義出來,我們現(xiàn)在可以回答第一個(gè)問題,到底在哪個(gè)時(shí)刻事務(wù)真正的提交?答案是持久性位點(diǎn)后事務(wù)可以被完整的恢復(fù)出來;而原子性位點(diǎn)后事務(wù)真正的被其他事務(wù)視作提交。但是二者卻并不是分離性的,這如何理解呢?

我認(rèn)為這其實(shí)是原子性實(shí)現(xiàn)的一種妥協(xié),因?yàn)槲覀儧]有必要把二者統(tǒng)一,我們只需要保證關(guān)鍵性的一點(diǎn),只要兩個(gè)位點(diǎn)的順序能夠使得在不同狀態(tài)下的數(shù)據(jù)具有一致性,那么就可以認(rèn)為它符合我們?cè)有缘亩x。

  • 在持久性位點(diǎn)前崩潰或回滾,此時(shí)事務(wù)失敗,崩潰前或恢復(fù)后數(shù)據(jù)版本都是T。
  • 在持久性位點(diǎn)后原子性位點(diǎn)間崩潰或回滾,此時(shí)事務(wù)的可見性版本是T,也就是說對(duì)于數(shù)據(jù)庫中的所有事務(wù)來說,我們看到的都是T。回滾后,數(shù)據(jù)被重新回放到了T+1;而此時(shí)數(shù)據(jù)庫重啟后會(huì)發(fā)現(xiàn),在數(shù)據(jù)庫崩潰前的事務(wù)拿到快照看到的數(shù)據(jù)版本是T,崩潰后重啟拿到快照看到的數(shù)據(jù)版本是T+1,仿佛事務(wù)被隱式的提交了。但是這并不違背數(shù)據(jù)的一致性。
  • 在原子位點(diǎn)后崩潰。這個(gè)事務(wù)已經(jīng)提交了,崩潰前崩潰后事務(wù)看到的都是T+1版本的數(shù)據(jù)。

最后我們考慮兩個(gè)位點(diǎn)為什么沒有選擇合并。持久性位點(diǎn)的操作是WAL日志的刷盤,這個(gè)涉及到了磁盤IO的問題;另一方面原子性位點(diǎn)做的事情是寫ProcArray,這就要拿到ProcArray上的一把爭(zhēng)搶很嚴(yán)重的大鎖,可以認(rèn)為是一次高頻的共享內(nèi)存寫行為;二者本身都關(guān)乎數(shù)據(jù)庫事務(wù)的效率,如果綁定了二者成為一個(gè)原子操作,無疑會(huì)使得二者等待相當(dāng)嚴(yán)重,可能會(huì)對(duì)事務(wù)的運(yùn)行效率造成較大影響。從這個(gè)角度來說二者的行為分離是一個(gè)效率上的考慮。

二者順序是否可以顛倒?

顯然不可以,通過上述的示意圖我們可以看到中間這一段時(shí)間可能出現(xiàn)既不滿足原子性要求,也不滿足持久性要求的區(qū)域。

具體而言,如果先進(jìn)行原子性位點(diǎn),再進(jìn)行持久性位點(diǎn),則設(shè)想二者中間崩潰的事務(wù)情形。其他事務(wù)在崩潰前會(huì)看到T+1版本的數(shù)據(jù),崩潰后看到了T版本的數(shù)據(jù),這樣看到未來數(shù)據(jù)的行為顯然是不被允許的。

如何定義真正的提交

真正的提交就是原子性位點(diǎn)提交。

還是最基本的道理,真正提交的標(biāo)志就是數(shù)據(jù)版本從T升級(jí)為T+1。這個(gè)位點(diǎn)就是原子性位點(diǎn)。在這個(gè)點(diǎn)之前,其他事務(wù)看到的數(shù)據(jù)版本都是T,說真正的提交是不恰當(dāng)?shù)?在這個(gè)點(diǎn)之后事務(wù)無法被回滾。這足以說明這就是事務(wù)真正的提交點(diǎn)。

其他操作

我們最后關(guān)注1/3號(hào)操作:

  • 1號(hào)操作是寫wal commit日志到xlog buffer,這個(gè)寫日志對(duì)于事務(wù)提交來說并不關(guān)鍵;因?yàn)槿绻鼘懭肓藳]有刷到磁盤上,那么它其實(shí)還是毫無作用。
  • 3號(hào)操作是在clog buffer 中標(biāo)記本事務(wù)為已提交狀態(tài);這個(gè)操作對(duì)事務(wù)提交來說也不關(guān)鍵。因?yàn)槿绻麛?shù)據(jù)庫運(yùn)行正常,它不影響本事務(wù)快照的可見性;如果數(shù)據(jù)庫崩潰,這個(gè)clog狀態(tài)不論是否已經(jīng)持久化,事務(wù)狀態(tài)都可以被xlog中的 Commmit/Abort日志給回放出來。

六、PolarDB的原子性過程

1.事務(wù)提交

本節(jié)我們回到事務(wù)提交函數(shù)中,看到這幾個(gè)操作在函數(shù)調(diào)用棧中的位置。

  • 事務(wù)提交流程是帶有事務(wù)ID的事務(wù),不帶事務(wù)ID的事務(wù)沒有這個(gè)過程。由于不帶事務(wù)ID的事務(wù)大概率是只讀操作,不會(huì)對(duì)數(shù)據(jù)庫中數(shù)據(jù)一致性造成任何影響。
  • 提交xlog前會(huì)開啟嚴(yán)格模式,這個(gè)模式下任何錯(cuò)誤都會(huì)是致命錯(cuò)誤,數(shù)據(jù)庫直接崩潰重啟。
  • xlog刷盤和CLOG寫內(nèi)存的順序是在同步模式下進(jìn)行的,異步模式下不保證xlog刷盤,所以可能會(huì)崩潰后丟失數(shù)據(jù)。
  • 3/4中間有一步關(guān)鍵的操作,Replication等待。實(shí)際上此時(shí)數(shù)據(jù)xlog已經(jīng)刷盤,但是還沒有真正的提交,在同步模式下主庫會(huì)等待被庫將刷到磁盤上的xlog應(yīng)用完畢,之后再進(jìn)行下一步。
  • 寫ProcArray本事務(wù)提交,事務(wù)真正提交完成,事務(wù)不再可回滾。
  • 清理資源狀態(tài),此時(shí)工作已和本事務(wù)沒有任何關(guān)系。

2.事務(wù)回滾

  • 沒有事務(wù)ID的事務(wù)回滾會(huì)直接跳過。
  • 回滾前會(huì)首先判斷事務(wù)是否已提交,這個(gè)判斷是基于CLOG進(jìn)行的。一個(gè)事務(wù)怎么能又提交又回滾呢?這就是我們之前討論的3-4之間的狀態(tài),如果CLOG記錄了提交,那么遇到回滾命令數(shù)據(jù)庫直接發(fā)生致命故障崩潰重啟。
  • 回滾中也會(huì)相應(yīng)的寫入xlog回滾日志,不過是異步刷到磁盤。可以設(shè)想其實(shí)回滾日志即使不寫入,數(shù)據(jù)也是不可見的。
  • 當(dāng)事務(wù)在ProcArray中寫入回滾日志后,事務(wù)在進(jìn)程中真正的回滾了(其實(shí)這個(gè)狀態(tài)對(duì)其他事務(wù)沒有影響,之前后拿到的數(shù)據(jù)版本都是T)。

七、總結(jié)與展望

最后對(duì)全文做一個(gè)總結(jié),本文主要圍繞著“如何實(shí)現(xiàn)事務(wù)原子性”這個(gè)話題展開,分別從數(shù)據(jù)庫的崩潰恢復(fù)特性和事務(wù)可見性來說明了PolarDB數(shù)據(jù)庫實(shí)現(xiàn)原子性的底層原理。在介紹預(yù)寫日志+buffer IO原理的過程中還談到了shared buffer、WAL日志、clog、ProcArray、這些對(duì)原子性來說重要的數(shù)據(jù)結(jié)構(gòu)。在事務(wù)這個(gè)整體下數(shù)據(jù)庫的各個(gè)模塊巧妙的搭接起來,充分利用磁盤、緩存、IO這些計(jì)算機(jī)資源組成了一套完整的數(shù)據(jù)庫系統(tǒng)。

聯(lián)想到計(jì)算機(jī)科學(xué)其他的模型,如ISO網(wǎng)絡(luò)模型中傳輸層TCP協(xié)議在一個(gè)不可靠的信道上提供可靠的通信服務(wù)。數(shù)據(jù)庫事務(wù)實(shí)現(xiàn)了類似的思想,即在一個(gè)不可靠的操作系統(tǒng)(隨時(shí)可能崩潰)和磁盤存儲(chǔ)(無法大量數(shù)據(jù)的原子寫)上可靠的存儲(chǔ)數(shù)據(jù)。這一簡(jiǎn)單而重要的思想可謂是數(shù)據(jù)庫系統(tǒng)的基石,它如此重要以至于整個(gè)數(shù)據(jù)庫中最核心的數(shù)據(jù)結(jié)構(gòu)大多其有關(guān)?;蛟S隨著數(shù)據(jù)庫的發(fā)展未來技術(shù)更迭出更先進(jìn)的數(shù)據(jù)庫架構(gòu)體系,但是我們不能忘記是原子性、持久性仍舊應(yīng)當(dāng)是數(shù)據(jù)庫設(shè)計(jì)的核心。

八、思考

到這里事務(wù)原子性的重點(diǎn)就結(jié)束了,最后針對(duì)本文提到的觀點(diǎn)留下幾個(gè)問題供大家思考。

如何理解事務(wù)提交的原子性和持久性位點(diǎn)?

思考單個(gè)事務(wù)原子性和多個(gè)事務(wù)原子性的關(guān)系?崩潰恢復(fù)和可見性是否是一體的?

PolarDB中存在異步提交的概念,即不要求事務(wù)提交時(shí)不要求xlog日志落盤。請(qǐng)思考在這個(gè)模式下可能違背事務(wù)的哪些特性?是否違背原子性和持久性?

 

責(zé)任編輯:武曉燕 來源: 51CTO專欄
相關(guān)推薦

2021-06-03 14:00:35

PolarDB

2023-01-05 12:30:32

Redis

2013-08-09 09:27:31

2019-02-27 09:28:15

Redis服務(wù)器事務(wù)

2021-09-08 08:06:57

Redis原子性數(shù)據(jù)類型

2021-01-12 07:39:48

線程線程安全

2021-05-16 17:14:30

線程安全性

2012-05-23 12:49:58

Java自增操作原子性

2011-08-22 14:19:23

linuxUNIXwrite

2025-04-22 08:00:00

2021-05-06 19:20:05

Java內(nèi)存模型

2021-07-03 17:44:34

并發(fā)高并發(fā)原子性

2022-08-17 07:53:10

Volatile關(guān)鍵字原子性

2021-09-22 12:56:19

編程技能Golang

2015-08-31 14:37:12

物聯(lián)網(wǎng)企業(yè)

2014-01-09 09:45:41

原子飛原子

2020-11-25 08:15:57

存儲(chǔ)器

2023-05-17 08:52:56

Java原子性可見性

2021-09-07 10:33:42

MySQL事務(wù)隔離性

2023-11-07 08:04:19

Go并發(fā)程序原子操作
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)