4. 双写一致性挑战

在单库单表时代,我们习惯把原子性、隔离性、持久性(A、I、D)当作数据库免费赠送的“默认能力”。一旦进入金融账务双写场景——例如交易库写流水、账务库写分录,或缓存与数据库同时更新——这三个保证会以非常具体的方式失效。本节不讨论理论定义,而是聚焦它们在分布式双写中的失效点、表现形式与识别信号,为后续补偿与恢复设计建立问题清单。

一、核心原理:ACID 的边界止于单库事务

ACID 的成立前提是“单一事务管理器 + 单一持久化资源”。当一次业务操作需要跨两个数据源写入时,本地事务只能保证其中一个资源的原子性,另一个资源的提交或回滚不受同一事务协调。于是出现三类典型失效:

  • 原子性失效:A 库提交成功、B 库提交失败,系统进入“半完成”状态,且没有任何本地机制能自动回滚已提交的 A。
  • 隔离性失效:两个数据源各自隔离,但跨源的读写顺序无法被统一快照约束,出现“读到中间态”。
  • 持久性失效:消息已发出但未落盘、缓存已更新但数据库未提交,进程崩溃后状态不可恢复。
关键判断:双写一致性问题的本质不是“数据库不够强”,而是事务边界与业务边界不重合。设计补偿与恢复,首先要承认 ACID 在跨源场景下不再免费。

二、失效的四种典型表现与识别信号

下面这张对照表把抽象属性落到可观测现象上,便于在日志、监控和对账中快速定位。

失效属性 典型场景 可观测表现 识别信号
原子性 交易库写成功、账务库写失败 流水存在但无对应分录 对账差集、分录缺失告警
原子性 先写账务后写流水,流水失败 有分录无业务流水 孤儿分录、反向对账不平
隔离性 并发双写同一账户 读到未提交的中间余额 余额抖动、重复扣款
持久性 消息发送后进程崩溃 下游未收到、上游已提交 消息丢失、补偿任务空跑
持久性 缓存更新后 DB 回滚 缓存与 DB 长期不一致 缓存命中值与 DB 差异

三、方法流程:从“假设一致”到“可检测、可补偿”

面对上述失效,工程上不是追求“永不失败”,而是建立一条可闭环的处理链路。推荐流程如下:

  1. 显式记录意图:在本地事务中写入业务流水与“待处理任务”,保证意图与业务数据同库同事务。
  2. 异步驱动第二写:通过消息或任务表驱动账务库写入,避免在业务主链路中同步双写。
  3. 幂等与去重:为每笔分录分配全局唯一业务键,重复消费时直接跳过或覆盖。
  4. 对账检测:定时比对流水与分录的差集,输出“缺失、多余、金额不符”三类结果。
  5. 补偿执行:对缺失补写、对多余冲正、对金额不符进入人工复核队列。
-- 任务表关键字段示例
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 做幂等。修复后,即使账务服务短暂不可用,任务也会重试直至成功或进入人工队列。

双写一致性:失效点与补偿闭环 业务主链路 扣款 + 写本地流水 任务表 待写分录 / 幂等键 账务库 分录写入 对账检测 差集 / 金额比对 补偿执行 补写 / 冲正 / 复核 人工队列 异常挂起处理 重试 失效点:同步双写超时、消息丢失、缓存与 DB 不一致 → 通过任务表 + 对账 + 补偿闭环收敛

五、误区与复盘:三个常见错误认知

  • 误区一:加分布式事务就能解决一切。强一致方案会带来性能与可用性代价,金融账务更常用“最终一致 + 对账补偿”,而非全程强锁。
  • 误区二:重试就是补偿。没有幂等键的重试会造成重复入账,补偿必须建立在可识别、可去重的基础上。
  • 误区三:对账只是财务的事。技术侧必须提供差集明细与可重放的任务记录,否则补偿无从下手。
持久性失效最隐蔽:消息“发送成功”不等于“下游已持久化”。在双写链路中,任何跨进程的确认都必须以对方落盘或可重放为准,不能以调用返回成功为准。

总结来看,双写一致性挑战的核心,是把 ACID 从“默认假设”降级为“需要显式设计的目标”。原子性靠本地事务 + 任务表兜底,隔离性靠幂等与顺序控制,持久性靠可重放日志与对账补偿。只有先承认失效,才能设计出真正可恢复的账务系统。