3150 字
约 10 分钟
16
S11-A07 · 数据同步架构:CDC、双写与批量对齐
2024-12-31
2026-11-09

"我们加个监听,把变更同步过去"——这句话背后藏着四个必须同时解决的问题:初始全量怎么灌、增量怎么抓、冲突怎么裁、错了怎么修。只做增量是绝大多数同步方案走向不一致的起点:订阅从"现在"开始,而历史数据从未被搬运过,第一次全量对齐之前,下游一直缺一段数据,且没人知道缺的是哪一段。

这篇给的是判据与代价,不是搭 Canal 的步骤。


1. 同步要解决的四个问题

问题 不解决的后果 常见做法 成本性质
初始全量 下游从第一天就缺历史 快照导出 + 增量衔接 一次性,但有锁定与时长
增量捕获 变更漏掉,且不可回放 CDC / 双写 / 定时拉取 持续,取决于捕获方式的完整性
冲突处理 同一行两个版本,谁赢靠运气 以源为准 / 时间戳 / 业务合并 逻辑复杂度,最容易被漏掉
对账修复 错了没人知道,知道也没法修 抽样比对 + 修复任务 长期运行成本

"只做增量必然不一致"的原因:增量流只描述"发生了什么",不描述"原本是什么"。丢失一条消息、消费失败一次、订阅晚起一天,都表现为一段永久空洞,而增量流本身没有任何机制能发现这个空洞。能发现空洞的只有全量与对账。


2. 四种捕获方式:完整性与侵入性对照

方式 完整性 对业务侵入 延迟 主要风险
业务双写 依赖代码正确,最脆弱 高,写路径里加逻辑 最低 写库成功写消息失败
CDC 日志订阅 高,覆盖所有写入路径 低,几乎零侵入 秒级 位点、schema 变更、解析开销
定时批量 中,靠时间窗与版本列 低 分钟到小时 窗口内多次变更被折叠
触发器 高 中,DDL 要同步维护 低 触发器逻辑难测,写入放大
-- 定时批量的必备前提:可增量识别的版本列
SELECT id, name, version, updated_at FROM item
WHERE updated_at >= :last_watermark AND updated_at < :now
ORDER BY updated_at LIMIT 1000;
-- 水位线只能前进,且必须用"数据库时间",不能用应用服务器时间

选型的判据顺序是:先看写入路径能否被日志覆盖(能,优先 CDC),再看能否容忍延迟(不能且路径单一,才考虑双写),最后看数据量是否小到可以整表对齐(小到可以,就别做增量)。


3. 双写的一致性缺口与事务性发件箱

缺口 发生时机 表现 缓解
写库成功、发消息失败 消息中间件抖动 下游永久缺这条 发件箱 + 重试投递
先发消息、后写库 顺序颠倒 下游读到不存在的记录 只允许"库先行"
写库成功、消息重复 重试与超时并存 下游重复计数 消费侧幂等键
双写都在事务内 事务回滚但消息已发 下游多出一条幽灵记录 消息只能事务外投递
@Transactional
public void create(Order o) {
    orderMapper.insert(o);                          // 同一本地事务
    outboxMapper.insert(Outbox.of("order", o.id));  // 只落库,不发消息
}
// 独立投递器轮询 outbox → 发送 → 标记已投递;失败可重放

事务性发件箱把"两件事原子完成"降级为"一件事原子完成 + 可重放的投递"。代价是多一张表、多一个投递器、多一份延迟,收益是缺口从"永久"变成"可检测可重放"。投递器本身是至少一次语义,所以消费侧仍必须幂等——发件箱解决的是"不丢",不解决"不重"。

CREATE TABLE outbox (
  id BIGINT PRIMARY KEY AUTO_INCREMENT,
  agg_type VARCHAR(32), agg_id BIGINT,
  payload JSON, created_at DATETIME(3),
  sent_at DATETIME(3) NULL,
  KEY idx_unsent (sent_at, id)
);

4. CDC 的落地要素

要素 具体内容 漏掉会怎样
位点管理 binlog 文件名 + position 或 GTID 重启后从头或跳过一段
schema 变更 加列、改类型、拆表的下游兼容 解析失败或字段错位
幂等消费 以主键 + 版本或事件 ID 去重 重放导致重复计数
延迟监控 位点差、端到端延迟 积压到磁盘打满才发现
全量衔接 快照位点与增量起点的对接 衔接区间重复或缺失
# MySQL 侧前置条件(按版本核对参数名)
log_bin = ON
binlog_format = ROW
binlog_row_image = FULL
binlog_expire_logs_seconds = 604800

binlog_row_image=FULL 常被忽略:设成 MINIMAL 时变更事件只带被改动的列,下游拿不到完整行,更新语义退化成"部分字段合并",删改顺序一乱就出错。

还有一个隐蔽前提:CDC 通常连从库而不是主库。从库延迟会直接变成同步延迟,且主从切换后位点可能跳变,所以"连从库"必须配"延迟阈值 + 位点校验",否则你监控的是一段不知道通向哪的流。


5. 全量对齐:一致点怎么选

衔接方式 一致点 风险 适用
先锁表快照,再起增量 快照时刻的位点 锁定窗口,大表不可接受 小表、可停机
先起增量,再导快照 快照开始时的位点 需过滤快照期间的回放 常用,要求幂等
按主键分片滚动快照 每片的位点 片间时间不一致 超大表
只做定时全量 每次全量即一致点 窗口内不准 小数据量
正确顺序(常用):
1) 记录当前位点 P0
2) 启动增量消费,位点从 P0 开始
3) 导出快照,记录快照开始时间 T0 与位点 P1
4) 快照写入下游(按主键覆盖写,幂等)
5) 回放 P0..P1 之间的事件,覆盖写语义保证最终一致

关键判据:全量写入必须是"覆盖写"而不是"插入",否则快照与增量的重叠区间会撞主键或产生双份。一致点不是"某个时刻",而是"某段可重叠、可幂等重放的区间"。


6. 冲突、删除与 schema 演进

冲突处理的三种策略:

策略 判定依据 适用 风险
以源为准 方向固定,源覆盖目标 单向同步 双向时会互相覆盖
以时间戳为准 最后写入获胜 多写、弱冲突 时钟漂移导致旧值胜出
业务合并 按字段或规则合并 库存、标签等可加量 逻辑复杂,难回放
删除传播的两种做法:
A) 软删:UPDATE item SET deleted=1 → 下游保留行,查询侧过滤
   优点:可回滚、可对账;缺点:下游存储只增不减
B) 物理删:DELETE → 下游必须收到删除事件
   优点:干净;缺点:删除事件丢失后下游永久残留,且无法靠全量对齐修复

结论很直接:需要能对账的系统,删除必须是软删或"删除墓碑表"。物理删一旦丢事件,就只能整表重灌。

双向同步要额外说一句:它把冲突处理从"边缘情况"变成"常态"。只要两边都能写,就必然出现同一行两次不同方向的写入,此时"以源为准"失去意义,只剩时间戳与业务合并两选,而这两者都需要一个全局单调的版本来源。

schema 演进的三类变更与下游姿态:加字段(下游忽略未知字段)、改类型(下游按字符串兜底再解析)、拆表(下游按视图或聚合层吸收)。共性是下游必须先于上游具备兼容能力,顺序反了就是一次全量重灌。


7. 延迟、积压与对账周期

指标 口径 判据
位点差 源端最新位点 − 消费位点 持续增长即消费能力不足
端到端延迟 事件产生时间到写入下游 与业务可接受窗口比
消费速率 条/秒 需大于写入峰值,否则永久落后
失配率 抽样比对不一致条数占比 趋势上升即逻辑有 bug
-- 对账抽样:按最近变更取一批主键,逐条比对版本与关键字段
SELECT id, version, updated_at FROM item
WHERE updated_at > NOW() - INTERVAL 10 MINUTE LIMIT 500;
-- 失配率超阈值 → 触发按主键范围的重灌,而不是全表重来

对账周期的设计原则:频率由"可容忍的错误暴露时间"决定,不由成本决定。日批对账意味着最长 24 小时的数据错误窗口,如果业务无法承受,就得改成小时级抽样 + 每日全量校验两级结构。

积压出现时的顺序同样有讲究:先确认是"并发不够"还是"单条太慢"。前者扩并行度立刻见效,后者扩并行只会把下游打得更乱。


8. 排错

现象 原因 处理
下游数据"停在某天" 消费停止但监控只看进程存活 监控改成位点差
反复出现重复行 消费不幂等,重放即重复 主键 + 版本去重
全量后数据对不上 快照与增量重叠区间未覆盖写 改覆盖写 + 回放区间
某张表突然不同步 schema 变更导致解析失败 变更前先验证下游兼容
延迟指标正常但数据错 指标只测消费速度,不测正确性 加抽样比对
删除后下游仍有数据 物理删事件丢失 改软删或墓碑表
积压越滚越大 消费速率低于写入峰值 先扩并行度,再查单条处理耗时

9. 边界:这些情况不该做同步架构

场景 更合适的做法 理由
数据量小 定时全量覆盖 增量机制的复杂度远超收益
下游能实时查源 直接查或开只读接口 同步是复制,查询是引用
强一致要求 同库或分布式事务 同步天生是最终一致
只读报表 离线跑批 不需要秒级
一次性迁移 一次性脚本 + 校验 不需要常驻管道

最后一类边界最容易被忽视:同步方案一旦引入,就永久成为系统的一部分——它需要监控、需要演练、需要有人懂。把它当成"临时脚本"上线的团队,最后都在维护一条没人敢动的管道。


10. 自测

  1. 为什么"只做增量"在没有全量核对的情况下必然不一致?
  2. 双写里"先发消息后写库"会造成什么,为什么只允许库先行?
  3. 事务性发件箱把什么问题降级成了什么问题?
  4. binlog_row_image=MINIMAL 时下游会失去什么能力?
  5. 全量快照与增量衔接的重叠区间,靠什么语义保证不出错?
  6. 什么情况下"下游直接查源"比同步更正确?
S11-A07 · 数据同步架构:CDC、双写与批量对齐
https://www.clxhxhhr.top/posts/6086/
作者
clxstart
发布于
2024-12-31
许可协议
CC BY-NC-SA 4.0
评论
0 条
还没有评论,先写一条吧。