使用 Postgres Transactions 解決分散式系統挑戰
使用 Postgres Transactions 解決分散式系統挑戰
將工作流狀態與數據共置
將應用程式工作流狀態共置於 Postgres 資料庫中,可以讓開發者消除「雙寫」(dual-write)問題——即系統更新了資料庫但未能通知訊息佇列,或反之亦然的風險。透過將資料庫視為業務數據與工作流進度的單一事實來源(single source of truth),開發者可以確保狀態轉換及其對應的副作用(side effect)是原子性地提交的。
這種方法將分散式協調問題轉化為本地事務(local transaction)問題。系統不再嘗試在兩個獨立的系統(例如:一個資料庫和一個訊息代理)之間協調事務,而是在單個 ACID 事務中,將數據變更與預期的動作同時寫入同一個資料庫。
Transactional Outbox Pattern
Transactional Outbox pattern 是實現資料庫與外部訊息佇列之間原子性的主要機制。它的運作方式是將資料庫的角色拆分為兩個部分:主要狀態與一個「outbox」表。
- Atomic Write:應用程式在單個 Postgres 事務中更新業務狀態並將訊息插入 outbox 表。
- Asynchronous Dispatch:一個獨立的程序會輪詢 outbox 表,或使用資料庫觸發器/UDF 來將訊息推送到外部佇列。
- Confirmation:一旦外部系統確認收到,該訊息就會被標記為已處理或從 outbox 中刪除。
正如社群成員所指出的,這種模式有效地「假裝」資料庫與佇列之間存在一個事務,因為它確保了發送訊息的意圖與數據本身一樣持久地被保存下來。
在金融系統中消除雙寫 Bug
在像資金轉移系統這樣的高風險環境中,將檢查點(checkpoint)與寫入操作共置是防止「半提交」(half-committed)狀態的關鍵。如果系統在工作流中途崩潰,恢復程序可以查看 Postgres 中持久化的工作流狀態,以確定程序確切停止的位置,並從該點恢復,而不會導致事務重複或遺漏。
"This is the trick that kills the dual-write bug in money-movement systems: co-locate the checkpoint with the write so a mid-workflow crash can't leave you half-committed."
權衡與實作考量
雖然使用 Postgres 作為工作流引擎提供了強大的保證,但它也引入了特定的架構權衡:
耦合與複雜度
將工作流進度單元與資料庫提交單元對齊,會在資料庫 Schema 與應用程式工作流之間建立緊密耦合。雖然這簡化了 outbox pattern,但這使得未來很難將工作流引擎與資料庫分離。然而,對於許多服務而言,資料庫是技術棧中最穩定的部分,因此這種權衡是可以接受的。
冪等性與副作用
資料庫事務無法回滾已經發生在物理世界中的副作用(例如:發送電子郵件)。因此,對於任何外部服務交互,冪等性(idempotency)是強制性的。
例如,在電子郵件通知系統中,常見的策略是優先考慮「至少一次」(at-least-once)交付而非「exactly-once」交付。系統會在將訊息從待處理清單中移除並關閉事務之前,確認電子郵件已成功發送,並接受極少數的失敗可能導致重複發送電子郵件,而非遺失電子郵件。
中心化風險
使用單一資料庫同時處理狀態與協調,會造成單一故障點(single point of failure)。雖然這簡化了運維開銷——因為資料庫故障會導致整個系統一致地停機——但這也消除了獨立於數據層之外擴展協調層的能力。
與分散式替代方案的比較
有人認為這種方法在嚴格意義上並非「分散式系統」,因為它依賴於一個中央資料庫。然而,它作為 Temporal 或複製狀態機(replicated state machines)等複雜分散式協調工具的實用替代方案。對於步驟數量可控且需要工作流進度永久記錄的系統,直接在 Postgres 中實作持久化工作流通常比引入 Redis 或 Valkey 等額外基礎設施更有效率。