← 数据库 · MySQL

MySQL CDC 的一致性边界:快照、位点与下游幂等

沿 MySQL、Debezium、Kafka 到下游状态表逐段检查重复、乱序、删除与重放,避免把连接器保证误当作业务保证。

Read in English →

工程学习与设计笔记。示例使用合成场景;适用版本与参考资料见正文。

适用范围:MySQL 8.0、Debezium 2.7 与 Kafka 3.9 的概念和文档语义。本文讨论通用设计,不是生产配置清单。

把数据库变更送进消息队列,通常很快就能跑通。更难的问题出现在故障恢复之后:同一条更新来了两遍怎么办?快照期间发生的修改会不会漏掉?一条迟到的旧更新,会不会把已经删除的对象重新创建出来?

先分清三种“位置”

源端 binlog 位点、连接器保存的 offset、下游消费的 Kafka offset,各自回答不同问题。它们不能互相替代,也不能单凭某一个位点增长就证明业务结果正确。

Debezium 的初始快照负责建立基线,随后衔接变更日志。异常恢复可能产生重复事件;如果恢复所需的 binlog 已被清理,连接器无法凭空补回那段历史。快照与故障行为应按具体模式核实,参见 Debezium 2.7 MySQL Connector 。

从运维角度,需要同时记录源日志保留窗口、最大可容忍停机时间、连接器延迟、下游积压,以及重建基线需要多久。只把 binlog 保存七天而没有恢复演练,“七天”仍只是配置,不是恢复能力。

幂等键应该对应业务对象还是变更事件

假设同步的是订单状态表,一条标准化事件可以包含:

{
  "entity_key": "order:1001",
  "event_id": "source-a:tx-17:row-2",
  "version": 12,
  "operation": "upsert",
  "value": {"status": "PAID"}
}

这是示意格式。event_id 必须由真实、稳定的事件标识规则生成;version 是上游业务显式维护的单调版本,并非 Debezium 默认提供的万能顺序号。数据库主从切换、快照重建和多源合并时,不能直接拿一个文件名或时间戳冒充跨来源全序。

对“保存最新状态”的下游,处理规则可以定义为:新版本覆盖旧版本,相同版本与相同内容视为重放,相同版本但内容不同进入冲突队列,低版本忽略并计数。比较与更新必须在同一个原子操作中完成,否则两个并发消费者仍可能先后覆盖错误。

对“每次变更加一”的聚合,下游按主键覆盖就不够了。需要记录已处理的事件 ID,并把去重记录与累加结果放在同一事务内,或者使用能一起提交状态与消费进度的计算系统。去重记录的保留时间必须覆盖可重放窗口;过期后再次重放,重复累加会重新出现。

删除和乱序最容易被漏掉

物理删除目标行会同时丢掉版本。如果旧的更新随后抵达,就可能重新插入这行。因此可考虑保留包含版本的逻辑删除标记,待超过重放边界再清理。这里的业务删除标记,与 Kafka compacted topic 中 value 为 null 的 tombstone 不是一个概念,不能混用。

还要覆盖主键变更、快照读取事件、schema 变更和解析失败。字段解析不了时直接跳过,延迟看起来可能很健康,但下游已经缺行。隔离失败事件时应保留脱敏后的上下文、错误类型和源位置,以便修复后重放。

为什么开启 Kafka 事务还不够

Kafka 的事务可以协调 Kafka 内部的输出与消费位点;写到外部数据库、搜索引擎或 HTTP 服务,还需要目标系统参与一致性设计。保证的边界见 Kafka 3.9 消息语义 。

例如下游数据库已经提交,消费者在提交 Kafka offset 前崩溃,恢复后就会再次读到相同事件。把下游写入改成带版本条件的幂等操作,可以让这次重复没有额外业务效果。反过来,先提交 offset 再写数据库,会留下“位点已前进,数据未落地”的缺口。

用故障矩阵验收链路

我更愿意用以下实验验收,而不是只比一次总行数:

注入点要验证的结果
快照期间持续更新同一主键最终状态与源端一致,衔接无缺口
目标提交成功后立即终止消费者重放不重复计数、不重复副作用
删除之后重放旧更新删除状态不被复活
同主键事件乱序版本较低的事件不能覆盖较高版本
schema 新增或字段类型变化有兼容策略或可恢复的隔离流程

最后按主键区间核对数量、关键字段与删除状态,并保留少量逐条 diff 样本。总数相等无法发现“一条漏掉、另一条重复”这种抵消错误。CDC 的可用性最终取决于:故障之后,能否解释、恢复并核对每一次业务状态变化。

References

← 继续阅读专题回到顶部 ↑