第 7 / 12 部 待錄

交易與隔離等級

《Designing Data-Intensive Applications(DDIA)》Podcast 準備稿:交易與隔離等級

書名: 設計資料密集型應用 Designing Data-Intensive Applications(The Big Ideas Behind Reliable, Scalable, and Maintainable Systems) 作者: Martin Kleppmann 系列: 啃一本大書(恩普拉氏) 公開標題建議: 啃一本大書|DDIA.ACID 的真相與隔離等級的陷阱 (7/12) 涵蓋範圍: 第8章 交易(ACID 真義、Read Committed/Snapshot Isolation/Serializable、更新遺失、write skew 與 phantom、序列執行/2PL/SSI 三種可序列化實作)

背景速覽

這是 DDIA 最厚實、也最「打臉日常認知」的一章。交易(transaction)是數十年來簡化資料正確性的核心抽象——把多個讀寫包成一個邏輯單元,要嘛全成、要嘛全敗,沒有中間狀態。但 Kleppmann 要告訴你一個殘酷真相:你以為的「ACID 資料庫」,預設用的其實是很弱的隔離等級,能防的並行問題比你想的少很多。 這集帶你逐級爬隔離階梯,看清每一級到底防得了什麼、防不了什麼。

一句話重點

ACID 是個被行銷稀釋的詞——「符合 ACID」不代表任何精確的保證;而隔離等級是一座階梯,從 Read Committed(防髒讀髒寫)到 Snapshot Isolation(防讀取偏斜)到 Serializable(防一切),每往上一級擋住更多並行幽靈,但效能代價更高;最該帶走的是:主流資料庫預設都不是 Serializable,所以 write skew、phantom 這類微妙的競爭條件,預設情況下沒人幫你擋——你得自己知道它們存在。

值得討論的重點

1. ACID 拆開來看,每個字母都沒你想的那麼硬

  • Atomicity(原子性):不是「並行不可分割」(那是 Isolation),而是故障時的行為——出錯就回復所有已完成的寫入,可安全重試。Kleppmann 說叫 abortability(可中止性) 更貼切。
  • Consistency(一致性):指應用層的不變式(帳戶借貸平衡)——但這本質是應用的責任,資料庫保證不了你的業務邏輯。C 其實是「為了湊出 ACID 這個縮寫硬塞進來的」。
  • Isolation(隔離性):並行交易互不干擾,結果等同依序執行。教科書定義是 serializability,但實務上因效能太貴,多數資料庫用較弱的等級
  • Durability(持久性):提交後不丟。但絕對的持久性不存在——所有硬碟同時壞、所有備份同時失效時,沒有技術救得了你。

這段「拆穿 ACID」是本集最有觀點的開場。

2. 隔離階梯第一級:Read Committed——防髒讀、防髒寫

最基本等級(Postgres、Oracle、SQL Server 預設)。兩個保證:無髒讀(只讀已提交的,不會看到別人未提交、可能被回滾的中間狀態)、無髒寫(只覆蓋已提交的,不會兩個交易的寫入交錯混合——二手車案例:車判給 Bob、發票卻寄給 Alice)。實作:髒寫用行級鎖防、髒讀用「同時記住新舊兩版、提交前給讀者舊值」防。

3. 隔離階梯第二級:Snapshot Isolation——防讀取偏斜,MVCC 登場

Read Committed 還是擋不住讀取偏斜(read skew):Alice 兩個帳戶共 $1000,轉帳進行中她查餘額,看到一個 $500(轉帳前)一個 $400(轉帳後),合計 $900——$100 憑空消失。對備份、對分析查詢這種長時間掃描尤其致命。

解法是快照隔離:每個交易從一個一致性快照讀——看到的是交易開始時所有已提交的資料,後續別人怎麼改都不影響我這個快照。核心實作技術是 MVCC(多版本並行控制),原則是「讀者不阻塞寫者、寫者不阻塞讀者」。注意命名混亂:Oracle 叫它 serializable、Postgres/MySQL 叫 repeatable read——「沒人真正知道 repeatable read 到底代表什麼」。

4. 兩個更微妙的幽靈:更新遺失、write skew、phantom

  • 更新遺失(lost update):讀-改-寫循環中,兩個交易各自讀、各自改、寫回,後者覆蓋前者(遞增計數器、同時編輯 wiki)。解法:原子寫入操作、顯式鎖(SELECT FOR UPDATE)、自動偵測、compare-and-set。
  • write skew:最微妙的一個。值班醫生案例——規定至少一人值班,Alice 和 Bob 同時請假,各自查到「還有兩人值班」覺得自己走 OK,結果都走了、沒人值班。它是更新遺失的推廣:兩個交易讀同一組物件、各自改其中不同的物件,所以髒寫和更新遺失偵測都抓不到。
  • phantom:一個交易的寫入改變了另一個交易的「查詢條件前提」(會議室預約、搶用戶名、防雙重支出)。問題在於——你無法對「不存在的行」加鎖。

5. Serializable 的三種實作:序列執行 / 2PL / SSI

要徹底防所有競爭條件,只有最強的 Serializable。三種實作:

  • 實際序列執行:乾脆單執行緒依序跑所有交易(VoltDB、Redis、Datomic)。約 2007 年後可行(RAM 變便宜、OLTP 交易短小)。簡單但吞吐量上限是單核;要求用 stored procedure 一次提交整個交易邏輯。
  • 兩階段鎖定(2PL):數十年的標準。比一般鎖更嚴——寫者阻塞讀者、讀者也阻塞寫者。正確但效能差、延遲不穩、死鎖頻繁。用謂詞鎖/索引範圍鎖防 phantom。
  • 可序列化快照隔離(SSI):2008 年的新演算法,樂觀並行控制——讓交易自由跑、提交時才檢查衝突。兼顧快照隔離的效能與可序列化的安全,事務不需互相等待(FoundationDB 用它)。代價是中止率,要求讀寫交易盡量短。最有前景的方向。

注意事項

⚠️ 這是全集最重要的一句警告:你的「ACID 資料庫」預設不是 Serializable。 Postgres、MySQL、Oracle、SQL Server 預設都是較弱的等級(Read Committed 或 Snapshot Isolation 的某種變體)。所以「處理金融資料就用 ACID 資料庫」這句話是危險的安心——即使號稱 ACID,預設也擋不住 write skew。Kleppmann 的立場:與其盲目信賴工具,不如理解每種並行問題的本質。 這條要反覆強調。

⚠️ repeatable read 是資料庫界最混亂的術語。 Oracle 的 serializable 約等於快照隔離、Postgres/MySQL 的 repeatable read 約等於快照隔離、IBM DB2 的 repeatable read 等於 serializability。SQL 標準的定義又跟實作對不上。提醒聽眾:看到隔離等級的名字,別望文生義,要查那家資料庫的實際文件。 名字一樣,行為可能天差地遠。

⚠️ write skew 是「測試很難抓、上線會出事」的那種 bug。 它非確定性、靠時機觸發、現有工具難偵測。值班醫生、會議室預約、搶用戶名、防透支——這些都是真實會虧錢的場景。如果你的應用有「先檢查條件、再根據結果寫入」的模式(check-then-act),就要警覺 write skew,最穩的解法是直接上 Serializable,次佳是 SELECT FOR UPDATE 顯式鎖。

專家補充

💡 這一章是「資料庫祛魅」的高峰。 很多工程師對「交易」有一種宗教式的安全感——「包在 transaction 裡就沒事了」。DDIA 把這層安全感拆開:交易確實強大,但它防的範圍取決於隔離等級,而你大概用的不是最強那級。聽完這集,聽眾再寫 BEGIN TRANSACTION 時會多一份清醒——這份清醒本身就是巨大的價值。

💡 MVCC 是現代資料庫的隱形主角。 「讀者不阻塞寫者、寫者不阻塞讀者」這個原則,是 Postgres 等資料庫高並行的基礎。它的實作(每行記 created_by / deleted_by 交易 ID、更新拆成「刪舊建新」、背景 GC 清舊版本)值得理解——你才懂為什麼 Postgres 需要 VACUUM、為什麼長交易會拖累系統(舊版本清不掉)。這是把書本知識接到日常維運的好橋樑。

💡 SSI 代表了一種思維轉向:從悲觀到樂觀。 2PL 是悲觀的(先鎖再說,假設一定會衝突);SSI 是樂觀的(先跑再說,提交時才檢查,假設衝突不常見)。這個「悲觀鎖 vs 樂觀鎖」的對立,在應用層開發裡也天天遇到(樂觀鎖的 version 欄位)。DDIA 把它提升到資料庫隔離等級的高度,讓你看到同一個思想在不同層級的呼應。

討論問題

🎙️ 開場鉤子:「你一定寫過 BEGIN TRANSACTION,然後覺得『好,現在資料安全了』。但我要告訴你一個讓人睡不著的真相——你那個號稱 ACID 的資料庫,預設能擋的並行問題,比你以為的少得多。」

🎙️ 自問自答:「兩個值班醫生同時請假、結果沒人值班——資料庫明明有交易,為什麼擋不住?」用 write skew 回答:因為他們改的是『不同的行』,髒寫偵測抓不到;只有最強的 Serializable 才看得穿這種『各退一步、合起來出事』的陷阱。

🎙️ 帶走的一題:「翻一下你的程式碼,找『先查一下、根據結果再寫入』的地方——搶用戶名、扣庫存、檢查餘額。每一個這種 check-then-act,在弱隔離等級下都可能是 write skew 的破口。它們配得上你資料庫的隔離設定嗎?」

更大範圍關聯

  • 單機交易 → 分散式事務的橋樑:這一章主要在單節點脈絡下談交易。而「分散式環境下的交易」是全新的怪獸——一邊提交成功、另一邊失敗怎麼辦?這直接引出第9集的兩階段提交(2PC)與共識。可以說這集是「在一台機器上把交易講透」,第9集是「把它推到多台機器後會遭遇什麼」。
  • 與軟體工匠傳統的呼應:隔離等級的「名字與實際行為不符」(repeatable read 的混亂),正是 Clean Code 講的「命名要誠實」在資料庫界的大型反面教材。一個誤導的名字,讓整個業界對隔離等級的認知混亂了數十年——命名困難,果然是電腦科學的兩大難題之一。
  • 與面試的分工:面試問「ACID 是什麼」太多人背得出四個字母,但答得出「主流資料庫預設不是 serializable、write skew 是什麼、SSI 和 2PL 差在哪」的人,立刻拉開段位。DDIA 這章是隔離等級的權威教材。(見 [軟體工程領域切分藍圖] §5「軟體架構演進」)

錄製建議

  • 建議時長:約 26–28 分鐘(這是 Part II 最厚的一集,內容紮實,值得多給)。配比:拆穿 ACID 約 5 分;隔離階梯(Read Committed → Snapshot Isolation/MVCC)約 8 分;三個微妙幽靈(更新遺失/write skew/phantom)約 8 分;三種 Serializable 實作約 6 分。
  • 討論策略:全集用「隔離階梯」這個視覺意象串起來——一級一級往上爬,每爬一級就問「這級防得了什麼、還防不了什麼」,自然帶出下一級的存在理由。情感高峰是 write skew 的值班醫生案例(簡單、震撼、難防)。開場的「拆穿 ACID」要有力,這是本集的觀點所在。
  • 三種 Serializable 實作用「序列執行=放棄並行、2PL=悲觀鎖、SSI=樂觀鎖」三個標籤記,聽眾好複述。結尾埋好伏筆:「這一切都在一台機器上——那如果交易要跨好幾台機器呢?下一集,我們進入分散式系統最深的水域。」