4. 双写一致性挑战
在单库单表时代,我们习惯把原子性、隔离性、持久性(A、I、D)当作数据库免费赠送的“默认能力”。一旦进入金融账务双写场景——例如交易库写流水、账务库写分录,或缓存与数据库同时更新——这三个保证会以非常具体的方式失效。本节不讨论理论定义,而是聚焦它们在分布式双写中的失效点、表现形式与识别信号,为后续补偿与恢复设计建立问题清单。
一、核心原理:ACID 的边界止于单库事务
ACID 的成立前提是“单一事务管理器 + 单一持久化资源”。当一次业务操作需要跨两个数据源写入时,本地事务只能保证其中一个资源的原子性,另一个资源的提交或回滚不受同一事务协调。于是出现三类典型失效:
- 原子性失效:A 库提交成功、B 库提交失败,系统进入“半完成”状态,且没有任何本地机制能自动回滚已提交的 A。
- 隔离性失效:两个数据源各自隔离,但跨源的读写顺序无法被统一快照约束,出现“读到中间态”。
- 持久性失效:消息已发出但未落盘、缓存已更新但数据库未提交,进程崩溃后状态不可恢复。
关键判断:双写一致性问题的本质不是“数据库不够强”,而是事务边界与业务边界不重合。设计补偿与恢复,首先要承认 ACID 在跨源场景下不再免费。
二、失效的四种典型表现与识别信号
下面这张对照表把抽象属性落到可观测现象上,便于在日志、监控和对账中快速定位。
| 失效属性 | 典型场景 | 可观测表现 | 识别信号 |
|---|---|---|---|
| 原子性 | 交易库写成功、账务库写失败 | 流水存在但无对应分录 | 对账差集、分录缺失告警 |
| 原子性 | 先写账务后写流水,流水失败 | 有分录无业务流水 | 孤儿分录、反向对账不平 |
| 隔离性 | 并发双写同一账户 | 读到未提交的中间余额 | 余额抖动、重复扣款 |
| 持久性 | 消息发送后进程崩溃 | 下游未收到、上游已提交 | 消息丢失、补偿任务空跑 |
| 持久性 | 缓存更新后 DB 回滚 | 缓存与 DB 长期不一致 | 缓存命中值与 DB 差异 |
三、方法流程:从“假设一致”到“可检测、可补偿”
面对上述失效,工程上不是追求“永不失败”,而是建立一条可闭环的处理链路。推荐流程如下:
- 显式记录意图:在本地事务中写入业务流水与“待处理任务”,保证意图与业务数据同库同事务。
- 异步驱动第二写:通过消息或任务表驱动账务库写入,避免在业务主链路中同步双写。
- 幂等与去重:为每笔分录分配全局唯一业务键,重复消费时直接跳过或覆盖。
- 对账检测:定时比对流水与分录的差集,输出“缺失、多余、金额不符”三类结果。
- 补偿执行:对缺失补写、对多余冲正、对金额不符进入人工复核队列。
-- 任务表关键字段示例
CREATE TABLE t_double_write_task (
task_id BIGINT PRIMARY KEY,
biz_key VARCHAR(64) NOT NULL, -- 全局唯一业务键
source_db VARCHAR(32), -- 来源库
target_db VARCHAR(32), -- 目标库
payload TEXT, -- 待写入内容
status TINYINT, -- 0待处理 1成功 2失败
retry_count INT DEFAULT 0,
created_at DATETIME,
updated_at DATETIME
);
幂等键建议使用“业务类型 + 业务单号 + 分录序号”,而不是数据库自增 ID。自增 ID 在跨库重放时无法保证语义唯一。
四、案例场景:一笔转账的双写失败复盘
某支付系统在“扣款成功”后同步调用账务服务写分录。一次网络抖动导致账务服务超时,但扣款事务已提交。结果:用户余额已减少,账务分录缺失,日终对账不平。
复盘发现三个问题:第一,双写在同一线程内同步执行,超时后无法判断对方是否已写入;第二,没有本地任务表记录“待写分录”;第三,缺少幂等键,重试时可能重复入账。修复方案是改为“本地事务写流水 + 任务表”,由独立消费者异步写账务,并以 biz_key 做幂等。修复后,即使账务服务短暂不可用,任务也会重试直至成功或进入人工队列。
五、误区与复盘:三个常见错误认知
- 误区一:加分布式事务就能解决一切。强一致方案会带来性能与可用性代价,金融账务更常用“最终一致 + 对账补偿”,而非全程强锁。
- 误区二:重试就是补偿。没有幂等键的重试会造成重复入账,补偿必须建立在可识别、可去重的基础上。
- 误区三:对账只是财务的事。技术侧必须提供差集明细与可重放的任务记录,否则补偿无从下手。
持久性失效最隐蔽:消息“发送成功”不等于“下游已持久化”。在双写链路中,任何跨进程的确认都必须以对方落盘或可重放为准,不能以调用返回成功为准。
总结来看,双写一致性挑战的核心,是把 ACID 从“默认假设”降级为“需要显式设计的目标”。原子性靠本地事务 + 任务表兜底,隔离性靠幂等与顺序控制,持久性靠可重放日志与对账补偿。只有先承认失效,才能设计出真正可恢复的账务系统。