本文从金融系统的特殊约束出发,深入讲解金融级分布式系统的设计实践:双时序数据库保证数据正确性、事件溯源保证计算可审计、账务系统的复式记账模型、支付系统的幂等设计、分库分表的动态扩容、跨机房容灾的两地三中心与三地五中心,以及金融系统与互联网系统在设计哲学上的根本差异。
相关文章:分布式一致性 · 分布式事务 · 分布式协调与选举 · 分布式系统设计模式
目录
| 章节 | 说明 |
|---|---|
| 金融系统的特殊约束 | 与互联网系统的根本差异 |
| 数据正确性:双时序数据库 | 发生时间 vs 记录时间,解决数据修改问题 |
| 计算正确性:不可变架构 | 事件溯源的金融实践,审计与时光机 |
| 账务系统设计 | 复式记账、流水账、余额账 |
| 支付系统:幂等与分布式事务 | TCC 在支付中的应用 |
| 对账系统设计 | 批量对账 vs 实时对账 |
| 分库分表:金融场景的挑战 | 动态分库、跨分片事务、全局流水号 |
| 容灾:两地三中心与三地五中心 | 数据中心级 vs 城市级容灾 |
| 合规与审计日志设计 | 金融监管对系统的要求 |
金融系统的特殊约束
金融系统和互联网系统的本质区别是:互联网系统追求可用性(AP),金融系统追求正确性(CP)。一旦涉及钱,错误的代价是不可控的。
金融 vs 互联网:设计哲学对比
| 维度 | 互联网系统 | 金融系统 |
|---|---|---|
| 核心目标 | 高可用、高并发、低延迟 | 正确性、零丢失、可审计 |
| 一致性要求 | 最终一致性(BASE) | 强一致性(ACID),部分场景线性一致 |
| 数据丢失 | 可接受极小概率丢失 | 零容忍,任何一分钱都不能丢 |
| 数据修改 | 直接 UPDATE | 只追加,不修改(双时序数据库) |
| 错误处理 | 降级、熔断、返回缓存数据 | 拒绝服务,不返回可能错误的数据 |
| "快"的定义 | 吞吐量大(支撑大量用户) | 延迟低(机构交易,时间就是金钱) |
| 系统寿命 | 几年迭代一次 | 可能运行几十年,需要长期可维护 |
| 边际成本 | 接近零(互联网大规模扩张的基础) | 很高(KYC、合规、风控流程成本高) |
金融系统的三层正确性要求
第一层:计算输入正确(正确时间的数据)
→ 双时序数据库:发生时间 + 记录时间
第二层:计算过程正确(可重现、可审计)
→ 事件溯源:命令→事件→状态,全程可追溯
第三层:计算结果正确(独立验证)
→ 异步对账:日切 + 对账系统
数据正确性:双时序数据库
核心问题:金融数据会被修改(如通货膨胀率事后修正),但历史合同必须使用签订时刻的数据。如何保证"过去的计算"不受"未来的修正"影响?
单时序 vs 双时序
| 类型 | 解决的问题 | 限制 |
|---|---|---|
| 单时序数据库 | 数据的增加(时间序列) | 无法处理数据修改 |
| 双时序数据库 | 数据的增加 + 修改 | 学习成本高,查询慢 |
两个时间维度
- 发生时间(Valid Time,VT):数据对应的业务发生时间(如:2018年3月的通胀率)
- 记录时间(Transaction Time,TT):数据被录入系统的时间(如:2018年4月录入)
坐标系(行业惯例):
纵轴 = 发生时间 (VT)
横轴 = 记录时间 (TT)
每个数据点 = (记录时间, 发生时间) = 坐标系上的一个点
数据的可见范围 = 该点右上方的矩形区域
查询语义
查询双时序数据库时,需要同时提供两个时间:
查询语义:
"坐上时光机回到记录时间 T1,查询在发生时间 T2 以前已生效的、
离 T2 最近的那条数据"
→ 解决了:
1. 30年前合同用的什么数据?(固定记录时间=签约时)
2. 如果修正了某月数据,对历史合同的影响是什么?(固定发生时间,调整记录时间)
优缺点
| 优点 | 缺点 |
|---|---|
| 数据不变性(不可被覆盖篡改) | 学习成本高(所有人都要理解二维时间) |
| 数据唯一性(每条数据有唯一坐标) | 查询速度慢(多一个时间维度的索引) |
| 支持情景分析(What-if 分析) | 不适合高频交易(股票、外汇) |
适用场景:场外交易(债券、期权、资产证券化等)交易量相对小但合同复杂的业务。高频交易(股票、外汇)因性能要求不使用双时序数据库。
金融数据存储选型总览
| 数据类型 | 推荐存储 | 原因 |
|---|---|---|
| 市场数据(股价、汇率) | 列存储时序数据库(KDB/ClickHouse) | 列存储适合按时间段聚合计算 |
| 业务合同数据 | 双时序数据库 | 需要追溯历史时刻的数据 |
| 交易流水 | 关系型数据库 | 事务支持,行级操作 |
| 风控/反洗钱 | 图数据库 | 关系网络查询 |
计算正确性:不可变架构
金融系统不仅要求"结果正确",还要求"能证明结果为什么正确"——这是监管合规的要求。事件溯源(Event Sourcing)是实现这一要求的核心架构。
不可变架构的核心原则
金融系统普遍采用不可变架构(Immutable Architecture):
互联网系统:记录当前状态(UPDATE users SET balance = 99 WHERE id = 1)
金融系统:记录导致状态变化的事件(INSERT INTO events VALUES ('扣款', 1, 100))
当前余额 = 所有事件的累积效果
为什么不可变:
- 数据一旦生成不能修改,防止篡改
- 任何"修改"都是新增一条修正记录
- 历史状态随时可重现(审计需要)
事件溯源的金融实践
命令 → 事件 → 状态 的严格约束:
命令(Command):外部指令,可能被拒绝
例:转账 200 元(但余额只有 100 元)→ 被拒绝,不生成事件
事件(Event):已验证的事实,一定执行,用过去式命名
例:已转账 100 元(transferred 100)
状态(State):所有事件的累积
例:余额 = 初始值 + 所有存款事件 - 所有取款事件
自动机的约束(不可违反,否则无法审计):
- 不能有随机数:用伪随机数,并把种子记录到事件中
- 不能有外部 I/O:所有外部输入必须先存入事件队列,再由自动机读取
- 事件必须有全序:使用 FIFO 队列保证顺序
时光机与快照
flowchart LR
E["事件队列<br/>e1 → e2 → e3 → ... → en"] --> SM["状态机<br/>(自动机)"]
SM --> S["当前状态 Sn"]
SM --> SNAP["快照<br/>(某时刻的完整状态)"]
SNAP --> R["恢复<br/>快照 + 快照后的事件"]
时光机的价值:
- 监管要求:能重现任意历史时刻的系统状态
- 容灾恢复:机器宕机后,从最近快照 + 增量事件恢复
- 错误排查:发现计算错误时,可回溯到错误发生前的状态重新计算
日切(End of Day):金融系统每天 0 点的快照,是天然的快照时机。月结、季结、年结复用日切快照。
分布式事件溯源:复制状态机
单机事件溯源无法容灾,分布式扩展时使用复制状态机(Replicated State Machine):
关键:用共识算法(Raft)复制的是事件队列,不是命令队列
原因:命令→事件的转换可能有随机性,不同节点可能产生不同事件
但事件→状态的转换是确定性的,所有节点从相同事件得到相同状态
CQRS 在分布式场景的应用:
- 写节点:主节点处理命令,生成事件,通过 Raft 同步到所有节点
- 读节点:从任意节点复制事件队列,维护独立的读模型(可以是不同的数据结构)
- 一致性读:将查询作为空命令走 Raft 共识路径,利用线性一致性
账务系统设计
复式记账(Double-Entry Bookkeeping)
500 年前威尼斯商人发明的记账方法,现代金融系统的基础。每一笔交易必须同时影响两个账户,且借贷必须相等。
核心原则:借方(Debit)= 贷方(Credit)
转账 100 元(用户 A → 用户 B):
用户 A 账户:借方 +100(余额减少)
用户 B 账户:贷方 +100(余额增加)
验证:借方总额 = 贷方总额 = 100 ✓
为什么金融系统用复式记账:
- 内置正确性校验:如果借贷不平衡,必然有错误
- 完整的资金流向追踪:每一分钱都有来源和去向
- 满足会计准则和监管要求
三种账务模型
| 模型 | 存储内容 | 优点 | 缺点 | 适用场景 |
|---|---|---|---|---|
| 余额账 | 当前余额快照 | 查询余额快 | 无历史,无法对账 | 不推荐单独使用 |
| 流水账 | 每笔交易记录 | 完整历史,可对账 | 查询余额需扫描全量 | 与余额账配合使用 |
| 复式记账 | 每笔交易的借贷双方 | 内置校验,满足会计准则 | 复杂度高 | 正规金融系统 |
生产实践:流水账 + 余额账配合使用:
- 写入时:同时写流水表(明细)+ 更新余额表(快照)
- 查询余额:直接读余额表(O(1))
- 对账时:用流水表重新计算余额,与余额表比对
账务系统的幂等设计
问题:转账接口被重复调用(网络重试),导致重复扣款
解决:
1. 每笔交易分配全局唯一流水号(订单号)
2. 数据库对流水号建唯一索引
3. 重复请求因唯一索引冲突被拒绝,返回"已处理"
支付系统:幂等与分布式事务
支付链路的分布式事务挑战
一笔支付涉及多个系统:
用户下单 → 订单系统(创建订单)
→ 库存系统(扣减库存)
→ 支付系统(扣款)
→ 账务系统(记账)
→ 通知系统(发短信)
每个系统都可能失败,需要保证"要么全部成功,要么全部回滚"。
TCC 在支付中的应用
支付场景适合 TCC,原因:
- 金额操作需要较强隔离(不能让其他人看到"预扣中"的资金)
- 操作时间短(秒级),符合 TCC 的适用场景
支付 TCC 示例:
sequenceDiagram
participant S as 支付协调者
participant A as 用户账户
participant B as 商家账户
Note over S,B: Try 阶段
S->>A: 冻结 100 元(余额 → 冻结金额)
S->>B: 预增 100 元(创建待入账记录)
Note over S,B: Confirm 阶段(全部 Try 成功)
S->>A: 扣减冻结金额(正式扣款)
S->>B: 确认入账(正式到账)
Note over S,B: Cancel 阶段(任意 Try 失败)
S->>A: 解冻金额(恢复余额)
S->>B: 删除待入账记录
TCC 的三个陷阱(必须处理):
| 陷阱 | 场景 | 解决方案 |
|---|---|---|
| 空回滚 | Cancel 在 Try 之前到达(网络乱序) | Cancel 检查 Try 是否执行,未执行则直接返回成功 |
| 悬挂 | Try 在 Cancel 之后到达 | 记录"已取消"标记,拒绝后续 Try |
| 幂等 | Confirm/Cancel 被重复调用 | 用唯一事务 ID 做幂等控制,已处理则直接返回成功 |
消息顺序性保证
金融系统对消息顺序有严格要求(转账 A→B 和 B→A 顺序不同,结果可能不同):
保证顺序的三个要求:
1. 不乱序:自增 ID + 消息缓冲区(类似 TCP 的序列号机制)
2. 不丢失(至少一次):消息持久化 + 重发机制 + 接收方幂等
3. 不重复(至多一次):唯一 ID 去重 + 幂等处理
实现:本地消息表(Outbox Pattern)+ 消费方幂等
对账系统设计
对账是金融系统的"最后一道防线"——即使实时处理有问题,对账能发现并修复。
日切对账(批量对账)
每天 0 点:
1. 停止业务(日切窗口,通常几分钟到几小时)
2. 打系统快照
3. 汇总当日所有流水
4. 与外部系统(银行、清算机构)的流水对比
5. 发现差异 → 人工处理或自动补账
6. 开始新的一天
对账的三种差异类型:
| 差异类型 | 含义 | 处理方式 |
|---|---|---|
| 长款 | 我方余额多于对方 | 退款或等待对方补录 |
| 短款 | 我方余额少于对方 | 补账或追查原因 |
| 时间差 | 对方已记录,我方未记录(或反之) | 等待下一个对账周期确认 |
实时对账
实时对账不等待日切,而是对每笔交易实时比对:
适用场景:高价值交易(大额转账、衍生品交易)
实现方式:
- 交易完成后立即发送对账消息给对方
- 对方确认后标记为"已对账"
- 超时未确认的交易进入人工处理队列
批量对账 vs 实时对账:
| 维度 | 批量对账 | 实时对账 |
|---|---|---|
| 延迟 | 最长 24 小时 | 秒级 |
| 系统复杂度 | 低 | 高 |
| 适用场景 | 零售支付、小额交易 | 机构交易、大额资金 |
| 资金占用 | 较长时间占用 | 快速释放 |
分库分表:金融场景的挑战
为什么金融系统分库分表更难
互联网系统分库分表的主要目标是吞吐量,可以接受最终一致性。金融系统分库分表面临额外挑战:
挑战 1:跨分片事务
- 转账从分片 A 的账户到分片 B 的账户
- 需要分布式事务,但分布式事务有性能代价
挑战 2:全局流水号
- 每笔交易需要全局唯一且有序的 ID
- 分库后不能依赖数据库自增 ID
- 解决方案:雪花算法 / 号段模式
挑战 3:对账复杂度
- 数据分散在多个分片,对账需要跨分片汇总
- 跨分片聚合查询性能差
动态分库:基于事件溯源的方案
传统分库需要停机,金融系统要求不停机分库。基于事件溯源架构的动态分库方案:
flowchart TD
A["分库前<br/>集群 A(全量数据)"] -->|"1. 新集群 B 以读模式<br/>同步集群 A 的数据"| B["集群 B<br/>(同步中)"]
B -->|"2. 数据差距缩小到秒级"| C["协调者发出分库命令"]
C -->|"3. 集群 A 写入分库命令<br/>更新内部配置"| D["集群 A<br/>(只处理自己的分片)"]
C -->|"4. 集群 B 读到分库命令<br/>更新内部配置"| E["集群 B<br/>(处理新分片)"]
关键设计:
- 分库命令写入事件队列(不可变),通过共识算法同步到所有节点
- 命令与业务逻辑无关(只记录"谁处理哪些数据")
- 集群 A 不停机,整个过程对业务影响仅几秒(集群 B 同步分库命令的时间)
分片策略选择
| 策略 | 优点 | 缺点 | 金融场景适用性 |
|---|---|---|---|
| 按哈希分片 | 数据分布均匀,防止热点 | 范围查询性能差 | 账户数据(随机访问) |
| 按范围分片 | 范围查询友好 | 可能产生热点(最新数据集中在一个分片) | 时序数据(按时间查询) |
| 按业务分片 | 同一业务数据在一起,减少跨片事务 | 需要预判业务边界 | 大客户独立分片 |
容灾:两地三中心与三地五中心
正确性的定义
金融系统容灾的"正确"包含两层含义:
| 要求 | 含义 | 违反代价 |
|---|---|---|
| SLA(服务质量协议) | 规定时间内恢复服务 | 按合同赔偿,风险可控 |
| 事务正确性(原子性) | 资金操作不能只完成一半 | 金额错误,风险不可控 |
关键结论:SLA 违约的赔偿是可控的,但事务正确性问题(如转账只扣了一方)是不可控的。因此事务正确性优先于 SLA。
两地三中心(数据中心级容灾)
城市 1:数据中心 A(2个节点)+ 数据中心 B(1个节点)
城市 2:数据中心 C(1个节点)
共 4 节点(Raft 需要 3 节点,实际用 3 或 5)
实际部署(3节点版):
城市 1:DC-A(1节点)+ DC-B(1节点)
城市 2:DC-C(1节点)
容灾能力:任意一个数据中心故障,剩余 2 节点满足 Raft 多数派
限制:城市 1 的光纤被挖断 → 城市 2 只有 1 节点,Raft 无法工作
三地五中心(城市级容灾)
城市 1:2 节点
城市 2:2 节点
城市 3:1 节点
任意一个城市网络中断,剩余 3 节点满足 Raft 多数派(5/2+1=3)
→ 可以容忍城市级灾难
flowchart LR
subgraph 城市1
N1[节点1]
N2[节点2]
end
subgraph 城市2
N3[节点3]
N4[节点4]
end
subgraph 城市3
N5[节点5]
end
N1 --- N3
N2 --- N4
N1 --- N5
style 城市1 fill:#e8f4f8
style 城市2 fill:#e8f4f8
style 城市3 fill:#e8f4f8
容灾级别选择
| 容灾方案 | 容灾级别 | 节点数 | 适用场景 |
|---|---|---|---|
| 同城双活 | 机架级 | 2+ | 成本敏感,容忍城市级故障 |
| 两地三中心 | 数据中心级 | 3 | 大多数金融机构的标准 |
| 三地五中心 | 城市级 | 5 | 系统性重要金融机构(SIFI) |
| 三地九中心 | 更高级别 | 9(分层 Raft) | Google 级别 |
无状态服务的容灾
无状态服务容灾相对简单:
- 服务调度算法将请求随机分配到多个数据中心
- 某数据中心故障时,更新路由,不再向其发送请求
- 消息重发问题:服务可能已处理但响应未返回,必须实现幂等性
合规与审计日志设计
监管对金融系统的要求
可解释性:能回答"这笔交易是怎么发生的"
可重现性:能重新计算任意历史时刻的状态
不可篡改:历史记录不能被修改(防止事后造假)
完整性:不能有任何交易记录丢失
审计日志的设计原则
基于事件溯源的审计日志天然满足上述要求:
| 要求 | 事件溯源如何满足 |
|---|---|
| 可解释性 | 命令→事件→状态的完整链路 |
| 可重现性 | 时光机功能,从任意快照重放 |
| 不可篡改 | 事件只追加,不修改(append-only) |
| 完整性 | 事件队列连续,有序列号,缺失可检测 |
情景分析(Scenario Analysis)
监管机构要求金融公司能回答"如果发生 X 事件,损失是多少":
传统方式:修改真实数据进行计算 → 有污染真实数据的风险
双时序数据库方式:
- 创建新的记录时间(假设时间)
- 在这个假设时间下修改市场数据
- 用假设时间查询,不影响真实时间的数据
→ 情景计算与真实数据完全隔离
数据版本号与完整性校验
金融系统传输数据时必须包含:
必须字段:
1. 版本号(数据格式版本,用于向下兼容)
2. 完整性校验(HMAC,防止传输中篡改)
3. 唯一标识符(用于幂等处理)
4. 时间戳(双时序数据库的记录时间)
示例(概念性):
{
"version": "2.1",
"txId": "uuid-xxx",
"recordTime": "2024-01-15T10:30:00Z",
"validTime": "2024-01-15T00:00:00Z",
"amount": 10000,
"hmac": "sha256-xxx"
}
参考资料
- 《分布式金融架构课》— 任杰(极客时间)
- 《分布式技术原理与算法解析》— 聂鹏程(极客时间)
- Google Spanner 论文
- Martin Fowler — Event Sourcing
- Designing Data-Intensive Applications — Martin Kleppmann
- 双时序数据库理论 — Snodgrass, R.T.
评论 (0)
发表评论