成人免费xxxxx在线视频软件_久久精品久久久_亚洲国产精品久久久_天天色天天色_亚洲人成一区_欧美一级欧美三级在线观看

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

開發 開發工具
在巍峨的數據庫大廈體系中,查詢優化器和事務體系是兩堵重要的承重墻,二者是如此重要以至于整個數據庫體系結構設計中大量的數據結構、機制和特性都是圍繞著二者搭建起來的。

[[403352]]

 一、前言

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

二、問題

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

數據庫原子性到底是如何保證的?使用了哪些特殊的數據結構?為什么要用?

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

為什么數據庫崩潰后可以完整的恢復出來邏輯上我已經提交的數據?

更進一步,什么是邏輯上已提交的數據?哪一個步驟才算是真正的提交?

三、背景

1.原子性在ACID中的位置

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

這是個人對于數據庫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。我們討論這個過程的原子性。

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

  • 數據庫存在一個事務真正成功提交的時間點。
  • 在這個時間點之前開啟的事務(或者獲取的快照)只應該看到T狀態,這個時間點之后開啟的事務(或者獲取的快照),只應該看到T+1狀態。
  • 在這個時間點之前任何時候的崩潰,數據庫都應該能夠回到T狀態;在這個時間點之后任何時候崩潰,數據庫都應該能回到T+1狀態。

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

四、原子性方案討論

1.從兩種簡單的方案說起

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

簡單Direct IO

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

簡單Buffer IO

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

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

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

2.預寫日志 + Buffer IO方案

方案總覽

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

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

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

事務提交與控制刷臟

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

  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機制。

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

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

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

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

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

  1. 當數據庫回放到寫入數據A的這條WAL日志時,它會從磁盤中讀出TABLE A這個頁面。這里的這條WAL日志是一條備份日志,這是由于CHECKPOINT后,每個回放頁面的第一條WAL日志都是備份日志。
  2. 當這條日志被回放時,備份日志有特殊的回放規則:它總是將自己頁面覆蓋掉原來的頁面,并將原來頁面的LSN升級為這個頁面的LSN。(為了保證數據一致性,正常回放頁面只會回放大于自己LSN號碼的WAL日志)。在這個例子中,由于備份塊的存在,寫壞的頁面被成功恢復了出來。
  3. 接下來PolarDB會按照正常的回放規則去回放后續的日志。

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

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

3.可見性機制

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

事務元組

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

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

快照

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

關于快照我們依舊不展開,我們知道通過快照可以從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的可見性有一套復雜的定義體系,需要通過許多信息組合定義出來,但是其中最直接的就是快照和元組頭。下面通過一個數據插入和更新的示例來說明元組頭和快照的可見性。

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

  • 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快照可見性的問題,這里補充下事務提交時的具體實現問題。

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

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

在實際第三步會將本事務提交的信息寫入clog buffer,此時事務標記clog是已提交,但實際上仍舊沒有提交。之后事務標記ProcArray已提交,這一步事務完成了真正的提交,這個時間點之后重新獲取的快照會更新數據版本。

五、PolarDB 中原子性的實現

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

1.事務崩潰恢復一致——持久性位點

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

  • 提交事務的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.事務的可見性一致——原子性位點

接下來我們繼續看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.持久性位點與原子性位點

原子性與持久性的要求

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

  • 原子性:一個事務要么執行成功,要么執行失敗。
  • 持久性:一個事務一旦執行成功,就可以永久的保存在數據庫中。

我們把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.事務提交

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

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

2.事務回滾

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

七、總結與展望

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

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

八、思考

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

如何理解事務提交的原子性和持久性位點?

思考單個事務原子性和多個事務原子性的關系?崩潰恢復和可見性是否是一體的?

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

 

責任編輯:武曉燕 來源: 51CTO專欄
相關推薦

2021-06-03 14:00:35

PolarDB

2023-01-05 12:30:32

Redis

2013-08-09 09:27:31

2021-09-08 08:06:57

Redis原子性數據類型

2019-02-27 09:28:15

Redis服務器事務

2021-05-16 17:14:30

線程安全性

2021-01-12 07:39:48

線程線程安全

2012-05-23 12:49:58

Java自增操作原子性

2025-04-22 08:00:00

2011-08-22 14:19:23

linuxUNIXwrite

2021-05-06 19:20:05

Java內存模型

2021-09-22 12:56:19

編程技能Golang

2022-08-17 07:53:10

Volatile關鍵字原子性

2021-07-03 17:44:34

并發高并發原子性

2015-08-31 14:37:12

物聯網企業

2014-01-09 09:45:41

原子飛原子

2023-05-17 08:52:56

Java原子性可見性

2020-11-25 08:15:57

存儲器

2021-09-07 10:33:42

MySQL事務隔離性

2023-11-07 08:04:19

Go并發程序原子操作
點贊
收藏

51CTO技術棧公眾號

主站蜘蛛池模板: 99国产精品99久久久久久粉嫩 | 精品欧美一区二区三区久久久 | 中文字幕观看 | 日本公妇乱淫xxxⅹ 国产在线不卡 | 日日摸天天添天天添破 | www.操.com| 久久久国产精品入口麻豆 | 亚洲一区中文字幕在线观看 | 国产成人精品免高潮在线观看 | 日日夜夜草 | 久久精品国产一区二区电影 | 日韩欧美在线精品 | 国产精品久久久久久久岛一牛影视 | 午夜精品影院 | 亚洲天天干 | 亚洲网站在线 | 在线免费激情视频 | 日日摸天天添天天添破 | 欧美特级黄色 | 久久国产日韩欧美 | 在线2区 | 草久久久 | 日韩欧美福利视频 | 日韩欧美精品在线播放 | av久久| 免费播放一级片 | 国产91在线播放 | 99精品在线 | 特黄色一级毛片 | 超碰在线免费av | 操人网站 | 国产精品一区二区三区四区五区 | 97国产精品视频人人做人人爱 | 日韩成人免费视频 | 91精品中文字幕一区二区三区 | 欧美一级片中文字幕 | 国产精品不卡一区 | 欧美日韩在线免费观看 | 精品一区二区在线看 | 欧洲性生活视频 | 久久青青|