交易所清结算、对账与高可用架构
30 秒版(开场)
交易所首先保证资金正确性,再在明确的不变量内优化性能。架构师需讲清:
实时 posting、持续 + 周期对账、受控链上资产与客户负债、单写者/fencing,以及
经过演练的 RPO/RTO。多副本或“多活”本身不等于不会双写。
3 分钟版(精讲深度)
- 是什么:成交后的资金结转、平台内外账目一致、以及交易/资金系统在故障与扩容下的可用性设计。
- 为什么:交易所事故多源于 对账不平、双花、热钱包超提;核心问题是「如何证明用户的钱是对的」。
- 怎么做:流水只追加并以冲正修正;余额由期初与已提交分录可重建;对账独立
运行。撮合和账务通常保持每个分片单写者,通过 quorum、lease epoch 与 fencing
做主备切换,而不是无约束双活写入。
10 分钟版(原理 + 图示)
清结算链路
flowchart LR
Trade[成交事件] --> Post[实时过账 Posting]
Post --> Journal[(流水 journal 只追加)]
Journal --> Balance[余额快照 / 物化视图]
Balance --> Available[可交易 / 可提余额]
subgraph EOD[日终批处理]
Snap[科目余额快照]
Trial[试算平衡]
Report[监管 / 审计报表]
end
Journal --> EOD
T+0 vs T+1
| 类型 |
交易所常见做法 |
| 现货成交 |
T+0 实时过账,可立即用于交易 |
| 提现 |
T+0 申请 + 链上 T+confirm 块 |
| 法币入金 |
T+1 或渠道确认后入账 |
| 返佣 |
T+0 累计,T+1 或阈值提现 |
三层对账模型
flowchart TB
subgraph L1[第一层:账务内部]
J[借贷平衡] --> T[试算平衡 Σdebit=Σcredit]
end
subgraph L2[第二层:业务↔账务]
OMS[订单/成交] --> J
Fee[手续费科目] --> J
end
subgraph L3[第三层:平台↔外部]
Assets[受控热/温/冷钱包 + 托管机构余额 + 明确在途项]
UserLiab[用户负债合计]
Equity[平台权益/准备金与调整项]
Assets -->|按资产核对| UserLiab
Equity --> Assets
end
L1 --> L2 --> L3
| 对账项 |
频率 |
不平处理 |
| 流水借贷平衡 |
实时 / 5min |
熔断过账 |
| 成交 vs 账务 |
15min |
补单 / 冲正 |
| 受控总资产 vs 用户负债/平台权益 |
持续增量 + 周期全量 |
按资产/链隔离,无法解释或超 materiality 时暂停相关提现 |
| 链上充值 vs 入账 |
每块 |
延迟入账 |
详见 S-EXCH-05、S-EXCH-03
高可用拓扑(CEX + Web3 托管)
flowchart TB
subgraph AZ1[可用区 A]
ME1[撮合 BTC-USDT Primary]
API1[API Go x N]
end
subgraph AZ2[可用区 B]
ME1S[撮合 BTC-USDT Standby 冷备]
API2[API Go x N]
end
LB[Global LB / DNS]
MQcls[MQ 集群 3 副本]
DBpri[(MySQL 主)]
DBrep[(只读副本)]
LB --> API1
LB --> API2
ME1 -->|WAL 复制| ME1S
API1 --> MQcls
API2 --> MQcls
MQcls --> DBpri
DBpri --> DBrep
RPO / RTO 讲解模板(以下数字只能作为需求示例,必须由复制与演练证明)
| 组件 |
RPO |
RTO |
手段 |
| 撮合 WAL |
目标 0;只有同步 quorum/复制确认后才能声称 |
分钟级示例 |
fencing 旧主、切换 epoch、快照 + 日志重放 |
| 账务 DB |
目标 0~秒级 |
15~30 min 示例 |
同步/半同步复制、备份恢复、事务日志与对账 |
| 行情 WS |
可丢秒级 |
自动 |
多实例无状态 |
| Indexer 游标 |
块级 |
重扫 |
游标 + 补块 |
故障域与降级
| 故障 |
降级策略 |
| 单 symbol 撮合挂 |
仅停该交易对;其他不受影响 |
| 账务 consumer lag |
暂停新开仓 / 暂停提现 |
| RPC 全挂 |
停链上提现;站内交易可继续(CEX) |
| 对账不平 |
先按资产/链/账户隔离;重大或无法定位时启用全局提现 kill switch |
| MQ 不可用 |
各事实源继续写本地 WAL/Outbox 到容量上限;恢复后续传。若账务/风险 lag 破坏安全阈值,应停止相关接单 |
生产场景
- 热钱包被盗/疑似失陷:立即隔离 signer、停止向该地址补资、轮换权限/地址、
暂停相关链提现并追踪;不能把冷钱包继续补到疑似被控的热钱包
- 重复消费成交 → 数据库唯一约束 + 幂等 posting 实时阻断;周期对账是第二道防线
- MySQL 主从延迟 → 提现 reservation 只读权威主库/强一致账务接口;不要另维护一份
无流水约束的“提现可用余额”
- 大促扩容:API/WS 水平扩;撮合可重新分配不同 symbol,同一订单簿不能简单加写实例
排查与工具
- Metrics:
ledger_posting_latency、reconciliation_diff、withdraw_pending_count
- 审计:流水表不可 UPDATE;操作人 + 审批流
- 工具:Grafana 对账大盘、链上余额脚本、Metabase 监管报表
架构取舍
| 方案 |
何时选 |
| 单库账务 |
写入量、锁竞争、恢复时间和合规隔离均满足时;不以用户数单指标决策 |
| 按 user_id 分库 |
账务 QPS 瓶颈 |
| 单元化(用户全链路同单元) |
超大规模、监管分区 |
| 链上 Merkle 证明储备 |
提供某一时点资产/负债包含性的证据;还需证明地址控制、负债完整性、隐私/负余额处理,且不能单独证明持续偿付能力 |
深挖问答
- 为什么流水不能改只能冲正? → 审计与监管;改账不可追溯。
- 用户余额存在哪? → 流水为准;余额表是缓存,可重建。
- 提现双重扣款怎么防? → 账务数据库唯一业务键 + 原子 reservation、持久化
状态机、签名 payload 幂等和 nonce/UTXO reservation。分布式锁只能辅助串行化,
不能替代这些事实约束(S-DIST-02)。
- 撮合和 DB 同时挂? → 先 fence 旧主并确定最后 durable epoch/sequence,再恢复
快照 + WAL、核对事件发布 offset 和账务已过账位置;对账通过前保持相关市场受限。
- Web3 所 PoR 和账务关系? → PoR 证明储备 ≥ 负债;不能替代复式记账。
反模式与事故
- 余额字段直接 UPDATE 无流水 → 无法对账
- 对账脚本与过账同进程 → 互相拖死
- 热钱包余额人工改库 → 刑事级风险
- 无全局提现熔断 → 对账不平仍放行
代码示例
// 示意:每个 Transfer 都在同一资产内从一个科目转到另一个科目,
// from/to 两侧金额相等;真实实现还要处理冻结科目、手续费与舍入。
func PostTrade(ctx context.Context, tx *gorm.DB, t TradeEvent) error {
transfers := []LedgerTransfer{
{
Asset: "USDT", From: reserved(t.BuyerID), To: available(t.SellerID),
Amount: t.QuoteAmount, Ref: t.TradeID + ":quote",
},
{
Asset: "BTC", From: reserved(t.SellerID), To: available(t.BuyerID),
Amount: t.BaseAmount, Ref: t.TradeID + ":base",
},
}
return postTransfersIdempotent(tx, transfers) // unique(ref),同事务更新 journal + balance
}
延伸阅读
相关链接