想象一个热闹的村庄,账房先生捧着全村唯一一本总账,记录着每一笔买卖。村民隔三差五就来问:“张三今天进了多少米?”“李四退了哪匹布?”账房先生要是挨个翻账复述,就再没时间安心写字了。于是他想了个聪明的办法:在账页下垫一张复写纸,每次落笔,墨迹就自动印到副本上。再找一个跑腿小学徒,专门负责把这叠复写纸副本送到各个村民手里——大家在自家门口就能看到最新账目,账房先生也落得清静。 在软件世界里,这个“账房”就是 MySQL、PostgreSQL 等关系型数据库,“总账”是数据表,“复写纸”是数据库的 binlog 事务日志,而那个“跑腿小学徒”,就是 变更数据捕获(Change Data Capture,CDC) 工具——其中最著名的当属 Debezium。它把数据库里每一次增删改都变成一条条流式事件,喂给 Kafka,让缓存、搜索引擎、数据仓库、微服务各取所需。这篇教程沿着“为什么需要 CDC → Debezium 平台 → 快照与增量 → 消息结构 → 部署 → 过滤 → 优化 → 实战场景”一路展开,把这条“账房流水线”彻底讲透。

岗位关联

掌握 CDC 与 Debezium,意味着你拥有了把数据库实时变更接入事件流生态的“标准姿势”,是做实时数仓、缓存同步、微服务集成和 Event Sourcing 的核心基本功,也是面试架构岗时的高频加分项。

目录
  1. 可视化演示
  2. 1. 核心概念:账房里的“复写纸”
  3. 2. Debezium 平台:升级版“跑腿学徒”
  4. 3. 快照与增量:先从家底抄起,再盯新墨迹
  5. 4. 消息结构:复写纸上的“一封家书”
  6. 5. 部署实践:给学徒安排工位
  7. 6. 过滤与转换:只送该送的信
  8. 7. 性能优化:让信跑得更快
  9. 8. 实战场景:复写纸在真实城市里的用法
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

动态展示日志式CDC链路、三种捕获方式对比、缓存实时更新场景与端到端精确一次语义。

本节配有交互动效(进入视口后自动加载,约 3KB)。

1. 核心概念:账房里的“复写纸”

每天清晨,账房先生都要先把前一天的账目誊抄一份,派学徒送到各户人家。这套“手动抄送”看似简单,实则苦不堪言:村民越来越多,要求越来越细,有人要看进货、有人要看退货、还有人要把账目同步到自己的小本本上。账房先生既要给主顾写新账,又要抽空去“复述旧账”,一次遗漏,全村对不上账。在软件系统里,“手动抄送”就是最原始的数据同步方式——应用双写(Dual Write):业务代码在写入数据库之后,再亲自调用缓存、搜索引擎、消息队列等下游服务,把数据挨个送一遍。

双写模式的痛苦显而易见:第一,侵入业务代码,每个写操作后面都要跟一串下游调用;第二,下游调用一旦失败,数据库和缓存就产生了“账目不一致”;第三,调用次数多了,接口响应时间直线上升。聪明的架构师发现,数据库自己有一本“复写纸”——事务日志(binlog / redo log),它忠实地记录了每一次写入。与其让应用亲自送信,不如让一个专职工具读取这本日志,再把变更广播出去。这个思路,就是 Change Data Capture(CDC,变更数据捕获)。

CDC 的核心链路非常简洁:数据库提交事务 → 日志落盘 → CDC 工具解析日志 → 把变更封装成事件 → 发布到消息队列。整个过程对业务代码完全透明,也不需要业务方关心到底同步给了谁。

所以,当你的系统需要把同一份数据同步给缓存、搜索引擎、数仓和多个微服务时,请不要让业务代码当“义务跑腿员”,让 CDC 接管这份苦力,这是走向事件驱动架构的第一步。

T_{\text{同步}} = T_{\text{业务写入}} + T_{\text{额外}},\qquadT_{\text{额外}}^{\text{双写}} = n \times T_{\text{下游调用}},\qquadT_{\text{额外}}^{\text{CDC}} \approx T_{\text{日志读取}}
双写模式下,下游每多一个,业务接口就多一份调用开销;而 CDC 只读一次日志,额外开销几乎恒定,下游数量再多也不怕。
  1. 业务写入耗时数据本身写入数据库所需的时间,这是两种方案都要付出的“基础账本记录时间”。
    T业务写入T_{\text{业务写入}}
  2. 双写的额外耗时应用需要挨个调用 n 个下游接口,总耗时等于调用次数乘以单次调用网络开销。
    n×T下游调用n \times T_{\text{下游调用}}
  3. CDC 的额外耗时只需读一次日志文件并解析,相当于复印一张纸,而不是挨家挨户跑腿。
    T日志读取T_{\text{日志读取}}

2. Debezium 平台:升级版“跑腿学徒”

账房先生虽然想到了复写纸,但总不能自己每天去撕副本、送信。他需要一位专职学徒:这位学徒必须认识每个村民、知道每封信该送哪家、还能把账房先生的潦草字迹誊写成工整的楷书。Debezium 正是 CDC 领域最出名的“跑腿学徒”,它由 Red Hat 开源,是 Kafka Connect 生态中最亮眼的 Source 连接器之一。

Debezium 的定位非常清晰:它不是一个独立的消息中间件,而是寄生在 Kafka Connect 框架上的连接器。Kafka Connect 提供了连接器运行时的“工装”:任务调度、偏移量管理、错误重试等能力;Debezium 则负责具体的“手艺”——连接源数据库、读取事务日志、把二进制日志解析成结构化事件。两者分工合作,好比学徒背上了统一的书信包,只管专心抄信送信。

Debezium 支持 MySQL、PostgreSQL、MongoDB、SQL Server、Oracle 等主流数据源。每个数据源对应一个专门的连接器,例如 io.debezium.connector.mysql.MySqlConnector。连接器启动后,会先监听源库的日志位置,然后把解析出来的每条变更封装成一个包含 key、value 和 header 的 Kafka 消息,发布到指定主题。对下游应用来说,它们不需要关心消息来自哪个数据库的什么日志格式,只需要消费标准化的 Kafka 事件即可。

有了 Debezium 这位“学徒”,源数据库的每一次变更都能在毫秒级被捕获并转述给需要的人,而且业务代码一行都不用改,这正是它相比双写模式的巨大优势。

Et=Connector(binlogt)={key,value,header}E_t = \text{Connector}(\text{binlog}_t) = \{ \text{key}, \text{value}, \text{header} \}
公式表达的是:Debezium 连接器把某个时刻的 binlog 日志内容,结构化地转换为一条包含 key、value 和 header 的 Kafka 消息,业务消费方看到的是统一信封,而非原始日志。
  1. 连接器读取连接器以数据库客户端身份连接源库,连续地从 binlog 拉取新的二进制日志片段。
    Connector(binlogt)\text{Connector}(\text{binlog}_t)
  2. 结构化封装把二进制日志解析成 key(主键)、value(操作数据)、header(元数据),相当于把潦草字迹誊成工整信件。
    {key,value,header}\{ \text{key}, \text{value}, \text{header} \}
  3. 发布到主题承载这条 Kafka 消息的主题名通常是“数据库名。表名”,于是下游按主题订阅即可。
    Et→Topic(库名.表名)E_t \to \text{Topic}(\text{库名}.\text{表名})

3. 快照与增量:先从家底抄起,再盯新墨迹

学徒刚上岗时面临一个难题:账房先生让他以后每天都抄送“新增的账目”,可那些好几年前就记在账本里的老记录怎么办?村民也得知道现有库存啊。Debezium 同样面临这个问题:如果数据库中已经有一张百万行的大表,只监听新日志的话,下游根本拿不到已存在的数据。于是 Debezium 设计了一套“新旧结合”的策略——快照(Snapshot)+ 增量(Incremental)。

当连接器第一次为某张表启动时,默认会先做一次全量快照:把表里当前所有数据逐行读出,封装成 op = "r"(read)的事件,发送到 Kafka 主题。与此同时,它还会记录当前 binlog 的文件名和位置(offset),确保在快照进行中新增的写入也不会被漏掉。快照完成后,连接器无缝切换到增量监听模式,把之后发生的每一条插入、更新、删除都实时捕获。两者叠加,就保证了“历史数据 + 新增数据”的完整衔接。

这里的关键点是 binlog 位点(offset):它类似于账本旁边夹着的书签,标明账房先生上次抄到哪一行。Debezium 会把这个位点持久化到 Kafka 的 offset topic 中,即使连接器重启或崩溃,也能从书签处继续工作,不丢不重。

实际操作中,你往往会为 snapshot.mode 配置选择“自动快照 + 增量”或“仅增量”等模式。理解了这个机制,你就明白了为什么 Debezium 能同时兼顾历史数据加载和实时流式同步。

D最终=S快照∪⋃i=1∞Δi,顺序=f(binlog_offset)\mathcal{D}_{\text{最终}} = \mathcal{S}_{\text{快照}} \cup \bigcup_{i=1}^{\infty} \Delta_i,\qquad\text{顺序} = f(\text{binlog\_offset})
快照提供全量基础,增量提供持续更新,而 binlog 位点保证了合并顺序的正确性——缺失任何一环,账本都不完整。
  1. 全量快照首次启动时,把目标表的现有行全部扫描出来变成初始事件,相当于把库存盘点一遍。
    S快照\mathcal{S}_{\text{快照}}
  2. 增量事件序列快照之后,每一条新 binlog 记录转换成一个变更事件,按时间顺序接续不断。
    ⋃i=1∞Δi\bigcup_{i=1}^{\infty} \Delta_i
  3. 位点保证顺序所有事件的先后顺序由 binlog 位点决定,位点就是账本里的页码+行号,重启后照旧续读。
    f(binlog_offset)f(\text{binlog\_offset})

4. 消息结构:复写纸上的“一封家书”

复写纸复制出来的不只是墨迹,更是一张完整的信息网。村民收到的每一页纸上,除了记录“谁改了账”,还要写清楚“改之前是什么、改之后是什么、什么时候改的”。Debezium 发出的每一条事件,同样是一封结构严谨的“家书”,业内常称为 变更事件信封(Change Event Envelope)。

一封标准的 Debezium 事件信封包含四个核心字段。op 字段标明操作类型:c 表示新增(create)、u 表示更新(update)、d 表示删除(delete)、r 表示快照读取(read)。before 和 after 字段分别保存该行数据在变更前和变更后的完整镜像,如果某个字段为空(null),说明该操作在这一侧没有数据。source 字段是一个嵌套对象,记录源库的类型、数据库名、表名、binlog 文件与位点等溯源信息——相当于信封上的邮戳,让下游知道这封信来自何处。

此外,事件的 key 通常是被修改行的主键值,这一点在 Kafka 分区策略中至关重要:因为 key 相同的事件会被路由到同一个分区,所以同一行数据的多次变更能够保持顺序。Debezium 默认使用 JSON 或 Avro 做序列化,前者便于调试,后者节省空间且自带 Schema 演进能力。

实际业务中,最常见的一个坑是误以为 after 永远是新的完整行数据——其实在删除事件中 after 为 null,而在某些非全字段更新场景下,字段值也可能不完整。所以,消费方一定要把 op 作为判断分支的第一依据。

R={op,before,after,source,ts_ms}\mathcal{R} = \{ \text{op}, \text{before}, \text{after}, \text{source}, \text{ts\_ms} \}
一条变更事件 = 操作类型 + 前镜像 + 后镜像 + 源信息 + 时间戳。读懂信封,就掌握了 CDC 的数据契约。
  1. 操作类型 op枚举值 c / u / d / r,分别代表新增、更新、删除和快照读取,是下游判断该做什么的“指令词”。
    op∈{c,u,d,r}\text{op} \in \{c, u, d, r\}
  2. 前后镜像before 是旧值,after 是新值,更新事件同时携带两者,让下游可以对比前后变化。
    before,after\text{before}, \text{after}
  3. 源信息与时间戳source 里包含库、表、日志位点,ts_ms 记录事件产生时间,构成完整的溯源链路。
    source,ts_ms\text{source}, \text{ts\_ms}

5. 部署实践:给学徒安排工位

账房决定正式雇下这位学徒,就得给他安排一个独立的工位、一套趁手的工具。Debezium 的“工位”通常有两种:一是把自己的连接器插件加载到 Kafka Connect 集群中,二是使用 Debezium Server 这种轻量级单机模式。在绝大多数生产环境,我们选择前者,因为它能借助 Kafka Connect 带来的分布式扩展、自动重平衡和 REST 管理接口。

部署的基础步骤非常清晰。第一步,准备基础设施:一套 Kafka 集群(至少一个 broker 和一个用于保存 offset 的 topic)、源数据库(确保开启 binlog 且账号有相应权限)。第二步,下载 Debezium 连接器插件包,放到 Kafka Connect 的插件目录,并配置 plugin.path。第三步,通过 Kafka Connect 的 REST API(默认 8083/connectors)提交一个连接器配置 JSON,指定连接器类、数据库地址、用户名密码、需要监听的表和主题前缀。

如果你只是想快速体验,docker-compose 是性价比最高的方案:一个容器跑 Zookeeper,一个容器跑 Kafka broker,一个容器跑 Kafka Connect,再配合一个 MySQL 容器,四台“工位”一键拉起。生产环境还要记得配置监控告警:Debezium 的心跳机制会定期在 Kafka 发送心跳消息,如果心跳中断,就说明源库或连接器出了问题;同时要关注 Kafka Connect 的消费延迟指标,防止事件堆积。

部署的本质是“把合适的任务分给合适的工人”。记住:连接器配置的微小差异(比如 tasks.max 设得多大、批次调多大)会直接决定生产环境的天壤之别。

R总=∑j=1mBjτjR_{\text{总}} = \sum_{j=1}^{m} \frac{B_j}{\tau_j}
这是部署后的“吞吐核算表”:总处理速率等于各任务批次大小除以批次处理时间之和。任务数越多、批次越大,整体吞吐越高。
  1. 任务数 mKafka Connect 会把连接器拆分成多个 task 并发运行,每个 task 处理一部分日志位点。
    ∑j=1m\sum_{j=1}^{m}
  2. 批次大小 B_j每个 task 每次轮询可以拉取的最大记录数,对应送信时一捆信件的数量。
    BjB_j
  3. 处理时长 τ_j任务处理这一批事件所花费的时间,包括解析、转换、发送 Kafka。
    τj\tau_j

6. 过滤与转换:只送该送的信

学徒送信送得久了,发现一个道理:不是所有人都需要全村全部的账目。粮店老板只关心粮食库存的变动,布庄老板娘只关心布匹入账,剩下的八卦消息给谁都是负担。Debezium 同样允许我们配置过滤规则,让事件按需定向送达。

先说过滤。table.include.list 和 table.exclude.list 指定需要监听或排除的表,例如只监听 customer.orders,则其他表的事件一律不产生。column.include.list / column.exclude.list 则控制列级别的裁剪,比如不想把 users 表中的密码字段传到 Kafka,可以在生成事件前就把它剔除。这些过滤发生在源头,能显著减少无效消息对网络和存储的压力。

再说转换。Debezium 提供了丰富的消息转换器(SMT,Single Message Transform),常见的有 Rename 重命名字段、ExtractNewRecordState 把“信封”压平为只有 after 数据的简洁消息、TopicRouting 把事件路由到自定义主题而不是默认的“库名。表名”。比如你只关心更新后的最新值,用 ExtractNewRecordState 就能把 before、source 等字段剥掉,减小下游解析成本。

理解过滤与转换,你就掌握了“给谁看什么”的控制权。但要小心:过滤条件写得太窄,将来新需求来了还得改配置重载连接器;转换做得太狠,原始信息可能就再也找不回了。

R′=R源头事件×∏k=1pαkR' = R_{\text{源头事件}} \times \prod_{k=1}^{p} \alpha_k
经过 p 层过滤和路由之后,真正到达下游的事件量等于源头事件乘以每一层的保留系数。过滤层越多,信号越纯,但也可能误伤有用信息。
  1. 源头事件量数据库真实的变更总量,是所有下游事件的“原材料”。
    R源头事件R_{\text{源头事件}}
  2. 每层保留系数每层过滤规则决定保留多少比例的事件,比如表过滤只保留 50% 的表,那么 α = 0.5。
    ∏k=1pαk\prod_{k=1}^{p} \alpha_k
  3. 最终事件量各层系数连乘后得到真正的下游接收量,决定了 Kafka 的存储和带宽成本。
    R′R'

7. 性能优化:让信跑得更快

当村庄变成城市,每天的账目从几百条暴涨到几百万条,再能干的学徒也会手忙脚乱。如何让 Debezium 这条流水线又快又稳?优化点主要落在三个层面:批量、并发、位点监控。

批量(Batching) 是提升吞吐最直接的手段。Kafka Connect 会以 max.poll.records(每次轮询最多拉取的记录数)为单位读取 binlog 并批量发送到 Kafka,max.request.size 和 max.batch.size 控制单个批次的消息大小与字节数。设想一下:送信时与其一封一封跑,不如把同一方向的信用麻袋装好一次送出去,批次越大,单位时间送的信就越多。

并发 方面,调大 tasks.max 可以让多个 task 并行消费不同表的日志;快照阶段则可以为 snapshot 场景配置 snapshot.fetch.size(一次 SELECT 拉多少行)和并行快照参数,避免全量扫描把源库拖垮。位点监控 是稳定性的“仪表盘”:你需要持续关注 Kafka 中源主题的消费延迟(Lag),如果 lag 持续增长,说明下游消费速度跟不上上游生产速度,应该先排查下游,再考虑调大批次。

性能优化不是一上来就堆资源,而是先找到瓶颈在哪一环:是日志读取太慢、网络传输太慢,还是下游消费太慢?对症下药,才能让复写纸流水线跑出最高效率。

Throughput≈B×Wτ\text{Throughput} \approx \frac{B \times W}{\tau}
端到端吞吐约等于“单批次记录数 × 并行 worker 数 ÷ 每批次处理时间”。三者乘积决定了流水线的最大流速。
  1. 批量大小 B单次 poll 或发送的记录数,批量越大,单条均摊的网络开销越小。
    BB
  2. 并发任务数 W连接器的 task 数量,并行度每翻一倍,理论上吞吐也翻一倍。
    WW
  3. 处理时间 τ从日志解析到发送 Kafka 的单批总耗时,主要受源库负载和网络影响。
    τ\tau

8. 实战场景:复写纸在真实城市里的用法

如今,账房学徒的复写纸服务已经成了整座城市的公共设施。不同角色都来订阅这份“变更日报”,各取所需。在真实企业架构里,Debezium 支撑着至少四类典型场景。

第一是缓存同步:电商系统的商品信息存在 MySQL 中,Redis 里缓存了一份热数据用于详情页展示。以前每次改价都要业务代码手动删缓存,现在 Debezium 监听到 product 表的 update 事件后推送到 Kafka,缓存消费者收到事件后自动更新或失效缓存。数据一致性大幅改善,业务代码零侵入。

第二是实时报表:交易系统的订单表每分每秒都在变化,数据仓库需要实时看到 GMV。Debezium 把订单变更推入 Kafka,Flink 消费 Kafka 事件流做实时聚合,再输出到大屏。整个过程不需要任何批处理“加班跑数”,报表天然就是最新的。

第三是Event Sourcing(事件溯源):在金融等强审计场景中,业务状态不能被直接覆盖,每一次变更都应作为不可变事件被存储。CDC 天然提供了这种能力:before / after 记录了每次状态跳变,source 记录了溯源信息,下游可以把这些事件追加到事件日志中,随时重放历史。第四是微服务集成:订单服务产生的数据变更,可以通过 CDC 自动同步到用户服务和库存服务,完成跨服务的最终一致性。

从缓存到报表,从溯源到微服务,CDC 的想象空间远不止“数据同步”四个字。它把数据库变成了事件的生产者,让整座软件城市都能够在“账本墨迹未干”时即刻响应。

E=⟨eventName,  tevent,  payload,  position⟩E = \langle \text{eventName}, \; t_{\text{event}}, \; \text{payload}, \; \text{position} \rangle
每个 CDC 事件本质上就是一个四元组:事件名、事件时间、业务负载和日志位置。抓住这个结构,任何下游都能正确理解和消费事件。
  1. 事件名通常由“操作类型 + 表名”组合而成,如 product.update,标明这是什么类型的变更。
    eventName\text{eventName}
  2. 事件时间记录变更发生的时间戳,用于时序分析、延迟监控和事件重放。
    teventt_{\text{event}}
  3. 负载与位置payload 是业务数据本身,position 是 binlog 位点,保证事件的可追溯性和顺序性。
    payload,  position\text{payload}, \; \text{position}

AI 应用场景

场景一:电商缓存秒级同步

阶段:数据接入与实时特征同步

解决的问题:商品改价、库存变动后,各端缓存需要快速更新。传统双写要改业务代码、删缓存易出错,缓存与数据库容易不一致。Debezium 监听商品表 binlog,将变更事件投递到 Kafka,缓存消费者收到事件后自动更新 Redis,整个过程对业务透明。

具体案例:某电商平台在促销高峰期,商品价格每小时变更数十万次。基于 Debezium + Kafka 的 CDC 链路,价格变更在 200ms 内自动同步到 Redis 缓存集群,详情页大促价格准确率达到 99.99%,彻底告别“下单价格和页面价格不一致”的投诉。

场景二:实时交易大屏与数仓

阶段:实时特征提取与流式分析

解决的问题:传统数仓 T+1 出报表,无法满足大屏实时展示需求。通过 Debezium 把订单表变更实时导入 Kafka,Flink 消费后进行窗口聚合,实时输出 GMV、订单量、客单价等指标。

具体案例:某金融科技公司搭建“双 11”实时作战大屏,Debezium 捕获支付流水表 binlog,每秒钟向 Kafka 推送数万条订单事件,Flink 聚合后以秒级延迟刷新大屏曲线,复盘效率从“次日”提升到“实时”。

场景三:Event Sourcing 与审计追溯

阶段:事件驱动模型构建

解决的问题:金融系统需要完整保留每笔订单的变更历史,用于风控审计和模型回溯。CDC 天然记录了 before/after 镜像和源信息,可以构建不可变的事件流作为模型训练和风控回放的素材。

具体案例:某支付平台把所有账户流水变更通过 Debezium 落成 Kafka 事件日志,风控 AI 模型实时消费事件流,在可疑交易发生时立即触发拦截,同时审计系统能任意时间点重放账户余额变化。

Python 演示

python83 行
import jsonimport time  class FakeBinlog:    """模拟 MySQL binlog 的极简日志管理器"""     def __init__(self):        self.entries = []     def append(self, table, before, after):        """写入一条变更记录"""        self.entries.append({"table": table, "before": before, "after": after})  class FakeKafka:    """用列表模拟一个极简 Kafka Topic"""     def __init__(self):        self.topic = []        self.offset = 0     def publish(self, message):        self.topic.append(message)        self.offset += 1  class FakeCDC:    """简化版 Debezium:快照 + 增量轮询"""     def __init__(self, db, binlog, kafka):        self.db = db        self.binlog = binlog        self.kafka = kafka        self.snapshot_done = False     def snapshot(self):        """全量快照:把现有行扫描成 read 事件"""        for table, rows in self.db.items():            for row in rows:                event = {"op": "r", "table": table, "before": None, "after": row}                self.kafka.publish(json.dumps(event, ensure_ascii=False))        self.snapshot_done = True        print("[snapshot] done, total", self.kafka.offset)     def poll(self):        """增量监听:一次轮询处理所有新 binlog 条目"""        for entry in self.binlog.entries:            if entry["before"] is None:                op = "c"            elif entry["after"] is None:                op = "d"            else:                op = "u"            event = {"op": op, "table": entry["table"],                     "before": entry["before"], "after": entry["after"]}            self.kafka.publish(json.dumps(event, ensure_ascii=False))        count = len(self.binlog.entries)        self.binlog.entries.clear()        print("[poll] processed", count, "new changes")  if __name__ == "__main__":    # 1. 模拟数据库初始数据    database = {"orders": [{"id": 1, "amount": 99}, {"id": 2, "amount": 199}]}     # 2. 建立 binlog 和 Kafka    binlog = FakeBinlog()    kafka = FakeKafka()     # 3. 启动 CDC,先做快照    cdc = FakeCDC(database, binlog, kafka)    cdc.snapshot()     # 4. 模拟业务继续写入    binlog.append("orders", None, {"id": 3, "amount": 299})    binlog.append("orders", {"id": 1, "amount": 99}, {"id": 1, "amount": 129})    cdc.poll()     # 5. 打印 Kafka 里的全部事件    for msg in kafka.topic:        print(msg)    time.sleep(0.1)
目的:用一个“假数据库 + 假 binlog + 假 Kafka”的迷你环境,把 Debezium 快照和增量的核心流程跑通,理解 CDC 事件从产生到落地的全过程。
  1. class FakeCDC:定义了一个简化版 Debezium 主类,快照和增量两个方法分别对应连接器的两种工作模式。
  2. def snapshot(self):全量快照方法:遍历数据库所有表的所有行,生成 op = "r" 的初始事件,发往 Kafka,相当于账房学徒先抄家底。
  3. def poll(self):增量轮询方法:一次性处理 binlog 中新增的所有条目,对应连接器持续“盯着新墨迹”的行为。
  4. if entry["before"] is None:判断逻辑:before 为空说明是新增(c),after 为空说明是删除(d),两者都有则是更新(u)。
  5. self.kafka.publish(json.dumps(event, ensure_ascii=False))把 Python 字典序列化成 JSON 字符串后发送到"Kafka",确保中文不被转义。
  6. cdc.snapshot()` 与 `cdc.poll()这两个调用完整复现了“先全量快照,再增量监听”的 Debezium 生命周期。

常见误区

误区以为 CDC 是每隔几秒扫一次数据库表,只要轮询间隔内有多次更新,就可能只看到最后一次,所以数据会丢。

正解主流的 CDC(如 Debezium)并不查表,而是直接读 binlog 事务日志。日志是数据库每次写入留下的“复写纸”,不管中间改了多少次,每条变更都按顺序完整记录。Debezium 从日志流中逐条读取并广播,不存在“只看到最后一次”的问题,前提是日志保留策略足够长。

为何常见:很多人把 CDC 和“定时全表比对”混为一谈,因为早期一些数据同步工具确实用轮询快照实现,而这个做法确实有丢失中间态的缺陷。

误区Debezium 要连着数据库一直读日志,肯定占用大量 CPU 和内存,会把线上数据库拖垮。

正解Debezium 读 binlog 的开销极小,相当于账房先生垫一张复写纸,而不是让学徒反复翻查账本。binlog 本来就是数据库为复制和高可用而必须写的东西,Debezium 只是以“从库”的身份来读它,对主库几乎无侵入。真正要小心的是全量快照阶段的大量 SELECT,但这可以通过调节快照批次和并发来控制。

为何常见:人们直觉上认为“多一个系统连数据库就多一分负担”,却忽略了日志读取与业务查询的本质区别。

误区既然一直讲 binlog,那 Debezium 应该只能读 MySQL 吧?PostgreSQL、MongoDB 用不上它。

正解Debezium 是“多语种学徒”,它通过不同的连接器支持 MySQL、PostgreSQL、MongoDB、SQL Server、Oracle、Db2 等主流数据库。每种数据库的日志机制不同(MySQL 是 binlog,PostgreSQL 是 WAL 逻辑解码,MongoDB 是 oplog),但 Debezium 都把它们转换成统一格式的 Kafka 消息。

为何常见:教程大多以 MySQL 为例讲解,读者容易把案例误当成全部能力边界。

误区启动连接器后,下游马上就能订阅到库里的全部数据,包括历史数据。

正解默认配置下 Debezium 确实会先做全量快照再增量监听,但如果你的表非常大,快照需要时间;而且如果你显式配置为“仅增量”模式,历史数据就不会产生了。正确理解是:快照负责“已有数据”、增量负责“新数据”,两者配合才能完整,启动后的前几秒看到的大多是快照事件(op = "r")。

为何常见:人们容易用“消息中间件”的直觉去理解 CDC——以为它只是“从今天开始的数据”,忽略了快照机制的存在。

面试题

Q1. 变更数据捕获(CDC)最核心的数据来源是?

单选题 · easy

  • A.数据库的事务日志← 正确答案
  • B.应用业务运行日志
  • C.反向代理访问日志
  • D.服务器监控采集报告

答案:A。CDC 的本质是读取数据库自身的 binlog / WAL 等事务日志,就像从复写纸上获取账本墨迹,而不是从应用日志或代理日志中间接拼凑。

Q2. Debezium 在 Kafka Connect 体系中扮演的角色是?

单选题 · easy

  • A.Sink 连接器
  • B.Source 连接器← 正确答案
  • C.分区选择器
  • D.序列化转换器

答案:B。Debezium 是典型的 Source 连接器,负责把外部数据源(数据库)的数据“引入” Kafka;Sink 连接器则相反,是把 Kafka 数据写出到其他系统。

Q3. Debezium 首次监听一张已有百万行的大表时,默认会怎么做?

单选题 · mid

  • A.忽略历史数据
  • B.清空目标表重建
  • C.执行全量快照← 正确答案
  • D.只捕获增量更新

答案:C。默认配置下,Debezium 会先做全量快照,把已有行转成 op="r" 的读取事件,再切换到增量监听。忽略历史或只做增量都会导致下游缺失存量数据。

Q4. Debezium 重启后如何准确找到上次的处理位置?

单选题 · mid

  • A.从 binlog 位点续读← 正确答案
  • B.从头再做一遍快照
  • C.依赖业务侧人工补偿
  • D.随机选择最近位点

答案:A。Debezium 会把 binlog 的文件名和位点(offset)持久化到 Kafka 的 offset topic,重启后从该位点继续读取。这就像账本里的书签,确保账目不重不漏。

Q5. 在 Debezium 的变更事件中,op 字段的值为 "u" 表示什么?

单选题 · mid

  • A.新增了一条记录
  • B.更新了一条记录← 正确答案
  • C.删除了一条记录
  • D.快照读取了一条记录

答案:B。op 是操作类型指令:c 是 create 新增,u 是 update 更新,d 是 delete 删除,r 是 read 快照读取。遇到 u,下游应该对比 before 和 after 做更新处理。

Q6. 只想把 customer 库的 orders 表变更同步到 Kafka,应该配置哪个参数?

单选题 · mid

  • A.table.include.list← 正确答案
  • B.offset.storage.topic
  • C.key.converter
  • D.connector.class

答案:A。table.include.list 指定需要监听的表白名单,格式如 customer.orders。其他选项分别管理 offset 存储、key 序列化和连接器类型,与过滤表无关。

Q7. 生产环境中,确保 Debezium“宕机重启后不丢事件”的推荐配套是什么?

单选题 · hard

  • A.持久化位点与心跳机制← 正确答案
  • B.每次全量重跑确认不丢
  • C.定时人工检查日志
  • D.关闭快照模式降低风险

答案:A。Debezium 通过 Kafka 持久化 binlog 位点,配合心跳机制(heartbeat)持续确认连接活性,才能在宕机后从断点续读。全量重跑或人工检查既不实时也不可靠。

Q8. 相比传统双写模式,基于日志的 CDC 有哪些明显优势?

多选题 · mid

  • A.无需侵入业务代码← 正确答案
  • B.不丢失中间变更记录← 正确答案
  • C.延迟一定为零
  • D.不需要任何日志支持

答案:AB。CDC 读日志对业务透明,也不存在“只同步最终状态”的丢中间态问题。但它并非零延迟,且高度依赖数据库日志(binlog)开启,所以 C、D 错误。

Q9. Debezium 官方支持以下哪些数据源的连接器?

多选题 · mid

  • A.MySQL← 正确答案
  • B.PostgreSQL← 正确答案
  • C.MongoDB← 正确答案
  • D.Redis

答案:ABC。Debezium 支持 MySQL、PostgreSQL、MongoDB、SQL Server、Oracle 等数据库。Redis 是缓存/键值存储,不在 Debezium 官方连接器支持列表内,因此 D 错误。

Q10. 下游系统消费 CDC 事件流时,要保证最终一致性的正确姿势是?

多选题 · hard

  • A.以至少一次模式消费← 正确答案
  • B.消费端做好幂等处理← 正确答案
  • C.强制依赖数据库事务
  • D.每次消费前清空目标表

答案:AB。Kafka 一般提供“至少一次”投递语义,因此下游需要幂等消费(按业务主键去重或状态机校验)来避免重复数据。依赖数据库事务或清表重建都不能解决乱序与重复问题,反而放大故障风险。