Polymarket 預測市場 (Design Polymarket)

一句話定位 + TL;DR(含 35 分鐘心智時鐘)

設計 Polymarket:使用者對未來事件交易 YES / NO outcome tokens 的預測市場。本質是一個 金融撮合交易所(matching exchange),外加一層「鏈下高頻撮合 + 鏈上最終結算」的特殊性。系統 寫密集且延遲敏感:下單 / 取消 p95 < 200ms、價格更新 次秒級(sub-second) 傳播、訂單管理需強一致
招牌取捨:撮合流程 append-only、deterministic、永不回滾——撮合一旦成立就寫進 WAL 成為唯一事實來源,即使後續 鏈上結算(on-chain settlement)失敗 也只 retry / 升級 ops絕不 rollback 已完成的 off-chain fill。回滾會破壞 ordering guarantee、讓 replica 與下游 consumer 狀態分歧。這就是用「最終一致的鏈上結算」換「鏈下撮合的確定性與可重建性」。其餘決勝點:in-memory order book + WAL(撮合在記憶體、持久化靠 WAL)、market_id sharding(每個市場單一 sequencer)、Kafka pub/sub 扇出把 MDS / OQS 讀路徑與撮合引擎解耦(CQRS)。
配速(35 min 預算):~5 min 釐清需求 → ~4 min 容量估算 → ~3 min API + ~4 min 資料模型 → ~7 min 高階架構 → ~9 min deep dive(訂單追蹤 OQS / 擴展 Kafka 解耦 / 容錯 failover)→ ~2 min 收尾。開場先一句話複述題目、宣告配速、把「唯一 token / 鏈上結算細節」標記為待 deep dive——宣告配速本身就是高分動作。

1. 釐清需求與範圍 (Clarify & Scope ~5 min)

開場後先框邊界,別急著畫撮合引擎。金融交易所題最容易在「一致性語意」與「鏈上 vs 鏈下分界」上翻車,必須先問清。

釐清問題 為何要問 / 影響到哪個決策 本題假設(依來源)
市場型態?只有二元(YES/NO)嗎? 決定 outcome token 與結算邏輯複雜度 全部 binary(YES/NO)
使用者要做哪些訂單操作? 驅動 API 與撮合引擎狀態機 建立 / 修改 / 取消未成交訂單
撮合與結算在哪裡跑?鏈上還鏈下? 這是本題的架構分水嶺 off-chain 撮合 + on-chain 結算(CTF / Polygon)
一致性要求?哪條路徑要強一致? 決定撮合用 in-memory + WAL 還是 DB 訂單管理強一致;讀服務允許小而有界的延遲
規模多大?DAU / trades / 並行市場數? 直接驅動估算與 sharding 決策 ~20M DAU、~100M trades/day、數千並行市場
延遲要求?哪條路徑最敏感? 決定撮合走記憶體、價格走 WebSocket 下單/取消 p95 < 200ms;價格次秒級傳播
市場怎麼結算(誰決定 YES/NO)? 決定要不要 oracle、payout 邏輯 UMA oracle 寫 outcome 上鏈 → CTF 兌付
defer 話術

鏈上結算的 atomic swap 與 token mint/burn 我先標記成 black box,等 deep dive 再展開——現在先假設『撮合成立後會有一個 Settlement Service 負責把它落鏈』,這樣我們能先把 下單→撮合→價格傳播 這條主幹串起來。」把鏈上細節推遲,保住撮合敘事的連貫性。

2. 功能性與非功能性需求 (FR / NFR 與設計北極星)

設計北極星(NFR 直接導出後續所有選型的判準):

NFR 導出的設計北極星 後續對映選型
撮合 p95 < 200ms 撮合不能進 DB——必須 in-memory order book in-memory matching engine + WAL 持久化
訂單強一致 + 已 ack 不遺失 每筆 order/fill 先落 durable log,且至少存活 2 台 WAL + primary/replica,≥1 ACK 才回成功
~100M trades/day + 讀很重 讀寫分離;重 UI 查詢不可拖累撮合延遲 CQRS:MDS / OQS tail WAL,獨立讀路徑
數千並行市場 + 獨立擴展 以市場為單位切分、每市場單一 sequencer market_id sharding + Kafka 解耦
價格次秒級傳播 server 主動推、不能靠輪詢 WebSocket / SSE + Redis Pub/Sub fan-out
鏈上慢 / 貴 / 低吞吐 高頻邏輯留鏈下,最終結算才上鏈 off-chain 撮合 + on-chain settlement(batching)
一句話北極星

「這是一個 寫密集、延遲敏感、撮合需確定性可重建、鏈下做事 / 鏈上結算 的系統」——記住這幾個詞,後面每個選型都能從它推回去。

3. 容量估算 (Back-of-Envelope ~4 min)

估算目的是 驅動 sharding 與讀路徑解耦決策(估算 → 選型 hand-off)。回連 02-Distributed-Systems/05-Numbers-to-Know

寫流量(撮合 QPS)

~100M trades/day
平均 QPS = 100,000,000 ÷ 86,400 s ≈ 1,157 trades / sec
撮合尖峰(熱門市場結算前、新聞事件)常為平均的 5~10x → ~6k~12k orders/sec(推導值)
→ 單一 Matching Engine 扛不住所有市場 → 必須依 market_id 分片

讀流量(價格 / 訂單查詢)

20M DAU,每人每天多次刷新價格 + 查訂單,讀遠多於寫(read-heavy 的 UI 層)
價格更新需「每筆 order event → 計算新價 → 推給所有訂閱者」的 fan-out
→ 讀路徑(MDS/OQS)流量量級遠高於撮合本身 → 必須與撮合解耦獨立擴展

狀態 / 儲存

數千並行市場,每個市場一份 in-memory order book(活躍訂單)
WAL:每筆 order/fill event append(永久或長期保留以供重建 / 重放)
Order DB(OQS materialized):歷史訂單 + 狀態,依 user_id sharding

下表 QPS 與尖峰倍數皆為 推導值(由 trades/day ÷ 86,400 與一般金融尖峰經驗推出;來源僅給 20M DAU / 100M trades/day / 數千市場 / <200ms / 次秒級等輸入)。

維度 數字 對後續決策的意義
平均撮合 QPS ≈ 1,157 /sec(推導值) 單機尚可,但尖峰 + 數千市場 → 依 market_id 分片
撮合尖峰 QPS ≈ 6k~12k /sec(推導值) 熱門市場結算前爆量 → 每市場單一 sequencer,分散到多 shard
讀路徑(價格/查詢) 遠高於撮合(read-heavy UI) CQRS:MDS/OQS 獨立服務,不可拖累撮合延遲
並行市場 數千 sharding 單位 = market_id,天然並行
撮合延遲預算 p95 < 200ms 撮合必須 in-memory,落 DB 不可能達標
估算的最大收穫

「撮合 p95 < 200ms」直接否決「order book 放 DB」——撮合必須在記憶體;而 ~100M trades/day × 尖峰 + 數千市場 直接導出 market_id sharding(每市場一個 sequencer)。讀路徑流量量級又遠高於撮合 → 必須 CQRS 解耦,否則重 UI 查詢會吃掉撮合的延遲預算。

4. 用到的積木 / Building Blocks Used

架構即目錄:先給導覽圖,再展開。下表每個決策都對映一個既有 concept note(無 alias 連結);本表即「本案例練到哪些積木」的單一事實來源(single source of truth)。

設計決策 積木 在本題的作用(≤1 行)
容量估算驅動選型 02-Distributed-Systems/05-Numbers-to-Know 算出 ~1.2k 平均 / 尖峰 6k+ QPS,導出 in-memory + 分片
撮合需強一致與序列化 05-Database-Advanced/01-Transactions 每市場單一 sequencer 序列化訂單,WAL 即 commit log
撮合的競爭 / race 處理 12-Ops-and-Reliability/01-Dealing-With-Contention 單一 writer per market 消除並發撮合的 race condition
已 ack 不遺失的持久化 12-Ops-and-Reliability/03-Reliable-Delivery WAL append + ≥1 replica ACK,failover 不丟單
撮合引擎複本 / failover 05-Database-Advanced/03-Replication primary/replica,leader 掛了 promote replica
leader election 防 split-brain 09-Messaging-Coord/05-Zookeeper 用 Raft / ZooKeeper / etcd 選主,避免雙主
依市場切分、獨立擴展 05-Database-Advanced/02-Sharding market_id 為 shard key,每市場單一 WAL = authoritative
讀路徑與撮合解耦(扇出) 09-Messaging-Coord/03-Kafka Kafka pub/sub 把 order event 扇出給 MDS / OQS,獨立擴展
事件管線 / 重放重建 09-Messaging-Coord/01-Data-Pipeline WAL → Kafka → consumer materialize;可 replay 重建狀態
讀寫分離(CQRS) 10-Design-Patterns/02-Scaling-Writes 寫走撮合引擎、讀走 MDS/OQS materialized view
訂單查詢專用讀模型 06-Database-Tech/04-OLTP-vs-OLAP OQS 把 event materialize 成查詢最佳化的 Order DB
即時價格推播 10-Design-Patterns/05-Real-time-Updates 初始 snapshot 走 HTTP,增量走 WebSocket/SSE
推播協定選擇 01-Networking/05-Realtime-Protocols server→client 主動推:WebSocket / SSE
價格 fan-out 通道 07-Caching-Storage/04-Redis 每市場一個 Redis channel,Pub/Sub 推給訂閱的 MDS server
入口路由 / 鑑權 11-Infrastructure/01-API-Gateway client → API Gateway → Matching Engine

5. API 設計 (API Design ~3 min)

由 FR 驅動,採 REST 資源風格(理據見 03-API-Design/02-REST);即時價格走「HTTP 取 baseline + WebSocket 串增量」雙軌。

Method Path 用途 重點
GET /markets/{marketId}/summary 取市場即時摘要 bestBid / bestAsk / lastTrade / timestamp,作初始 baseline
POST /orders 建立訂單(limit / market) body 帶 side/orderType/marketId/limitPrice?/numShares → Order
DELETE /orders/{orderId} 取消訂單 成功後 status: CANCELED
PUT /orders/{orderId} 修改訂單(price / size) 撮合引擎在記憶體定位該單後更新
GET /orders?status=OPEN 查自己的訂單(OQS) 分頁 / 篩選 / analytics 走 OQS,不打撮合引擎
WS /markets/{marketId}/stream 訂閱增量價格 HTTP snapshot 後 升級為 WebSocket/SSE 串 incremental updates
細節話術

POST /orders 回的 OrderstatusOPEN | PARTIALLY_FILLED | FILLED | CANCELED)與 filledShares——這替後面「使用者如何追蹤訂單狀態」的 deep dive(OQS materialized view)留好伏筆。重 UI 查詢(分頁 / 篩選 / 歷史)絕不打撮合引擎,全部走 OQS。

6. 資料模型與儲存選型 (Data Model & Storage Choice ~4 min)

核心不是「一張 DB 表」,而是 三種狀態載體:撮合引擎的 in-memory order book、持久化的 WAL、以及 OQS materialized 的 Order DB。

In-Memory Order Book(per market,撮合引擎內,極低延遲)
--------------------------------------------------
bids:  price → [orders FIFO]   (價格層級,由高到低)
asks:  price → [orders FIFO]   (價格層級,由低到高)
index: orderId → (price, side) ← O(1) 定位以做 cancel / modify
--------------------------------------------------
不落 DB:撮合需 < 200ms;當機靠 WAL + snapshot 重建

WAL(per market shard,append-only,唯一事實來源)
--------------------------------------------------
seq | leaderEpoch | type(NEW/CANCEL/MODIFY/FILL) | orderId | marketId | payload | ts
--------------------------------------------------
+ 定期 snapshot(in-memory book 快照 + 對應最後 WAL offset)
重啟:載入最新 snapshot → replay 該 offset 之後的 WAL → deterministic 重建

Order DB(OQS materialized view,依 user_id sharding)
--------------------------------------------------
orderId PK | userId(shard key) | marketId | side | type | limitPrice
          | numShares | filledShares | status | createdAt | updatedAt
--------------------------------------------------
為查詢最佳化(分頁 / 篩選 / 歷史 / analytics),由 order event 餵養更新

On-chain(CTF / Polygon,鏈上結算與所有權)
--------------------------------------------------
conditionId → outcomeIndex(YES=0 / NO=1)   ← UMA oracle resolve 後寫入
USDC collateral escrow ↔ YES/NO outcome tokens(mint / burn)

儲存選型:

狀態載體 選擇 理由
Order Book In-Memory 撮合 p95 < 200ms,不可能進 DB;持久化責任外包給 WAL
WAL Append-only durable log 撮合的 source of truth,可 replay 重建;天然 deterministic、ordering 保證
Order Query Materialized Order DB 重 UI 查詢(分頁 / join / analytics)不可拖累撮合 → 獨立讀模型(CQRS)
撮合引擎直接被 UI 查 撮合引擎為低延遲調校,不適合 pagination / flexible filter / big join
結算 / 所有權 On-chain(CTF/Polygon) 需不可否認 / 不可竄改的所有權與結算紀錄 → 上鏈(最終結算)

→ 積木:撮合的序列化與一致性見 05-Database-Advanced/01-Transactions;WAL 持久化見 12-Ops-and-Reliability/03-Reliable-Delivery;OQS 讀模型見 06-Database-Tech/04-OLTP-vs-OLAP10-Design-Patterns/02-Scaling-Writes;分片見 05-Database-Advanced/02-Sharding

邊界

「order book 放記憶體」是因為 撮合是延遲關鍵路徑;它能成立的前提是 WAL 兜底持久化 + snapshot 加速重建。沒有 WAL,記憶體一掉就丟單——這正是第 8.3 容錯 deep dive 的核心。

7. 高階架構 (High-Level Design ~7 min)

把積木串成端到端流程。先看三條核心 flow,再看 scaled 架構。

(A) 下單 / 撮合 flow(order → match)

Client
  │  POST /orders { side, type, marketId, limitPrice, numShares }
  ▼
API Gateway ──► Matching Engine(依 market_id 路由到對應 shard)
  │ 1. append order event → WAL(先持久化!)
  │ 2. 更新 in-memory order book(價格層級)
  │ 3. 撮合:找到 maker+taker → 產生 FILL event → append WAL → 更新 book
  │ 4. 把撮合對交給 Settlement Service(鏈上結算,非同步)
  ▼
回傳 Order { status, filledShares }   (撮合完成才回 success,且 WAL 已 ≥1 replica ACK)

(B) 鏈下執行 ↔ 鏈上結算 flow(off-chain ↔ on-chain)

撮合成立(off-chain,已寫 WAL)
        │  撮合對送 Settlement Service
        ▼
   Settlement Service
        │ 1. 確認 collateral / outcome-token reservation 已存在
        │ 2. JSON-RPC → Polygon 上的 CTF Exchange contract
        │       └─ atomic swap:USDC collateral ↔ YES/NO outcome tokens
        │ 3. 鏈上失敗 → retry / 升級 ops
        │       ★ 絕不 rollback 已完成的 off-chain fill(append-only / deterministic)
        ▼
   taker 錢包持有 outcome tokens
        │
        ▼(市場 resolve 時)
   UMA oracle 把 outcome 寫上鏈:conditionId → outcomeIndex(YES=0/NO=1)
   勝方呼叫 CTF → 兌 1 USDC;敗方 token → burned(價值 0)

(C) Scaled 架構(端到端 — CQRS 扇出)

                                Client
                                  │  下單/取消 (write)        訂閱價格/查訂單 (read)
                                  ▼                                  ▲
                            API Gateway                              │
                                  │ route by market_id              │
        ┌─────────────────────────┼──────────────────────────┐     │
        ▼                         ▼                            ▼     │
  Matching Engine          Matching Engine             Matching Engine
   (shard A: market 1..n)   (shard B ...)               (shard C ...)
   primary + replicas       每 shard:WAL + in-mem book
        │ append WAL → 更新 book → publish order events
        ▼
   ┌──────────────────────────────────────────────────────────┐
   │            Kafka pub/sub  (partition by market_id)         │ ← 解耦撮合與讀路徑
   └───────────────┬───────────────────────┬───────────────────┘
                   │ (consume)              │ (consume)
                   ▼                        ▼
            Market Data Svc (MDS)     Order Query Svc (OQS)
            算 best bid/ask/last       materialize → Order DB
            更新 price cache           (依 user_id sharding)
                   │                        │
       Redis Pub/Sub (per-market channel)   └──► GET /orders 查訂單
                   │ push incremental
                   ▼
          WebSocket / SSE ──► Client(次秒級價格)
                                  ▲
                       HTTP snapshot 取 baseline 後升級為 WS

關鍵 hand-off:API Gateway 依 market_id 路由 → 對應 Matching Engine shard(in-mem book + WAL,每市場單一 sequencer)→ publish 到 Kafka → MDS(價格 fan-out via Redis Pub/Sub + WebSocket)與 OQS(materialize 成可查的 Order DB)各自獨立消費。撮合的延遲關鍵路徑與重 UI 讀路徑 徹底解耦

8. 深入探討 (Deep Dives ~9 min)

不重複原則(link-don't-restate)

通用機制(Kafka 分區語意、replication 細節、WebSocket vs SSE、sharding 原理)只在 concept note 解釋;這裡只講「本題怎麼用、選哪個、為什麼」。任一機制超過 ~3 行就砍掉改連結。本題無 home note 的新材料(off-chain orderbook / CTF / UMA oracle)才在此 inline 教學並登記到 new_concepts_introduced

新材料:鏈下訂單簿 + 鏈上結算(本案例首次 introduce,尚無 atomic note → 已登記 new_concepts_introduced)

  • 為何鏈下做事:上鏈 慢、貴(gas)、不適合高頻寫入。所以高頻邏輯(下單、撮合、價格發現)留 off-chain;只有 最終結算 / 所有權 / 不可否認紀錄on-chain
  • CTF(Conditional Tokens Framework):鏈上把 collateral(USDC)escrow,mint 出 YES / NO outcome tokens 交易;市場 resolve 後勝方 token 兌 1 USDC、敗方 token burned(價值 0)。鏈上的 atomic swap 由 Exchange / CTF contract 執行。
  • UMA oracle:去中心化「truth machine」,把市場結果 conditionId → outcomeIndex(YES=0/NO=1) 寫上鏈,觸發 payout。Polymarket 監聽 oracle event 並通知使用者。

8.1 使用者如何追蹤訂單狀態(OQS)

問題:UX 上使用者要能查「我所有 open orders」「我最近 100 筆交易」「market X 的訂單」——這些是 pagination / 彈性篩選 / analytics / big join 型查詢。
為何是瓶頸:直接打 Matching Engine 取訂單看似最省事,但 撮合引擎是為低延遲撮合調校的,這類重查詢會 吃掉撮合的延遲預算(< 200ms),等於用 UI 流量拖垮交易主幹。

方案 做法 優點 缺點
A. UI 直接查 Matching Engine client 向撮合引擎要訂單 無需額外服務 / 即時 重查詢污染撮合延遲;撮合不擅分頁/join
B. 獨立 OQS(tail WAL) OQS 消費 order event,materialize 成 DB 撮合零干擾、查詢最佳化 多一個服務 + 小而有界的傳播延遲

選擇與理由:採 B(OQS)。OQS 像 MDS 一樣 tail WAL(後續改 tail Kafka),把 order event materialize 到查詢最佳化的 Order DB,client 的所有訂單查詢走 OQS。代價是 NFR 已允許的「小而有界的讀傳播延遲」——這正是強一致需求被刻意限定在「order management 本身」而非「讀服務」的原因。Order DB user_id sharding,同一使用者的訂單查詢只落單一 shard。
→ 積木:06-Database-Tech/04-OLTP-vs-OLAP(撮合 OLTP / OQS 偏分析查詢)、10-Design-Patterns/02-Scaling-Writes(CQRS 讀寫分離)、05-Database-Advanced/02-Sharding(user_id shard)

8.2 系統如何擴展到每日大量交易(Kafka 解耦 + 分片)

問題:~100M trades/day + 尖峰 + 數千市場,且 MDS / OQS 都要能水平擴展。
為何是瓶頸:原始設計裡 MDS 與 OQS 都直接 tail Matching Engine。一旦把這三者各自加 instance 水平擴展,Matching Engine 與 consumer 的連線數呈爆炸式成長,而且 Matching Engine 還得負責 MDS/OQS 的 downtime recovery——這會直接侵蝕撮合的即時效能。

方案 做法 優點 缺點
A. MDS/OQS 直 tail 撮合引擎 consumer 直連 Matching Engine WAL 簡單、少一個元件 連線爆炸、撮合得管 consumer 故障
B. 中間插 Kafka pub/sub 撮合 publish event → Kafka → 多 consumer 消費 撮合解耦、consumer 獨立擴展/重放 多一層基礎設施、多一跳延遲

選擇與理由:採 B。Matching Engine 在 append WAL + 更新 in-mem book 後,把 order event publish 到 Kafka一個 event 可被多 consumer(MDS / OQS)同時消費,各服務獨立擴展、各自管自己的 recovery(從 Kafka replay 而非回連撮合引擎)。Kafka market_id 分區,保證同一市場內訂單的 ordering 與 consistency。而 Matching Engine 本身也 market_id sharding——每市場 單一 sequencer,其 shard 的 WAL 即該市場所有訂單的 authoritative event source,故能靠 snapshot + WAL replay deterministic 重建 book。
→ 積木:09-Messaging-Coord/03-Kafka(分區 / ordering / consumer group)、09-Messaging-Coord/01-Data-Pipeline(event 管線 / 重放)、05-Database-Advanced/02-Sharding(market_id 分片)

8.3 容錯:撮合引擎掛了怎麼辦(failover 不丟單)

問題:Matching Engine 是某市場 order book 的 source of truth,一旦掛掉就失去權威市場資料。目標:即使 leader 回應後立刻 crash,已 ack 的 order / fill 一筆都不能丟。
為何是瓶頸:純 in-memory book 一掉電就全沒;單靠本地 WAL,若整台機器死了也救不回。

做法(per shard 配 replica)

1. Client 下單 → primary
2. primary append log entry → 本地 durable log(WAL)
3. primary 透過 TCP 把 WAL entry 送 replicas
4. replicas 套用 WAL 到各自 in-memory state machine
5. ≥1 replica ACK → 才回 client「success」(保證事件至少存活 2 台機器)

failover:
- replica 知道最後完整套用的 WAL sequence
- 一個 replica 被 promote 成新 primary(leader election 用 Raft / ZooKeeper / etcd 防 split-brain)
- 新 primary 以新的 leader epoch 繼續 append WAL
- 其餘 replica follow 新 leader
取捨維度 同步等待的 replica 數
replica 越多 一致性越強(已 ack 存活更多份),但每筆請求 延遲越高
replica 越少 延遲越低,但容錯越弱
本題選擇 至少 1 replica ACK(事件存活 ≥2 台),在 < 200ms 與不丟單之間取平衡

選擇與理由WAL + primary/replica + ≥1 ACK 是「不丟單」的最小代價組合;leader election 交給 共識協議 / ZooKeeper / etcd 防雙主。需要同步幾個 replica 是 latency vs consistency 的旋鈕——本題定在「≥1」以守住 200ms 延遲預算。
→ 積木:05-Database-Advanced/03-Replication(primary/replica / failover)、12-Ops-and-Reliability/03-Reliable-Delivery(ack / 不丟)、09-Messaging-Coord/05-Zookeeper(leader election / 防 split-brain)

8.4 招牌取捨:撮合 append-only、永不回滾

問題:撮合在鏈下已完成(寫進 WAL),但 鏈上結算可能失敗(gas / 合約 revert / 節點問題)。要不要把 off-chain fill 回滾以保持鏈上鏈下一致?
為何重要:這決定了整個系統的一致性語意,是本題的 core_tradeoff

方案 A:鏈上失敗就 rollback off-chain fill 方案 B:append-only,永不回滾(retry / 升級 ops)
鏈上鏈下原子性 ✅ 強原子(要嘛都成、要嘛都退) ❌ 鏈上最終一致(可能短暫不一致,靠 retry 收斂)
ordering 保證 ❌ 回滾破壞已寫入的事件順序 ✅ WAL 嚴格 append,順序不變
狀態可重建性 ❌ 回滾後 replica / 下游 consumer 狀態可能 分歧 ✅ 任何節點都能由 WAL replay deterministic 重建
複雜度 ❌ 需分散式回滾協調,極高 ✅ 簡單:撮合只管 append,結算非同步收斂

選擇與理由:選 B。撮合流程必須 deterministic 且 append-only——一旦回滾,會 破壞 ordering guarantee、增加狀態重建複雜度、導致 replica 與下游 consumer 狀態分歧。所以鏈上失敗時 只 retry 或升級 ops,絕不 rollback 已完成的 off-chain 撮合。實務上再用 settlement batching / 預先 pre-mint positions 讓撮合 不被鏈上吞吐量阻塞。用「鏈上結算只保證最終一致」換「鏈下撮合的確定性與可重建性」——這就是 core_tradeoff
→ 積木:09-Messaging-Coord/01-Data-Pipeline(append-only event log / 重放)、12-Ops-and-Reliability/03-Reliable-Delivery(retry / 最終一致收斂)

9. 擴展與瓶頸 (Scaling & Bottlenecks ~5 min)

主動找出系統會先在哪裂開並補強(訓練 drive 而非被動回答)。

瓶頸 / 風險 補強手段 積木
單一撮合引擎扛不住所有市場 / 尖峰 market_id sharding,每市場單一 sequencer,分散到多 shard 05-Database-Advanced/02-Sharding
MDS/OQS 直連撮合引擎 → 連線爆炸 Kafka pub/sub 解耦,consumer 獨立擴展、自管 recovery(replay) 09-Messaging-Coord/03-Kafka09-Messaging-Coord/01-Data-Pipeline
撮合 leader crash → 失去權威市場資料 primary/replica + ≥1 ACK + WAL;promote replica,Raft/ZK 防 split-brain 05-Database-Advanced/03-Replication09-Messaging-Coord/05-Zookeeper
鏈上吞吐 / gas 阻塞撮合 settlement batching / pre-mint positions;結算非同步、永不回滾撮合 12-Ops-and-Reliability/03-Reliable-Delivery
熱門市場(新聞 / 結算前)價格 fan-out 爆量 每市場一個 Redis Pub/Sub channel,只有有訂閱者的 MDS server 才 SUBSCRIBE 07-Caching-Storage/04-Redis10-Design-Patterns/05-Real-time-Updates
重 UI 查詢污染撮合延遲 OQS materialized 讀模型,所有分頁 / 篩選 / 歷史走 OQS,不打撮合引擎 06-Database-Tech/04-OLTP-vs-OLAP10-Design-Patterns/02-Scaling-Writes
drive 的關鍵句

「系統會 先在『撮合引擎與讀路徑耦合』處裂開——MDS/OQS 直連撮合引擎時連線爆炸、且撮合得管 consumer 故障;所以我用 Kafka 解耦 + 依 market_id sharding。撮合本身的可用性靠 WAL + replica + ≥1 ACK 撐;價格 fan-out 靠 Redis Pub/Sub + WebSocket;鏈上慢則用 batching / pre-mint永不回滾撮合。」先指出最可能裂開處再補強。

10. 取捨總表與收尾 (Trade-off Recap & Wrap-up ~2 min)

決策點 選擇 放棄了什麼 為什麼值得
撮合 vs 鏈上 off-chain 撮合 + on-chain 結算 全鏈上的不可竄改 / 簡單 鏈上慢 / 貴 / 低吞吐,高頻邏輯必須留鏈下
鏈上失敗處理 append-only,永不 rollback fill 鏈上鏈下強原子性 守住 ordering / deterministic / 可重建(招牌取捨
order book 存放 in-memory + WAL 持久化 DB 的簡單與天然持久 撮合 p95 < 200ms,落 DB 不可能達標
讀路徑 CQRS:Kafka → MDS / OQS 直連撮合的即時 / 少元件 重 UI 查詢不可污染撮合延遲;consumer 獨立擴展 / 重放
分片 key 撮合 / Kafka 用 market_id;OQS 用 user_id 單一 key 的一致 兩種存取模式(撮合以市場、查詢以使用者)各取所需
撮合容錯 primary/replica + ≥1 ACK 更高一致(多同步 replica) 在「不丟單」與「< 200ms」之間取平衡
價格推播 HTTP snapshot + WebSocket 增量 純輪詢的簡單 次秒級傳播只能靠 server 主動推
收尾話術

「整題的單一錨點是 撮合 append-only、永不回滾:撮合一旦寫進 WAL 就是唯一事實,鏈上結算失敗也只 retry / 升級,絕不 rollback——因為回滾會毀掉 ordering、可重建性、與下游一致。圍繞這個錨點:order book 在 記憶體 + WAL(< 200ms)、依 market_id 分片(每市場單一 sequencer)、用 Kafka 把讀路徑(MDS/OQS)與撮合解耦(CQRS),價格靠 Redis Pub/Sub + WebSocket 次秒級推送。鏈上慢就 batching / pre-mint,撮合容錯靠 replica + ≥1 ACK。」引用既有決策用連結,不重新推導。

後續追問演練 (Interviewer Follow-ups — "What if…")

為什麼 order book 要放記憶體而不是資料庫?

鏈上結算失敗時,為什麼不回滾已完成的 off-chain 撮合?

MDS 與 OQS 怎麼拿到撮合引擎的資料?

流量放大 100x(撮合尖峰 ~1M orders/sec)哪裡先撐不住?怎麼補?

某個市場突然爆紅(大選結算前),單一市場的撮合 + 價格 fan-out 都壓在一個 shard / channel 上怎麼辦?

撮合 leader 在回應「success」後立刻 crash,已成交的單會丟嗎?

撮合明明要強一致,為什麼 OQS 卻允許延遲?這不衝突嗎?

為什麼撮合 / Kafka 用 market_id 分片,OQS 卻用 user_id

鏈上結算這麼慢,撮合會不會被它卡住?

價格更新怎麼做到「次秒級」傳給數百萬訂閱者?

主動回想:白板重建 (Whiteboard from Scratch)

不看上文,照下面提示把整個設計重建一次。每格都該能 30 秒內口述。

常見陷阱 (Exam Traps for This Problem)