支付平台上兩個錢包之間的直接餘額轉帳:更快、通常免手續費。難點是在 100 萬 TPS 下保證跨帳戶轉帳的正確性,還要能事後重建每一分錢的歷史。
📋 Requirements
- 功能:只做兩個數位錢包之間的餘額轉帳(不含外幣兌換)。
- 規模:1,000,000 TPS;每筆轉帳是「一扣一存」兩個操作,實際要處理近 200 萬 TPS。
- 非功能:可靠性至少 99.99%、支援交易 (transactions)、支援可重現性 (reproducibility)——能從頭重播資料重建歷史餘額。
- API:
POST /v1/wallet/balance_transfer,帶from_account、to_account、amount、currency、transaction_id。amount用 string 而非 double,避免精度問題。
🧠 Core Ideas
TIP
設計沿著三代方案演進:記憶體分片 → 分散式交易 → 事件溯源。核心目標之一是提升單一節點能處理的 TPS:每節點越快,達到百萬 TPS 所需的節點數越少(1,000/節點需 2,000 個節點,10,000/節點只需 200 個),硬體成本越低。
先看最直覺的記憶體分片:帳戶餘額是 <user, balance> map,用 Redis 叢集分片(對 account ID 取 hash 再除以分區數),分片資訊放 Zookeeper,無狀態的錢包服務定位節點後更新兩邊餘額。問題是:一筆轉帳要更新兩個 Redis 節點,無法保證兩次都成功——若服務在第一次更新後、第二次前當機,就是不完整的轉帳。這兩次更新必須在單一原子交易中完成。
⚖️ Reference Design
分散式交易 — 2PC / TC-C / Saga
把 Redis 換成交易型關聯式資料庫後,跨庫的兩個更新用分散式交易做到原子性:
- 2PC(低階):協調者(錢包服務)先要各庫 prepare,全 yes 才 commit,任一 no 就 abort。缺點是等待期間鎖持有很久、效能差,且協調者是單點故障。
- TC-C(高階,補償式):兩階段各自是獨立交易——Try 保留資源,再依結果 Confirm 或 Cancel。補償(undo)寫在業務邏輯裡,與資料庫無關,但複雜度落到應用層。轉帳的有效順序只有「先從 A 扣款、C 做 NOP」;Try 階段結束會出現帳戶總和暫時少 1 美元的不平衡狀態,這對應用是可見且可控的。
- Saga:所有操作排成線性序列,依序執行,失敗則反向用補償交易回滾;協調用編舞(去中心化)或編排(單一協調者),數位錢包偏好編排。
TC-C 操作可並行、Saga 只能線性;對延遲敏感且服務多選 TC-C,想跟微服務趨勢選 Saga。用 phase status 表(存在被扣款帳戶那個庫)記錄進度,才能在協調者重啟後恢復;並用亂序旗標處理 Cancel 比 Try 先到的情況。
事件溯源 — 可稽核與可重現
分散式交易能轉帳,但只存「更新後的餘額」,難以追溯為何改變、歷史餘額也遺失了。事件溯源用四個概念解決:Command(外界意圖,放 FIFO 佇列如 Kafka)、Event(驗證後的既成事實,過去式、確定性)、State(帳戶餘額 map)、State machine(驗證 command 產生 event、套用 event 更新 state;行為必須確定性、不含隨機或 I/O)。
所有變更先以不可變歷史存下,資料庫只是最新檢視。因為 event 列表不可變、state machine 確定性,隨時可從頭重播 events 重建任一時間點的歷史餘額——這回答了稽核員的三問(任意時點餘額、餘額是否正確、改版後邏輯是否仍正確)。CQRS 進一步只發布 events:一個寫入用的 state machine,加上多個唯讀 state machine 各自建構查詢檢視,最終一致。
高效能、可靠與可擴展
- 高效能:把 command/event/state 從遠端 Kafka 與 DB 改存本地磁碟——event 列表僅可附加、循序寫入極快,用 mmap 同時落盤與記憶體快取;state 用 SQLite 或 RocksDB(LSM tree,寫入優化)。定期拍 snapshot 存物件儲存(如 HDFS),重播時從 snapshot 續跑而非從頭。
- 可靠性:只有資料需要可靠(運算可在他處重跑),而四類資料中 state 與 snapshot 都能由 event 重建、command 又不保證確定性,故只有 event 列表需要強可靠性。用 Raft 共識把 event 列表複製到多節點:多數在線即可運作(3 節點容忍 1 壞、5 節點容忍 2 壞),leader 收 command 轉 event 並複製給 follower,leader 當機自動重選。
- 可擴展:分片成多個 Raft 群組,群組間再用 TC-C 或 Saga 做分散式交易;加反向代理讓唯讀 state machine 一收到 event 就 push 狀態,把非同步框架對外變成同步回應。
🔑 Takeaways
- 三代演進:記憶體 Redis(不持久)→ 交易型 DB + 2PC/TC-C/Saga(難稽核)→ 事件溯源(可重現、可稽核)。
- 2PC vs TC-C vs Saga:2PC 由資料庫維護、鎖久且協調者單點;TC-C 與 Saga 是應用層補償式,前者可並行、後者線性。轉帳恆先扣後存。
- 事件溯源的價值在可重現性:不可變 event + 確定性 state machine → 隨時重播重建歷史,CQRS 分離讀寫。
- 可靠性收斂到 event 列表:其餘資料皆可由 event 重建,故只需用 Raft 把 event 列表複製到多數節點;請求去重靠
transaction_id(見冪等)。
No notes yet — jot your takeaways or Q&A here.