专栏文章
专栏文章
分布式系统系列
1. 分布式系统 #01:分布式一致性 2. 分布式系统 #02:分布式事务 3. 分布式系统 #03:分布式协调与选举 4. 分布式系统 #04:分布式系统设计模式 5. 分布式系统 #05:金融级分布式系统实践

分布式系统 #05:金融级分布式系统实践

发布于 2026-05-28 14:04 👁 11 次阅读
#架构#分布式#consistency#financial

本文从金融系统的特殊约束出发,深入讲解金融级分布式系统的设计实践:双时序数据库保证数据正确性、事件溯源保证计算可审计、账务系统的复式记账模型、支付系统的幂等设计、分库分表的动态扩容、跨机房容灾的两地三中心与三地五中心,以及金融系统与互联网系统在设计哲学上的根本差异。

相关文章分布式一致性 · 分布式事务 · 分布式协调与选举 · 分布式系统设计模式


目录

章节 说明
金融系统的特殊约束 与互联网系统的根本差异
数据正确性:双时序数据库 发生时间 vs 记录时间,解决数据修改问题
计算正确性:不可变架构 事件溯源的金融实践,审计与时光机
账务系统设计 复式记账、流水账、余额账
支付系统:幂等与分布式事务 TCC 在支付中的应用
对账系统设计 批量对账 vs 实时对账
分库分表:金融场景的挑战 动态分库、跨分片事务、全局流水号
容灾:两地三中心与三地五中心 数据中心级 vs 城市级容灾
合规与审计日志设计 金融监管对系统的要求

金融系统的特殊约束

金融系统和互联网系统的本质区别是:互联网系统追求可用性(AP),金融系统追求正确性(CP)。一旦涉及钱,错误的代价是不可控的。

dist financial arch

金融 vs 互联网:设计哲学对比

维度 互联网系统 金融系统
核心目标 高可用、高并发、低延迟 正确性、零丢失、可审计
一致性要求 最终一致性(BASE) 强一致性(ACID),部分场景线性一致
数据丢失 可接受极小概率丢失 零容忍,任何一分钱都不能丢
数据修改 直接 UPDATE 只追加,不修改(双时序数据库)
错误处理 降级、熔断、返回缓存数据 拒绝服务,不返回可能错误的数据
"快"的定义 吞吐量大(支撑大量用户) 延迟低(机构交易,时间就是金钱)
系统寿命 几年迭代一次 可能运行几十年,需要长期可维护
边际成本 接近零(互联网大规模扩张的基础) 很高(KYC、合规、风控流程成本高)

金融系统的三层正确性要求

第一层:计算输入正确(正确时间的数据)
  → 双时序数据库:发生时间 + 记录时间
  
第二层:计算过程正确(可重现、可审计)
  → 事件溯源:命令→事件→状态,全程可追溯
  
第三层:计算结果正确(独立验证)
  → 异步对账:日切 + 对账系统

数据正确性:双时序数据库

核心问题:金融数据会被修改(如通货膨胀率事后修正),但历史合同必须使用签订时刻的数据。如何保证"过去的计算"不受"未来的修正"影响?

单时序 vs 双时序

类型 解决的问题 限制
单时序数据库 数据的增加(时间序列) 无法处理数据修改
双时序数据库 数据的增加 + 修改 学习成本高,查询慢

两个时间维度

坐标系(行业惯例):
纵轴 = 发生时间 (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):所有事件的累积
  例:余额 = 初始值 + 所有存款事件 - 所有取款事件

自动机的约束(不可违反,否则无法审计):

  1. 不能有随机数:用伪随机数,并把种子记录到事件中
  2. 不能有外部 I/O:所有外部输入必须先存入事件队列,再由自动机读取
  3. 事件必须有全序:使用 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 在分布式场景的应用


账务系统设计

复式记账(Double-Entry Bookkeeping)

500 年前威尼斯商人发明的记账方法,现代金融系统的基础。每一笔交易必须同时影响两个账户,且借贷必须相等。

核心原则借方(Debit)= 贷方(Credit)

转账 100 元(用户 A → 用户 B):
  用户 A 账户:借方 +100(余额减少)
  用户 B 账户:贷方 +100(余额增加)
  
  验证:借方总额 = 贷方总额 = 100 ✓

为什么金融系统用复式记账

三种账务模型

模型 存储内容 优点 缺点 适用场景
余额账 当前余额快照 查询余额快 无历史,无法对账 不推荐单独使用
流水账 每笔交易记录 完整历史,可对账 查询余额需扫描全量 与余额账配合使用
复式记账 每笔交易的借贷双方 内置校验,满足会计准则 复杂度高 正规金融系统

生产实践:流水账 + 余额账配合使用:

账务系统的幂等设计

问题:转账接口被重复调用(网络重试),导致重复扣款

解决:
1. 每笔交易分配全局唯一流水号(订单号)
2. 数据库对流水号建唯一索引
3. 重复请求因唯一索引冲突被拒绝,返回"已处理"

支付系统:幂等与分布式事务

支付链路的分布式事务挑战

一笔支付涉及多个系统:

用户下单 → 订单系统(创建订单)
         → 库存系统(扣减库存)
         → 支付系统(扣款)
         → 账务系统(记账)
         → 通知系统(发短信)

每个系统都可能失败,需要保证"要么全部成功,要么全部回滚"。

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/>(处理新分片)"]

关键设计

分片策略选择

策略 优点 缺点 金融场景适用性
按哈希分片 数据分布均匀,防止热点 范围查询性能差 账户数据(随机访问)
按范围分片 范围查询友好 可能产生热点(最新数据集中在一个分片) 时序数据(按时间查询)
按业务分片 同一业务数据在一起,减少跨片事务 需要预判业务边界 大客户独立分片

容灾:两地三中心与三地五中心

正确性的定义

金融系统容灾的"正确"包含两层含义:

要求 含义 违反代价
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"
}

参考资料

← 返回列表

评论 (0)

暂无评论,来留下第一条吧。

发表评论