天天看點

如何實作事務原子性?PolarDB原子性深度剖析

如何實作事務原子性?PolarDB原子性深度剖析

作者 | 佑熙

來源 | 阿裡技術公衆号

一 前言

在巍峨的資料庫大廈體系中,查詢優化器和事務體系是兩堵重要的承重牆,二者是如此重要以至于整個資料庫體系結構設計中大量的資料結構、機制和特性都是圍繞着二者搭建起來的。他們一個負責如何更快的查詢到資料,更有效的組織起底層資料體系;一個負責安全、穩定、持久的存儲資料,為使用者的讀寫并發提供邏輯實作。我們今天探索的主題是事務體系,然而事務體系太過龐大,我們需要分成若幹次的内容。本文就針對PolarDB事務體系中的原子性進行剖析。

二 問題

在閱讀本文之前,首先提出幾個重要的問題,這幾個問題或許在接觸資料庫之前你也曾經疑惑過。但是曾經這些問題的答案可能隻是簡單的被諸如“預寫日志”,“崩潰恢複機制”等簡單的答案回答過了,本文希望能夠更深一步的讨論這些機制的實作及内在原理。

  • 資料庫原子性到底是如何保證的?使用了哪些特殊的資料結構?為什麼要用?
  • 為什麼我寫入成功的資料能夠被保證不丢失?
  • 為什麼資料庫崩潰後可以完整的恢複出來邏輯上我已經送出的資料?
  • 更進一步,什麼是邏輯上已送出的資料?哪一個步驟才算是真正的送出?

三 背景

1 原子性在ACID中的位置

大名鼎鼎的ACID特性被提出後這個概念不斷的被引用(最初被寫入SQL92标準),這四種特性可以大概概括出人們心中對于資料庫最核心的訴求。本文要講的原子性便是其中第一個特性,我們先關注原子性在事務ACID中的位置。

如何實作事務原子性?PolarDB原子性深度剖析

這是個人對于資料庫ACID特性關系的了解,我認為資料庫ACID特性其實可以分為兩個視角去定義,其中AID(原子、持久、隔離)特性是從事務本身的視角去定義,而C(一緻)特性是從使用者的視角去定義。下面我會分别談下自己的了解。

  • 原子性:我們還是從這些特性的概念出發去讨論,原子性的概念是一個事務要麼執行成功,要麼執行失敗,即All or nothing。這種特質我們可以用一個最小的事務模型去定義出來,我們假設有一個事務,我們通過一套機制能夠實作它真正的送出或復原,這個目的就達成了,使用者隻是通過我們的系統進行了一次送出,而原子性的重心不在于事務成功或失敗本身;而是保證了事務體系隻接受成功或失敗兩種狀态,而且有相應的政策來保證成功或失敗的實體結果和邏輯結果是一緻的。原子性可以通過最小事務單元的特性定義出來,是整個事務體系的基石。
  • 持久性:而持久性指的是事務一旦送出後就可以永久的儲存在資料庫中。持久性的範圍與視角幾乎與原子性是一緻的,其實也導緻了二者在概念和實作上也是緊密相連的。二者都一定意義上保證了資料的一緻和可恢複性,而界限便是事務送出的時刻。舉例來說,一個資料目前的狀态是T,如果某個事務A試圖将狀态更新到T+1,如果這個事務A失敗了,那麼資料庫狀态回到T,這是原子性保證的;如果事務A送出成功了,那麼事務狀态變成T+1的那一刻,這個是原子性保證的;而一旦事務狀态變成T+1且事務成功送出,事務已經結束不再存在原子性,這個T+1的狀态就是由持久性負責保證。從這個角度可以推斷原子性保證了事務送出前資料的崩潰恢複,而持久性保證了事務送出後的崩潰恢複。
  • 隔離性:隔離性同樣是定義在事務層面的一個機制,給事務并發提供了某種程度的隔離保證。隔離性的本質是防止事務并發會導緻不一緻的狀态。由于不是本文的重點這裡不做詳述。
  • 一緻性:相較于其他幾個特性很特殊,一緻性的概念是資料庫在經過一個或多個事務後,資料庫必須保持在一緻性的狀态。如果從事務的角度去了解,保證了AID就可以保證事務是可串行、可恢複、原子性的,但是這種事務狀态的一緻性就是真正的一緻性嗎?破壞了AID就一定破壞C,但是反之AID都保證了C一定會被保證嗎?如果答案是是的話那這個概念就會失去它的意義。我們可以保證AID來保證事務是一緻的,但是是否能夠證明事務的一緻一定保證資料的一緻呢?另外資料一緻這個概念通過事務很難去準确定義,而如果通過使用者層面就很好定義。資料一緻就是使用者認為資料庫中資料任何時候的狀态是滿足其業務邏輯的。比如銀行存款不能是負數,是以使用者定義了一個非負限制。我認為這是概念設計者的一個留白,傾向于将一緻性視為一種高階目标。

本文主要還是圍繞原子性進行,而中間涉及到崩潰恢複的話題可能會涉及到持久性。隔離性和一緻性本文不讨論,在可見性的部分我們預設資料庫具有完成的隔離性,即可串行化的隔離級别。

2 原子性的内在要求

上面講了很多對于資料庫事務特性的了解,下面進入我們的主題原子性。我們還是需要拿剛才的例子來繼續闡述原子性。目前資料庫的狀态是T,現在希望通過一個事務A将資料狀态更新為T+1。我們讨論這個過程的原子性。

如何實作事務原子性?PolarDB原子性深度剖析

如果我們要保證這個事務是原子的,那麼我們可以定義三個要求,隻有滿足了下者,才可以說這個事務是原子性的:

  • 資料庫存在一個事務真正成功送出的時間點。
  • 在這個時間點之前開啟的事務(或者擷取的快照)隻應該看到T狀态,這個時間點之後開啟的事務(或者擷取的快照),隻應該看到T+1狀态。
  • 在這個時間點之前任何時候的崩潰,資料庫都應該能夠回到T狀态;在這個時間點之後任何時候崩潰,資料庫都應該能回到T+1狀态。

注意這個時間點我們并沒有定義出來,甚至我們都不能确定2/3中的這個時間點是不是同一個時間點。我們能确定的是這個時間點一定存在,否則就沒辦法說事務是原子性的,原子性确定了送出/復原必須有一個确定的時間點。另外根據我們剛才的描述,可以推測出2中的時間點,我們可以定義為原子性位點。由于原子性位點之前的送出我們不可見,之後可見,那麼這個原子性位點對于資料庫中其他事務來說就是該事務送出的時間點;而3中的位點可以定位為持久性位點,由于這符合持久性對于崩潰恢複的定義。即對于持久性來說,3這個位點後事務已經送出了。

四 原子性方案讨論

1 從兩種簡單的方案說起

首先我們從兩個簡單的方案來談起原子性,這一步的目的是試圖說明為什麼我們接下來每一步介紹的資料結構都是為了實作原子性必不可少的。

簡單Direct IO

如何實作事務原子性?PolarDB原子性深度剖析

設想我們存在這樣一個資料庫,每次使用者操作都會把資料寫到磁盤中。我們把這種方式叫做簡單Direct IO,簡單的意思是指我們沒有記錄任何資料日志而隻記錄了資料本身。假設初始的資料版本是T,這樣當我們插入了一些資料之後如果發生了資料崩潰,磁盤上會寫着一個T+0.5版本的資料頁,并且我們沒有任何辦法去復原或繼續進行後續的操作。這樣失敗的CASE無疑打破了原子性,因為目前的狀态既不是送出也不是復原而是一個介于中間的狀态,是以這是一次失敗的嘗試。

簡單Buffer IO

如何實作事務原子性?PolarDB原子性深度剖析

接下來我們有了一種新的方案,這種方案叫做簡單Buffer IO。同樣我們沒有日志,但是我們加入了一個新的資料結構叫做“共享緩存池”。這樣當我們每次寫資料頁的時候并不是直接把資料寫到資料庫上,而是寫到了shared buffer pool 中;這樣會有顯而易見的優勢,首先讀寫效率會大大的提高,我們每次寫都不必等待資料頁真實的寫入磁盤,而可以異步的進行;其次如果資料庫在事務未送出前復原或者崩潰掉了,我們隻需要丢棄掉shared buffer pool中的資料,隻有當資料庫成功送出時,它才可以真正的把資料刷到磁盤上,這樣從可見性和崩潰恢複性上看,我們看似已經滿足了要求。

但是上述方案還是有一個難以解決的問題,即資料落盤這件事并不像我們想象的這麼簡單。比如shared buffer pool中有10個髒頁,我們可以通過存儲技術來保證單個頁面的刷盤是原子的,但是在這10個頁面的中間任何時候資料庫都可能崩潰。繼而不論我們何時決定資料落盤,隻要資料落盤的過程中機器發生了崩潰,這個資料都可能在磁盤上産生一個T+0.5的版本,并且我們在重新開機後還是沒辦法去重做或者復原。

上面兩個例子的闡述似乎注定了資料庫沒有辦法通過不依賴其他結構的情況下保證資料的一緻性(還有一種流行的方案是SQLite資料庫的Shadow Paging技術,這裡不讨論),是以如果想解決這些問題,我們需要引入下一個重要的資料結構,資料日志。

2 預寫日志 + Buffer IO方案

方案總覽

我們在Buffer IO的基礎上引入了資料日志這樣的資料結構,用來解決資料不一緻的問題。

如何實作事務原子性?PolarDB原子性深度剖析

在資料緩存的部分與之前的想法一樣,不同的是我們在寫資料之前會額外記錄一個xlog buffer。這些xlog buffer是一個有序列的日志,他的序列号被稱為lsn,我們會把這個資料對應的日志lsn記錄在資料頁面上。每一個資料頁頁面都記錄了更新它最新的日志序号。這一特性是為了保證日志與資料的一緻性。

設想一下,如果我們能夠引入的日志與資料版本是完全一緻的,并且保證資料日志先于日志持久化,那麼不論何時資料崩潰我們都可以通過這個一緻的日志頁恢複出來。這樣就可以解決之前說的資料崩潰問題。不論事務送出前或者送出後崩潰,我們都可以通過回放日志的方案來回放出正确的資料版本,這樣就可以實作崩潰恢複的原子性。另外關于可見性的部分我們可以通過多版本快照的方式實作。保證資料日志和資料一緻并不容易,下面我們詳細講下如何保證,還有崩潰時資料如何恢複。

事務送出與控制刷髒

WAL日志被設計出來的目的是為了保證資料的可恢複性,而為了保證WAL日志與資料的一緻性,當資料緩存被持久化到磁盤時,持久化的資料頁對應的WAL日志必須先一步被持久化到磁盤中,這句話闡述了控制刷髒的本質含義。

如何實作事務原子性?PolarDB原子性深度剖析
  1. 資料庫背景存在這樣一個程序叫做checkpoint程序,其周期性的進行checkpoint操作。當checkpoint發生的時候,它會向xlog日志中寫入一條checkpoint日志,這條checkpoint日志包含了目前的REDO位點。checkpoint保證了目前所有髒資料已經被刷到了磁盤當中。
  2. 進行第一次插入操作,此時共享記憶體找不到這個頁面,它會把這個頁面從磁盤加載到共享記憶體中,之後寫入本次插入的輸入,并且插入一條寫資料的xlog到xlog buffer中,将這個表的日志标記從LSN0更新到LSN1。
  3. 在事務送出的時刻,事務會寫入一條事務送出日志,之後wal buffer pool上所有本次事務送出的WAL日志會一并被刷到磁盤上。
  4. 之後插入第二條資料B,他會插入一條寫資料的xlog到xlog buffer中,将這個表的日志标記從LSN1更新到LSN2。
  5. 同3一樣的操作。

之後如果資料庫正常運作,接下來的bgwriter/checkpoint程序會把資料頁異步的刷到磁盤上;而一旦資料庫發生崩潰,由于A、B兩條日志對應的資料日志與事務送出日志都已經被刷到了磁盤上,是以可以通過日志回放在shared buffer pool中重新回放出這些資料,之後異步寫入磁盤。

fullpage機制保證可恢複性

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

如何實作事務原子性?PolarDB原子性深度剖析

在每一個checkpoin動作之後的第一次修改資料時,PolarDB會将這條修改的資料連同整個資料頁寫入到wal buffer中之後再刷入磁盤,這種包含整個資料頁的WAL日志被稱為備份塊。備份塊的存在使得在任何情況下WAL日志都可以将完整的資料頁給回放出來。下面是一個完整的過程。

  1. checkpoint動作
  2. 進行第一次插入操作,此時共享記憶體找不到這個頁面,它會把這個頁面從磁盤加載到共享記憶體中,之後寫入本次插入的輸入。這時不同于上一節的操作,PolarDB序号為LSN1的這條WAL日志會把從磁盤上讀上來标記為LSN 0的整個頁面寫入到wal buffer pool中。
  3. 事務送出,此時整個WAL日志被強制刷入磁盤上的WAL段中。
  4. 同上節

這時如果資料庫發生了崩潰,在資料庫重新拉起恢複時,一旦它遇到了壞掉的頁面,便可以通過最初的WAL日志中記錄的最初版本的頁面一步一步的把正确的資料給回放出來。

基于WAL日志的崩潰恢複機制

有了前兩節的基礎上,我們可以繼續示範如果資料庫崩潰後,資料是如何被回放出來的。我們示範一種資料頁被寫壞的回放。

如何實作事務原子性?PolarDB原子性深度剖析
  • 當資料庫回放到寫入資料A的這條WAL日志時,它會從磁盤中讀出TABLE A這個頁面。這裡的這條WAL日志是一條備份日志,這是由于CHECKPOINT後,每個回放頁面的第一條WAL日志都是備份日志。
  • 當這條日志被回放時,備份日志有特殊的回放規則:它總是将自己頁面覆寫掉原來的頁面,并将原來頁面的LSN更新為這個頁面的LSN。(為了保證資料一緻性,正常回放頁面隻會回放大于自己LSN号碼的WAL日志)。在這個例子中,由于備份塊的存在,寫壞的頁面被成功恢複了出來。
  • 接下來PolarDB會按照正常的回放規則去回放後續的日志。

最後資料回放成功後,shared buffer pool中的資料便可以異步的被刷到磁盤上去替換之前損壞的資料。

我們花了很大的篇幅來說明資料庫是如何通過預寫日志而進行崩潰恢複的,這似乎可以解釋持久性位點的含義;下面我我們還需要再解釋可見性的問題。

3 可見性機制

由于我們對于原子性的說明中會涉及可見性的概念,這個概念在PolarDB中由一套複雜的MVCC機制來實作,且大多屬于隔離性的範疇。這裡會對可見性進行一個簡單的說明,而更詳細的說明會放到隔離性的文章中繼續闡述。

事務元組

第一個要說到的是事務元組。他是一條資料的最小單元,真正存放了資料,這裡我們隻關注幾個字段就好了。

如何實作事務原子性?PolarDB原子性深度剖析
  • t_xmin:生成該資料的事務ID
  • t_xmax:修改該事務資料的事務ID(删除或鎖定資料的事務ID)
  • t_cid:同一事務中對該元組操作的一個序号
  • t_ctid:一個由段号/偏移量組成的指針,指向最新版本的資料

快照

第二個要說到的是快照。快照記錄了某一個時間點資料庫中事務的狀态。

如何實作事務原子性?PolarDB原子性深度剖析

關于快照我們依舊不展開,我們知道通過快照可以從procArray中擷取到某一個時間點資料庫中所有可能事務的狀态即可。

目前事務狀态

第三點要說的到的是目前事務狀态,事務狀态是指資料庫中決定事務運作狀态的的機制。在并發的環境中,決定看到的事務狀态是非常重要的一件事。

在檢視一個tuple中的事務狀态時,可能會涉及到三個資料結構t_infomask、procArray、clog:

  • infomask:位于tuple頭部的緩存标志位,标志了該元組xmin/xmax兩個事務的運作狀态,這個狀态可以看作是clog的一層異步緩存,用來加速事務狀态的擷取;其狀态設定是異步設定,在事務送出時并不将所有事務相關的元組都立即更新,而是等待當第一個足夠新的能夠看到本次更新的快照設定時再去設定。
  • procArray快照:快照中的事務狀态,快照的擷取實際上就是在procArray中拿到這一瞬間資料庫中所有事務的狀态,快照一旦擷取狀态恒定,除非再次擷取(同一事務中擷取内容是否改變取決于事務隔離級别)。
  • clog:事務的實際狀态,分為clog buffer和clog檔案兩部分。clog buffer中實時的記錄了所有的事務狀态。

在一個可見性判斷過程中,三者通路的順序是[infomask -> 快照,clog],而三者的決定性順序是[快照 -> clog -> infomask] 。

infomask是最容易擷取的資訊,就記錄在元組的頭部,在部分條件下通過infomask就可以明确目前事務的可見性,不需要涉及到後面的資料結構;快照擁有最進階的決定權,最終決定xmin/xmax事務的狀态是運作/未運作;而clog用來輔助可見性的判斷,并且輔助設定infomask的值。舉例而言,如果這個判斷xmin事務可見性時發現在快照/clog中都已經送出,那麼會把t_infomask置為已送出;而如果xmin事務可見性時發現在快照送出,而clog未送出,則系統判斷發生了崩潰或復原,将infomask設定為事務非法。

事務快照可見性

在介紹元組和快照後,我們就可以繼續讨論快照可見性的話題。PolarDB的可見性有一套複雜的定義體系,需要通過許多資訊組合定義出來,但是其中最直接的就是快照和元組頭。下面通過一個資料插入和更新的示例來說明元組頭和快照的可見性。

如何實作事務原子性?PolarDB原子性深度剖析

本文不讨論隔離性,我們假設隔離級别是可串行化:

  • Snapshot1時刻:此時事務1184/1187都未開始,元組中也沒有記錄,student表是一張空表;通過Snapshot1快照可以得到的資料是空,我們把這個版本記做T。
  • Snapshot1 - Snapshot2時,此刻我們擷取快照那麼拿到的還是Snapshot1,那麼他看到的資料應該還是T。
  • Snapshot2時刻:此時事務1184已經結束,1187還未開始。是以1184的修改對使用者可見,1187仍舊不可見。具體到元組中可以看到 (1184/0) 這樣的元組頭,是以看到的是資料版本Tom,我們把這個版本記做T+1。
  • Snapshot2 - Snapshot3時,此刻我們擷取快照那麼拿到的還是Snapshot2,那麼他看到的資料應該還是T+1。
  • Snapshot3時刻:此刻事務1184/1187都已經結束,二者都可見,是以我們可以看到元組中(1184,1187)和(1187,1187)二者都不可見,而(1187,0)即Susan是可見的。我們把這個版本記做T+2。

通過上述分析我們可以得到一個簡單的結論,資料庫的可見性取決于快照的時機。我們原子性中所謂的可見性版本不同其實是指拿到的快照不同,快照決定了一個正在執行中的事務是否已經送出。這種送出與事務标記送出狀态甚至是記錄clog送出都沒有關系,我們可以通過這種方法來使得我們拿到的快照與事務送出具有一緻性。

事務原子性中的可見性

上文中我們已經簡述了PolarDB快照可見性的問題,這裡補充下事務送出時的具體實作問題。

如何實作事務原子性?PolarDB原子性深度剖析

我們設計可見性機制的核心思想是:“事務隻應該看到它應該看到的資料版本”。如何定義應該看到,這裡隻舉一個簡單的例子,如果一個元組的xmin事務沒有送出,其他事務大機率是看不到的;而如果一個元組的xmin事務已經送出,其他事務就可能會看到。如何知道這個xmin有沒有送出,上文已經提到了我們通過快照來決定,是以我們事務送出時的關鍵機制就是新快照的更新機制。

可見性在事務送出時涉及到兩個重要的資料結構clog buffer和procArray 。二者的關系在上文已經給出了解釋,他們在判斷事務可見性時發揮一定的作用,當然procArray起到了決定性的作用。這是因為快照的擷取實際上就是一個周遊ProcArray的過程。

在實際第三步會将本事務送出的資訊寫入clog buffer,此時事務标記clog是已送出,但實際上仍舊沒有送出。之後事務标記ProcArray已送出,這一步事務完成了真正的送出,這個時間點之後重新擷取的快照會更新資料版本。

五 PolarDB 中原子性的實作

在完成了PolarDB崩潰恢複及可見性理論的說明之後,我們可以知道PolarDB可以通過這樣一套預寫日志+BufferIO的方案來保證事務的崩潰恢複和可見一緻性,進而實作原子性。下面我們将針對事務送出中最重要的環節進行探究,找出我們最初提到的原子性位點到底指什麼。

1 事務崩潰恢複一緻——持久性位點

如何實作事務原子性?PolarDB原子性深度剖析

簡單來說事務送出中有這樣四個操作對于事務的原子性來說是最為核心和重要的。本節我們先考慮前兩個操作。

  • 送出事務的Commit日志(即Commit 的WAL日志)。
  • 将本次事務所有的送出的WAL日志全部強制刷盤,持久化到存儲。

我們标記這個xlog(WAL日志)落盤的位點,我們設想兩種情況:

  • 如果在這個位點前事務崩潰或者復原了,那麼不管資料日志有沒有刷盤,Commit日志一定沒有刷盤,由于WAL日志具有順序性,Commit日志一定是最後一個持久化到磁盤中。此時如果我們對資料進行回放,我們發現缺少Commit日志的事務無法被标記為已送出狀态,而根據可見性這種狀态相關的資料一定是不可見。這些資料之後會被視為髒資料給清理掉。是以我們可以得出結論,在這個節點前崩潰,事務實際上就是沒有送出。資料庫實質上是恢複到了狀态T。
  • 如果在這個位點後崩潰或復原了,此時我們不論它在哪一步崩潰或復原,我們都可以确定Commit日志一定刷到了磁盤上。而一旦Commit日志被刷到了磁盤上,那麼這個事務所寫的資料一定可以被回放出來且标記為已送出。那麼這個資料就是可見的。這個事務實際上已經送出了,資料庫被恢複到了T+1。

這個現象表明,2号位點似乎就是崩潰恢複的臨界點,它标注了資料庫崩潰恢複可以回到T或者T+1狀态。那麼我們如何稱呼這個位點?回想持久性的概念:事務一旦送出,該事務對于資料庫的修改就永久的保留在了資料庫中。二者實際上是吻合的。是以我們将這個2号位點稱為持久性位點。

另外關于xlog刷盤還有一點需要說明的是xlog刷盤和回放具有單個檔案的原子性;WAL日志頭部的CRC校驗提供了單個WAL日志檔案的合法性校驗,如果WAL日志寫磁盤損壞,這條WAL日志的内容無效,確定不會出現資料的部分回放。

2 事務的可見性一緻——原子性位點

如何實作事務原子性?PolarDB原子性深度剖析

接下來我們繼續看3、4号操作:

  • 将本次事務送出寫入到Clog buffer中。
  • 将本次事務送出的結果寫入到ProcArray中。

3号操作是在Clog buffer中記錄了事務的目前狀态,可以看作是一層日志緩存。4号操作将送出操作寫入到了ProcArray中,這是非常重要的一步操作,通過剛才的說明我們知道快照判斷事務狀态是通過ProcArray進行的。即這一步決定了其他事務看到的該事務狀态。

如果在4号操作前事務崩潰或復原,那麼資料庫中所有其他事務看到的資料版本都是T,相當于事務沒有真正的送出。這個判斷即通過可見性 -> 快照 -> Procarray這個順序決定的。

而當4号操作後,針對所有觀察者來說這個事務已經送出了,因為所有在這個時間點之後拿到的快照資料版本都是T+1。

從這一點考慮,4号操作完全切合原子性操作的含義。因為4号操作的進行與否影響了事務能否成功送出。4号操作前事務總是允許復原的,因為沒有其他事務看到該事務的T+1狀态;但是4号操作過後,事務便不允許復原,不然一旦存在讀到T+1版本的其他事務就會造成資料的不一緻。而原子性的概念即是,事務成功送出或失敗復原。由于4号操作後不允許復原,那4号操作就完全可以作為事務成功送出的标志。

綜上所述,我們可以将4号操作定義為事務的原子性位點。

3 持久性位點與原子性位點

如何實作事務原子性?PolarDB原子性深度剖析

原子性與持久性的要求

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

  • 原子性:一個事務要麼執行成功,要麼執行失敗。
  • 持久性:一個事務一旦執行成功,就可以永久的儲存在資料庫中。

我們把4号操作标記為原子性位點,是因為在4号操作的時刻,客觀上所有的觀察者都認為這個事務已經送出了,快照的版本從T更新為T+1,事務不再可復原。那麼事務一旦送出,原子性是否就不生效了,我認為是的,原子性至多隻保證事務成功送出那一刻的資料一緻性,事務已經結束了我們就沒辦法再說原子性。是以原子性在原子性位點前保證了事務的可見、可恢複。

我們把2号位點标記為持久性位點,是因為持久性認為事務成功後就可以永久的保留。根據上述的推測,這個位點無疑就是2号這個持久性位點。是以從2号位點開始後的所有時間我們都應該保證持久性。

如何了解兩個位點

在解釋完2、4号兩個位點之後,我們最終可以把事務送出時涉及到的兩個最重要概念定義出來,我們現在可以回答第一個問題,到底在哪個時刻事務真正的送出?答案是持久性位點後事務可以被完整的恢複出來;而原子性位點後事務真正的被其他事務視作送出。但是二者卻并不是分離性的,這如何了解呢?

我認為這其實是原子性實作的一種妥協,因為我們沒有必要把二者統一,我們隻需要保證關鍵性的一點,隻要兩個位點的順序能夠使得在不同狀态下的資料具有一緻性,那麼就可以認為它符合我們原子性的定義。

  • 在持久性位點前崩潰或復原,此時事務失敗,崩潰前或恢複後資料版本都是T。
  • 在持久性位點後原子性位點間崩潰或復原,此時事務的可見性版本是T,也就是說對于資料庫中的所有事務來說,我們看到的都是T。復原後,資料被重新回放到了T+1;而此時資料庫重新開機後會發現,在資料庫崩潰前的事務拿到快照看到的資料版本是T,崩潰後重新開機拿到快照看到的資料版本是T+1,仿佛事務被隐式的送出了。但是這并不違背資料的一緻性。
  • 在原子位點後崩潰。這個事務已經送出了,崩潰前崩潰後事務看到的都是T+1版本的資料。

最後我們考慮兩個位點為什麼沒有選擇合并。持久性位點的操作是WAL日志的刷盤,這個涉及到了磁盤IO的問題;另一方面原子性位點做的事情是寫ProcArray,這就要拿到ProcArray上的一把争搶很嚴重的大鎖,可以認為是一次高頻的共享記憶體寫行為;二者本身都關乎資料庫事務的效率,如果綁定了二者成為一個原子操作,無疑會使得二者等待相當嚴重,可能會對事務的運作效率造成較大影響。從這個角度來說二者的行為分離是一個效率上的考慮。

二者順序是否可以颠倒?

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

具體而言,如果先進行原子性位點,再進行持久性位點,則設想二者中間崩潰的事務情形。其他事務在崩潰前會看到T+1版本的資料,崩潰後看到了T版本的資料,這樣看到未來資料的行為顯然是不被允許的。

如何定義真正的送出

真正的送出就是原子性位點送出。

還是最基本的道理,真正送出的标志就是資料版本從T更新為T+1。這個位點就是原子性位點。在這個點之前,其他事務看到的資料版本都是T,說真正的送出是不恰當的;在這個點之後事務無法被復原。這足以說明這就是事務真正的送出點。

其他操作

我們最後關注1/3号操作:

  • 1号操作是寫wal commit日志到xlog buffer,這個寫日志對于事務送出來說并不關鍵;因為如果它寫入了沒有刷到磁盤上,那麼它其實還是毫無作用。
  • 3号操作是在clog buffer 中标記本事務為已送出狀态;這個操作對事務送出來說也不關鍵。因為如果資料庫運作正常,它不影響本事務快照的可見性;如果資料庫崩潰,這個clog狀态不論是否已經持久化,事務狀态都可以被xlog中的 Commmit/Abort日志給回放出來。

六 PolarDB的原子性過程

1 事務送出

本節我們回到事務送出函數中,看到這幾個操作在函數調用棧中的位置。

如何實作事務原子性?PolarDB原子性深度剖析
  • 事務送出流程是帶有事務ID的事務,不帶事務ID的事務沒有這個過程。由于不帶事務ID的事務大機率是隻讀操作,不會對資料庫中資料一緻性造成任何影響。
  • 送出xlog前會開啟嚴格模式,這個模式下任何錯誤都會是緻命錯誤,資料庫直接崩潰重新開機。
  • xlog刷盤和CLOG寫記憶體的順序是在同步模式下進行的,異步模式下不保證xlog刷盤,是以可能會崩潰後丢失資料。
  • 3/4中間有一步關鍵的操作,Replication等待。實際上此時資料xlog已經刷盤,但是還沒有真正的送出,在同步模式下主庫會等待被庫将刷到磁盤上的xlog應用完畢,之後再進行下一步。
  • 寫ProcArray本事務送出,事務真正送出完成,事務不再可復原。
  • 清理資源狀态,此時工作已和本事務沒有任何關系。

2 事務復原

如何實作事務原子性?PolarDB原子性深度剖析
  • 沒有事務ID的事務復原會直接跳過。
  • 復原前會首先判斷事務是否已送出,這個判斷是基于CLOG進行的。一個事務怎麼能又送出又復原呢?這就是我們之前讨論的3-4之間的狀态,如果CLOG記錄了送出,那麼遇到復原指令資料庫直接發生緻命故障崩潰重新開機。
  • 復原中也會相應的寫入xlog復原日志,不過是異步刷到磁盤。可以設想其實復原日志即使不寫入,資料也是不可見的。
  • 當事務在ProcArray中寫入復原日志後,事務在程序中真正的復原了(其實這個狀态對其他事務沒有影響,之前後拿到的資料版本都是T)。

七 總結與展望

最後對全文做一個總結,本文主要圍繞着“如何實作事務原子性”這個話題展開,分别從資料庫的崩潰恢複特性和事務可見性來說明了PolarDB資料庫實作原子性的底層原理。在介紹預寫日志+buffer IO原理的過程中還談到了shared buffer、WAL日志、clog、ProcArray、這些對原子性來說重要的資料結構。在事務這個整體下資料庫的各個子產品巧妙的搭接起來,充分利用磁盤、緩存、IO這些計算機資源組成了一套完整的資料庫系統。

聯想到計算機科學其他的模型,如ISO網絡模型中傳輸層TCP協定在一個不可靠的信道上提供可靠的通信服務。資料庫事務實作了類似的思想,即在一個不可靠的作業系統(随時可能崩潰)和磁盤存儲(無法大量資料的原子寫)上可靠的存儲資料。這一簡單而重要的思想可謂是資料庫系統的基石,它如此重要以至于整個資料庫中最核心的資料結構大多其有關。或許随着資料庫的發展未來技術更疊出更先進的資料庫架構體系,但是我們不能忘記是原子性、持久性仍舊應當是資料庫設計的核心。

八 思考

到這裡事務原子性的重點就結束了,最後針對本文提到的觀點留下幾個問題供大家思考。

  • 如何了解事務送出的原子性和持久性位點?
  • 思考單個事務原子性和多個事務原子性的關系?崩潰恢複和可見性是否是一體的?
  • PolarDB中存在異步送出的概念,即不要求事務送出時不要求xlog日志落盤。請思考在這個模式下可能違背事務的哪些特性?是否違背原子性和持久性?
參考資料 https://www.interdb.jp/pg/

免費領取電子書

《Dubbo分布式服務治理實戰》

Dubbo是阿裡巴巴開源的高性能分布式RPC服務治理架構,提供了六大核心能力:面向接口代理的高性能RPC調用、智能容錯和負載均衡、服務自動注冊和發現、高度可擴充能力、運作期流量排程、可視化的服務治理與運維。本書将帶同學們了解并掌握Dubbo3.0新特性及相關實踐實戰。

掃碼加阿裡妹好友,回複“治理”擷取吧~(若掃碼無效,可直接添加alimei4、alimei5、alimei6、alimei7)

如何實作事務原子性?PolarDB原子性深度剖析