8. 事務
有些作者聲稱,支援通用的兩階段提交代價太大,會帶來效能與可用性的問題。我們認為,讓程式設計師來處理過度使用事務導致的效能問題,總比缺少事務程式設計好得多。
James Corbett 等人,Spanner:Google 的全球分散式資料庫(2012)
在資料系統的殘酷現實中,很多事情都可能出錯:
- 資料庫軟體或硬體可能在任意時刻發生故障(包括寫操作進行到一半時)。
- 應用程式可能在任意時刻崩潰(包括一系列操作的中間)。
- 網路中斷可能會意外切斷應用程式與資料庫的連線,或資料庫節點之間的連線。
- 多個客戶端可能會同時寫入資料庫,覆蓋彼此的更改。
- 客戶端可能讀取到無意義的資料,因為資料只更新了一部分。
- 客戶端之間的競態條件可能導致令人驚訝的錯誤。
為了實現可靠性,系統必須處理這些故障,確保它們不會導致整個系統的災難性故障。然而,實現容錯機制需要大量工作。它需要仔細考慮所有可能出錯的事情,並進行大量測試,以確保解決方案真正有效。
數十年來,事務一直是簡化這些問題的首選機制。事務是應用程式將多個讀寫操作組合成一個邏輯單元的一種方式。從概念上講,事務中的所有讀寫操作被視作單個操作來執行:整個事務要麼成功(提交),要麼失敗(中止、回滾)。如果失敗,應用程式可以安全地重試。對於事務來說,應用程式的錯誤處理變得簡單多了,因為它不用再擔心部分失敗——即某些操作成功,某些失敗(無論出於何種原因)。
如果你與事務打交道多年,它們可能看起來顯而易見,但我們不應該將其視為理所當然。事務不是自然法則;它們是有目的地建立的,即為了簡化應用程式的程式設計模型。透過使用事務,應用程式可以自由地忽略某些潛在的錯誤場景和併發問題,因為資料庫會替應用處理好這些(我們稱之為安全保證)。
並非所有應用程式都需要事務,有時弱化事務保證或完全放棄事務也有好處(例如,為了獲得更高的效能或更高的可用性)。某些安全屬性可以在沒有事務的情況下實現。另一方面,事務可以防止很多麻煩:例如,郵局 Horizon 醜聞(參見“可靠性有多重要?”)背後的技術原因可能是底層會計系統缺乏 ACID 事務1。
你如何確定是否需要事務?為了回答這個問題,我們首先需要準確理解事務可以提供哪些安全保證,以及相關的成本。儘管事務乍看起來很簡單,但實際上有許多細微但重要的細節在起作用。
在本章中,我們將研究許多可能出錯的案例,並探索資料庫用於防範這些問題的演算法。我們將特別深入併發控制領域,討論可能發生的各種競態條件,以及資料庫如何實現讀已提交、快照隔離和可序列化等隔離級別。
併發控制對單節點和分散式資料庫都很重要。在本章後面的“分散式事務”部分,我們將研究兩階段提交協議和在分散式事務中實現原子性的挑戰。
事務到底是什麼?
今天,幾乎所有的關係型資料庫和一些非關係資料庫都支援事務。它們大多遵循 1975 年由 IBM System R(第一個 SQL 資料庫)引入的風格2 3 4。儘管一些實現細節發生了變化,但總體思路在 50 年裡幾乎保持不變:MySQL、PostgreSQL、Oracle、SQL Server 等的事務支援與 System R 驚人地相似。
在 2000 年代後期,非關係(NoSQL)資料庫開始流行起來。它們旨在透過提供新的資料模型選擇(參見第 3 章),以及預設包含複製(第 6 章)和分片(第 7 章)來改進關係型資料庫的現狀。事務是這一運動的主要犧牲品:許多這一代資料庫完全放棄了事務,或者重新定義了這個詞,用來描述比以前理解的更弱的保證集。
圍繞 NoSQL 分散式資料庫的炒作導致了一種流行的信念,即事務從根本上是不可擴充套件的,任何大規模系統都必須放棄事務以保持良好的效能和高可用性。最近,這種信念被證明是錯誤的。所謂的"NewSQL"資料庫,如 CockroachDB5、TiDB6、Spanner7、FoundationDB8 和 Yugabyte 已經證明,事務系統可以擴充套件到大資料量和高吞吐量。這些系統將分片與共識協議(第 10 章)相結合,以大規模提供強 ACID 保證。
然而,這並不意味著每個系統都必須是事務型的:與任何其他技術設計選擇一樣,事務有優點也有侷限性。為了理解這些權衡,讓我們深入瞭解事務可以提供的保證的細節——無論是在正常操作中還是在各種極端(但現實)的情況下。
ACID 的含義
事務提供的安全保證通常由眾所周知的首字母縮略詞 ACID 來描述,它代表原子性(Atomicity)、一致性(Consistency)、隔離性(Isolation)和永續性(Durability)。它由 Theo Härder 和 Andreas Reuter 於 1983 年提出9,旨在為資料庫中的容錯機制建立精確的術語。
然而,在實踐中,一個數據庫的 ACID 實現並不等同於另一個數據庫的實現。例如,正如我們將看到的,隔離性的含義有很多歧義10。高層次的想法是合理的,但魔鬼在細節中。今天,當一個系統聲稱自己"符合 ACID"時,實際上你能期待什麼保證並不清楚。不幸的是,ACID 基本上已經成為了一個營銷術語。
(不符合 ACID 標準的系統有時被稱為 BASE,它代表基本可用(Basically Available)、軟狀態(Soft state)和最終一致性(Eventual consistency)11。這比 ACID 的定義更加模糊。似乎 BASE 唯一合理的定義是"非 ACID";即,它幾乎可以代表任何你想要的東西。)
讓我們深入瞭解原子性、一致性、隔離性和永續性的定義,這將讓我們提煉出事務的思想。
原子性
一般來說,原子是指不能分解成更小部分的東西。這個詞在計算機的不同分支中意味著相似但又微妙不同的東西。例如,在多執行緒程式設計中,如果一個執行緒執行原子操作,這意味著另一個執行緒無法看到該操作的半完成結果。系統只能處於操作之前或操作之後的狀態,而不是介於兩者之間。
相比之下,在 ACID 的上下文中,原子性不是關於併發的。它不描述如果幾個程序試圖同時訪問相同的資料會發生什麼,因為這包含在字母 I(隔離性)中(參見“隔離性”)。
相反,ACID 原子性描述了當客戶端想要進行多次寫入,但在某些寫入被處理後發生故障時會發生什麼——例如,程序崩潰、網路連線中斷、磁碟變滿或違反了某些完整性約束。如果這些寫入被分組到一個原子事務中,並且由於故障無法完成(提交)事務,則事務被中止,資料庫必須丟棄或撤消該事務中迄今為止所做的任何寫入。
如果沒有原子性,如果在進行多處更改的中途發生錯誤,很難知道哪些更改已經生效,哪些沒有。應用程式可以重試,但這有進行兩次相同更改的風險,導致資料重複或錯誤。原子性簡化了這個問題:如果事務被中止,應用程式可以確定它沒有改變任何東西,因此可以安全地重試。
在錯誤時中止事務並丟棄該事務的所有寫入的能力是 ACID 原子性的定義特徵。也許可中止性比原子性更好,但我們將堅持使用原子性,因為這是常用詞。
一致性
一致性這個詞被嚴重濫用:
- 在第 6 章中,我們討論了副本一致性和非同步複製系統中出現的最終一致性問題(參見“複製延遲的問題”)。
- 資料庫的一致快照(例如,用於備份)是整個資料庫在某一時刻存在的快照。更準確地說,它與先發生關係(happens-before relation)一致(參見““先發生"關係和併發”):也就是說,如果快照包含在特定時間寫入的值,那麼它也反映了在該值寫入之前發生的所有寫入。
- 一致性雜湊是某些系統用於再平衡的分片方法(參見“一致性雜湊”)。
- 在 CAP 定理中(參見第 10 章),一致性一詞用於表示線性一致性(參見“線性一致性”)。
- 在 ACID 的上下文中,一致性是指應用程式特定的資料庫處於"良好狀態"的概念。
不幸的是,同一個詞至少有五種不同的含義。
ACID 一致性的思想是,你對資料有某些陳述(不變式)必須始終為真——例如,在會計系統中,所有賬戶的貸方和借方必須始終平衡。如果事務從滿足這些不變式的有效資料庫開始,並且事務期間的任何寫入都保持有效性,那麼你可以確定不變式始終得到滿足。(不變式可能在事務執行期間暫時違反,但在事務提交時應該再次滿足。)
如果你希望資料庫強制執行你的不變式,你需要將它們宣告為模式的一部分的約束。例如,外部索引鍵約束、唯一性約束或檢查約束(限制單個行中可以出現的值)通常用於對特定型別的不變式建模。更複雜的一致性要求有時可以使用觸發器或物化檢視建模12。
然而,複雜的不變式可能很難或不可能使用資料庫通常提供的約束來建模。在這種情況下,應用程式有責任正確定義其事務,以便它們保持一致性。如果你寫入違反不變式的錯誤資料,但你沒有宣告這些不變式,資料庫無法阻止你。因此,ACID 中的 C 通常取決於應用程式如何使用資料庫,而不僅僅是資料庫的屬性。
隔離性
大多數資料庫都會同時被多個客戶端訪問。如果它們讀寫資料庫的不同部分,這沒有問題,但如果它們訪問相同的資料庫記錄,你可能會遇到併發問題(競態條件)。
圖 8-1 是這種問題的一個簡單例子。假設你有兩個客戶端同時遞增儲存在資料庫中的計數器。每個客戶端需要讀取當前值,加 1,然後寫回新值(假設資料庫中沒有內建的遞增操作)。在圖 8-1 中,計數器應該從 42 增加到 44,因為發生了兩次遞增,但實際上由於競態條件只增加到 43。

ACID 意義上的隔離性意味著同時執行的事務彼此隔離:它們不能相互干擾。經典的資料庫教科書將隔離性形式化為可序列化,這意味著每個事務可以假裝它是唯一在整個資料庫上執行的事務。資料庫確保當事務已經提交時,結果與它們序列執行(一個接一個)相同,即使實際上它們可能是併發執行的13。
然而,可序列化有效能成本。在實踐中,許多資料庫使用比可序列化更弱的隔離形式:也就是說,它們允許併發事務以有限的方式相互干擾。一些流行的資料庫,如 Oracle,甚至沒有實現它(Oracle 有一個稱為"可序列化"的隔離級別,但它實際上實現了快照隔離,這是比可序列化更弱的保證10 14)。這意味著某些型別的競態條件仍然可能發生。我們將在“弱隔離級別”中探討快照隔離和其他形式的隔離。
永續性
資料庫系統的目的是提供一個安全的地方來儲存資料,而不用擔心丟失它。永續性是一個承諾,即一旦事務成功提交,它寫入的任何資料都不會被遺忘,即使發生硬體故障或資料庫崩潰。
在單節點資料庫中,永續性通常意味著資料已經寫入非易失性儲存,如硬碟或 SSD。定期檔案寫入通常在傳送到磁碟之前在記憶體中緩衝,這意味著如果突然斷電它們將丟失;因此,許多資料庫使用 fsync()
系統呼叫來確保資料真正寫入磁碟。資料庫通常還有預寫日誌或類似的(參見“使 B 樹可靠”),這允許它們在寫入過程中發生崩潰時恢復。
在複製資料庫中,永續性可能意味著資料已成功複製到某些節點。為了提供永續性保證,資料庫必須等到這些寫入或複製完成,然後才報告事務成功提交。然而,如“可靠性和容錯”中所討論的,完美的永續性不存在:如果所有硬碟和所有備份同時被銷燬,顯然你的資料庫無法挽救你。
複製與永續性
歷史上,永續性意味著寫入歸檔磁帶。然後它被理解為寫入磁碟或 SSD。最近,它已經適應為意味著複製。哪種實現更好?
事實是,沒有什麼是完美的:
- 如果你寫入磁碟而機器宕機,即使你的資料沒有丟失,在你修復機器或將磁碟轉移到另一臺機器之前,它也是不可訪問的。複製系統可以保持可用。
- 相關故障——停電或導致每個節點在特定輸入上崩潰的錯誤——可以一次性摧毀所有副本(參見“可靠性和容錯”),失去任何僅在記憶體中的資料。因此,寫入磁碟對於複製資料庫仍然相關。
- 在非同步複製系統中,當領導者變得不可用時,最近的寫入可能會丟失(參見“處理節點故障”)。
- 當電源突然切斷時,SSD 特別被證明有時會違反它們應該提供的保證:即使
fsync
也不能保證正常工作15。磁碟韌體可能有錯誤,就像任何其他型別的軟體一樣16 17,例如,導致驅動器在正好 32,768 小時操作後失敗18。而且fsync
很難使用;即使 PostgreSQL 使用它不正確超過 20 年19 20 21。 - 儲存引擎和檔案系統實現之間的微妙互動可能導致難以追蹤的錯誤,並可能導致磁碟上的檔案在崩潰後損壞22 23。一個副本上的檔案系統錯誤有時也會傳播到其他副本24。
- 磁碟上的資料可能在未被檢測到的情況下逐漸損壞25。如果資料已經損壞了一段時間,副本和最近的備份也可能損壞。在這種情況下,你需要嘗試從歷史備份中恢復資料。
- 一項關於 SSD 的研究發現,在前四年的執行中,30% 到 80% 的驅動器會開發至少一個壞塊,其中只有一些可以透過韌體糾正26。磁碟驅動器的壞扇區率較低,但完全故障率高於 SSD。
- 當磨損的 SSD(經歷了許多寫/擦除週期)斷電時,它可能在幾周到幾個月的時間尺度上開始丟失資料,具體取決於溫度27。對於磨損水平較低的驅動器,這不是問題28。
在實踐中,沒有一種技術可以提供絕對保證。只有各種降低風險的技術,包括寫入磁碟、複製到遠端機器和備份——它們可以而且應該一起使用。一如既往,明智的做法是對任何理論上的"保證"持健康的懷疑態度。
單物件與多物件操作
回顧一下,在 ACID 中,原子性和隔離性描述了如果客戶端在同一事務中進行多次寫入,資料庫應該做什麼:
- 原子性
- 如果在寫入序列的中途發生錯誤,事務應該被中止,並且到該點為止所做的寫入應該被丟棄。換句話說,資料庫讓你免於擔心部分失敗,透過提供全有或全無的保證。
- 隔離性
- 併發執行的事務不應該相互干擾。例如,如果一個事務進行多次寫入,那麼另一個事務應該看到所有或不看到這些寫入,但不是某些子集。
這些定義假設你想要同時修改多個物件(行、文件、記錄)。這種多物件事務通常需要保持多塊資料同步。圖 8-2 顯示了一個來自電子郵件應用程式的示例。要顯示使用者的未讀訊息數,你可以查詢類似這樣的內容:
SELECT COUNT(*) FROM emails WHERE recipient_id = 2 AND unread_flag = true

然而,如果有很多電子郵件,你可能會發現這個查詢太慢,並決定將未讀訊息的數量儲存在一個單獨的欄位中(一種反規範化,我們在“規範化、反規範化和連線”中討論)。現在,每當有新訊息進來時,你必須增加未讀計數器,每當訊息被標記為已讀時,你也必須減少未讀計數器。
在圖 8-2 中,使用者 2 遇到了異常:郵箱列表顯示有未讀訊息,但計數器顯示零未讀訊息,因為計數器增量尚未發生。(如果電子郵件應用程式中的錯誤計數器看起來太微不足道,請考慮客戶賬戶餘額而不是未讀計數器,以及支付事務而不是電子郵件。)隔離本可以透過確保使用者 2 看到插入的電子郵件和更新的計數器,或者兩者都不看到,但不是不一致的中間點,來防止這個問題。
圖 8-3 說明了對原子性的需求:如果在事務過程中某處發生錯誤,郵箱的內容和未讀計數器可能會失去同步。在原子事務中,如果對計數器的更新失敗,事務將被中止,插入的電子郵件將被回滾。

多物件事務需要某種方式來確定哪些讀寫操作屬於同一事務。在關係資料庫中,這通常基於客戶端與資料庫伺服器的 TCP 連線:在任何特定連線上,BEGIN TRANSACTION
和 COMMIT
語句之間的所有內容都被認為是同一事務的一部分。如果 TCP 連線中斷,事務必須被中止。
另一方面,許多非關係資料庫沒有這樣的方式來將操作組合在一起。即使有多物件 API(例如,鍵值儲存可能有一個多重放置操作,在一個操作中更新多個鍵),這並不一定意味著它具有事務語義:該命令可能在某些鍵上成功而在其他鍵上失敗,使資料庫處於部分更新狀態。
單物件寫入
當單個物件被更改時,原子性和隔離性也適用。例如,假設你正在向資料庫寫入 20 KB 的 JSON 文件:
- 如果在傳送了前 10 KB 後網路連線中斷,資料庫是否儲存了無法解析的 10 KB JSON 片段?
- 如果資料庫正在覆蓋磁碟上的先前值的過程中電源失效,你是否最終會將新舊值拼接在一起?
- 如果另一個客戶端在寫入過程中讀取該文件,它會看到部分更新的值嗎?
這些問題會令人非常困惑,因此儲存引擎幾乎普遍的目標是在一個節點上的單個物件(如鍵值對)上提供原子性和隔離性。原子性可以使用日誌實現崩潰恢復(參見“使 B 樹可靠”),隔離性可以使用每個物件上的鎖來實現(一次只允許一個執行緒訪問物件)。
某些資料庫還提供更複雜的原子操作,例如遞增操作,它消除了像圖 8-1 中那樣的讀-修改-寫迴圈的需求。類似流行的是條件寫入操作,它允許僅在值未被其他人併發更改時才進行寫入(參見“條件寫入(比較並設定)”),類似於共享記憶體併發中的比較並設定或比較並交換(CAS)操作。
Note
嚴格來說,術語原子遞增在多執行緒程式設計的意義上使用了原子這個詞。在 ACID 的上下文中,它實際上應該被稱為隔離或可序列化遞增,但這不是通常的術語。
這些單物件操作很有用,因為它們可以防止多個客戶端嘗試同時寫入同一物件時的丟失更新(參見“防止丟失更新”)。然而,它們不是通常意義上的事務。例如,Cassandra 和 ScyllaDB 的"輕量級事務"功能以及 Aerospike 的"強一致性"模式在單個物件上提供線性一致(參見“線性一致性”)讀取和條件寫入,但不保證跨多個物件。
多物件事務的需求
我們是否需要多物件事務?是否可能僅使用鍵值資料模型和單物件操作來實現任何應用程式?
在某些用例中,單物件插入、更新和刪除就足夠了。然而,在許多其他情況下,需要協調對多個不同物件的寫入:
- 在關係資料模型中,一個表中的行通常具有對另一個表中行的外部索引鍵引用。類似地,在類似圖的資料模型中,頂點具有指向其他頂點的邊。多物件事務允許你確保這些引用保持有效:插入引用彼此的多個記錄時,外部索引鍵必須正確且最新,否則資料變得毫無意義。
- 在文件資料模型中,需要一起更新的欄位通常在同一文件內,它被視為單個物件——更新單個文件時不需要多物件事務。然而,缺乏連線功能的文件資料庫也鼓勵反規範化(參見“何時使用哪種模型”)。當需要更新反規範化資訊時,如圖 8-2 的示例,你需要一次更新多個文件。事務在這種情況下非常有用,可以防止反規範化資料失去同步。
- 在具有二級索引的資料庫中(幾乎除了純鍵值儲存之外的所有資料庫),每次更改值時都需要更新索引。從事務的角度來看,這些索引是不同的資料庫物件:例如,如果沒有事務隔離,記錄可能出現在一個索引中但不在另一個索引中,因為對第二個索引的更新尚未發生(參見“分片和二級索引”)。
這些應用程式仍然可以在沒有事務的情況下實現。然而,沒有原子性的錯誤處理變得更加複雜,缺乏隔離性可能導致併發問題。我們將在“弱隔離級別”中討論這些問題,並在[待補充連結]中探索替代方法。
處理錯誤和中止
事務的一個關鍵特性是,如果發生錯誤,它可以被中止並安全地重試。ACID 資料庫基於這樣的哲學:如果資料庫有違反其原子性、隔離性或永續性保證的危險,它寧願完全放棄事務,也不允許它保持半完成狀態。
然而,並非所有系統都遵循這種哲學。特別是,具有無領導者複製的資料儲存(參見“無領導者複製”)更多地基於"盡力而為"的基礎工作,可以總結為"資料庫將盡其所能,如果遇到錯誤,它不會撤消已經完成的操作”——因此,從錯誤中恢復是應用程式的責任。
錯誤不可避免地會發生,但許多軟體開發人員更願意只考慮快樂路徑,而不是錯誤處理的複雜性。例如,流行的物件關係對映(ORM)框架,如 Rails 的 ActiveRecord 和 Django,不會重試中止的事務——錯誤通常導致異常冒泡到堆疊中,因此任何使用者輸入都被丟棄,使用者收到錯誤訊息。這是一種遺憾,因為中止的全部意義是啟用安全重試。
儘管重試中止的事務是一種簡單有效的錯誤處理機制,但它並不完美:
- 如果事務實際上成功了,但在伺服器嘗試向客戶端確認成功提交時網路中斷(因此從客戶端的角度來看超時),那麼重試事務會導致它被執行兩次——除非你有額外的應用程式級去重機制。
- 如果錯誤是由於過載或併發事務之間的高爭用,重試事務會使問題變得更糟,而不是更好。為了避免這種反饋迴圈,你可以限制重試次數,使用指數退避,並以不同的方式處理與過載相關的錯誤與其他錯誤(參見“當過載系統無法恢復時”)。
- 僅在瞬態錯誤後重試才值得(例如,由於死鎖、隔離違規、臨時網路中斷和故障轉移);在永久錯誤後(例如,約束違規)重試將毫無意義。
- 如果事務在資料庫之外也有副作用,即使事務被中止,這些副作用也可能發生。例如,如果你正在傳送電子郵件,你不會希望每次重試事務時都再次傳送電子郵件。如果你想確保幾個不同的系統一起提交或中止,兩階段提交可以提供幫助(我們將在“兩階段提交(2PC)”中討論這個問題)。
- 如果客戶端程序在重試時崩潰,它試圖寫入資料庫的任何資料都會丟失。
弱隔離級別
如果兩個事務不訪問相同的資料,或者都是隻讀的,它們可以安全地並行執行,因為它們互不依賴。僅當一個事務讀取另一個事務併發修改的資料時,或者當兩個事務嘗試同時修改相同的資料時,才會出現併發問題(競態條件)。
併發錯誤很難透過測試發現,因為這些錯誤只有在時機不巧時才會觸發。這種時機問題可能非常罕見,通常難以重現。併發也很難推理,特別是在大型應用程式中,你不一定知道程式碼的其他部分正在訪問資料庫。如果只有一個使用者,應用程式開發就已經夠困難了;有許多併發使用者會讓情況變得更加困難,因為任何資料都可能在任何時候意外地發生變化。
出於這個原因,資料庫長期以來一直試圖透過提供事務隔離來嚮應用程式開發人員隱藏併發問題。理論上,隔離應該讓你的生活更輕鬆,讓你假裝沒有併發發生:可序列化隔離意味著資料庫保證事務具有與序列執行(即一次一個,沒有任何併發)相同的效果。
在實踐中,隔離不幸並不那麼簡單。可序列化隔離有效能成本,許多資料庫不願意支付這個代價10。因此,系統通常使用較弱的隔離級別,這些級別可以防止某些併發問題,但不是全部。這些隔離級別更難理解,它們可能導致微妙的錯誤,但它們在實踐中仍然被使用29。
由弱事務隔離引起的併發錯誤不僅僅是理論問題。它們已經導致了鉅額資金損失30 31 32,引發了金融審計師的調查33,並導致客戶資料損壞34。對此類問題披露的一個流行評論是"如果你正在處理金融資料,請使用 ACID 資料庫!"——但這沒有抓住重點。即使許多流行的關係資料庫系統(通常被認為是"ACID")使用弱隔離,因此它們不一定能防止這些錯誤發生。
Note
順便說一句,銀行系統的大部分依賴於透過安全 FTP 交換的文字檔案35。在這種情況下,擁有審計跟蹤和一些人為級別的欺詐預防措施實際上比 ACID 屬性更重要。
這些例子還強調了一個重要觀點:即使併發問題在正常操作中很少見,你也必須考慮攻擊者故意向你的 API 傳送大量高度併發請求以故意利用併發錯誤的可能性30。因此,為了構建可靠和安全的應用程式,你必須確保系統地防止此類錯誤。
在本節中,我們將研究實踐中使用的幾種弱(非可序列化)隔離級別,並詳細討論哪些競態條件可以發生和不能發生,以便你可以決定哪個級別適合你的應用程式。完成後,我們將詳細討論可序列化(參見“可序列化”)。我們對隔離級別的討論將是非正式的,使用示例。如果你想要嚴格的定義和對其屬性的分析,你可以在學術文獻中找到它們36 37 38 39。
讀已提交
最基本的事務隔離級別是讀已提交。它提供兩個保證:
- 從資料庫讀取時,你只會看到已經提交的資料(沒有髒讀)。
- 寫入資料庫時,你只會覆蓋已經提交的資料(沒有髒寫)。
某些資料庫支援更弱的隔離級別,稱為讀未提交。它防止髒寫,但不防止髒讀。讓我們更詳細地討論這兩個保證。
沒有髒讀
想象一個事務已經向資料庫寫入了一些資料,但事務尚未提交或中止。另一個事務能看到那個未提交的資料嗎?如果能,這稱為髒讀3。
在讀已提交隔離級別下執行的事務必須防止髒讀。這意味著事務的任何寫入只有在該事務提交時才對其他人可見(然後它的所有寫入立即變得可見)。這在圖 8-4 中說明,其中使用者 1 已設定 x = 3,但使用者 2 的 get x 仍返回舊值 2,因為使用者 1 尚未提交。

有幾個原因說明為什麼防止髒讀是有用的:
- 如果事務需要更新多行,髒讀意味著另一個事務可能看到某些更新但不是其他更新。例如,在圖 8-2 中,使用者看到新的未讀電子郵件但沒有看到更新的計數器。這是電子郵件的髒讀。看到資料庫處於部分更新狀態會讓使用者感到困惑,並可能導致其他事務做出錯誤的決定。
- 如果事務中止,它所做的任何寫入都需要回滾(如圖 8-3)。如果資料庫允許髒讀,這意味著事務可能看到後來被回滾的資料——即從未實際提交到資料庫的資料。任何讀取未提交資料的事務也需要被中止,導致稱為級聯中止的問題。
沒有髒寫
如果兩個事務併發嘗試更新資料庫中的同一行會發生什麼?我們不知道寫入將以什麼順序發生,但我們通常假設後面的寫入會覆蓋前面的寫入。
然而,如果前面的寫入是尚未提交的事務的一部分,因此後面的寫入覆蓋了一個未提交的值,會發生什麼?這稱為髒寫36。在讀已提交隔離級別下執行的事務必須防止髒寫,通常透過延遲第二個寫入直到第一個寫入的事務已提交或中止。
透過防止髒寫,這個隔離級別避免了某些型別的併發問題:
- 如果事務更新多行,髒寫可能導致糟糕的結果。例如,考慮圖 8-5,它說明了一個二手車銷售網站,兩個人 Aaliyah 和 Bryce 同時嘗試購買同一輛車。購買汽車需要兩次資料庫寫入:網站上的列表需要更新以反映買家,銷售發票需要傳送給買家。在圖 8-5 的情況下,銷售被授予 Bryce(因為他對
listings
表執行了獲勝的更新),但發票被傳送給 Aaliyah(因為她對invoices
表執行了獲勝的更新)。讀已提交防止了這種事故。 - 然而,讀已提交不防止圖 8-1 中兩個計數器遞增之間的競態條件。在這種情況下,第二個寫入發生在第一個事務提交之後,所以它不是髒寫。它仍然是不正確的,但原因不同——在“防止丟失更新”中,我們將討論如何使此類計數器遞增安全。

實現讀已提交
讀已提交是一個非常流行的隔離級別。它是 Oracle Database、PostgreSQL、SQL Server 和許多其他資料庫中的預設設定10。
最常見的是,資料庫透過使用行級鎖來防止髒寫:當事務想要修改特定行(或文件或其他物件)時,它必須首先獲取該行的鎖。然後它必須持有該鎖直到事務提交或中止。任何給定行只能有一個事務持有鎖;如果另一個事務想要寫入同一行,它必須等到第一個事務提交或中止後才能獲取鎖並繼續。這種鎖定由資料庫在讀已提交模式(或更強的隔離級別)下自動完成。
我們如何防止髒讀?一種選擇是使用相同的鎖,並要求任何想要讀取行的事務短暫地獲取鎖,然後在讀取後立即再次釋放它。這將確保在行具有髒的、未提交的值時無法進行讀取(因為在此期間鎖將由進行寫入的事務持有)。
然而,要求讀鎖的方法在實踐中效果不佳,因為一個長時間執行的寫事務可以強制許多其他事務等待,直到長時間執行的事務完成,即使其他事務只讀取並且不向資料庫寫入任何內容。這會損害只讀事務的響應時間,並且對可操作性不利:應用程式一個部分的減速可能會由於等待鎖而在應用程式的完全不同部分產生連鎖效應。
儘管如此,在某些資料庫中使用鎖來防止髒讀,例如 IBM Db2 和 Microsoft SQL Server 在 read_committed_snapshot=off
設定中29。
防止髒讀的更常用方法是圖 8-4 中說明的方法:對於每個被寫入的行,資料庫記住舊的已提交值和當前持有寫鎖的事務設定的新值。當事務正在進行時,任何其他讀取該行的事務都只是被給予舊值。只有當新值被提交時,事務才會切換到讀取新值(有關更多詳細資訊,請參見“多版本併發控制(MVCC)”)。
快照隔離與可重複讀
如果你膚淺地看待讀已提交隔離,你可能會被原諒認為它做了事務需要做的一切:它允許中止(原子性所需),它防止讀取事務的不完整結果,並且它防止併發寫入混淆。確實,這些是有用的功能,比沒有事務的系統能獲得的保證要強得多。
然而,使用這個隔離級別時,仍然有很多方式可能出現併發錯誤。例如,圖 8-6 說明了讀已提交可能發生的問題。

假設 Aaliyah 在銀行有 1,000 美元的儲蓄,分成兩個賬戶,每個 500 美元。現在一筆事務從她的一個賬戶轉賬 100 美元到另一個賬戶。如果她不幸在該事務處理的同時檢視她的賬戶餘額列表,她可能會看到一個賬戶餘額在收款到達之前(餘額為 500 美元),另一個賬戶在轉出之後(新余額為 400 美元)。對 Aaliyah 來說,現在她的賬戶總共只有 900 美元——似乎 100 美元憑空消失了。
這種異常稱為讀偏斜,它是不可重複讀的一個例子:如果 Aaliyah 在事務結束時再次讀取賬戶 1 的餘額,她會看到與之前查詢中看到的不同的值(600 美元)。讀偏斜在讀已提交隔離下被認為是可接受的:Aaliyah 看到的賬戶餘額確實是在她讀取它們時已提交的。
Note
術語偏斜不幸地被過載了:我們之前在具有熱點的不平衡工作負載的意義上使用它(參見“傾斜負載和緩解熱點”),而這裡它意味著時序異常。
在 Aaliyah 的情況下,這不是一個持久的問題,因為如果她幾秒鐘後重新載入線上銀行網站,她很可能會看到一致的賬戶餘額。然而,某些情況不能容忍這種臨時的不一致性:
- 備份
- 進行備份需要複製整個資料庫,對於大型資料庫可能需要幾個小時。在備份過程執行期間,寫入將繼續對資料庫進行。因此,你最終可能會得到備份的某些部分包含較舊版本的資料,而其他部分包含較新版本。如果你需要從這樣的備份恢復,不一致性(如消失的錢)將變成永久性的。
- 分析查詢和完整性檢查
- 有時,你可能想要執行掃描資料庫大部分的查詢。此類查詢在分析中很常見(參見“分析與運營系統”),或者可能是定期完整性檢查的一部分,以確保一切正常(監控資料損壞)。如果這些查詢在不同時間點觀察資料庫的不同部分,它們很可能返回無意義的結果。
快照隔離36 是解決這個問題的最常見方法。其思想是每個事務從資料庫的一致快照讀取——也就是說,事務看到事務開始時資料庫中已提交的所有資料。即使資料隨後被另一個事務更改,每個事務也只能看到該特定時間點的舊資料。
快照隔離對於長時間執行的只讀查詢(如備份和分析)來說是一個福音。如果查詢操作的資料在查詢執行的同時發生變化,很難推理查詢的含義。當事務可以看到資料庫的一致快照(凍結在特定時間點)時,理解起來就容易得多。
快照隔離是一個流行的功能:它的變體受到 PostgreSQL、使用 InnoDB 儲存引擎的 MySQL、Oracle、SQL Server 等的支援,儘管詳細行為因系統而異29 40 41。某些資料庫,如 Oracle、TiDB 和 Aurora DSQL,甚至選擇快照隔離作為它們的最高隔離級別。
多版本併發控制(MVCC)
與讀已提交隔離一樣,快照隔離的實現通常使用寫鎖來防止髒寫(參見“實現讀已提交”),這意味著進行寫入的事務可以阻止寫入同一行的另一個事務的進度。但是,讀取不需要任何鎖。從效能的角度來看,快照隔離的一個關鍵原則是讀者永遠不會阻塞寫者,寫者永遠不會阻塞讀者。這允許資料庫在一致快照上處理長時間執行的讀查詢,同時正常處理寫入,兩者之間沒有任何鎖爭用。
為了實現快照隔離,資料庫使用了我們在圖 8-4 中看到的防止髒讀機制的泛化。資料庫必須潛在地保留每行的幾個不同的已提交版本,而不是每行的兩個版本(已提交版本和被覆蓋但尚未提交的版本),因為各種正在進行的事務可能需要在不同時間點看到資料庫的狀態。因為它並排維護一行的多個版本,所以這種技術被稱為多版本併發控制(MVCC)。
圖 8-7 說明了 PostgreSQL 中如何實現基於 MVCC 的快照隔離40 42 43(其他實現類似)。當事務啟動時,它被賦予一個唯一的、始終遞增的事務 ID(txid
)。每當事務向資料庫寫入任何內容時,它寫入的資料都用寫入者的事務 ID 標記。(準確地說,PostgreSQL 中的事務 ID 是 32 位整數,因此它們在大約 40 億個事務後溢位。清理過程執行清理以確保溢位不會影響資料。)

表中的每一行都有一個 inserted_by
欄位,包含將此行插入表中的事務的 ID。此外,每行都有一個 deleted_by
欄位,最初為空。如果事務刪除一行,該行實際上不會從資料庫中刪除,而是透過將 deleted_by
欄位設定為請求刪除的事務的 ID 來標記為刪除。在稍後的某個時間,當確定沒有事務可以再訪問已刪除的資料時,資料庫中的垃圾收集過程會刪除任何標記為刪除的行並釋放它們的空間。
更新在內部被轉換為刪除和插入44。例如,在圖 8-7 中,事務 13 從賬戶 2 中扣除 100 美元,將餘額從 500 美元更改為 400 美元。accounts
表現在實際上包含賬戶 2 的兩行:餘額為 500 美元的行被事務 13 標記為已刪除,餘額為 400 美元的行由事務 13 插入。
行的所有版本都儲存在同一個資料庫堆中(參見“在索引中儲存值”),無論寫入它們的事務是否已提交。同一行的版本形成一個連結串列,從最新版本到最舊版本或相反,以便查詢可以在內部迭代行的所有版本45 46。
觀察一致快照的可見性規則
當事務從資料庫讀取時,事務 ID 用於決定它可以看到哪些行版本以及哪些是不可見的。透過仔細定義可見性規則,資料庫可以嚮應用程式呈現資料庫的一致快照。這大致如下工作43:
- 在每個事務開始時,資料庫列出當時正在進行(尚未提交或中止)的所有其他事務。這些事務所做的任何寫入都被忽略,即使事務隨後提交。這確保我們看到一個不受另一個事務提交影響的一致快照。
- 具有較晚事務 ID(即在當前事務開始後開始,因此不包括在正在進行的事務列表中)的事務所做的任何寫入都被忽略,無論這些事務是否已提交。
- 中止事務所做的任何寫入都被忽略,無論該中止何時發生。這樣做的好處是,當事務中止時,我們不需要立即從儲存中刪除它寫入的行,因為可見性規則會將它們過濾掉。垃圾收集過程可以稍後刪除它們。
- 所有其他寫入對應用程式的查詢可見。
這些規則適用於行的插入和刪除。在圖 8-7 中,當事務 12 從賬戶 2 讀取時,它看到 500 美元的餘額,因為 500 美元餘額的刪除是由事務 13 進行的(根據規則 2,事務 12 無法看到事務 13 進行的刪除),而 400 美元餘額的插入尚不可見(根據相同的規則)。
換句話說,如果以下兩個條件都為真,則行是可見的:
- 在讀者事務開始時,插入該行的事務已經提交。
- 該行未標記為刪除,或者如果是,請求刪除的事務在讀者事務開始時尚未提交。
長時間執行的事務可能會長時間繼續使用快照,繼續讀取(從其他事務的角度來看)早已被覆蓋或刪除的值。透過永遠不更新原地的值,而是在每次更改值時插入新版本,資料庫可以提供一致的快照,同時只產生很小的開銷。
索引與快照隔離
索引如何在多版本資料庫中工作?最常見的方法是每個索引條目指向與該條目匹配的行的一個版本(最舊或最新版本)。每個行版本可能包含對下一個最舊或下一個最新版本的引用。使用索引的查詢必須迭代行以找到可見的行,並且值與查詢要查詢的內容匹配。當垃圾收集刪除不再對任何事務可見的舊行版本時,相應的索引條目也可以被刪除。
許多實現細節影響多版本併發控制的效能45 46。例如,如果同一行的不同版本可以適合同一頁面,PostgreSQL 有避免索引更新的最佳化40。其他一些資料庫避免儲存修改行的完整副本,而只儲存版本之間的差異以節省空間。
CouchDB、Datomic 和 LMDB 使用另一種方法。儘管它們也使用 B 樹(參見“B 樹”),但它們使用不可變(寫時複製)變體,在更新時不會覆蓋樹的頁面,而是建立每個修改頁面的新副本。父頁面,直到樹的根,被複制並更新以指向其子頁面的新版本。任何不受寫入影響的頁面都不需要複製,並且可以與新樹共享47。
使用不可變 B 樹,每個寫事務(或事務批次)都會建立一個新的 B 樹根,特定的根是建立時資料庫的一致快照。不需要基於事務 ID 過濾行,因為後續寫入無法修改現有的 B 樹;它們只能建立新的樹根。這種方法還需要後臺程序進行壓縮和垃圾收集。
快照隔離、可重複讀和命名混淆
MVCC 是資料庫常用的實現技術,通常用於實現快照隔離。然而,不同的資料庫有時使用不同的術語來指代同一件事:例如,快照隔離在 PostgreSQL 中稱為"可重複讀",在 Oracle 中稱為"可序列化"29。有時不同的系統使用相同的術語來表示不同的東西:例如,雖然在 PostgreSQL 中"可重複讀"意味著快照隔離,但在 MySQL 中它意味著比快照隔離更弱一致性的 MVCC 實現41。
這種命名混淆的原因是 SQL 標準沒有快照隔離的概念,因為該標準基於 System R 1975 年的隔離級別定義3,而快照隔離當時還沒有被髮明。相反,它定義了可重複讀,表面上看起來類似於快照隔離。PostgreSQL 將其快照隔離級別稱為"可重複讀",因為它符合標準的要求,因此他們可以聲稱符合標準。
不幸的是,SQL 標準對隔離級別的定義是有缺陷的——它是模糊的、不精確的,並且不像標準應該的那樣獨立於實現36。即使幾個資料庫實現了可重複讀,它們實際提供的保證也有很大差異,儘管表面上是標準化的29。研究文獻中有可重複讀的正式定義37 38,但大多數實現不滿足該正式定義。最重要的是,IBM Db2 使用"可重複讀"來指代可序列化10。
因此,沒有人真正知道可重複讀意味著什麼。
防止丟失更新
到目前為止,我們討論的讀已提交和快照隔離級別主要是關於只讀事務在併發寫入存在的情況下可以看到什麼的保證。我們大多忽略了兩個事務併發寫入的問題——我們只討論了髒寫(參見“沒有髒寫”),這是可能發生的一種特定型別的寫-寫衝突。
併發寫入事務之間還可能發生其他幾種有趣的衝突。其中最著名的是丟失更新問題,在圖 8-1 中以兩個併發計數器遞增的例子說明。
如果應用程式從資料庫讀取某個值,修改它,然後寫回修改後的值(讀-修改-寫迴圈),就會出現丟失更新問題。如果兩個事務併發執行此操作,其中一個修改可能會丟失,因為第二個寫入不包括第一個修改。(我們有時說後面的寫入覆蓋了前面的寫入。)這種模式出現在各種不同的場景中:
- 遞增計數器或更新賬戶餘額(需要讀取當前值,計算新值,並寫回更新的值)
- 對複雜值進行本地更改,例如,向 JSON 文件中的列表新增元素(需要解析文件,進行更改,並寫回修改後的文件)
- 兩個使用者同時編輯 wiki 頁面,每個使用者透過將整個頁面內容傳送到伺服器來儲存他們的更改,覆蓋資料庫中當前的任何內容
因為這是一個如此常見的問題,已經開發了各種解決方案48。
原子寫操作
許多資料庫提供原子更新操作,消除了在應用程式程式碼中實現讀-修改-寫迴圈的需要。如果你的程式碼可以用這些操作來表達,它們通常是最好的解決方案。例如,以下指令在大多數關係資料庫中是併發安全的:
UPDATE counters SET value = value + 1 WHERE key = 'foo';
類似地,文件資料庫(如 MongoDB)提供原子操作來對 JSON 文件的一部分進行本地修改,Redis 提供原子操作來修改資料結構(如優先順序佇列)。並非所有寫入都可以輕鬆地用原子操作來表達——例如,對 wiki 頁面的更新涉及任意文字編輯,可以使用“CRDT 和操作轉換”中討論的演算法來處理——但在可以使用原子操作的情況下,它們通常是最佳選擇。
原子操作通常透過在讀取物件時對其進行獨佔鎖來實現,以便在應用更新之前沒有其他事務可以讀取它。另一種選擇是簡單地強制所有原子操作在單個執行緒上執行。
不幸的是,物件關係對映(ORM)框架很容易意外地編寫執行不安全的讀-修改-寫迴圈的程式碼,而不是使用資料庫提供的原子操作49 50 51。這可能是難以透過測試發現的微妙錯誤的來源。
顯式鎖定
如果資料庫的內建原子操作不提供必要的功能,另一個防止丟失更新的選項是應用程式顯式鎖定要更新的物件。然後應用程式可以執行讀-修改-寫迴圈,如果任何其他事務嘗試併發更新或鎖定同一物件,它將被迫等到第一個讀-修改-寫迴圈完成。
例如,考慮一個多人遊戲,其中幾個玩家可以同時移動同一個棋子。在這種情況下,原子操作可能不夠,因為應用程式還需要確保玩家的移動遵守遊戲規則,這涉及一些你無法合理地作為資料庫查詢實現的邏輯。相反,你可以使用鎖來防止兩個玩家同時移動同一個棋子,如例 8-1 所示。
例 8-1. 顯式鎖定行以防止丟失更新
BEGIN TRANSACTION;
SELECT * FROM figures
WHERE name = 'robot' AND game_id = 222
FOR UPDATE; ❶
-- 檢查移動是否有效,然後更新
-- 前一個 SELECT 返回的棋子的位置。
UPDATE figures SET position = 'c4' WHERE id = 1234;
COMMIT;
❶:FOR UPDATE
子句表示資料庫應該對此查詢返回的所有行進行鎖定。
這是有效的,但要正確執行,你需要仔細考慮你的應用程式邏輯。很容易忘記在程式碼中的某個地方新增必要的鎖,從而引入競態條件。
此外,如果你鎖定多個物件,則存在死鎖的風險,其中兩個或多個事務正在等待彼此釋放鎖。許多資料庫會自動檢測死鎖,並中止涉及的事務之一,以便系統可以取得進展。你可以在應用程式級別透過重試中止的事務來處理這種情況。
自動檢測丟失的更新
原子操作和鎖是透過強制讀-修改-寫迴圈按順序發生來防止丟失更新的方法。另一種選擇是允許它們並行執行,如果事務管理器檢測到丟失的更新,則中止事務並強制它重試其讀-修改-寫迴圈。
這種方法的一個優點是資料庫可以與快照隔離一起有效地執行此檢查。實際上,PostgreSQL 的可重複讀、Oracle 的可序列化和 SQL Server 的快照隔離級別會自動檢測何時發生丟失的更新並中止有問題的事務。然而,MySQL/InnoDB 的可重複讀不檢測丟失的更新29 41。一些作者36 38 認為資料庫必須防止丟失的更新才能提供快照隔離,因此根據這個定義,MySQL 不提供快照隔離。
丟失更新檢測是一個很好的功能,因為它不需要應用程式程式碼使用任何特殊的資料庫功能——你可能忘記使用鎖或原子操作從而引入錯誤,但丟失更新檢測會自動發生,因此不太容易出錯。但是,你還必須在應用程式級別重試中止的事務。
條件寫入(比較並設定)
在不提供事務的資料庫中,你有時會發現一個條件寫入操作,它可以透過僅在值自你上次讀取以來未更改時才允許更新來防止丟失的更新(之前在“單物件寫入”中提到)。如果當前值與你之前讀取的不匹配,則更新無效,必須重試讀-修改-寫迴圈。它是許多 CPU 支援的原子比較並設定或比較並交換(CAS)指令的資料庫等價物。
例如,為了防止兩個使用者同時更新同一個 wiki 頁面,你可以嘗試類似這樣的操作,期望僅當頁面內容自使用者開始編輯以來沒有更改時才進行更新:
-- 這可能安全也可能不安全,取決於資料庫實現
UPDATE wiki_pages SET content = 'new content'
WHERE id = 1234 AND content = 'old content';
如果內容已更改並且不再匹配 'old content'
,則此更新將無效,因此你需要檢查更新是否生效並在必要時重試。你也可以使用在每次更新時遞增的版本號列,並且僅在當前版本號未更改時才應用更新,而不是比較完整內容。這種方法有時稱為樂觀鎖定52。
請注意,如果另一個事務併發修改了 content
,則根據 MVCC 可見性規則,新內容可能不可見(參見“觀察一致快照的可見性規則”)。MVCC 的許多實現對此場景有可見性規則的例外,其中其他事務寫入的值對 UPDATE
和 DELETE
查詢的 WHERE
子句的評估可見,即使這些寫入在快照中不可見。
衝突解決與複製
在複製資料庫中(參見第 6 章),防止丟失的更新具有另一個維度:由於它們在多個節點上有資料副本,並且資料可能在不同節點上併發修改,因此需要採取一些額外的步驟來防止丟失的更新。
鎖和條件寫入操作假設有一個最新的資料副本。然而,具有多領導者或無領導者複製的資料庫通常允許多個寫入併發發生並非同步複製它們,因此它們不能保證有一個最新的資料副本。因此,基於鎖或條件寫入的技術在此上下文中不適用。(我們將在“線性一致性”中更詳細地重新討論這個問題。)
相反,如“處理衝突寫入”中所討論的,此類複製資料庫中的常見方法是允許併發寫入建立值的多個衝突版本(也稱為兄弟節點),並使用應用程式程式碼或特殊資料結構在事後解決和合並這些版本。
如果更新是可交換的(即,你可以在不同副本上以不同順序應用它們,仍然得到相同的結果),合併衝突值可以防止丟失的更新。例如,遞增計數器或向集合新增元素是可交換操作。這就是 CRDT 背後的想法,我們在“CRDT 和操作轉換”中遇到過。然而,某些操作(如條件寫入)不能成為可交換的。
另一方面,最後寫入獲勝(LWW)衝突解決方法容易丟失更新,如“最後寫入獲勝(丟棄併發寫入)”中所討論的。不幸的是,LWW 是許多複製資料庫中的預設值。
寫偏斜與幻讀
在前面的部分中,我們看到了髒寫和丟失更新,這是當不同事務併發嘗試寫入相同物件時可能發生的兩種競態條件。為了避免資料損壞,需要防止這些競態條件——要麼由資料庫自動防止,要麼透過使用鎖或原子寫操作等手動保護措施。
然而,這並不是併發寫入之間可能發生的潛在競態條件列表的結尾。在本節中,我們將看到一些更微妙的衝突示例。
首先,想象這個例子:你正在為醫生編寫一個應用程式來管理他們在醫院的值班班次。醫院通常試圖在任何時候都有幾位醫生值班,但絕對必須至少有一位醫生值班。醫生可以放棄他們的班次(例如,如果他們自己生病了),前提是該班次中至少有一位同事留在值班53 54。
現在想象 Aaliyah 和 Bryce 是特定班次的兩位值班醫生。兩人都感覺不舒服,所以他們都決定請假。不幸的是,他們碰巧大約在同一時間點選了下班的按鈕。接下來發生的事情如圖 8-8 所示。

在每個事務中,你的應用程式首先檢查當前是否有兩個或更多醫生在值班;如果是,它假設一個醫生下班是安全的。由於資料庫使用快照隔離,兩個檢查都返回 2
,因此兩個事務都繼續到下一階段。Aaliyah 更新她自己的記錄讓自己下班,Bryce 同樣更新他自己的記錄。兩個事務都提交,現在沒有醫生值班。你至少有一個醫生值班的要求被違反了。
描述寫偏斜
這種異常稱為寫偏斜36。它既不是髒寫也不是丟失的更新,因為兩個事務正在更新兩個不同的物件(分別是 Aaliyah 和 Bryce 的值班記錄)。這裡發生衝突不太明顯,但這絕對是一個競態條件:如果兩個事務一個接一個地執行,第二個醫生將被阻止下班。異常行為只有在事務併發執行時才可能。
你可以將寫偏斜視為丟失更新問題的概括。如果兩個事務讀取相同的物件,然後更新其中一些物件(不同的事務可能更新不同的物件),就會發生寫偏斜。在不同事務更新同一物件的特殊情況下,你會得到髒寫或丟失更新異常(取決於時機)。
我們看到有各種不同的方法可以防止丟失的更新。對於寫偏斜,我們的選擇更受限制:
- 原子單物件操作沒有幫助,因為涉及多個物件。
- 不幸的是,你在某些快照隔離實現中發現的丟失更新的自動檢測也沒有幫助:寫偏斜在 PostgreSQL 的可重複讀、MySQL/InnoDB 的可重複讀、Oracle 的可序列化或 SQL Server 的快照隔離級別中不會自動檢測到29。自動防止寫偏斜需要真正的可序列化隔離(參見“可序列化”)。
- 某些資料庫允許你配置約束,然後由資料庫強制執行(例如,唯一性、外部索引鍵約束或對特定值的限制)。但是,為了指定至少有一個醫生必須值班,你需要一個涉及多個物件的約束。大多數資料庫沒有對此類約束的內建支援,但你可能能夠使用觸發器或物化檢視實現它們,如“一致性”中所討論的12。
- 如果你不能使用可序列化隔離級別,在這種情況下,第二好的選擇可能是顯式鎖定事務所依賴的行。在醫生示例中,你可以編寫如下內容:
BEGIN TRANSACTION;
SELECT * FROM doctors
WHERE on_call = true
AND shift_id = 1234 FOR UPDATE; ❶
UPDATE doctors
SET on_call = false
WHERE name = 'Aaliyah'
AND shift_id = 1234;
COMMIT;
❶:和以前一樣,FOR UPDATE
告訴資料庫鎖定此查詢返回的所有行。
更多寫偏斜的例子
寫偏斜起初可能看起來是一個深奧的問題,但一旦你意識到它,你可能會注意到更多可能發生的情況。以下是更多示例:
- 會議室預訂系統
- 假設你想強制同一會議室在同一時間不能有兩個預訂55。當有人想要預訂時,你首先檢查是否有任何衝突的預訂(即,具有重疊時間範圍的同一房間的預訂),如果沒有找到,你就建立會議(參見例 8-2)。
例 8-2. 會議室預訂系統試圖避免重複預訂(在快照隔離下不安全)
BEGIN TRANSACTION; -- 檢查是否有任何現有預訂與中午 12 點到 1 點的時間段重疊 SELECT COUNT(*) FROM bookings WHERE room_id = 123 AND end_time > '2025-01-01 12:00' AND start_time < '2025-01-01 13:00'; -- 如果前一個查詢返回零: INSERT INTO bookings (room_id, start_time, end_time, user_id) VALUES (123, '2025-01-01 12:00', '2025-01-01 13:00', 666); COMMIT;
不幸的是,快照隔離不會阻止另一個使用者併發插入衝突的會議。為了保證你不會出現排程衝突,你再次需要可序列化隔離。
- 多人遊戲
- 在例 8-1 中,我們使用鎖來防止丟失的更新(即,確保兩個玩家不能同時移動同一個棋子)。但是,鎖不會阻止玩家將兩個不同的棋子移動到棋盤上的同一位置,或者可能做出違反遊戲規則的其他移動。根據你要執行的規則型別,你可能能夠使用唯一約束,但否則你很容易受到寫偏斜的影響。
- 宣告使用者名稱
- 在每個使用者都有唯一使用者名稱的網站上,兩個使用者可能同時嘗試使用相同的使用者名稱建立賬戶。你可以使用事務來檢查名稱是否被佔用,如果沒有,使用該名稱建立賬戶。但是,就像前面的例子一樣,這在快照隔離下是不安全的。幸運的是,唯一約束在這裡是一個簡單的解決方案(嘗試註冊使用者名稱的第二個事務將由於違反約束而被中止)。
- 防止重複消費
- 允許使用者花錢或積分的服務需要檢查使用者不會花費超過他們擁有的。你可以透過在使用者賬戶中插入暫定支出專案,列出賬戶中的所有專案,並檢查總和是否為正來實現這一點。有了寫偏斜,可能會發生兩個支出專案併發插入,它們一起導致餘額變為負數,但沒有任何事務注意到另一個。
導致寫偏斜的幻讀
所有這些例子都遵循類似的模式:
SELECT
查詢透過搜尋匹配某些搜尋條件的行來檢查是否滿足某些要求(至少有兩個醫生值班,該房間在該時間沒有現有預訂,棋盤上的位置還沒有另一個棋子,使用者名稱尚未被佔用,賬戶中仍有錢)。- 根據第一個查詢的結果,應用程式程式碼決定如何繼續(也許繼續操作,或者向用戶報告錯誤並中止)。
- 如果應用程式決定繼續,它會向資料庫進行寫入(
INSERT
、UPDATE
或DELETE
)並提交事務。
此寫入的效果改變了步驟 2 決策的前提條件。換句話說,如果你在提交寫入後重復步驟 1 的 SELECT
查詢,你會得到不同的結果,因為寫入改變了匹配搜尋條件的行集(現在少了一個醫生值班,會議室現在已為該時間預訂,棋盤上的位置現在被移動的棋子佔據,使用者名稱現在被佔用,賬戶中的錢現在更少)。
步驟可能以不同的順序發生。例如,你可以先進行寫入,然後進行 SELECT
查詢,最後根據查詢結果決定是中止還是提交。
在醫生值班示例的情況下,步驟 3 中被修改的行是步驟 1 中返回的行之一,因此我們可以透過鎖定步驟 1 中的行(SELECT FOR UPDATE
)來使事務安全並避免寫偏斜。但是,其他四個示例是不同的:它們檢查不存在匹配某些搜尋條件的行,而寫入新增了匹配相同條件的行。如果步驟 1 中的查詢不返回任何行,SELECT FOR UPDATE
就無法附加鎖56。
這種效果,其中一個事務中的寫入改變另一個事務中搜索查詢的結果,稱為幻讀4。快照隔離避免了只讀查詢中的幻讀,但在我們討論的讀寫事務中,幻讀可能導致特別棘手的寫偏斜情況。ORM 生成的 SQL 也容易出現寫偏斜50 51。
物化衝突
如果幻讀的問題是沒有物件可以附加鎖,也許我們可以在資料庫中人為地引入一個鎖物件?
例如,在會議室預訂情況下,你可以想象建立一個時間段和房間的表。此表中的每一行對應於特定時間段(例如,15 分鐘)的特定房間。你提前為所有可能的房間和時間段組合建立行,例如,接下來的六個月。
現在,想要建立預訂的事務可以鎖定(SELECT FOR UPDATE
)表中對應於所需房間和時間段的行。獲取鎖後,它可以像以前一樣檢查重疊的預訂並插入新的預訂。請注意,附加表不用於儲存有關預訂的資訊——它純粹是一組鎖,用於防止同一房間和時間範圍的預訂被併發修改。
這種方法稱為物化衝突,因為它採用了幻讀並將其轉化為存在於資料庫中的具體行集上的鎖衝突14。不幸的是,很難且容易出錯地弄清楚如何物化衝突,並且讓併發控制機制洩漏到應用程式資料模型中是醜陋的。出於這些原因,如果沒有其他選擇,物化衝突應被視為最後的手段。在大多數情況下,可序列化隔離級別要好得多。
可序列化
在本章中,我們已經看到了幾個容易出現競態條件的事務示例。某些競態條件被讀已提交和快照隔離級別所防止,但其他的則沒有。我們遇到了一些特別棘手的寫偏斜和幻讀示例。這是一個令人沮喪的情況:
- 隔離級別很難理解,並且在不同資料庫中的實現不一致(例如,“可重複讀"的含義差異很大)。
- 如果你檢視你的應用程式程式碼,很難判斷在特定隔離級別下執行是否安全——特別是在大型應用程式中,你可能不知道所有可能併發發生的事情。
- 沒有好的工具來幫助我們檢測競態條件。原則上,靜態分析可能有所幫助33,但研究技術尚未進入實際使用。測試併發問題很困難,因為它們通常是非確定性的——只有在時機不巧時才會出現問題。
這不是一個新問題——自 1970 年代引入弱隔離級別以來一直如此3。一直以來,研究人員的答案都很簡單:使用可序列化隔離!
可序列化隔離是最強的隔離級別。它保證即使事務可能並行執行,最終結果與它們序列執行(一次一個,沒有任何併發)相同。因此,資料庫保證如果事務在單獨執行時行為正確,那麼在併發執行時它們繼續保持正確——換句話說,資料庫防止了所有可能的競態條件。
但如果可序列化隔離比弱隔離級別的混亂要好得多,那為什麼不是每個人都在使用它?要回答這個問題,我們需要檢視實現可序列化的選項,以及它們的效能如何。今天提供可序列化的大多數資料庫使用以下三種技術之一,我們將在本章的其餘部分探討:
- 字面上序列執行事務(參見“實際序列執行”)
- 兩階段鎖定(參見“兩階段鎖定(2PL)”),幾十年來這是唯一可行的選擇
- 樂觀併發控制技術,如可序列化快照隔離(參見“可序列化快照隔離(SSI)”)
實際序列執行
避免併發問題的最簡單方法是完全消除併發:在單個執行緒上按序列順序一次執行一個事務。透過這樣做,我們完全迴避了檢測和防止事務之間衝突的問題:所產生的隔離根據定義是可序列化的。
儘管這似乎是一個顯而易見的想法,但直到 2000 年代,資料庫設計者才決定執行事務的單執行緒迴圈是可行的57。如果在過去 30 年中多執行緒併發被認為是獲得良好效能的必要條件,那是什麼改變使得單執行緒執行成為可能?
兩個發展導致了這種重新思考:
- RAM 變得足夠便宜,對於許多用例,現在可以將整個活動資料集儲存在記憶體中(參見“將所有內容儲存在記憶體中”)。當事務需要訪問的所有資料都在記憶體中時,事務的執行速度比必須等待從磁碟載入資料要快得多。
- 資料庫設計者意識到 OLTP 事務通常很短,只進行少量讀寫(參見“分析與運營系統”)。相比之下,長時間執行的分析查詢通常是隻讀的,因此它們可以在序列執行迴圈之外的一致快照上執行(使用快照隔離)。
序列執行事務的方法在 VoltDB/H-Store、Redis 和 Datomic 等中實現58 59 60。為單執行緒執行設計的系統有時可以比支援併發的系統性能更好,因為它可以避免鎖定的協調開銷。但是,其吞吐量限於單個 CPU 核心。為了充分利用該單執行緒,事務需要以不同於傳統形式的方式構建。
將事務封裝在儲存過程中
在資料庫的早期,意圖是資料庫事務可以包含整個使用者活動流程。例如,預訂機票是一個多階段過程(搜尋路線、票價和可用座位;決定行程;預訂行程中每個航班的座位;輸入乘客詳細資訊;付款)。資料庫設計者認為,如果整個過程是一個事務,以便可以原子地提交,那將是很好的。
不幸的是,人類做決定和響應的速度非常慢。如果資料庫事務需要等待使用者的輸入,資料庫需要支援潛在的大量併發事務,其中大多數是空閒的。大多數資料庫無法有效地做到這一點,因此幾乎所有 OLTP 應用程式都透過避免在事務中互動式地等待使用者來保持事務簡短。在 Web 上,這意味著事務在同一 HTTP 請求中提交——事務不跨越多個請求。新的 HTTP 請求開始新的事務。
即使人類已經從關鍵路徑中移除,事務仍然以互動式客戶端/伺服器風格執行,一次一個語句。應用程式進行查詢,讀取結果,可能根據第一個查詢的結果進行另一個查詢,依此類推。查詢和結果在應用程式程式碼(在一臺機器上執行)和資料庫伺服器(在另一臺機器上)之間來回傳送。
在這種互動式事務風格中,大量時間花在應用程式和資料庫之間的網路通訊上。如果你要在資料庫中禁止併發並一次只處理一個事務,吞吐量將是可怕的,因為資料庫將大部分時間都在等待應用程式為當前事務發出下一個查詢。在這種資料庫中,為了獲得合理的效能,必須併發處理多個事務。
因此,具有單執行緒序列事務處理的系統不允許互動式多語句事務。相反,應用程式必須將自己限制為包含單個語句的事務,或者提前將整個事務程式碼作為儲存過程提交給資料庫61。
互動式事務和儲存過程之間的差異如圖 8-9 所示。前提是事務所需的所有資料都在記憶體中,儲存過程可以非常快速地執行,而無需等待任何網路或磁碟 I/O。

儲存過程的利弊
儲存過程在關係資料庫中已經存在了一段時間,自 1999 年以來一直是 SQL 標準(SQL/PSM)的一部分。它們因各種原因獲得了一些不好的聲譽:
- 傳統上,每個資料庫供應商都有自己的儲存過程語言(Oracle 有 PL/SQL,SQL Server 有 T-SQL,PostgreSQL 有 PL/pgSQL 等)。這些語言沒有跟上通用程式語言的發展,因此從今天的角度來看,它們看起來相當醜陋和過時,並且缺乏大多數程式語言中的庫生態系統。
- 在資料庫中執行的程式碼很難管理:與應用程式伺服器相比,除錯更困難,版本控制和部署更尷尬,測試更棘手,並且難以與監控的指標收集系統整合。
- 資料庫通常比應用程式伺服器對效能更敏感,因為單個數據庫例項通常由許多應用程式伺服器共享。資料庫中編寫不當的儲存過程(例如,使用大量記憶體或 CPU 時間)可能比應用程式伺服器中等效的編寫不當的程式碼造成更多麻煩。
- 在允許租戶編寫自己的儲存過程的多租戶系統中,在與資料庫核心相同的程序中執行不受信任的程式碼是一個安全風險62。
然而,這些問題可以克服。儲存過程的現代實現已經放棄了 PL/SQL,而是使用現有的通用程式語言:VoltDB 使用 Java 或 Groovy,Datomic 使用 Java 或 Clojure,Redis 使用 Lua,MongoDB 使用 Javascript。
儲存過程在應用程式邏輯無法輕鬆嵌入其他地方的情況下也很有用。例如,使用 GraphQL 的應用程式可能透過 GraphQL 代理直接公開其資料庫。如果代理不支援複雜的驗證邏輯,你可以使用儲存過程將此類邏輯直接嵌入資料庫中。如果資料庫不支援儲存過程,你必須在代理和資料庫之間部署驗證服務來進行驗證。
使用儲存過程和記憶體資料,在單個執行緒上執行所有事務變得可行。當儲存過程不需要等待 I/O 並避免其他併發控制機制的開銷時,它們可以在單個執行緒上實現相當好的吞吐量。
VoltDB 還使用儲存過程進行復制:它不是將事務的寫入從一個節點複製到另一個節點,而是在每個副本上執行相同的儲存過程。因此,VoltDB 要求儲存過程是確定性的(在不同節點上執行時,它們必須產生相同的結果)。例如,如果事務需要使用當前日期和時間,它必須透過特殊的確定性 API 來實現(有關確定性操作的更多詳細資訊,請參見“持久執行和工作流”)。這種方法稱為狀態機複製,我們將在第 10 章中回到它。
分片
序列執行所有事務使併發控制變得簡單得多,但將資料庫的事務吞吐量限制為單臺機器上單個 CPU 核心的速度。只讀事務可以使用快照隔離在其他地方執行,但對於具有高寫入吞吐量的應用程式,單執行緒事務處理器可能成為嚴重的瓶頸。
為了擴充套件到多個 CPU 核心和多個節點,你可以對資料進行分片(參見第 7 章),VoltDB 支援這一點。如果你可以找到一種對資料集進行分片的方法,使每個事務只需要讀取和寫入單個分片內的資料,那麼每個分片可以有自己的事務處理執行緒,獨立於其他分片執行。在這種情況下,你可以給每個 CPU 核心分配自己的分片,這允許你的事務吞吐量與 CPU 核心數量線性擴充套件59。
但是,對於需要訪問多個分片的任何事務,資料庫必須協調它所涉及的所有分片之間的事務。儲存過程需要在所有分片上同步執行,以確保整個系統的可序列化。
由於跨分片事務具有額外的協調開銷,因此它們比單分片事務慢得多。VoltDB 報告的跨分片寫入吞吐量約為每秒 1,000 次,這比其單分片吞吐量低幾個數量級,並且無法透過新增更多機器來增加61。最近的研究探索了使多分片事務更具可伸縮性的方法63。
事務是否可以是單分片的很大程度上取決於應用程式使用的資料結構。簡單的鍵值資料通常可以很容易地分片,但具有多個二級索引的資料可能需要大量的跨分片協調(參見“分片和二級索引”)。
序列執行總結
序列執行事務已成為在某些約束條件下實現可序列化隔離的可行方法:
- 每個事務必須小而快,因為只需要一個緩慢的事務就可以阻止所有事務處理。
- 它最適合活動資料集可以適合記憶體的情況。很少訪問的資料可能會移到磁碟,但如果需要在單執行緒事務中訪問,系統會變得非常慢。
- 寫入吞吐量必須足夠低,可以在單個 CPU 核心上處理,否則事務需要分片而不需要跨分片協調。
- 跨分片事務是可能的,但它們的吞吐量很難擴充套件。
兩階段鎖定(2PL)
大約 30 年來,資料庫中只有一種廣泛使用的可序列化演算法:兩階段鎖定(2PL),有時稱為強嚴格兩階段鎖定(SS2PL),以區別於 2PL 的其他變體。
2PL 不是 2PC
兩階段鎖定(2PL)和兩階段提交(2PC)是兩個非常不同的東西。2PL 提供可序列化隔離,而 2PC 在分散式資料庫中提供原子提交(參見“兩階段提交(2PC)”)。為避免混淆,最好將它們視為完全獨立的概念,並忽略名稱中不幸的相似性。
我們之前看到鎖通常用於防止髒寫(參見“沒有髒寫”):如果兩個事務併發嘗試寫入同一物件,鎖確保第二個寫入者必須等到第一個完成其事務(中止或提交)後才能繼續。
兩階段鎖定類似,但使鎖要求更強。只要沒有人寫入,多個事務就可以併發讀取同一物件。但是一旦有人想要寫入(修改或刪除)物件,就需要獨佔訪問:
- 如果事務 A 已讀取物件而事務 B 想要寫入該物件,B 必須等到 A 提交或中止後才能繼續。(這確保 B 不能在 A 背後意外地更改物件。)
- 如果事務 A 已寫入物件而事務 B 想要讀取該物件,B 必須等到 A 提交或中止後才能繼續。(像圖 8-4 中那樣讀取物件的舊版本在 2PL 下是不可接受的。)
在 2PL 中,寫入者不僅阻塞其他寫入者;它們還阻塞讀者,反之亦然。快照隔離有這樣的口號:讀者永遠不會阻塞寫者,寫者永遠不會阻塞讀者(參見“多版本併發控制(MVCC)”),這捕捉了快照隔離和兩階段鎖定之間的關鍵區別。另一方面,因為 2PL 提供可序列化,它可以防止早期討論的所有競態條件,包括丟失的更新和寫偏斜。
兩階段鎖定的實現
2PL 由 MySQL(InnoDB)和 SQL Server 中的可序列化隔離級別以及 Db2 中的可重複讀隔離級別使用29。
讀者和寫者的阻塞是透過在資料庫中的每個物件上有一個鎖來實現的。鎖可以處於共享模式或獨佔模式(也稱為多讀者單寫者鎖)。鎖的使用如下:
- 如果事務想要讀取物件,它必須首先以共享模式獲取鎖。多個事務可以同時以共享模式持有鎖,但如果另一個事務已經對該物件具有獨佔鎖,則這些事務必須等待。
- 如果事務想要寫入物件,它必須首先以獨佔模式獲取鎖。沒有其他事務可以同時持有鎖(無論是共享模式還是獨佔模式),因此如果物件上有任何現有鎖,事務必須等待。
- 如果事務首先讀取然後寫入物件,它可以將其共享鎖升級為獨佔鎖。升級的工作方式與直接獲取獨佔鎖相同。
- 獲取鎖後,事務必須繼續持有鎖直到事務結束(提交或中止)。這就是"兩階段"名稱的來源:第一階段(事務執行時)是獲取鎖,第二階段(事務結束時)是釋放所有鎖。
由於使用了如此多的鎖,很容易發生事務 A 等待事務 B 釋放其鎖,反之亦然的情況。這種情況稱為死鎖。資料庫自動檢測事務之間的死鎖並中止其中一個,以便其他事務可以取得進展。中止的事務需要由應用程式重試。
兩階段鎖定的效能
兩階段鎖定的主要缺點,以及自 1970 年代以來並非每個人都使用它的原因,是效能:在兩階段鎖定下,事務吞吐量和查詢響應時間明顯比弱隔離下差。
這部分是由於獲取和釋放所有這些鎖的開銷,但更重要的是由於併發性降低。按設計,如果兩個併發事務嘗試執行任何可能以任何方式導致競態條件的操作,其中一個必須等待另一個完成。
例如,如果你有一個需要讀取整個表的事務(例如,備份、分析查詢或完整性檢查,如“快照隔離與可重複讀”中所討論的),該事務必須對整個表進行共享鎖。因此,讀取事務首先必須等到所有正在寫入該表的進行中事務完成;然後,在讀取整個表時(對於大表可能需要很長時間),所有想要寫入該表的其他事務都被阻塞,直到大型只讀事務提交。實際上,資料庫在很長一段時間內無法進行寫入。
因此,執行 2PL 的資料庫可能具有相當不穩定的延遲,如果工作負載中存在爭用,它們在高百分位數可能非常慢(參見“描述效能”)。可能只需要一個緩慢的事務,或者一個訪問大量資料並獲取許多鎖的事務,就會導致系統的其餘部分停滯不前。
儘管死鎖可能發生在基於鎖的讀已提交隔離級別下,但在 2PL 可序列化隔離下(取決於事務的訪問模式)它們發生得更頻繁。這可能是一個額外的效能問題:當事務由於死鎖而被中止並重試時,它需要重新完成所有工作。如果死鎖頻繁,這可能意味著大量的浪費努力。
謂詞鎖
在前面的鎖描述中,我們掩蓋了一個微妙但重要的細節。在“導致寫偏斜的幻讀”中,我們討論了幻讀的問題——即一個事務改變另一個事務的搜尋查詢結果。具有可序列化隔離的資料庫必須防止幻讀。
在會議室預訂示例中,這意味著如果一個事務已經搜尋了某個時間視窗內某個房間的現有預訂(參見例 8-2),另一個事務不允許併發插入或更新同一房間和時間範圍的另一個預訂。(併發插入其他房間的預訂,或同一房間不影響擬議預訂的不同時間的預訂是可以的。)
我們如何實現這一點?從概念上講,我們需要一個謂詞鎖4。它的工作方式類似於前面描述的共享/獨佔鎖,但它不屬於特定物件(例如,表中的一行),而是屬於匹配某些搜尋條件的所有物件,例如:
SELECT * FROM bookings
WHERE room_id = 123 AND
end_time > '2025-01-01 12:00' AND
start_time < '2025-01-01 13:00';
謂詞鎖限制訪問如下:
- 如果事務 A 想要讀取匹配某些條件的物件,就像在該
SELECT
查詢中一樣,它必須在查詢條件上獲取共享模式謂詞鎖。如果另一個事務 B 當前對匹配這些條件的任何物件具有獨佔鎖,A 必須等到 B 釋放其鎖後才允許進行查詢。 - 如果事務 A 想要插入、更新或刪除任何物件,它必須首先檢查舊值或新值是否匹配任何現有的謂詞鎖。如果存在事務 B 持有的匹配謂詞鎖,則 A 必須等到 B 提交或中止後才能繼續。
這裡的關鍵思想是,謂詞鎖甚至適用於資料庫中尚不存在但將來可能新增的物件(幻讀)。如果兩階段鎖定包括謂詞鎖,資料庫將防止所有形式的寫偏斜和其他競態條件,因此其隔離變為可序列化。
索引範圍鎖
不幸的是,謂詞鎖的效能不佳:如果活動事務有許多鎖,檢查匹配鎖變得耗時。因此,大多數具有 2PL 的資料庫實際上實現了索引範圍鎖定(也稱為間隙鎖),這是謂詞鎖定的簡化近似54 64。
透過使謂詞匹配更大的物件集來簡化謂詞是安全的。例如,如果你對中午到下午 1 點之間房間 123 的預訂有謂詞鎖,你可以透過鎖定房間 123 在任何時間的預訂來近似它,或者你可以透過鎖定中午到下午 1 點之間的所有房間(不僅僅是房間 123)來近似它。這是安全的,因為匹配原始謂詞的任何寫入肯定也會匹配近似。
在房間預訂資料庫中,你可能在 room_id
列上有索引,和/或在 start_time
和 end_time
上有索引(否則前面的查詢在大型資料庫上會非常慢):
- 假設你的索引在
room_id
上,資料庫使用此索引查詢房間 123 的現有預訂。現在資料庫可以簡單地將共享鎖附加到此索引條目,表示事務已搜尋房間 123 的預訂。 - 或者,如果資料庫使用基於時間的索引查詢現有預訂,它可以將共享鎖附加到該索引中的值範圍,表示事務已搜尋與 2025 年 1 月 1 日中午到下午 1 點的時間段重疊的預訂。
無論哪種方式,搜尋條件的近似都附加到其中一個索引。現在,如果另一個事務想要插入、更新或刪除同一房間和/或重疊時間段的預訂,它將必須更新索引的相同部分。在這樣做的過程中,它將遇到共享鎖,並被迫等到鎖被釋放。
這提供了對幻讀和寫偏斜的有效保護。索引範圍鎖不如謂詞鎖精確(它們可能鎖定比嚴格維護可序列化所需的更大範圍的物件),但由於它們的開銷要低得多,它們是一個很好的折衷。
如果沒有合適的索引可以附加範圍鎖,資料庫可以退回到整個表的共享鎖。這對效能不利,因為它將阻止所有其他事務寫入表,但這是一個安全的後備位置。
可序列化快照隔離(SSI)
本章描繪了資料庫併發控制的黯淡畫面。一方面,我們有效能不佳(兩階段鎖定)或擴充套件性不佳(序列執行)的可序列化實現。另一方面,我們有效能良好但容易出現各種競態條件(丟失的更新、寫偏斜、幻讀等)的弱隔離級別。可序列化隔離和良好效能從根本上是對立的嗎?
似乎不是:一種稱為可序列化快照隔離(SSI)的演算法提供完全可序列化,與快照隔離相比只有很小的效能損失。SSI 相對較新:它於 2008 年首次描述53 65。
今天,SSI 和類似演算法用於單節點資料庫(PostgreSQL 中的可序列化隔離級別54、SQL Server 的記憶體 OLTP/Hekaton66 和 HyPer67)、分散式資料庫(CockroachDB5 和 FoundationDB8)以及嵌入式儲存引擎(如 BadgerDB)。
悲觀併發控制與樂觀併發控制
兩階段鎖定是所謂的悲觀併發控制機制:它基於這樣的原則,即如果任何事情可能出錯(如另一個事務持有的鎖所示),最好等到情況再次安全後再做任何事情。它就像互斥,用於保護多執行緒程式設計中的資料結構。
序列執行在某種意義上是悲觀到極端:它本質上相當於每個事務在事務期間對整個資料庫(或資料庫的一個分片)具有獨佔鎖。我們透過使每個事務執行得非常快來補償悲觀主義,因此它只需要短時間持有"鎖”。
相比之下,可序列化快照隔離是一種樂觀併發控制技術。在這種情況下,樂觀意味著,如果發生潛在危險的事情,事務不會阻塞,而是繼續進行,希望一切都會好起來。當事務想要提交時,資料庫會檢查是否發生了任何不好的事情(即,是否違反了隔離);如果是,事務將被中止並必須重試。只允許可序列執行的事務提交。
樂觀併發控制是一個老想法68,其優缺點已經爭論了很長時間69。如果存在高爭用(許多事務嘗試訪問相同的物件),它的效能很差,因為這會導致大部分事務需要中止。如果系統已經接近其最大吞吐量,重試事務的額外事務負載可能會使效能變差。
但是,如果有足夠的備用容量,並且事務之間的爭用不太高,樂觀併發控制技術往往比悲觀技術性能更好。可交換原子操作可以減少爭用:例如,如果幾個事務併發想要遞增計數器,應用遞增的順序無關緊要(只要計數器在同一事務中沒有被讀取),因此併發遞增都可以應用而不會發生衝突。
顧名思義,SSI 基於快照隔離——也就是說,事務中的所有讀取都從資料庫的一致快照進行(參見“快照隔離與可重複讀”)。在快照隔離的基礎上,SSI 添加了一種演算法來檢測讀寫之間的序列化衝突,並確定要中止哪些事務。
基於過時前提的決策
當我們之前討論快照隔離中的寫偏斜時(參見“寫偏斜與幻讀”),我們觀察到一個反覆出現的模式:事務從資料庫讀取一些資料,檢查查詢結果,並根據它看到的結果決定採取某些行動(寫入資料庫)。但是,在快照隔離下,原始查詢的結果在事務提交時可能不再是最新的,因為資料可能在此期間被修改。
換句話說,事務基於前提(事務開始時為真的事實,例如,“當前有兩名醫生值班”)採取行動。後來,當事務想要提交時,原始資料可能已更改——前提可能不再為真。
當應用程式進行查詢(例如,“當前有多少醫生值班?")時,資料庫不知道應用程式邏輯如何使用該查詢的結果。為了安全起見,資料庫需要假設查詢結果(前提)中的任何更改都意味著該事務中的寫入可能無效。換句話說,事務中的查詢和寫入之間可能存在因果依賴關係。為了提供可序列化隔離,資料庫必須檢測事務可能基於過時前提採取行動的情況,並在這種情況下中止事務。
資料庫如何知道查詢結果是否可能已更改?有兩種情況需要考慮:
- 檢測陳舊的 MVCC 物件版本的讀取(未提交的寫入發生在讀取之前)
- 檢測影響先前讀取的寫入(寫入發生在讀取之後)
檢測陳舊的 MVCC 讀取
回想一下,快照隔離通常由多版本併發控制(MVCC;參見“多版本併發控制(MVCC)”)實現。當事務從 MVCC 資料庫中的一致快照讀取時,它會忽略在拍攝快照時尚未提交的任何其他事務所做的寫入。
在圖 8-10 中,事務 43 看到 Aaliyah 的 on_call = true
,因為事務 42(修改了 Aaliyah 的值班狀態)未提交。但是,當事務 43 想要提交時,事務 42 已經提交。這意味著從一致快照讀取時被忽略的寫入現在已生效,事務 43 的前提不再為真。當寫入者插入以前不存在的資料時,事情變得更加複雜(參見“導致寫偏斜的幻讀”)。我們將在“檢測影響先前讀取的寫入”中討論為 SSI 檢測幻寫。

為了防止這種異常,資料庫需要跟蹤事務由於 MVCC 可見性規則而忽略另一個事務的寫入的時間。當事務想要提交時,資料庫會檢查是否有任何被忽略的寫入現在已經提交。如果是,事務必須被中止。
為什麼要等到提交?為什麼不在檢測到陳舊讀取時立即中止事務 43?好吧,如果事務 43 是隻讀事務,它就不需要被中止,因為沒有寫偏斜的風險。在事務 43 進行讀取時,資料庫還不知道該事務是否稍後會執行寫入。此外,事務 42 可能還會中止,或者在事務 43 提交時可能仍未提交,因此讀取可能最終不是陳舊的。透過避免不必要的中止,SSI 保留了快照隔離對從一致快照進行長時間執行讀取的支援。
檢測影響先前讀取的寫入
要考慮的第二種情況是另一個事務在資料被讀取後修改資料。這種情況如圖 8-11 所示。

在兩階段鎖定的上下文中,我們討論了索引範圍鎖(參見“索引範圍鎖”),它允許資料庫鎖定對匹配某些搜尋查詢的所有行的訪問,例如 WHERE shift_id = 1234
。我們可以在這裡使用類似的技術,除了 SSI 鎖不會阻塞其他事務。
在圖 8-11 中,事務 42 和 43 都在班次 1234
期間搜尋值班醫生。如果 shift_id
上有索引,資料庫可以使用索引條目 1234 來記錄事務 42 和 43 讀取此資料的事實。(如果沒有索引,可以在表級別跟蹤此資訊。)此資訊只需要保留一段時間:在事務完成(提交或中止)並且所有併發事務完成後,資料庫可以忘記它讀取的資料。
當事務寫入資料庫時,它必須在索引中查詢最近讀取受影響資料的任何其他事務。此過程類似於獲取受影響鍵範圍的寫鎖,但它不是阻塞直到讀者提交,而是充當絆線:它只是通知事務它們讀取的資料可能不再是最新的。
在圖 8-11 中,事務 43 通知事務 42 其先前的讀取已過時,反之亦然。事務 42 首先提交,並且成功:儘管事務 43 的寫入影響了 42,但 43 尚未提交,因此寫入尚未生效。但是,當事務 43 想要提交時,來自 42 的衝突寫入已經提交,因此 43 必須中止。
可序列化快照隔離的效能
與往常一樣,許多工程細節會影響演算法在實踐中的工作效果。例如,一個權衡是跟蹤事務讀寫的粒度。如果資料庫詳細跟蹤每個事務的活動,它可以精確地確定哪些事務需要中止,但簿記開銷可能變得很大。不太詳細的跟蹤速度更快,但可能導致比嚴格必要更多的事務被中止。
在某些情況下,事務讀取被另一個事務覆蓋的資訊是可以的:根據發生的其他情況,有時可以證明執行結果仍然是可序列化的。PostgreSQL 使用這一理論來減少不必要中止的數量14 54。
與兩階段鎖定相比,可序列化快照隔離的主要優點是一個事務不需要阻塞等待另一個事務持有的鎖。與快照隔離一樣,寫入者不會阻塞讀者,反之亦然。這種設計原則使查詢延遲更可預測且變化更少。特別是,只讀查詢可以在一致快照上執行而無需任何鎖,這對於讀取密集型工作負載非常有吸引力。
與序列執行相比,可序列化快照隔離不限於單個 CPU 核心的吞吐量:例如,FoundationDB 將序列化衝突的檢測分佈在多臺機器上,允許它擴充套件到非常高的吞吐量。即使資料可能分片在多臺機器上,事務也可以在多個分片中讀取和寫入資料,同時確保可序列化隔離。
與非可序列化快照隔離相比,檢查可序列化違規的需要引入了一些效能開銷。這些開銷有多大是一個爭論的問題:有些人認為可序列化檢查不值得70,而其他人認為可序列化的效能現在已經很好,不再需要使用較弱的快照隔離67。
中止率顯著影響 SSI 的整體效能。例如,長時間讀取和寫入資料的事務可能會遇到衝突並中止,因此 SSI 要求讀寫事務相當短(長時間執行的只讀事務是可以的)。但是,SSI 對慢事務的敏感性低於兩階段鎖定或序列執行。
分散式事務
前幾節重點討論了隔離的併發控制,即 ACID 中的 I。我們看到的演算法適用於單節點和分散式資料庫:儘管在使併發控制演算法可擴充套件方面存在挑戰(例如,為 SSI 執行分散式可序列化檢查),但分散式併發控制的高層思想與單節點併發控制相似8。
一致性和永續性在轉向分散式事務時也沒有太大變化。但是,原子性需要更多關注。
對於在單個數據庫節點執行的事務,原子性通常由儲存引擎實現。當客戶端要求資料庫節點提交事務時,資料庫使事務的寫入持久化(通常在預寫日誌中;參見“使 B 樹可靠”),然後將提交記錄附加到磁碟上的日誌。如果資料庫在此過程中崩潰,事務將在節點重新啟動時從日誌中恢復:如果提交記錄在崩潰前成功寫入磁碟,則事務被認為已提交;如果沒有,該事務的任何寫入都將回滾。
因此,在單個節點上,事務提交關鍵取決於資料持久寫入磁碟的順序:首先是資料,然後是提交記錄22。事務提交或中止的關鍵決定時刻是磁碟完成寫入提交記錄的時刻:在那一刻之前,仍然可能中止(由於崩潰),但在那一刻之後,事務已提交(即使資料庫崩潰)。因此,是單個裝置(連線到特定節點的特定磁碟驅動器的控制器)使提交成為原子的。
但是,如果多個節點參與事務會怎樣?例如,也許你在分片資料庫中有多物件事務,或者有全域性二級索引(其中索引條目可能與主資料在不同的節點上;參見“分片和二級索引”)。大多數"NoSQL"分散式資料儲存不支援此類分散式事務,但各種分散式關係資料庫支援。
在這些情況下,僅向所有節點發送提交請求並在每個節點上獨立提交事務是不夠的。如圖 8-12 所示,提交可能在某些節點上成功,在其他節點上失敗:
- 某些節點可能檢測到約束違規或衝突,需要中止,而其他節點能夠成功提交。
- 某些提交請求可能在網路中丟失,最終由於超時而中止,而其他提交請求透過。
- 某些節點可能在提交記錄完全寫入之前崩潰並在恢復時回滾,而其他節點成功提交。

如果某些節點提交事務而其他節點中止它,節點之間就會變得不一致。一旦事務在一個節點上提交,如果後來發現它在另一個節點上被中止,就不能撤回了。這是因為一旦資料被提交,它在讀已提交或更強的隔離下對其他事務可見。例如,在圖 8-12 中,當用戶 1 注意到其在資料庫 1 上的提交失敗時,使用者 2 已經從資料庫 2 上的同一事務讀取了資料。如果使用者 1 的事務後來被中止,使用者 2 的事務也必須被還原,因為它基於被追溯宣告不存在的資料。
更好的方法是確保參與事務的節點要麼全部提交,要麼全部中止,並防止兩者的混合。確保這一點被稱為原子提交問題。
兩階段提交(2PC)
兩階段提交是一種跨多個節點實現原子事務提交的演算法。它是分散式資料庫中的經典演算法13 71 72。2PC 在某些資料庫內部使用,也以 XA 事務73 的形式提供給應用程式(例如,Java 事務 API 支援),或透過 WS-AtomicTransaction 用於 SOAP Web 服務74 75。
2PC 的基本流程如圖 8-13 所示。與單節點事務的單個提交請求不同,2PC 中的提交/中止過程分為兩個階段(因此得名)。

圖 8-13. 兩階段提交(2PC)的成功執行。
2PC 使用一個通常不會出現在單節點事務中的新元件:協調器(也稱為事務管理器)。協調器通常作為請求事務的同一應用程式程序中的庫實現(例如,嵌入在 Java EE 容器中),但它也可以是單獨的程序或服務。此類協調器的示例包括 Narayana、JOTM、BTM 或 MSDTC。
使用 2PC 時,分散式事務從應用程式在多個數據庫節點上正常讀寫資料開始。我們稱這些資料庫節點為事務中的參與者。當應用程式準備提交時,協調器開始第 1 階段:它向每個節點發送準備請求,詢問它們是否能夠提交。然後協調器跟蹤參與者的響應:
- 如果所有參與者回覆"是”,表示他們準備提交,那麼協調器在第 2 階段發出提交請求,提交實際發生。
- 如果任何參與者回覆"否",協調器在第 2 階段向所有節點發送中止請求。
這個過程有點像西方文化中的傳統婚禮儀式:牧師分別詢問新娘和新郎是否願意嫁給對方,通常從兩人那裡得到"我願意"的答案。在收到兩個確認後,牧師宣佈這對夫婦為夫妻:事務已提交,這個快樂的事實向所有參加者廣播。如果新娘或新郎沒有說"是",儀式就被中止了76。
系統性的承諾
從這個簡短的描述中,可能不清楚為什麼兩階段提交確保原子性,而跨多個節點的單階段提交卻不能。準備和提交請求在兩階段情況下同樣容易丟失。是什麼讓 2PC 不同?
要理解它為什麼有效,我們必須更詳細地分解這個過程:
- 當應用程式想要開始分散式事務時,它從協調器請求事務 ID。此事務 ID 是全域性唯一的。
- 應用程式在每個參與者上開始單節點事務,並將全域性唯一的事務 ID 附加到單節點事務。所有讀寫都在這些單節點事務之一中完成。如果在此階段出現任何問題(例如,節點崩潰或請求超時),協調器或任何參與者都可以中止。
- 當應用程式準備提交時,協調器向所有參與者傳送準備請求,標記有全域性事務 ID。如果這些請求中的任何一個失敗或超時,協調器向所有參與者傳送該事務 ID 的中止請求。
- 當參與者收到準備請求時,它確保它可以在任何情況下明確提交事務。
這包括將所有事務資料寫入磁碟(崩潰、電源故障或磁碟空間不足不是稍後拒絕提交的可接受藉口),並檢查任何衝突或約束違規。透過向協調器回覆"是",節點承諾在請求時無錯誤地提交事務。換句話說,參與者放棄了中止事務的權利,但沒有實際提交它。 5. 當協調器收到所有準備請求的響應時,它對是否提交或中止事務做出明確決定(僅當所有參與者投票"是"時才提交)。協調器必須將該決定寫入其磁碟上的事務日誌,以便在隨後崩潰時知道它是如何決定的。這稱為提交點。 6. 一旦協調器的決定被寫入磁碟,提交或中止請求就會發送給所有參與者。如果此請求失敗或超時,協調器必須永遠重試,直到成功。沒有回頭路:如果決定是提交,那麼必須執行該決定,無論需要多少次重試。如果參與者在此期間崩潰,事務將在恢復時提交——因為參與者投票"是",它在恢復時不能拒絕提交。
因此,該協議包含兩個關鍵的"不歸路":當參與者投票"是"時,它承諾它肯定能夠稍後提交(儘管協調器仍可能選擇中止);一旦協調器決定,該決定是不可撤銷的。這些承諾確保了 2PC 的原子性。(單節點原子提交將這兩個事件合併為一個:將提交記錄寫入事務日誌。)
回到婚姻比喻,在說"我願意"之前,你和你的新娘/新郎有自由透過說"不行!"(或類似的話)來中止事務。但是,在說"我願意"之後,你不能撤回該宣告。如果你在說"我願意"後暈倒,沒有聽到牧師說"你們現在是夫妻",這並不改變事務已提交的事實。當你稍後恢復意識時,你可以透過向牧師查詢你的全域性事務 ID 的狀態來了解你是否已婚,或者你可以等待牧師下一次重試提交請求(因為重試將在你失去意識期間繼續)。
協調器故障
我們已經討論了如果參與者之一或網路在 2PC 期間失敗會發生什麼:如果任何準備請求失敗或超時,協調器將中止事務;如果任何提交或中止請求失敗,協調器將無限期地重試它們。但是,如果協調器崩潰會發生什麼就不太清楚了。
如果協調器在傳送準備請求之前失敗,參與者可以安全地中止事務。但是一旦參與者收到準備請求並投票"是",它就不能再單方面中止——它必須等待協調器回覆事務是提交還是中止。如果協調器此時崩潰或網路失敗,參與者除了等待別無他法。參與者在此狀態下的事務稱為存疑或不確定。
這種情況如圖 8-14 所示。在這個特定的例子中,協調器實際上決定提交,資料庫 2 收到了提交請求。但是,協調器在向資料庫 1 傳送提交請求之前崩潰了,因此資料庫 1 不知道是提交還是中止。即使超時在這裡也沒有幫助:如果資料庫 1 在超時後單方面中止,它將與已提交的資料庫 2 不一致。同樣,單方面提交也不安全,因為另一個參與者可能已中止。

圖 8-14. 協調器在參與者投票“是”後崩潰。資料庫 1 不知道是提交還是中止。
沒有協調器的訊息,參與者無法知道是提交還是中止。原則上,參與者可以相互通訊,瞭解每個參與者如何投票並達成某種協議,但這不是 2PC 協議的一部分。
2PC 完成的唯一方法是等待協調器恢復。這就是為什麼協調器必須在向參與者傳送提交或中止請求之前將其提交或中止決定寫入磁碟上的事務日誌:當協調器恢復時,它透過讀取其事務日誌來確定所有存疑事務的狀態。協調器日誌中沒有提交記錄的任何事務都將中止。因此,2PC 的提交點歸結為協調器上的常規單節點原子提交。
三階段提交
由於 2PC 可能會卡住等待協調器恢復,因此兩階段提交被稱為阻塞原子提交協議。可以使原子提交協議非阻塞,以便在節點失敗時不會卡住。但是,在實踐中使其工作並不那麼簡單。
作為 2PC 的替代方案,已經提出了一種稱為三階段提交(3PC)的演算法13 77。但是,3PC 假設具有有界延遲的網路和具有有界響應時間的節點;在大多數具有無界網路延遲和程序暫停的實際系統中(參見第 9 章),它無法保證原子性。
實踐中更好的解決方案是用容錯共識協議替換單節點協調器。我們將在第 10 章中看到如何做到這一點。
跨不同系統的分散式事務
分散式事務和兩階段提交的聲譽參差不齊。一方面,它們被認為提供了一個重要的安全保證,否則很難實現;另一方面,它們因導致操作問題、扼殺效能並承諾超過它們可以提供的東西而受到批評78 79 80 81。許多雲服務由於它們引起的操作問題而選擇不實現分散式事務82。
某些分散式事務的實現會帶來沉重的效能損失。兩階段提交固有的大部分效能成本是由於崩潰恢復所需的額外磁碟強制(fsync
)和額外的網路往返。
但是,與其直接否定分散式事務,我們應該更詳細地研究它們,因為從中可以學到重要的教訓。首先,我們應該準確說明"分散式事務"的含義。兩種完全不同型別的分散式事務經常被混淆:
- 資料庫內部分散式事務
- 某些分散式資料庫(即,在其標準配置中使用複製和分片的資料庫)支援該資料庫節點之間的內部事務。例如,YugabyteDB、TiDB、FoundationDB、Spanner、VoltDB 和 MySQL Cluster 的 NDB 儲存引擎都有這樣的內部事務支援。在這種情況下,參與事務的所有節點都執行相同的資料庫軟體。
- 異構分散式事務
- 在異構事務中,參與者是兩個或多個不同的技術:例如,來自不同供應商的兩個資料庫,甚至是非資料庫系統(如訊息代理)。跨這些系統的分散式事務必須確保原子提交,即使系統在底層可能完全不同。
資料庫內部事務不必與任何其他系統相容,因此它們可以使用任何協議並應用特定於該特定技術的最佳化。因此,資料庫內部分散式事務通常可以很好地工作。另一方面,跨異構技術的事務更具挑戰性。
精確一次訊息處理
異構分散式事務允許以強大的方式整合各種系統。例如,當且僅當處理訊息的資料庫事務成功提交時,來自訊息佇列的訊息才能被確認為已處理。這是透過在單個事務中原子地提交訊息確認和資料庫寫入來實現的。有了分散式事務支援,即使訊息代理和資料庫是在不同機器上執行的兩種不相關的技術,這也是可能的。
如果訊息傳遞或資料庫事務失敗,兩者都會中止,因此訊息代理可以稍後安全地重新傳遞訊息。因此,透過原子地提交訊息及其處理的副作用,我們可以確保訊息被有效地精確處理一次,即使在成功之前需要幾次重試。中止會丟棄部分完成事務的任何副作用。這被稱為精確一次語義。
但是,只有當受事務影響的所有系統都能夠使用相同的原子提交協議時,這種分散式事務才有可能。例如,假設處理訊息的副作用是傳送電子郵件,而電子郵件伺服器不支援兩階段提交:如果訊息處理失敗並重試,可能會發生電子郵件被傳送兩次或更多次。但是,如果處理訊息的所有副作用在事務中止時都會回滾,那麼處理步驟可以安全地重試,就好像什麼都沒有發生一樣。
我們將在本章後面回到精確一次語義的主題。讓我們首先看看允許此類異構分散式事務的原子提交協議。
XA 事務
X/Open XA(eXtended Architecture 的縮寫)是跨異構技術實現兩階段提交的標準73。它於 1991 年推出並得到廣泛實現:XA 受到許多傳統關係資料庫(包括 PostgreSQL、MySQL、Db2、SQL Server 和 Oracle)和訊息代理(包括 ActiveMQ、HornetQ、MSMQ 和 IBM MQ)的支援。
XA 不是網路協議——它只是用於與事務協調器介面的 C API。此 API 的繫結存在於其他語言中;例如,在 Java EE 應用程式的世界中,XA 事務使用 Java 事務 API(JTA)實現,而 JTA 又由許多使用 Java 資料庫連線(JDBC)的資料庫驅動程式和使用 Java 訊息服務(JMS)API 的訊息代理驅動程式支援。
XA 假設你的應用程式使用網路驅動程式或客戶端庫與參與者資料庫或訊息服務進行通訊。如果驅動程式支援 XA,這意味著它呼叫 XA API 來確定操作是否應該是分散式事務的一部分——如果是,它將必要的資訊傳送到資料庫伺服器。驅動程式還公開回調,協調器可以透過回撥要求參與者準備、提交或中止。
事務協調器實現 XA API。該標準沒有指定應該如何實現它,但在實踐中,協調器通常只是載入到發出事務的應用程式的同一程序中的庫(而不是單獨的服務)。它跟蹤事務中的參與者,在要求他們準備後收集參與者的響應(透過驅動程式的回撥),並使用本地磁碟上的日誌來跟蹤每個事務的提交/中止決定。
如果應用程式程序崩潰,或者執行應用程式的機器宕機,協調器也隨之消失。任何準備但未提交事務的參與者都陷入存疑。由於協調器的日誌在應用程式伺服器的本地磁碟上,該伺服器必須重新啟動,協調器庫必須讀取日誌以恢復每個事務的提交/中止結果。然後,協調器才能使用資料庫驅動程式的 XA 回撥來要求參與者提交或中止(視情況而定)。資料庫伺服器無法直接聯絡協調器,因為所有通訊都必須透過其客戶端庫。
存疑時持有鎖
為什麼我們如此關心事務陷入存疑?系統的其餘部分不能繼續工作,忽略最終會被清理的存疑事務嗎?
問題在於鎖定。如“讀已提交”中所討論的,資料庫事務通常對它們修改的任何行進行行級獨佔鎖,以防止髒寫。此外,如果你想要可序列化隔離,使用兩階段鎖定的資料庫還必須對事務讀取的任何行進行共享鎖。
資料庫在事務提交或中止之前不能釋放這些鎖(如圖 8-13 中的陰影區域所示)。因此,使用兩階段提交時,事務必須在存疑期間保持鎖。如果協調器崩潰並需要 20 分鐘才能重新啟動,這些鎖將保持 20 分鐘。如果協調器的日誌由於某種原因完全丟失,這些鎖將永遠保持——或者至少直到管理員手動解決情況。
當這些鎖被持有時,沒有其他事務可以修改這些行。根據隔離級別,其他事務甚至可能被阻止讀取這些行。因此,其他事務不能簡單地繼續他們的業務——如果他們想要訪問相同的資料,他們將被阻塞。這可能導致你的應用程式的大部分變得不可用,直到存疑事務得到解決。
從協調器故障中恢復
理論上,如果協調器崩潰並重新啟動,它應該從日誌中乾淨地恢復其狀態並解決任何存疑事務。但是,在實踐中,孤立的存疑事務確實會發生83 84——也就是說,協調器由於某種原因(例如,由於軟體錯誤導致事務日誌丟失或損壞)無法決定結果的事務。這些事務無法自動解決,因此它們永遠留在資料庫中,持有鎖並阻塞其他事務。
即使重新啟動資料庫伺服器也無法解決此問題,因為 2PC 的正確實現必須即使在重新啟動時也保留存疑事務的鎖(否則它將冒著違反原子性保證的風險)。這是一個棘手的情況。
唯一的出路是管理員手動決定是提交還是回滾事務。管理員必須檢查每個存疑事務的參與者,確定是否有任何參與者已經提交或中止,然後將相同的結果應用於其他參與者。解決問題可能需要大量的手動工作,並且很可能需要在嚴重的生產中斷期間在高壓力和時間壓力下完成(否則,為什麼協調器會處於如此糟糕的狀態?)。
許多 XA 實現都有一個名為啟發式決策的緊急逃生艙口:允許參與者在沒有協調器明確決定的情況下單方面決定中止或提交存疑事務73。明確地說,這裡的啟發式是可能破壞原子性的委婉說法,因為啟發式決策違反了兩階段提交中的承諾系統。因此,啟發式決策僅用於擺脫災難性情況,而不用於常規使用。
XA 事務的問題
單節點協調器是整個系統的單點故障,使其成為應用程式伺服器的一部分也是有問題的,因為協調器在其本地磁碟上的日誌成為持久系統狀態的關鍵部分——與資料庫本身一樣重要。
原則上,XA 事務的協調器可以是高可用和複製的,就像我們對任何其他重要資料庫的期望一樣。不幸的是,這仍然不能解決 XA 的一個根本問題,即它沒有為事務的協調器和參與者提供直接相互通訊的方式。它們只能透過呼叫事務的應用程式程式碼以及呼叫參與者的資料庫驅動程式進行通訊。
即使協調器被複制,應用程式程式碼也將是單點故障。解決這個問題需要完全重新設計應用程式程式碼的執行方式,使其複製或可重啟,這可能看起來類似於持久執行(參見“持久執行和工作流”)。但是,實踐中似乎沒有任何工具實際採用這種方法。
另一個問題是,由於 XA 需要與各種資料系統相容,它必然是最低公分母。例如,它無法檢測跨不同系統的死鎖(因為這需要系統交換有關每個事務正在等待的鎖的資訊的標準化協議),並且它不適用於 SSI(參見“可序列化快照隔離(SSI)”),因為這需要跨不同系統識別衝突的協議。
這些問題在某種程度上是跨異構技術執行事務所固有的。但是,保持幾個異構資料系統彼此一致仍然是一個真實而重要的問題,因此我們需要為其找到不同的解決方案。這可以做到,我們將在下一節和[待補充連結]中看到。
資料庫內部的分散式事務
如前所述,跨多個異構儲存技術的分散式事務與系統內部的分散式事務之間存在很大差異——即,參與節點都是執行相同軟體的同一資料庫的分片。此類內部分散式事務是"NewSQL"資料庫的定義特徵,例如 CockroachDB5、TiDB6、Spanner7、FoundationDB8 和 YugabyteDB。某些訊息代理(如 Kafka)也支援內部分散式事務85。
這些系統中的許多系統使用兩階段提交來確保寫入多個分片的事務的原子性,但它們不會遇到與 XA 事務相同的問題。原因是,由於它們的分散式事務不需要與任何其他技術介面,它們避免了最低公分母陷阱——這些系統的設計者可以自由使用更可靠、更快的更好協議。
XA 的最大問題可以透過以下方式解決:
- 複製協調器,如果主協調器崩潰,自動故障轉移到另一個協調器節點;
- 允許協調器和資料分片直接通訊,而不透過應用程式程式碼;
- 複製參與分片,以減少由於分片中的故障而必須中止事務的風險;以及
- 將原子提交協議與支援跨分片死鎖檢測和一致讀取的分散式併發控制協議耦合。
共識演算法通常用於複製協調器和資料庫分片。我們將在第 10 章中看到如何使用共識演算法實現分散式事務的原子提交。這些演算法透過自動從一個節點故障轉移到另一個節點來容忍故障,無需任何人工干預,同時繼續保證強一致性屬性。
為分散式事務提供的隔離級別取決於系統,但跨分片的快照隔離和可序列化快照隔離都是可能的。有關其工作原理的詳細資訊,請參見本章末尾引用的論文。
再談精確一次訊息處理
我們在“精確一次訊息處理”中看到,分散式事務的一個重要用例是確保某些操作精確生效一次,即使在處理過程中發生崩潰並且需要重試處理。如果你可以跨訊息代理和資料庫原子地提交事務,則當且僅當成功處理訊息並且從處理過程產生的資料庫寫入被提交時,你可以向代理確認訊息。
但是,你實際上不需要這樣的分散式事務來實現精確一次語義。另一種方法如下,它只需要資料庫中的事務:
- 假設每條訊息都有唯一的 ID,並且在資料庫中有一個已處理訊息 ID 的表。當你開始從代理處理訊息時,你在資料庫上開始一個新事務,並檢查訊息 ID。如果資料庫中已經存在相同的訊息 ID,你知道它已經被處理,因此你可以向代理確認訊息並丟棄它。
- 如果訊息 ID 尚未在資料庫中,你將其新增到表中。然後你處理訊息,這可能會導致在同一事務中對資料庫進行額外的寫入。完成處理訊息後,你提交資料庫上的事務。
- 一旦資料庫事務成功提交,你就可以向代理確認訊息。
- 一旦訊息成功確認給代理,你知道它不會再次嘗試處理相同的訊息,因此你可以從資料庫中刪除訊息 ID(在單獨的事務中)。
如果訊息處理器在提交資料庫事務之前崩潰,事務將被中止,訊息代理將重試處理。如果它在提交後但在向代理確認訊息之前崩潰,它也將重試處理,但重試將在資料庫中看到訊息 ID 並丟棄它。如果它在確認訊息後但在從資料庫中刪除訊息 ID 之前崩潰,你將有一個舊的訊息 ID 留下,除了佔用一點儲存空間外不會造成任何傷害。如果在資料庫事務中止之前發生重試(如果訊息處理器和資料庫之間的通訊中斷,這可能會發生),訊息 ID 表上的唯一性約束應該防止兩個併發事務插入相同的訊息 ID。
因此,實現精確一次處理只需要資料庫中的事務——跨資料庫和訊息代理的原子性對於此用例不是必需的。在資料庫中記錄訊息 ID 使訊息處理冪等,因此可以安全地重試訊息處理而不會重複其副作用。流處理框架(如 Kafka Streams)中使用類似的方法來實現精確一次語義,我們將在[待補充連結]中看到。
但是,資料庫內的內部分散式事務對於此類模式的可伸縮性仍然有用:例如,它們將允許訊息 ID 儲存在一個分片上,而訊息處理更新的主資料儲存在其他分片上,並確保跨這些分片的事務提交的原子性。
總結
事務是一個抽象層,允許應用程式假裝某些併發問題和某些型別的硬體和軟體故障不存在。大量錯誤被簡化為簡單的事務中止,應用程式只需要重試。
在本章中,我們看到了許多事務有助於防止的問題示例。並非所有應用程式都容易受到所有這些問題的影響:具有非常簡單的訪問模式的應用程式(例如,僅讀取和寫入單個記錄)可能可以在沒有事務的情況下管理。但是,對於更複雜的訪問模式,事務可以大大減少你需要考慮的潛在錯誤情況的數量。
沒有事務,各種錯誤場景(程序崩潰、網路中斷、停電、磁碟已滿、意外併發等)意味著資料可能以各種方式變得不一致。例如,反規範化資料很容易與源資料失去同步。沒有事務,很難推理複雜的互動訪問對資料庫可能產生的影響。
在本章中,我們特別深入地探討了併發控制的主題。我們討論了幾種廣泛使用的隔離級別,特別是讀已提交、快照隔離(有時稱為可重複讀)和可序列化。我們透過討論各種競態條件的示例來描述這些隔離級別,總結在表 8-1 中:
表 8-1. 各種隔離級別可能發生的異常總結
隔離級別 | 髒讀 | 讀偏斜 | 幻讀 | 丟失更新 | 寫偏斜 |
---|---|---|---|---|---|
讀未提交 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 |
讀已提交 | ✓ 防止 | ✗ 可能 | ✗ 可能 | ✗ 可能 | ✗ 可能 |
快照隔離 | ✓ 防止 | ✓ 防止 | ✓ 防止 | ? 視情況 | ✗ 可能 |
可序列化 | ✓ 防止 | ✓ 防止 | ✓ 防止 | ✓ 防止 | ✓ 防止 |
- 髒讀
- 一個客戶端在另一個客戶端的寫入提交之前讀取它們。讀已提交隔離級別和更強的級別防止髒讀。
- 髒寫
- 一個客戶端覆蓋另一個客戶端已寫入但尚未提交的資料。幾乎所有事務實現都防止髒寫。
- 讀偏斜
- 客戶端在不同時間點看到資料庫的不同部分。某些讀偏斜的情況也稱為不可重複讀。這個問題最常透過快照隔離來防止,它允許事務從對應於特定時間點的一致快照讀取。它通常使用多版本併發控制(MVCC)實現。
- 丟失更新
- 兩個客戶端併發執行讀-修改-寫迴圈。一個覆蓋另一個的寫入而不合並其更改,因此資料丟失。某些快照隔離的實現會自動防止此異常,而其他實現需要手動鎖(
SELECT FOR UPDATE
)。 - 寫偏斜
- 事務讀取某些內容,根據它看到的值做出決定,並將決定寫入資料庫。但是,在進行寫入時,決策的前提不再為真。只有可序列化隔離才能防止此異常。
- 幻讀
- 事務讀取匹配某些搜尋條件的物件。另一個客戶端進行影響該搜尋結果的寫入。快照隔離防止直接的幻讀,但寫偏斜上下文中的幻讀需要特殊處理,例如索引範圍鎖。
弱隔離級別可以防止某些異常,但讓你(應用程式開發人員)手動處理其他異常(例如,使用顯式鎖定)。只有可序列化隔離可以防止所有這些問題。我們討論了實現可序列化事務的三種不同方法:
- 字面上序列執行事務
- 如果你可以使每個事務執行得非常快(通常透過使用儲存過程),並且事務吞吐量足夠低,可以在單個 CPU 核心上處理或可以分片,這是一個簡單有效的選擇。
- 兩階段鎖定
- 幾十年來,這一直是實現可序列化的標準方法,但許多應用程式由於其效能不佳而避免使用它。
- 可序列化快照隔離(SSI)
- 一種相對較新的演算法,避免了前面方法的大部分缺點。它使用樂觀方法,允許事務在不阻塞的情況下進行。當事務想要提交時,它會被檢查,如果執行不可序列化,它將被中止。
最後,我們研究了當事務分佈在多個節點上時如何實現原子性,使用兩階段提交。如果這些節點都執行相同的資料庫軟體,分散式事務可以很好地工作,但跨不同儲存技術(使用 XA 事務),2PC 是有問題的:它對協調器和驅動事務的應用程式程式碼中的故障非常敏感,並且與併發控制機制的互動很差。幸運的是,冪等性可以確保精確一次語義,而無需跨不同儲存技術的原子提交,我們將在後面的章節中看到更多相關內容。
本章中的示例使用了關係資料模型。但是,如“多物件事務的需求”中所討論的,無論使用哪種資料模型,事務都是有價值的資料庫功能。
參考
Steven J. Murdoch. What went wrong with Horizon: learning from the Post Office Trial. benthamsgaze.org, July 2021. Archived at perma.cc/CNM4-553F ↩︎
Donald D. Chamberlin, Morton M. Astrahan, Michael W. Blasgen, James N. Gray, W. Frank King, Bruce G. Lindsay, Raymond Lorie, James W. Mehl, Thomas G. Price, Franco Putzolu, Patricia Griffiths Selinger, Mario Schkolnick, Donald R. Slutz, Irving L. Traiger, Bradford W. Wade, and Robert A. Yost. A History and Evaluation of System R. Communications of the ACM, volume 24, issue 10, pages 632–646, October 1981. doi:10.1145/358769.358784 ↩︎
Jim N. Gray, Raymond A. Lorie, Gianfranco R. Putzolu, and Irving L. Traiger. Granularity of Locks and Degrees of Consistency in a Shared Data Base. in Modelling in Data Base Management Systems: Proceedings of the IFIP Working Conference on Modelling in Data Base Management Systems, edited by G. M. Nijssen, pages 364–394, Elsevier/North Holland Publishing, 1976. Also in Readings in Database Systems, 4th edition, edited by Joseph M. Hellerstein and Michael Stonebraker, MIT Press, 2005. ISBN: 978-0-262-69314-1 ↩︎ ↩︎ ↩︎ ↩︎
Kapali P. Eswaran, Jim N. Gray, Raymond A. Lorie, and Irving L. Traiger. The Notions of Consistency and Predicate Locks in a Database System. Communications of the ACM, volume 19, issue 11, pages 624–633, November 1976. doi:10.1145/360363.360369 ↩︎ ↩︎ ↩︎
Rebecca Taft, Irfan Sharif, Andrei Matei, Nathan VanBenschoten, Jordan Lewis, Tobias Grieger, Kai Niemi, Andy Woods, Anne Birzin, Raphael Poss, Paul Bardea, Amruta Ranade, Ben Darnell, Bram Gruneir, Justin Jaffray, Lucy Zhang, and Peter Mattis. CockroachDB: The Resilient Geo-Distributed SQL Database. At ACM SIGMOD International Conference on Management of Data (SIGMOD), pages 1493–1509, June 2020. doi:10.1145/3318464.3386134 ↩︎ ↩︎ ↩︎
Dongxu Huang, Qi Liu, Qiu Cui, Zhuhe Fang, Xiaoyu Ma, Fei Xu, Li Shen, Liu Tang, Yuxing Zhou, Menglong Huang, Wan Wei, Cong Liu, Jian Zhang, Jianjun Li, Xuelian Wu, Lingyu Song, Ruoxi Sun, Shuaipeng Yu, Lei Zhao, Nicholas Cameron, Liquan Pei, and Xin Tang. TiDB: a Raft-based HTAP database. Proceedings of the VLDB Endowment, volume 13, issue 12, pages 3072–3084. doi:10.14778/3415478.3415535 ↩︎ ↩︎
James C. Corbett, Jeffrey Dean, Michael Epstein, Andrew Fikes, Christopher Frost, JJ Furman, Sanjay Ghemawat, Andrey Gubarev, Christopher Heiser, Peter Hochschild, Wilson Hsieh, Sebastian Kanthak, Eugene Kogan, Hongyi Li, Alexander Lloyd, Sergey Melnik, David Mwaura, David Nagle, Sean Quinlan, Rajesh Rao, Lindsay Rolig, Dale Woodford, Yasushi Saito, Christopher Taylor, Michal Szymaniak, and Ruth Wang. Spanner: Google’s Globally-Distributed Database. At 10th USENIX Symposium on Operating System Design and Implementation (OSDI), October 2012. ↩︎ ↩︎
Jingyu Zhou, Meng Xu, Alexander Shraer, Bala Namasivayam, Alex Miller, Evan Tschannen, Steve Atherton, Andrew J. Beamon, Rusty Sears, John Leach, Dave Rosenthal, Xin Dong, Will Wilson, Ben Collins, David Scherer, Alec Grieser, Young Liu, Alvin Moore, Bhaskar Muppana, Xiaoge Su, and Vishesh Yadav. FoundationDB: A Distributed Unbundled Transactional Key Value Store. At ACM International Conference on Management of Data (SIGMOD), June 2021. doi:10.1145/3448016.3457559 ↩︎ ↩︎ ↩︎ ↩︎
Theo Härder and Andreas Reuter. Principles of Transaction-Oriented Database Recovery. ACM Computing Surveys, volume 15, issue 4, pages 287–317, December 1983. doi:10.1145/289.291 ↩︎
Peter Bailis, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. HAT, not CAP: Towards Highly Available Transactions. At 14th USENIX Workshop on Hot Topics in Operating Systems (HotOS), May 2013. ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Armando Fox, Steven D. Gribble, Yatin Chawathe, Eric A. Brewer, and Paul Gauthier. Cluster-Based Scalable Network Services. At 16th ACM Symposium on Operating Systems Principles (SOSP), October 1997. doi:10.1145/268998.266662 ↩︎
Tony Andrews. Enforcing Complex Constraints in Oracle. tonyandrews.blogspot.co.uk, October 2004. Archived at archive.org ↩︎ ↩︎
Philip A. Bernstein, Vassos Hadzilacos, and Nathan Goodman. Concurrency Control and Recovery in Database Systems. Addison-Wesley, 1987. ISBN: 978-0-201-10715-9, available online at microsoft.com. ↩︎ ↩︎ ↩︎
Alan Fekete, Dimitrios Liarokapis, Elizabeth O’Neil, Patrick O’Neil, and Dennis Shasha. Making Snapshot Isolation Serializable. ACM Transactions on Database Systems, volume 30, issue 2, pages 492–528, June 2005. doi:10.1145/1071610.1071615 ↩︎ ↩︎ ↩︎
Mai Zheng, Joseph Tucek, Feng Qin, and Mark Lillibridge. Understanding the Robustness of SSDs Under Power Fault. At 11th USENIX Conference on File and Storage Technologies (FAST), February 2013. ↩︎
Laurie Denness. SSDs: A Gift and a Curse. laur.ie, June 2015. Archived at perma.cc/6GLP-BX3T ↩︎
Adam Surak. When Solid State Drives Are Not That Solid. blog.algolia.com, June 2015. Archived at perma.cc/CBR9-QZEE ↩︎
Hewlett Packard Enterprise. Bulletin: (Revision) HPE SAS Solid State Drives - Critical Firmware Upgrade Required for Certain HPE SAS Solid State Drive Models to Prevent Drive Failure at 32,768 Hours of Operation. support.hpe.com, November 2019. Archived at perma.cc/CZR4-AQBS ↩︎
Craig Ringer et al. PostgreSQL’s handling of fsync() errors is unsafe and risks data loss at least on XFS. Email thread on pgsql-hackers mailing list, postgresql.org, March 2018. Archived at perma.cc/5RKU-57FL ↩︎
Anthony Rebello, Yuvraj Patel, Ramnatthan Alagappan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. Can Applications Recover from fsync Failures? At USENIX Annual Technical Conference (ATC), July 2020. ↩︎
Thanumalayan Sankaranarayana Pillai, Vijay Chidambaram, Ramnatthan Alagappan, Samer Al-Kiswany, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. Crash Consistency: Rethinking the Fundamental Abstractions of the File System. ACM Queue, volume 13, issue 7, pages 20–28, July 2015. doi:10.1145/2800695.2801719 ↩︎
Thanumalayan Sankaranarayana Pillai, Vijay Chidambaram, Ramnatthan Alagappan, Samer Al-Kiswany, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. All File Systems Are Not Created Equal: On the Complexity of Crafting Crash-Consistent Applications. At 11th USENIX Symposium on Operating Systems Design and Implementation (OSDI), October 2014. ↩︎ ↩︎
Chris Siebenmann. Unix’s File Durability Problem. utcc.utoronto.ca, April 2016. Archived at perma.cc/VSS8-5MC4 ↩︎
Aishwarya Ganesan, Ramnatthan Alagappan, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. Redundancy Does Not Imply Fault Tolerance: Analysis of Distributed Storage Reactions to Single Errors and Corruptions. At 15th USENIX Conference on File and Storage Technologies (FAST), February 2017. ↩︎
Lakshmi N. Bairavasundaram, Garth R. Goodson, Bianca Schroeder, Andrea C. Arpaci-Dusseau, and Remzi H. Arpaci-Dusseau. An Analysis of Data Corruption in the Storage Stack. At 6th USENIX Conference on File and Storage Technologies (FAST), February 2008. ↩︎
Bianca Schroeder, Raghav Lagisetty, and Arif Merchant. Flash Reliability in Production: The Expected and the Unexpected. At 14th USENIX Conference on File and Storage Technologies (FAST), February 2016. ↩︎
Don Allison. SSD Storage – Ignorance of Technology Is No Excuse. blog.korelogic.com, March 2015. Archived at perma.cc/9QN4-9SNJ ↩︎
Gordon Mah Ung. Debunked: Your SSD won’t lose data if left unplugged after all. pcworld.com, May 2015. Archived at perma.cc/S46H-JUDU ↩︎
Martin Kleppmann. Hermitage: Testing the ‘I’ in ACID. martin.kleppmann.com, November 2014. Archived at perma.cc/KP2Y-AQGK ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Todd Warszawski and Peter Bailis. ACIDRain: Concurrency-Related Attacks on Database-Backed Web Applications. At ACM International Conference on Management of Data (SIGMOD), May 2017. doi:10.1145/3035918.3064037 ↩︎ ↩︎
Tristan D’Agosta. BTC Stolen from Poloniex. bitcointalk.org, March 2014. Archived at perma.cc/YHA6-4C5D ↩︎
bitcointhief2. How I Stole Roughly 100 BTC from an Exchange and How I Could Have Stolen More! reddit.com, February 2014. Archived at archive.org ↩︎
Sudhir Jorwekar, Alan Fekete, Krithi Ramamritham, and S. Sudarshan. Automating the Detection of Snapshot Isolation Anomalies. At 33rd International Conference on Very Large Data Bases (VLDB), September 2007. ↩︎ ↩︎
Michael Melanson. Transactions: The Limits of Isolation. michaelmelanson.net, November 2014. Archived at perma.cc/RG5R-KMYZ ↩︎
Edward Kim. How ACH works: A developer perspective — Part 1. engineering.gusto.com, April 2014. Archived at perma.cc/7B2H-PU94 ↩︎
Hal Berenson, Philip A. Bernstein, Jim N. Gray, Jim Melton, Elizabeth O’Neil, and Patrick O’Neil. A Critique of ANSI SQL Isolation Levels. At ACM International Conference on Management of Data (SIGMOD), May 1995. doi:10.1145/568271.223785 ↩︎ ↩︎ ↩︎ ↩︎ ↩︎ ↩︎
Atul Adya. Weak Consistency: A Generalized Theory and Optimistic Implementations for Distributed Transactions. PhD Thesis, Massachusetts Institute of Technology, March 1999. Archived at perma.cc/E97M-HW5Q ↩︎ ↩︎
Peter Bailis, Aaron Davidson, Alan Fekete, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. Highly Available Transactions: Virtues and Limitations. At 40th International Conference on Very Large Data Bases (VLDB), September 2014. ↩︎ ↩︎ ↩︎
Natacha Crooks, Youer Pu, Lorenzo Alvisi, and Allen Clement. Seeing is Believing: A Client-Centric Specification of Database Isolation. At ACM Symposium on Principles of Distributed Computing (PODC), pages 73–82, July 2017. doi:10.1145/3087801.3087802 ↩︎
Bruce Momjian. MVCC Unmasked. momjian.us, July 2014. Archived at perma.cc/KQ47-9GYB ↩︎ ↩︎ ↩︎
Peter Alvaro and Kyle Kingsbury. MySQL 8.0.34. jepsen.io, December 2023. Archived at perma.cc/HGE2-Z878 ↩︎ ↩︎ ↩︎
Egor Rogov. PostgreSQL 14 Internals. postgrespro.com, April 2023. Archived at perma.cc/FRK2-D7WB ↩︎
Hironobu Suzuki. The Internals of PostgreSQL. interdb.jp, 2017. ↩︎ ↩︎
Rohan Reddy Alleti. Internals of MVCC in Postgres: Hidden costs of Updates vs Inserts. medium.com, March 2025. Archived at perma.cc/3ACX-DFXT ↩︎
Andy Pavlo and Bohan Zhang. The Part of PostgreSQL We Hate the Most. cs.cmu.edu, April 2023. Archived at perma.cc/XSP6-3JBN ↩︎ ↩︎
Yingjun Wu, Joy Arulraj, Jiexi Lin, Ran Xian, and Andrew Pavlo. An empirical evaluation of in-memory multi-version concurrency control. Proceedings of the VLDB Endowment, volume 10, issue 7, pages 781–792, March 2017. doi:10.14778/3067421.3067427 ↩︎ ↩︎
Nikita Prokopov. Unofficial Guide to Datomic Internals. tonsky.me, May 2014. ↩︎
Daniil Svetlov. A Practical Guide to Taming Postgres Isolation Anomalies. dansvetlov.me, March 2025. Archived at perma.cc/L7LE-TDLS ↩︎
Nate Wiger. An Atomic Rant. nateware.com, February 2010. Archived at perma.cc/5ZYB-PE44 ↩︎
James Coglan. Reading and writing, part 3: web applications. blog.jcoglan.com, October 2020. Archived at perma.cc/A7EK-PJVS ↩︎ ↩︎
Peter Bailis, Alan Fekete, Michael J. Franklin, Ali Ghodsi, Joseph M. Hellerstein, and Ion Stoica. Feral Concurrency Control: An Empirical Investigation of Modern Application Integrity. At ACM International Conference on Management of Data (SIGMOD), June 2015. doi:10.1145/2723372.2737784 ↩︎ ↩︎
Jaana Dogan. Things I Wished More Developers Knew About Databases. rakyll.medium.com, April 2020. Archived at perma.cc/6EFK-P2TD ↩︎
Michael J. Cahill, Uwe Röhm, and Alan Fekete. Serializable Isolation for Snapshot Databases. At ACM International Conference on Management of Data (SIGMOD), June 2008. doi:10.1145/1376616.1376690 ↩︎ ↩︎
Dan R. K. Ports and Kevin Grittner. Serializable Snapshot Isolation in PostgreSQL. At 38th International Conference on Very Large Databases (VLDB), August 2012. ↩︎ ↩︎ ↩︎ ↩︎
Douglas B. Terry, Marvin M. Theimer, Karin Petersen, Alan J. Demers, Mike J. Spreitzer and Carl H. Hauser. Managing Update Conflicts in Bayou, a Weakly Connected Replicated Storage System. At 15th ACM Symposium on Operating Systems Principles (SOSP), December 1995. doi:10.1145/224056.224070 ↩︎
Hans-Jürgen Schönig. Constraints over multiple rows in PostgreSQL. cybertec-postgresql.com, June 2021. Archived at perma.cc/2TGH-XUPZ ↩︎
Michael Stonebraker, Samuel Madden, Daniel J. Abadi, Stavros Harizopoulos, Nabil Hachem, and Pat Helland. The End of an Architectural Era (It’s Time for a Complete Rewrite). At 33rd International Conference on Very Large Data Bases (VLDB), September 2007. ↩︎
John Hugg. H-Store/VoltDB Architecture vs. CEP Systems and Newer Streaming Architectures. At Data @Scale Boston, November 2014. ↩︎
Robert Kallman, Hideaki Kimura, Jonathan Natkins, Andrew Pavlo, Alexander Rasin, Stanley Zdonik, Evan P. C. Jones, Samuel Madden, Michael Stonebraker, Yang Zhang, John Hugg, and Daniel J. Abadi. H-Store: A High-Performance, Distributed Main Memory Transaction Processing System. Proceedings of the VLDB Endowment, volume 1, issue 2, pages 1496–1499, August 2008. ↩︎ ↩︎
Rich Hickey. The Architecture of Datomic. infoq.com, November 2012. Archived at perma.cc/5YWU-8XJK ↩︎
John Hugg. Debunking Myths About the VoltDB In-Memory Database. dzone.com, May 2014. Archived at perma.cc/2Z9N-HPKF ↩︎ ↩︎
Xinjing Zhou, Viktor Leis, Xiangyao Yu, and Michael Stonebraker. OLTP Through the Looking Glass 16 Years Later: Communication is the New Bottleneck. At 15th Annual Conference on Innovative Data Systems Research (CIDR), January 2025. ↩︎
Xinjing Zhou, Xiangyao Yu, Goetz Graefe, and Michael Stonebraker. Lotus: scalable multi-partition transactions on single-threaded partitioned databases. Proceedings of the VLDB Endowment (PVLDB), volume 15, issue 11, pages 2939–2952, July 2022. doi:10.14778/3551793.3551843 ↩︎
Joseph M. Hellerstein, Michael Stonebraker, and James Hamilton. Architecture of a Database System. Foundations and Trends in Databases, volume 1, issue 2, pages 141–259, November 2007. doi:10.1561/1900000002 ↩︎
Michael J. Cahill. Serializable Isolation for Snapshot Databases. PhD Thesis, University of Sydney, July 2009. Archived at perma.cc/727J-NTMP ↩︎
Cristian Diaconu, Craig Freedman, Erik Ismert, Per-Åke Larson, Pravin Mittal, Ryan Stonecipher, Nitin Verma, and Mike Zwilling. Hekaton: SQL Server’s Memory-Optimized OLTP Engine. At ACM SIGMOD International Conference on Management of Data (SIGMOD), pages 1243–1254, June 2013. doi:10.1145/2463676.2463710 ↩︎
Thomas Neumann, Tobias Mühlbauer, and Alfons Kemper. Fast Serializable Multi-Version Concurrency Control for Main-Memory Database Systems. At ACM SIGMOD International Conference on Management of Data (SIGMOD), pages 677–689, May 2015. doi:10.1145/2723372.2749436 ↩︎ ↩︎
D. Z. Badal. Correctness of Concurrency Control and Implications in Distributed Databases. At 3rd International IEEE Computer Software and Applications Conference (COMPSAC), November 1979. doi:10.1109/CMPSAC.1979.762563 ↩︎
Rakesh Agrawal, Michael J. Carey, and Miron Livny. Concurrency Control Performance Modeling: Alternatives and Implications. ACM Transactions on Database Systems (TODS), volume 12, issue 4, pages 609–654, December 1987. doi:10.1145/32204.32220 ↩︎
Marc Brooker. Snapshot Isolation vs Serializability. brooker.co.za, December 2024. Archived at perma.cc/5TRC-CR5G ↩︎
B. G. Lindsay, P. G. Selinger, C. Galtieri, J. N. Gray, R. A. Lorie, T. G. Price, F. Putzolu, I. L. Traiger, and B. W. Wade. Notes on Distributed Databases. IBM Research, Research Report RJ2571(33471), July 1979. Archived at perma.cc/EPZ3-MHDD ↩︎
C. Mohan, Bruce G. Lindsay, and Ron Obermarck. Transaction Management in the R* Distributed Database Management System. ACM Transactions on Database Systems, volume 11, issue 4, pages 378–396, December 1986. doi:10.1145/7239.7266 ↩︎
X/Open Company Ltd. Distributed Transaction Processing: The XA Specification. Technical Standard XO/CAE/91/300, December 1991. ISBN: 978-1-872-63024-3, archived at perma.cc/Z96H-29JB ↩︎ ↩︎ ↩︎
Ivan Silva Neto and Francisco Reverbel. Lessons Learned from Implementing WS-Coordination and WS-AtomicTransaction. At 7th IEEE/ACIS International Conference on Computer and Information Science (ICIS), May 2008. doi:10.1109/ICIS.2008.75 ↩︎
James E. Johnson, David E. Langworthy, Leslie Lamport, and Friedrich H. Vogt. Formal Specification of a Web Services Protocol. At 1st International Workshop on Web Services and Formal Methods (WS-FM), February 2004. doi:10.1016/j.entcs.2004.02.022 ↩︎
Jim Gray. The Transaction Concept: Virtues and Limitations. At 7th International Conference on Very Large Data Bases (VLDB), September 1981. ↩︎
Dale Skeen. Nonblocking Commit Protocols. At ACM International Conference on Management of Data (SIGMOD), April 1981. doi:10.1145/582318.582339 ↩︎
Gregor Hohpe. Your Coffee Shop Doesn’t Use Two-Phase Commit. IEEE Software, volume 22, issue 2, pages 64–66, March 2005. doi:10.1109/MS.2005.52 ↩︎
Pat Helland. Life Beyond Distributed Transactions: An Apostate’s Opinion. At 3rd Biennial Conference on Innovative Data Systems Research (CIDR), January 2007. ↩︎
Jonathan Oliver. My Beef with MSDTC and Two-Phase Commits. blog.jonathanoliver.com, April 2011. Archived at perma.cc/K8HF-Z4EN ↩︎
Oren Eini (Ahende Rahien). The Fallacy of Distributed Transactions. ayende.com, July 2014. Archived at perma.cc/VB87-2JEF ↩︎
Clemens Vasters. Transactions in Windows Azure (with Service Bus) – An Email Discussion. learn.microsoft.com, July 2012. Archived at perma.cc/4EZ9-5SKW ↩︎
Ajmer Dhariwal. Orphaned MSDTC Transactions (-2 spids). eraofdata.com, December 2008. Archived at perma.cc/YG6F-U34C ↩︎
Paul Randal. Real World Story of DBCC PAGE Saving the Day. sqlskills.com, June 2013. Archived at perma.cc/2MJN-A5QH ↩︎
Guozhang Wang, Lei Chen, Ayusman Dikshit, Jason Gustafson, Boyang Chen, Matthias J. Sax, John Roesler, Sophie Blee-Goldman, Bruno Cadonna, Apurva Mehta, Varun Madan, and Jun Rao. Consistency and Completeness: Rethinking Distributed Stream Processing in Apache Kafka. At ACM International Conference on Management of Data (SIGMOD), June 2021. doi:10.1145/3448016.3457556 ↩︎