前言

當你接受了 In-Memory + Event Sourcing + MQ 這套思路之後,下一個現實問題就來了:資料量到底怎麼扛? 幣安、OKX 類型的交易所不是只有訂單表很大而已,而是訂單、成交、資產流水、風控事件、行情快照、K 線、稽核紀錄都會一起爆炸,而且不同資料的成長速度完全不同。

這也是為什麼大型交易所一定會走向 PartitionSharding、冷熱資料分層,以及分角色的資料模型設計。若沒有把這一層拆清楚,再好的撮合引擎都會被下游儲存拖垮。

本文會用交易所最常見的三種切分維度來講解:依交易對、依使用者、依時間。你也會看到 MySQL 與 PostgreSQL 在這個問題上各自擅長的方向,以及它們在擴展時最容易踩到的坑。

系列文章導航

  1. 撮合引擎、In-Memory、Ring Buffer 與批次處理
  2. Event Sourcing、Outbox Pattern、Message Queue 與一致性
  3. Partition、Sharding 與 MySQL / PostgreSQL 擴展策略(本篇)
  4. Leader Election、高可用切換與跨服務協調
  5. MySQL、PostgreSQL 的擴展、調校與效能優化

Partition 與 Sharding 到底差在哪裡

這兩個詞常常一起出現,但不要混在一起看。

Partition

Partition 通常指的是在同一個資料庫實例內,把一張大表拆成多個分區。常見目的包括:

  • 讓查詢只掃部分資料
  • 讓歷史資料容易歸檔
  • 降低單一索引膨脹速度
  • 加速刪除舊資料

例如在 MySQL 或 PostgreSQL 中,針對 trade_eventscreated_attrading_dayRANGE PARTITION,就是典型 partition。

如果你是把資料拆成 trade_events_20260406trade_events_20260407 這種獨立資料表,那比較接近 table-per-day 歷史拆表策略,不能和「同一張表內的原生分區功能」完全畫上等號。

Sharding

Sharding 通常指的是把資料切到不同資料庫節點或不同叢集。常見目的包括:

  • 撐更大的總容量
  • 分散寫入壓力
  • 隔離熱門工作負載
  • 讓不同機器各自承擔部分流量

例如把使用者帳務依 user_id 分到 64 個 shard,就是典型 sharding。

兩者的關係

在大型交易所裡,兩者往往是一起使用的:

  • 先用 sharding 把資料分散到多台節點
  • 再在每個 shard 內用 partition 管理時間維度或歷史資料

所以比較準確的理解是:partition 是單節點內部整理資料,sharding 是跨節點水平擴展資料。


交易所最常見的三種切分維度

不同資料不應該用同一種 shard key,這是交易所場景最容易被忽略的一點。

1. 依交易對切分

適合的資料:

  • order book
  • 撮合事件
  • 市場行情
  • 深度快照

原因很直觀:同一個交易對的撮合順序必須穩定,放在同一條處理鏈路最容易保證順序。

2. 依使用者切分

適合的資料:

  • 帳戶資產
  • 充提紀錄
  • 風控資料
  • 使用者查詢模型

因為使用者查自己帳戶時,天然以 user_id 為主要維度。

3. 依時間切分

適合的資料:

  • 成交明細
  • K 線資料
  • 稽核日誌
  • outbox / event store

因為這類資料通常量大、讀寫模式偏 append-only,且很常需要做保留期管理與歸檔。

一張圖看懂資料切分邏輯

flowchart TD
    A[交易所資料] --> B[撮合與行情]
    A --> C[帳戶與資產]
    A --> D[歷史事件與報表]

    B --> B1[依 Symbol 切分]
    C --> C1[依 UserID 切分]
    D --> D1[依 Time 切分]

這張圖最想傳達的不是架構圖漂亮,而是:同一個平台的不同資料模型,應該有不同的切分策略。


熱門交易對為什麼會毀掉你的平均分布

很多人做 sharding 時會先想「平均分配」,但交易所實務中常見的麻煩不是平均,而是極度不平均

例如:

  • BTC/USDTETH/USDT 這類熱門交易對流量遠高於其他交易對。
  • 牛市或暴跌時,少數熱門市場會瞬間吃掉整個叢集大半資源。
  • 合約市場的熱門標的通常比冷門標的熱很多。

這表示即使你有 100 個 shard,也可能只有 3 個 shard 被打爆。

Hot Partition 問題的典型對策

  • 把超熱門 symbol 獨立拆到專屬節點
  • 熱門現貨與熱門合約市場分開部署
  • 讓行情與撮合的讀取負載分流
  • 對超熱 symbol 使用更細的資源隔離策略

這也是為什麼大型交易所的資源分配很少是一開始就完全平均,而是會根據實際市場熱度持續調整。


Redis 事前扣留也有自己的分片與 Hot Key 問題

如果你把餘額檢查與資金扣留前移到 Redis,資料庫鎖競爭確實會下降,但新的分片問題會立刻出現。

最常見的幾種新熱點

  • 熱門使用者或大戶帳戶,會變成單一 account / asset hot key。
  • 槓桿、全倉、組合保證金帳戶,天然就比較容易牽涉多資產狀態。
  • 如果在 Redis Cluster 裡用 Lua Script 做原子扣留,key 還必須落在同一個 hash slot。

這意味著 reservation key 的設計通常不能只看可讀性,而要考慮 co-location。例如常見做法會使用:

  • balance:{account_id}:USDT
  • reservation:{account_id}:order_123

如此一來,同一個帳戶相關 key 才能被同一段 Lua Script 原子操作。

不要把全站餘額想成一個全域單執行緒狀態機

「單一執行緒狀態機」在教學上很直觀,但如果把全站用戶、所有資產、所有保證金帳戶都塞進同一個全域 worker,擴展性與故障半徑都會很差。

實務上更常見的是:

  • user_idaccount_id 或資產域做分片
  • 每個 shard 各自維護自己的記憶體狀態與 reservation
  • 盡量把跨 shard 操作壓縮到少數明確場景

也就是說,完整 Event Sourcing 或記憶體餘額核心並不等於「整個世界只剩一個單執行緒」。大多數時候,它仍然是分片後的多個局部單寫者


Go 示例:在應用層做簡單的分片路由

下列程式碼只是示意,目的是讓你理解「交易所往往不只一種 shard key」這件事:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
package routing

import "hash/fnv"

type Router struct {
MatchingShardCount int
LedgerShardCount int
}

func (r Router) MatchingShard(symbol string) int {
h := fnv.New32a()
_, _ = h.Write([]byte(symbol))
return int(h.Sum32()) % r.MatchingShardCount
}

func (r Router) LedgerShard(userID uint64) int {
return int(userID % uint64(r.LedgerShardCount))
}

這段程式碼表面上很簡單,但背後其實就是交易所常見的路由思維:

  • 撮合資料可能依 symbol 分配。
  • 帳務資料可能依 user_id 分配。
  • 歷史事件表還會再依 date 做 partition。

也就是說,交易所不會期待一把萬用 shard key 解決所有問題。


MySQL 在交易所分片與分區上的常見打法

1. 把 MySQL 當高吞吐 OLTP 核心

如果你的模型偏向:

  • 欄位相對固定
  • 查詢路徑清楚
  • 主鍵查詢很多
  • 需要成熟的分庫分表與代理層工具

那 MySQL 往往是非常務實的選擇。

2. 交易表常做時間分區

例如以 created_attrading_dayRANGE PARTITION,或在歷史資料治理上搭配按日拆表策略,優點包括:

  • 舊資料清理方便
  • 歷史查詢只掃部分分區
  • 單分區索引比較不容易失控

3. 搭配 ProxySQL 做路由、Vitess 做更完整的 sharding 治理

MySQL 在大型平台常見優勢是生態成熟:

  • ProxySQL 可以做讀寫分流、查詢規則與路由輔助,但它不是完整的分片控制平面
  • Vitess 更接近大規模的分片治理、resharding 與 topology 管理工具鏈
  • Group ReplicationInnoDB Cluster 等 HA 解法相對成熟

4. MySQL 需要特別注意的地方

  • 分區表的唯一鍵設計要和分區鍵一起思考。
  • 太多二級索引會直接放大寫入成本。
  • 完全亂序的主鍵會讓 InnoDB page split 壓力變大。
  • 跨 shard 交易成本高,應用層需要更明確的資料歸屬設計。

簡單說,MySQL 在交易所場景中常見的優勢是穩定、成熟、工具多、OLTP 很強;但前提是你要願意在應用層做好路由與跨 shard 邏輯。


PostgreSQL 在交易所分區與擴展上的常見打法

1. 用 declarative partitioning 管理大表

PostgreSQL 對時間分區表、歷史事件表、審計表很有發揮空間。當你的資料天生就適合依時間管理時,例如:

  • trade_events
  • ledger_entries
  • audit_logs
  • outbox_events

PostgreSQL 的 partition pruning 會非常有價值。

2. 對複雜查詢與稽核場景更友善

若你經常需要:

  • 追查某個交易事故整條因果鏈
  • 針對事件 payload 做條件查詢
  • 做 ad-hoc investigation
  • 同時查 relational 欄位與 JSON payload

那 PostgreSQL 通常比 MySQL 更舒服。

3. 配合 Citus 或 logical replication 做更大規模擴展

PostgreSQL 近年在水平擴展上進步很快,常見方向包括:

  • Citus 讓 distributed table 與 coordinator 模式更容易管理
  • logical replication 把讀模型或分析模型分出去
  • pg_partman 等工具協助分區維護

4. PostgreSQL 需要特別注意的地方

  • 熱更新表如果沒有控制好 vacuum,容易膨脹。
  • 每連線資源成本相對高,通常要搭配連線池。
  • 若濫用 JSONB + GIN 索引,寫入成本也會很可觀。
  • 分區策略與唯一鍵、索引策略要一起設計,不能只看查詢方便。

簡單說,PostgreSQL 在交易所場景中常見的優勢是查詢彈性高、稽核能力強、資料模型表達力好;但要撐住超高寫入量,維運與調校也必須跟上。


MySQL 與 PostgreSQL 的差異可以怎麼看

題目 MySQL PostgreSQL
大規模分片生態 很成熟,工具鏈完整 持續進步,但選型較分散
複雜查詢 夠用且進步很多 通常更強、更靈活
稽核與事件檢索 可行,但設計偏保守 表達力通常更好
高吞吐 OLTP 很強 也強,但要更注意 vacuum / WAL / 連線
彈性資料模型 偏向固定 schema 較舒服 JSONB 與進階索引很有優勢

如果你問「大型交易所該選誰」,答案通常不是一句話,而是看:

  • 你最核心的瓶頸是高寫入還是複雜查詢?
  • 團隊更熟 MySQL 分片生態,還是 PostgreSQL 維運生態?
  • 你要的是單一資料庫標準化,還是針對不同服務做更細緻選型?

冷熱分層、歸檔與分析系統為什麼也屬於這個主題

大型交易所不只要解決線上查詢,還要解決「一年後資料還在不在、查不查得到」。因此常見做法是:

  • 熱資料留在主 OLTP 資料庫
  • 溫資料留在歷史分區或較便宜的節點
  • 冷資料進歸檔儲存或分析系統

例如:

  • 最近 7 天成交明細在主庫
  • 30 天以上資料移到歷史庫
  • 長期報表進資料倉儲或分析引擎

這不是額外題外話,而是 partition / sharding 做到最後必然會碰到的現實。


總結

交易所在面對上億筆資料時,真正有效的做法不是只喊「我要分庫分表」,而是先搞清楚:

  1. 哪些資料應該依 symbol 切。
  2. 哪些資料應該依 user_id 切。
  3. 哪些資料應該依 time 切。
  4. 哪些切分是 partition,哪些切分是 sharding。

到了這一步,MySQL 與 PostgreSQL 的差異也更清楚了:MySQL 在大規模 OLTP 與成熟分片工具鏈上很有優勢;PostgreSQL 在複雜查詢、事件檢索與資料表達力上更突出。

下一篇我們會處理另一個更接近生產事故的問題:當多個節點都想接手同一個工作時,誰是 leader?故障切換時怎麼避免 split brain? 這就會進到 Leader Election 與高可用協調設計。

系列文章導航

  1. 撮合引擎、In-Memory、Ring Buffer 與批次處理
  2. Event Sourcing、Outbox Pattern、Message Queue 與一致性
  3. Partition、Sharding 與 MySQL / PostgreSQL 擴展策略(本篇)
  4. Leader Election、高可用切換與跨服務協調
  5. MySQL、PostgreSQL 的擴展、調校與效能優化