1. 课程导入
在金融系统里,“钱扣了但账没记”“账记了但余额没变”“两个系统各记一套、对不上”是最典型、也最昂贵的一类问题。它往往不是单点 bug,而是业务状态与账务状态不一致的系统性结果。本课程围绕“双写、补偿、异常恢复”三条主线,帮助你把这类问题从“事后救火”变成“可设计、可验证、可恢复”的工程能力。
一、为什么金融账务必须谈“双写”
现代金融业务很少只写一个库。一次支付、一笔理财申购、一次信贷放款,通常同时涉及:交易流水库、账户余额库、总账/分户账、清结算系统、对账文件、风控与额度系统。业务侧要“快”,账务侧要“准”,两者天然存在张力。
双写(Dual Write)指同一笔业务动作需要向两个或多个存储/系统写入数据。它带来的核心矛盾是:多个写入点无法共享同一个本地事务。数据库本地事务能保证单库 ACID,但跨库、跨服务、跨中间件时,只能退而求其次,用最终一致性、补偿、对账来兜底。
一句话理解:双写不是“写两次”这么简单,而是“两个写入点之间没有原子性保证”,因此必须额外设计一致性机制。
二、核心原理:从本地事务到最终一致
要理解后续所有方案,先建立三个层次的心智模型:
- 原子性边界:本地事务只能覆盖一个数据库连接内的一组操作;跨库即失效。
- 可见性与时序:先写 A 再写 B,中间任何一步失败、超时、重试,都会产生中间态。
- 幂等与补偿:既然无法“永不失败”,就要保证失败可重试、重试不重复扣款、错误可反向修正。
常见的一致性实现路径包括:本地消息表、事务消息、TCC(Try-Confirm-Cancel)、Saga、最大努力通知、对账补偿。它们不是互斥的,而是按业务容忍度组合使用。
三、方法流程:一笔双写请求的完整生命周期
下面用一笔“用户支付 100 元”为例,展示双写请求从进入到最终一致的典型流程。注意每一步都可能失败,因此每一步都要有幂等与补偿设计。
- 接收请求:生成全局唯一业务单号(如
pay_20250101_0001),作为幂等键。 - 写交易库:本地事务写入订单/流水,状态置为“处理中”。
- 发消息或调用账务:通过本地消息表或事务消息,异步通知账务系统。
- 账务处理:账务系统按幂等键校验,执行扣款或记账,返回结果。
- 状态回写:交易库更新为“成功/失败”,并记录账务流水号。
- 对账与补偿:定时对账,发现差异进入补偿队列,人工或自动冲正。
-- 幂等去重表示例
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、对账补偿等方案;
- 设计幂等键、状态机、重试退避与冲正逻辑;
- 搭建可观测的对账与异常恢复机制,定位并修复账实差异。
学习建议:本课程强调“可落地”。每学一个机制,都问自己三个问题:失败会怎样?重试会怎样?对不上账怎么发现和修复?