1. 课程导入

在金融系统里,“钱扣了但账没记”“账记了但余额没变”“两个系统各记一套、对不上”是最典型、也最昂贵的一类问题。它往往不是单点 bug,而是业务状态与账务状态不一致的系统性结果。本课程围绕“双写、补偿、异常恢复”三条主线,帮助你把这类问题从“事后救火”变成“可设计、可验证、可恢复”的工程能力。

一、为什么金融账务必须谈“双写”

现代金融业务很少只写一个库。一次支付、一笔理财申购、一次信贷放款,通常同时涉及:交易流水库、账户余额库、总账/分户账、清结算系统、对账文件、风控与额度系统。业务侧要“快”,账务侧要“准”,两者天然存在张力。

双写(Dual Write)指同一笔业务动作需要向两个或多个存储/系统写入数据。它带来的核心矛盾是:多个写入点无法共享同一个本地事务。数据库本地事务能保证单库 ACID,但跨库、跨服务、跨中间件时,只能退而求其次,用最终一致性、补偿、对账来兜底。

一句话理解:双写不是“写两次”这么简单,而是“两个写入点之间没有原子性保证”,因此必须额外设计一致性机制。

二、核心原理:从本地事务到最终一致

要理解后续所有方案,先建立三个层次的心智模型:

  • 原子性边界:本地事务只能覆盖一个数据库连接内的一组操作;跨库即失效。
  • 可见性与时序:先写 A 再写 B,中间任何一步失败、超时、重试,都会产生中间态。
  • 幂等与补偿:既然无法“永不失败”,就要保证失败可重试、重试不重复扣款、错误可反向修正。

常见的一致性实现路径包括:本地消息表、事务消息、TCC(Try-Confirm-Cancel)、Saga、最大努力通知、对账补偿。它们不是互斥的,而是按业务容忍度组合使用。

金融账务双写:一致性机制全景 业务动作 支付 / 申购 / 放款 交易库写入 流水、订单、状态 账务库写入 余额、分户账、总账 一致性保障手段 本地消息表 / 事务消息 TCC / Saga 最大努力通知 对账 + 补偿任务 异常恢复能力 幂等键 / 去重表 重试 + 退避 冲正 / 反向补偿 人工干预与审计 典型风险 重复扣款 账实不符 悬挂中间态 对账差异累积

三、方法流程:一笔双写请求的完整生命周期

下面用一笔“用户支付 100 元”为例,展示双写请求从进入到最终一致的典型流程。注意每一步都可能失败,因此每一步都要有幂等与补偿设计。

  1. 接收请求:生成全局唯一业务单号(如 pay_20250101_0001),作为幂等键。
  2. 写交易库:本地事务写入订单/流水,状态置为“处理中”。
  3. 发消息或调用账务:通过本地消息表或事务消息,异步通知账务系统。
  4. 账务处理:账务系统按幂等键校验,执行扣款或记账,返回结果。
  5. 状态回写:交易库更新为“成功/失败”,并记录账务流水号。
  6. 对账与补偿:定时对账,发现差异进入补偿队列,人工或自动冲正。
-- 幂等去重表示例
CREATE TABLE t_idempotent (
  biz_no      VARCHAR(64) PRIMARY KEY,
  status      TINYINT NOT NULL,   -- 0处理中 1成功 2失败
  created_at  DATETIME NOT NULL,
  updated_at  DATETIME NOT NULL
);

-- 账务扣款前先插入幂等键,冲突则直接返回已有结果
INSERT INTO t_idempotent(biz_no, status, created_at, updated_at)
VALUES ('pay_20250101_0001', 0, NOW(), NOW());

四、案例与场景:三种典型双写失败

场景现象根因应对策略
交易成功、账务失败用户看到支付成功,余额未扣跨库无原子性,账务调用超时本地消息表 + 重试 + 对账补偿
账务成功、交易失败余额已扣,订单仍处理中回写状态失败或消息丢失状态机 + 定时补偿 + 冲正
重复写入同一笔扣款执行两次重试未做幂等幂等键 + 唯一索引 + 去重表

常见误区:认为“加了重试就安全了”。重试解决的是“可能失败”,但解决不了“重复执行”。没有幂等设计的重试,等于主动制造重复扣款。

五、学习目标与课程框架

学完本课程,你应当能够:

  • 识别金融账务双写的原子性边界与一致性风险点;
  • 根据业务容忍度选择本地消息表、TCC、Saga、对账补偿等方案;
  • 设计幂等键、状态机、重试退避与冲正逻辑;
  • 搭建可观测的对账与异常恢复机制,定位并修复账实差异。

学习建议:本课程强调“可落地”。每学一个机制,都问自己三个问题:失败会怎样?重试会怎样?对不上账怎么发现和修复?