Database per Service 与跨服务数据一致性¶
30 秒版(开场)¶
Database per Service 的核心是 数据写入所有权,不要求每个服务必须独占一台 数据库。跨 owner 的流程用本地事务、Outbox/日志发布、幂等、Saga 和持续对账; 资金 reservation 的强一致边界必须先定义,不能把所有问题都笼统归为“最终一致”。
3 分钟版(精讲深度)¶
- 是什么:每个微服务独占数据库 schema/实例,数据通过 API 或事件协作。
- 为什么:独立演进与故障隔离,并防止其他服务绕过账务规则写表。模块化单体 或共享物理数据库也可以正确实现资金系统,关键是 owner、事务边界和容量证据, 不是“单体库必然锁竞争”。
- 怎么做:本地事务写业务表 + Outbox;MQ 投递;下游幂等消费;定时对账修差异。
10 分钟版¶
库归属(交易所)¶
订单与账务:各自持有权威库¶
flowchart TB
subgraph order_db
Orders[orders]
Outbox1[outbox_events]
end
subgraph ledger_db
Journal[journal_entries]
Balance[balance_snapshot]
end
OMS[order-svc] --> order_db
ME[matching-svc] -->|TradeEvent MQ| Ledger[ledger-svc]
Ledger --> ledger_db
ME -.->|不写| ledger_db
钱包与索引:事件连接上下文¶
flowchart LR
subgraph wallet_db
Withdraw[withdrawals]
ChainTx[chain_tx]
end
subgraph indexer_db
Watermark[scan_watermark]
Events[chain_events]
end
Wallet[wallet-svc] --> wallet_db
Wallet -->|DepositEvent| Ledger[ledger-svc]
Idx[indexer-svc] --> indexer_db
Idx -->|SwapEvent| Kline[kline-svc]
典型跨服务流程¶
CEX:成交 → 账务¶
- matching-svc 将已排序命令/结果持久化到可恢复日志,由 publisher 从该日志续传
trade.matched;避免把“写 WAL + 直接发 MQ”做成无恢复点的双写 - ledger-svc 消费,幂等
trade_id写 journal - 失败时重试并隔离 poison event/相关分区,告警后从原事件重放;不能丢进 DLQ 后继续处理依赖其顺序的同账户事件
CEX:提现 Saga¶
sequenceDiagram
participant API
participant Wallet
participant Ledger
participant Chain
API->>Wallet: CreateWithdraw
Wallet->>Ledger: gRPC Freeze
Ledger-->>Wallet: OK
Wallet->>Chain: BroadcastTx(payloadHash)
alt 链上成功并达到结算条件
Chain-->>Wallet: confirmed
Wallet->>Ledger: SettleDeduct
else 明确终态失败且确认无有效替代交易
Wallet->>Ledger: Unfreeze
else RPC 超时/状态未知
Wallet->>Wallet: 保持 reservation,按 tx/nonce/UTXO 查询与恢复
end
DEX:Indexer → K 线 → 返佣¶
indexer_db幂等(chain_id, tx_hash, log_index)- kline-svc 只读 Swap 事件,写
kline_db - rebate-svc 消费同一事件或独立
rebate_db
一致性模式选型¶
| 场景 | 模式 | 说明 |
|---|---|---|
| 成交入账 | 事件 + 幂等 | 至少一次消费 |
| 提现 | Saga | 补偿解冻 |
| 充提+账务 | Saga + 各服务本地 Outbox | Outbox 只原子覆盖本服务数据库,不能让两个服务共享一个“分布式 Outbox” |
| 资产/负债对账 | 实时增量 + 周期全量 | 检测和隔离差异;对账不是用来掩盖缺失的一致性设计 |
参见 S-DIST-05
查询怎么办(无 JOIN)¶
| 需求 | 方案 |
|---|---|
| 用户资产总览 | BFF 聚合权威余额与链上读模型,并返回 asOf/数据新鲜度;多个独立响应不天然是同一时点快照 |
| 订单+成交 | order-svc 存快照;或 CQRS 读库 |
| 管理后台报表 | ES / 数仓同步事件 |
生产场景¶
- MQ 重复消费:ledger 侧
uk(trade_id, entry_type) - Indexer reorg:发布带原 event id、orphaned block hash 和新 canonical version 的
reversal/recompute 指令;不是所有 RPC/历史回补都会直接给出
removed=true - 对账不平:暂停提现(S-EXCH-15)
深挖问答¶
- 能否共享一个 MySQL 实例多 schema? → 可以,但 逻辑上 仍 per-service;避免跨 schema 事务。
- 强一致下单扣款? → 若 ledger 是余额权威源,就在接受订单前幂等 reserve; 也可把“可交易余额 + reservation”放进同一交易账户聚合并由账务消费其确定性 资金事件。不能在 order_db 自说“已冻结”,再异步通知 ledger,却仍宣称强一致。
- 与 2PC/XA? → 是否采用取决于参与数据库、延迟预算、故障恢复与团队能力; 常见交易热路径更偏好本地事务 + durable event,但不应把 XA 说成行业绝对禁用。
反模式¶
- ledger 开放 SQL 给 order-svc
- 无 Outbox 直接「写库后发 MQ」
- DEX 索引与返佣同一事务跨聚合