想象你经营着一家大型物流仓库,每天有成千上万个包裹入库、改址、退货。客户最关心的不是仓库有多大,而是“我的包裹现在到哪了”。如果每个人都打电话来问,客服早就崩溃了。好在仓库有一本“扫描台账”——每一件包裹被扫码枪扫过,都会按顺序记上一行。真正聪明的做法是安排一个调度员坐在台账旁边:只要台账出现新的一行,就立刻抄送给关心这个包裹的人。这个“盯着台账、照抄变化、实时广播”的机制,就是今天的主角——CDC(Change Data Capture,变更数据捕获)。在分布式系统里,数据库的每一次 INSERT、UPDATE、DELETE 都像包裹的一笔动态,而 Canal 与 Debezium 就是两套成熟的“调度班组”:一个出身阿里巴巴、与 MySQL 深度绑定;一个拥抱 Kafka、多数据库通吃。读完这篇文章,你就能从“台账怎么记”一路看到“通知怎么送”,把整条实时数据链路装进自己的工具箱。
掌握 CDC 的日志解析、同步策略与实时链路设计,是数据平台工程师搭建实时数据体系的核心能力;在 AI 时代,“数据变化传播得有多快”直接决定了模型特征与业务决策能有多快。
可视化演示
展示CDC(变更数据捕获)核心机制:数据流管道模式可视化MySQL binlog经Canal/Debezium解析后通过Kafka投递至下游消费者(ES/缓存/仓库),架构对比模式展示Canal与Debezium的六层架构差异与关键指标对比。
本节配有交互动效(进入视口后自动加载,约 3KB)。
CDC数据流管道 MySQL binlog变更源 Canal/Debezium解析捕获 延迟≤100ms 实时同步至下游
CDC原理概述
你经营着一家大型物流仓库,每天有成千上万个包裹入库、改址、退货。客户最关心的不是仓库有多大,而是“我的包裹现在到哪了”。如果每个人都打电话来问,客服早就崩溃了。好在仓库有一本“扫描台账”——每一件包裹被扫码枪扫过,都会按顺序记上一行。真正聪明的做法是安排一个调度员坐在台账旁边:只要台账出现新的一行,就立刻抄送给关心这个包裹的人。这个“盯着台账、照抄变化、实时广播”的机制,就是 CDC(Change Data Capture,变更数据捕获)。
CDC 是一种从数据库日志中捕获数据变更、并把变更推送给下游系统的技术。它不修改业务代码,不要求业务方在写库的同时“顺便”通知别人,而是让数据库自己留下的日志成为唯一事实来源。对 AI 从业者而言,这相当于给数据平台接上了一根“神经”:实时特征、模型样本、缓存更新、搜索索引,全都依赖这根神经传递的变化信号。
在日志解析方案流行之前,业界常见两种笨办法。一是全表轮询:每隔几秒 SELECT 一次全表,和上次对比找出变化,白白消耗数据库 IO,延迟还高。二是应用双写:业务代码里同时写数据库和消息队列,侵入性强、容易漏写。相比之下,日志解析方案直接读取 MySQL binlog——数据库本身就把每次变更写进了日志,我们只是去“照抄”,因而低侵入、低延迟、高可靠,还能拿到变更前后的数据镜像。
就像调度员抄写台账的速度决定了客户收到通知的快慢,CDC 方案的好坏,第一个要看的就是“延迟”——从数据库提交事务,到下游真正拿到消息,中间隔了多久。延迟越低,业务越接近“实时”;延迟一旦抖动,往往意味着解析、网络或消费端出现了瓶颈。我们把这份延迟写成一个公式,它贯穿整条链路的设计。
- 记录提交时刻数据库事务执行成功的那一刻,记下时间戳
T_commit,它是整条链路计时的起点,相当于包裹扫码入库的时刻。 - 记录到达时刻下游系统真正收到并处理完这条事件时,记下时间戳
T_sink,相当于客户收到通知的时刻。 - 相减得到延迟用到达时刻减去提交时刻,得到延迟 ΔT。延迟越小,说明调度员抄送得越及时。
binlog日志解析
为了让每一笔包裹动态都有据可查,仓库规定:所有扫码记录必须按时间顺序写进台账,不许撕页、不许插队。MySQL 也有一本这样的“台账”——binlog(二进制日志),它按顺序记录所有改变数据的操作,包括插入、更新、删除,甚至建表。它不是给人看的,而是给机器“追剧”用的:日积月累,就是数据库的一生。
你可能早就听说过 MySQL 主从复制:主库写 binlog,从库把 binlog 拉到本地“重放”,于是从库和主库拥有相同的数据副本。有趣的是,Canal、Debezium 正是搭了这趟顺风车:它们把自己伪装成 MySQL 的一个从库,向主库申请 binlog 的读取权限,但拿到日志后并不重放 SQL,而是把日志解析成一条条结构化的“变更事件”,转卖给下游。
binlog 有三种记录格式:statement 记录 SQL 语句本身,日志小但结果不可控;row 模式记录每一行数据变更的“前镜像”和“后镜像”,精确无损,是 CDC 工具最爱的格式;mixed 则是两者混合。在主从复制里,“从库读到哪一行”由位点(position)标记;在 CDC 里,同样靠位点实现断点续传——下次启动,从上次读到的位置继续。
台账不是无限期的,binlog 也默认只保留若干天(例如 expire_logs_days)。这意味着 CDC 擅长的是“从今天开始往后的每一笔”,而不是“把十年前的历史也找回来”。历史数据要用全量快照或备份来补齐——这个分工,后面的同步策略会细说。
- 读取当前位点
pos_current表示解析器已读到日志文件的哪个字节偏移,相当于台账翻到第几行。 - 计算事件长度
len(event)表示刚读到的这条变更事件占了多少字节,相当于台账上一行记录占几格。 - 推进位点两者相加得到下一条事件的起始位置,这就是需要持久化的断点续传值。
Canal架构设计
现在给仓库正式配备一个调度组,名字叫 Canal。这个组内部有明确分工:有人守在台账前逐行抄录(Parser),有人把抄录内容翻译成统一格式的通知单(Sink),有人把通知单暂时存放在中转盒中(Store),最后还有专门的联络员把通知单送给外面的客户(Client)。四个人各司其职,整条抄送流水线就不会堵。
Canal 是阿里巴巴开源的中间件,核心机制就是伪装成 MySQL 从库,向主库发送 dump 协议请求,持续读取 binlog。它的基本运行单元叫 instance,内部由三大模块组成:EventParser 负责连接主库、拉取并解析 binlog;EventSink 负责过滤、加工和分发事件;EventStore 负责在内存中暂存解析好的事件,等待客户端来取。
光有流水线还不够,还要保证“消息不丢”。Canal 采用 ack/rollback 机制:客户端消费完一条消息后回传 ack,Canal 才推进位点;如果消费失败,可以 rollback 让消息留在 Store 里重新投递。另外,Canal 支持集群高可用:通过 ZooKeeper 协调多台 server,同一个 instance 同一时刻只有一台在解析日志,故障时自动切换,客户几乎无感知。
一句话总结,Canal 是一支为 MySQL 量身定制的“抄送班组”,性能强劲、与 Java 生态配合默契,但和 MySQL 绑定较紧。如果你的数据源不止 MySQL,或者想要一套更“标准化”的通知协议,那就该认识另一支队伍——Debezium。
- 定义全集
E_all是 binlog 中所有变更事件的集合,相当于台账上全部扫描记录。 - 设定过滤条件对每条事件 e 取出所属表名
table(e),判断它是否在允许列表T_allow中。 - 取出子集满足条件的事件才被保留转发,不满足的丢弃。
Debezium架构
假设你的公司同时租用了好几家仓库,还对接了无数外部系统。这时你会希望所有通知沿用统一格式,不管包裹来自哪个仓库,客户收到的通知单长一个样。Debezium 就是这支讲究“标准协议”的调度队。它把自己建立在 Kafka Connect 之上,把所有数据库的变化都编码成一种通用记录格式,谁拿到都能直接消费。
Debezium 是一个开源分布式平台,通过“连接器(Connector)”支持 MySQL、PostgreSQL、MongoDB、Oracle 等多种数据库。每个连接器负责盯一个数据源,以 Kafka Connect Source Connector 的身份运行,把捕获到的变更持续写入 Kafka 的 topic。对使用者来说,部署变成了“启动一个连接器”,而不是维护一套自研工具。
Debezium 输出的记录称为 Source Record,核心字段包括:source(数据库、表名、binlog 位点等来源信息)、op(操作类型,c 表示新增、u 表示更新、d 表示删除、r 表示快照记录)、ts_ms(事件发生的时间戳)以及 before/after(变更前后的数据)。它还内置快照机制:首次启动会自动做一次全量快照,随后无缝切换成增量监听,整个过程对业务透明。
对比 Canal 和 Debezium:前者轻量、贴近 MySQL、性能调优空间大;后者更标准化、多数据库支持好、与 Kafka 生态天然融合。选型没有绝对优劣,关键看你的数据源是“纯 MySQL 电商后端”还是“多源异构的数据湖或微服务”。无论选谁,都绕不开一个问题:历史数据怎么补?增量从哪开始?这就是下一节的同步策略。
- 全量快照
S_snapshot是启动时捕获的一份完整数据,相当于大盘点登记造册的结果。 - 增量变更
S_incremental是快照之后数据库继续产生的所有变更事件。 - 取并集两者相并才是完整数据视图,缺了任何一部分,目标库都不完整。
数据同步策略
接一个新客户系统,老板要求:把所有包裹当前状态同步过去,并且此后一直保持最新。聪明的做法分两步:先安排一次彻底盘点,把此刻仓库里所有包裹登记造册;然后让调度员持续跟踪新的进出记录。麻烦的是,盘点过程里包裹还在不断进出——怎么保证盘点结束时不重不漏?这时需要给两条流水线约定一个“交接点”。
第一步全量同步,通常用快照工具把当前数据整体搬走,同时记录快照那一刻 binlog 的位点;第二步增量同步,从那个位点开始持续消费 binlog 中新发生的变更并写入目标。这个“交接点”就是位点(position / offset),如同台账上的书签,精确记录“我已经读到哪一行”。全量与增量以位点为界,无缝衔接。
位点管理直接决定同步的可靠程度。Canal 以客户端 ack 为前提推进位点;Debezium 则把 offset 定时提交到 Kafka 内部 topic,重启后从已提交位点继续。由于网络和重启都可能带来重复投递,业界普遍采用“至少一次投递 + 下游幂等”的组合:下游按主键做 upsert 或去重,同一事件处理两遍结果一样,从而做到不丢不重。
大白话就是:盘点负责“把存量搬过去”,调度员负责“把增量追上来”,位点是两者之间唯一的接力棒。接力棒保存得好,系统重启一百次也不怕;保存得含糊,轻则重复消费,重则静默丢数据。下一节,我们把这些零件装进一条完整的实时链路,看看它怎么在真实世界转起来。
- 已落盘位点
pos_persist是同步服务定期写入存储的位点,相当于把书签夹在台账某处。 - 已确认位点
pos_ack是下游回执 ack 之后才更新的位点,代表“这行之前的消息都处理完了”。 - 取较晚者位点只能前进不能倒退,所以取两者的最大值作为保存值,防止重启后丢消息。
实时链路实践
至此,整个通知中心已经成型:扫码枪产生记录 → 调度员抄写翻译 → 放进中转站 → 客户取走通知单。对应到技术栈,就是 MySQL 写 binlog → Canal / Debezium 解析 → 消息队列(Kafka / RocketMQ)缓冲分发 → 下游应用消费。每个环节职责单一,任何一环故障都不会拖垮整条链路。
消息队列是这条链路的中转站。Canal 可以通过内置 adapter 把事件写入 Kafka 或 RocketMQ,Debezium 则天生以 Kafka Connect Source 的身份写入 topic。下游按主题订阅,像订杂志一样各取所需:订单组只订订单 topic,用户组只订用户 topic。消费端组成消费组水平扩展,处理完一条提交一次 offset,提交成功才代表这条消息“消化”了。
故障恢复是链路设计的试金石。下游服务挂了不要紧,消息留在队列里,offset 停在那里,等恢复后继续消费即可;同步进程整个宕机,重启后从持久化位点继续读日志,最多重复、不会丢失。真正需要盯紧的是监控指标:lag(消费积压量)、解析吞吐、错误率、位点是否停滞。lag 一旦持续上涨,说明消费能力跟不上生产速度,就得扩容或优化。
回头看这条链路:数据库每一次细微变动,都被日志忠实记录,被调度工具实时捕获,被消息队列稳稳传递,最终被下游系统及时消化——从“客户打电话问包裹”到“包裹一动全公司都知道”,这就是 CDC 给分布式系统带来的秩序感。而这份秩序,正是数据平台工程师在 AI 时代最值钱的基本功。
- 生产端位置
pos_producer是消息队列中最新一条消息的位置,相当于中转站货架上最新的通知单编号。 - 消费端位置
pos_consumer是当前消费组已成功处理到的位置,相当于客户已经取走的通知单编号。 - 求差得到积压量差值 L 就是 lag,反映还有多少消息在排队等待处理。
AI 应用场景
实时特征与推荐系统
阶段:特征工程 / 实时数据接入
解决的问题:用户点击、下单等行为写入 MySQL 后,传统批处理要等数小时才能刷新特征,推荐效果严重滞后。CDC 把订单、行为表的变更实时推送出来,特征平台秒级刷新,模型才能“跟上用户的手速”。
具体案例:某电商在大促期间用 Canal 监听订单表 binlog,把订单状态变化写入 Kafka,实时计算用户累计消费金额、类目偏好等特征,驱动推荐排序和风控决策,整体特征延迟从小时级降到秒级。
缓存与搜索引擎一致性
阶段:数据同步 / 索引更新
解决的问题:业务代码双写数据库和 Elasticsearch,容易漏写或不一致。改用 CDC 单向监听,数据库为主、下游索引跟着日志走,彻底摆脱双写负担。
具体案例:商品中心修改价格后,Debezium 捕获 UPDATE 事件并写入 Kafka,消费端据此更新 ES 商品索引和 Redis 缓存,搜索结果与数据库价格保持一致,误写概率大幅下降。
异构数据库与数据湖入湖
阶段:数据采集 / 数据湖构建
解决的问题:多套业务库要汇入数据湖供 AI 训练,定时抽取延迟高、增量难追踪。用 Debezium 把 MySQL、PostgreSQL 的变更实时写入 Kafka,再落成数据湖表,样本 T+0 可用。
具体案例:某数据平台团队为核心交易库部署 Debezium 连接器,把 binlog 变更以近似实时的速度写入数据湖的 Delta 表,模型训练样本从“前一天快照”升级为“当天实时流水”。
Python 演示
"""Simulate a CDC pipeline: read binlog events, parse, push to MQ."""import json class BinlogReader: """Simulate MySQL binlog; yield change events one by one.""" def __init__(self, events): self.events = events self._pos = 0 def read_event(self): if self._pos >= len(self.events): return None event = self.events[self._pos] self._pos += 1 return event class CanalParser: """Simulate Canal; parse raw events and filter by table.""" def __init__(self, tables=None): self.tables = list(tables or []) self.gc_count = 0 def parse(self, event): if self.tables and event["table"] not in self.tables: return None self.gc_count += 1 return { "schema": event["db"], "table": event["table"], "eventType": event["type"], "data": event["data"], "old": event.get("old"), } class MessageQueue: """Simulate a MQ buffer between producer and consumer.""" def __init__(self): self.items = [] def publish(self, msg): self.items.append(msg) def consume(self): if not self.items: return None return self.items.pop(0) def main(): events = [ {"db": "shop", "table": "orders", "type": "INSERT", "data": {"id": 1, "amount": 99, "status": "paid"}}, {"db": "shop", "table": "orders", "type": "UPDATE", "data": {"id": 1, "status": "shipped"}, "old": {"status": "paid"}}, {"db": "shop", "table": "orders", "type": "DELETE", "data": {"id": 2, "amount": 50, "status": "cancel"}}, ] reader = BinlogReader(events) parser = CanalParser(tables=["orders"]) mq = MessageQueue() while True: raw = reader.read_event() if raw is None: break msg = parser.parse(raw) if msg is not None: mq.publish(json.dumps(msg)) print("pushed:", msg["eventType"], msg["table"], msg["data"]) print("queue size:", len(mq.items)) while True: msg = mq.consume() if msg is None: break print("consumed:", msg) if __name__ == "__main__": main()
class BinlogReader:模拟 MySQL 的 binlog 数据源,就像仓库的扫描台账。self._pos = 0记录日志读到哪一行,等价于 binlog 的位点(position)。def read_event(self):每次调用返回一条变更记录,模拟 Canal 按顺序读取日志。class CanalParser:模拟 Canal 的解析模块,负责把原始日志变成结构化消息。self.tables = list(tables or [])保存允许转发的表白名单,对应 Canal 的过滤配置。def parse(self, event):按表名过滤并转换事件,等价于 Canal 的 filter + parser。if self.tables and event["table"] not in self.tables:只转发关注表的变更,避免下游收到无关数据。mq.publish(json.dumps(msg))把结构化消息序列化,等价于投递给 MQ 的字节流。def consume(self):模拟下游消费者从队列拉取变更。if __name__ == "__main__":可执行入口,直接运行即可看到推送与消费的完整日志。
常见误区
误区我听说主从复制也是读 binlog,Canal 也是读 binlog,那它们不就是一回事吗?部署了 Canal 是不是就不用配从库了?
正解主从复制是把 binlog 拿到另一台 MySQL 上“重放”,目的是得到一份一模一样的数据副本;CDC 是把 binlog 解析成“变更事件”推送给任意下游系统(缓存、搜索引擎、消息队列),目的是广播变化。可以说它们同源(都靠 binlog)但不同路:一个复制数据,一个分发事件。比如主从复制不会告诉你有条订单从“待支付”变成了“已支付”,而 CDC 会把这条 UPDATE 连同前后镜像都告诉你。
为何常见:两者都伪装成从库或读取 binlog,机制相似,初学者容易把“复制数据”和“分发事件”混为一谈。
误区既然是“实时”同步,那我在数据库里 UPDATE 一条,下游应该立刻(0 毫秒)就看到吧?
正解CDC 的“实时”是指近实时,一般毫秒到秒级。链条上每一环都有开销:binlog 写入与 dump、解析器处理、网络传输、消息队列排队、消费端落库。用公式衡量就是 ΔT = T_sink − T_commit。如果 lag 涨到几十秒,就该检查消费端是不是出现了瓶颈,而不是怀疑 CDC 本身。
为何常见:“实时”这个词容易让人误以为是零延迟,忽略了分布式系统中每一跳传输和处理的物理成本。
误区既然 binlog 记录了所有变更,那我想看三个月前的数据,让 Canal 从开头再读一遍不就行了?
正解binlog 是滚动保留的,默认只有几天(expire_logs_days / binlog_expire_logs_seconds),最早的历史早已被清理。CDC 的定位是增量同步,历史存量要靠全量快照(mysqldump、Debezium snapshot)或备份来补齐。正确姿势是“先全量、再增量”,靠位点衔接,而不是指望日志无限回放。
为何常见:把 binlog 当成永久档案,忽略了数据库的滚动清理机制。
误区Canal/Debezium 已经把消息推送出去了,而且我消费端都收到了,数据一定不会丢,不用再做什么了。
正解消息“送达”不等于“处理成功”。Canal 的 ack 只代表客户端从 Store 取走了消息;Debezium 提交 offset 只代表连接器把数据写进了 Kafka。下游把数据写进自己的库、更新缓存,这一段的可靠性要靠消费端自己兜底:重试、幂等、失败重投。所以生产实践中“至少一次 + 幂等”才是标配。
为何常见:把“投递成功”和“消费成功”两个层面的语义混在了一起。
面试题
Q1. CDC 的核心思想是什么?
- A.定期轮询数据库全表数据
- B.解析数据库日志获取变更← 正确答案
- C.让数据库主动推送所有数据
- D.给业务数据库加触发器
答案:B。CDC 的本质是“读日志、抄变化”,对应台账比喻就是调度员抄台账。轮询是主动翻包裹,效率低且延迟高;双写和触发器侵入业务代码,都不是 CDC 的典型思路。
Q2. Canal 解析 MySQL 的哪种日志来捕获变更?
- A.错误日志 error log
- B.慢查询日志 slow log
- C.二进制日志 binlog← 正确答案
- D.通用日志 general log
答案:C。只有 binlog 按顺序记录了所有数据变更,主从复制和 CDC 都以它为数据源;错误日志、慢查询日志都与数据变更无关,就像台账只有扫描记录,其他本子记的不是包裹动态。
Q3. Canal 在协议层面把自己伪装成什么?
- A.伪装成 MySQL 客户端
- B.伪装成 MySQL 从库 slave← 正确答案
- C.伪装成 MySQL 主库 master
- D.伪装成独立数据库代理
答案:B。Canal 利用主从复制协议,向主库发送 dump 请求,主库把它当成正常从库持续推送 binlog。伪装成客户端拿不到 binlog 流,伪装成主库方向反了,代理则不是协议层面的身份。
Q4. Debezium 最常与哪种消息系统深度集成?
- A.Kafka 消息系统← 正确答案
- B.RocketMQ 消息系统
- C.不依赖任何消息系统
- D.仅支持文件输出
答案:A。Debezium 构建在 Kafka Connect 之上,连接器天然把变更写入 Kafka topic,Kafka 是它的主战场;RocketMQ 更多与 Canal 配合使用。
Q5. binlog 的 row 格式下,一条 UPDATE 事件通常包含什么?
- A.仅保存变更前的旧值镜像
- B.仅保存变更后的新值镜像
- C.同时保存前后两份数据镜像← 正确答案
- D.保存完整执行的 SQL 文本
答案:C。row 格式记录每行变更的 before 和 after 两份镜像,下游可以精确知道“从什么变成什么”,这正是 CDC 偏好的格式;statement 格式才保存 SQL 文本。
Q6. 关于位点(position/offset)管理,下列哪项说法正确?
- A.位点一旦保存就不可再更新
- B.位点记录消费进度并支持断点恢复← 正确答案
- C.位点只存在内存中不会落盘
- D.位点与消息队列的消费无关
答案:B。位点相当于台账书签,标记“已读到哪一行”。Canal 在 ack 后推进位点,Debezium 把 offset 提交到 Kafka topic,都是为了重启后从断点继续。A、C、D 都错在把位点描述成静止、内存态或与消费无关。
Q7. 全量+增量同步中,增量同步的起点如何确定?
- A.从 binlog 开头全部重新解析
- B.以全量快照时刻的位点为增量起点← 正确答案
- C.随机选择一个时间点开始
- D.只能从当前时刻开始同步
答案:B。先做全量快照并记录当时的 binlog 位点,快照完成后再从该位点消费增量,二者用“交接点”衔接,才能不重不漏。从开头解析不现实(binlog 会被清理),随机选点会造成数据缺口。
Q8. 关于 Canal 与 Debezium 的说法,下列哪些是正确的?
- A.两者都以解析数据库日志为手段← 正确答案
- B.Canal 只能把数据写入本地文件
- C.Debezium 常通过 Kafka Connect 部署← 正确答案
- D.两者都能取代数据库主从复制
答案:AC。两者都通过读 binlog 或日志捕获变更,这是 CDC 的核心;Debezium 以 Kafka Connect 连接器形式运行。Canal 可对接 MQ 等多种输出,不是只能写文件;CDC 是“广播事件”,替代不了“复制数据副本”的主从复制。
Q9. 为保证 CDC 链路不丢数据,常用手段包括哪些?
- A.消费成功后回传 ack 并保存位点← 正确答案
- B.下游消费逻辑做幂等处理← 正确答案
- C.关闭 binlog 以降低开销
- D.消息队列持久化并启用副本← 正确答案
答案:ABD。ack 加位点保证“断点续传”,幂等消化“重复投递”,MQ 持久化和副本保证“消息不因宕机丢失”。关闭 binlog 等于拔掉数据源,数据源都没有了,何谈捕获?
Q10. CDC 最适合解决下面哪类问题?
- A.把数据库变更实时同步到下游系统← 正确答案
- B.代替业务应用日志的记录
- C.提升复杂 SQL 查询的性能
- D.压缩数据库磁盘的存储空间
答案:A。CDC 的价值就是把数据库增删改实时广播给缓存、搜索、数仓、特征平台等下游,相当于调度员把台账变化抄送给所有关心的人。它不负责日志记录、SQL 优化或存储压缩——那些是其他工具的活。