想象你住在一个有上千户人家的小区里,物业想统计“每户今天是否收到了新快递”。最笨的办法是什么?是工作人员挨家挨户敲门,问完一千家才能得到答案——这就是“全量轮询”:每查一次都扫描整张表,住户越多成本越高,快递到了还得等半天才被发现。聪明的物业会怎么做?它盯紧收发室那本“快递登记本”——每来一个快递,门卫就按时间顺序记一行,物业只需翻看“上次看到第几行之后”的新内容,就能瞬间知道哪些户有变化。这本“登记本”,就是数据库的事务日志(如 MySQL 的 binlog、PostgreSQL 的 WAL),而那个“翻看新增记录”的聪明办法,就是今天的主角——CDC(Change Data Capture,变更数据捕获)。 CDC 是 ETL 数据集成中最重要的一项技术。它通过读取数据库日志,实时捕获表的插入、更新、删除操作,把“变化”本身变成一条条事件流,推送给下游数据仓库、消息队列或另一个业务系统。相比传统全量抽取,CDC 只搬变化不搬全表,既有秒级延迟的优势,又对源库几乎零侵入。当然,现实世界的日志也不是永远整齐的:事件可能重复、可能乱序、表结构还会悄悄改版——这些工程挑战,正是这篇故事的后半部分。
学会 CDC,就等于掌握了实时数仓与数据同步的“引擎”:无论是搭建 Flink/Spark 实时管道、做微服务事件驱动,还是维护跨系统数据一致性,CDC 都是 AI 工程师和数据工程师的基本功。
可视化演示
展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。
本节配有交互动效(进入视口后自动加载,约 3KB)。
系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T
CDC基础认知
物业最开始尝试过“每天深夜挨家敲门问快递”,这就是全量抽取:把整张业务表完整复制一份到数据仓库。一千户人家就要敲一千次门,一万户就要敲一万次——轮询成本随着数据量线性上涨,而且快递上午到、晚上才被统计,延迟高达数小时甚至一天。CDC 的思路完全不同:它不打扰任何住户,只在物业门口盯住登记本,谁有新变化就记谁。增量捕获,就是“只读上次检查点之后新增的日志记录”。
在数据库世界里,全量轮询意味着频繁执行 SELECT * FROM orders 这类大查询。当表有千万行时,每次轮询都会消耗大量 IO 和 CPU,还可能影响线上业务。而 CDC 把“全表扫描”变成了“日志消费”:业务表的数据量再大,只要单位时间内变化的行数是有限的,CDC 的开销就只跟变化量成正比。这也是为什么实时数仓几乎都选 CDC 作为数据入口。
回到快递的比喻:住户不会因为“物业半夜敲门”而生气,但如果每晚都被吵醒一次,谁都会投诉。CDC 的价值就在于——它让数据集成从“打扰千家万户”变成“安静地翻登记本”,这也是它成为数据管道标配的根本原因。
- 全表行数 N整张业务表的数据总量。轮询每次查询都要触碰全部 N 行,表越大单次开销越高。
- 轮询查询次数 Q单位时间内发起的全表查询次数。轮询间隔越短,Q 越大,开销也越大。
- 变更事件数 M真正发生变化的行数。CDC 只搬运这 M 个事件,通常远小于 N × Q。
数据库日志原理
物业的登记本有个特点:门卫只能往后追加新记录,绝不允许撕页或涂改,而且每一行都盖上时间戳。这个设计保证了“流水账”的完整性和顺序性。数据库的事务日志也是如此——MySQL 的 binlog(二进制日志)和 PostgreSQL 的 WAL(预写日志)都是追加写的文件,记录着每一次提交的事务对数据行的修改。
binlog 是 MySQL 在存储引擎层之上产生的逻辑日志。它有三种格式:STATEMENT 记录原始 SQL 语句,ROW 记录每一行变更前后的镜像,MIXED 是两者的混合。对于 CDC 来说,ROW 格式最为关键:它不关心 SQL 怎么写的,只忠实记录“哪张表的哪一行,从什么值变成了什么值”。PostgreSQL 的 WAL 则位于存储引擎底层,记录的是数据页的物理修改,Debezium 通过逻辑解码插件(如 pgoutput)把它翻译成行级变更事件。
登记本上的每一行都像这条日志:哪户、什么快递、送来前有没有旧件、送来后是什么样、几点签收的。正因为数据库把账记得这么细,CDC 才有“抄作业”的资本——这是它区别于轮询和触发器的天然优势。
- 表名 table发生变更的数据表,如
orders。 - 操作类型 opINSERT / UPDATE / DELETE 三种之一。
- 前后镜像 before 与 after变更前的值和变更后的值。DELETE 只有 before,INSERT 只有 after。
- 时间戳 ts事务提交的时间,用于后续排序与延迟计算。
基于日志的CDC
物业请来一位专职“抄写员”,他的工作只有一个:每隔几秒翻一次登记本,把新增的记录抄到业主群通知里。他既不敲门,也不进住户家,更不需要住户配合装什么设备——这就是基于日志的 CDC 的运作方式。与之相对的“查询式 CDC”则是在业务表上维护一个 update_time 字段,靠定时查询“最近五分钟改过的行”来发现变化。
日志读取流程可以拆成四步:第一步,定位当前日志偏移量(相当于记住“上次抄到第几行”);第二步,从偏移量开始顺序读取新日志;第三步,解析日志内容,还原出结构化的变更事件;第四步,把事件发送到下游管道(如 Kafka)。这套流程天然是异步的、无侵入的——业务数据库完全无感知。而查询式 CDC 有三个瓶颈:要求表里有时间戳字段、无法感知物理删除、高频率查询仍然会给数据库增加负担。
抄写员从不惊动住户,却把每一条新快递都通知到位。基于日志的 CDC 也是一样:它把“数据如何变化”从数据库的内务变成了可消费的事件流——这正是现代数据集成最优雅的地方。
- 日志序列号 lsn日志中的单调递增位置,是“读到哪了”的标记。
- 表名与操作 table, op说明哪张表的什么操作。
- 前后镜像 before, after行变更前后的完整数据快照。
- 事务 ID 与时间戳 txn\_id, t\_s标识变更所属事务和发生时间,是乱序处理和延迟监控的基础。
Debezium架构
如果物业想把“抄写员”能力标准化,干脆外包给一家专业的“物业数据服务公司”——Debezium 就是这样的角色。它本身不生产数据,而是基于 Kafka Connect 框架,通过一个个连接器(Connector) 接入不同的数据库源(MySQL、PostgreSQL、MongoDB 等),把数据库日志自动翻译成统一格式的变更事件,发布到 Kafka 的主题(Topic)里。
Debezium 的工作分为三个阶段。首先是初始快照:首次部署时,连接器会对存量数据做一次一致性快照,把历史数据先送一遍;接着记录当时的日志偏移量,再无缝切换到增量模式,保证存量与增量不重不漏。其次是事件生成:连接器内部通过解析 binlog 或 WAL,把每一行变更包装成结构化事件。最后是序列化:事件通常以 Avro 格式写入 Schema Registry,这样下游消费方可以在表结构变化时仍然正确解析数据。
找专业公司外包之后,物业只需要签好合同(配置连接器),剩下的事一概不用操心。Debezium 的成熟之处也在这里:你不用自己写日志解析代码,只需配置好源和目标,它就能源源不断地把变更“翻译”成标准事件流。
- Source Connector连接数据库、读取日志的“门户”,一个连接器对应一个数据源。
- ChangeEvent连接器产出的标准变更事件,包含前后镜像和元信息。
- Converter把事件转换成 Avro 或 JSON 字节流,方便在 Kafka 中传输。
- Kafka Topic事件最终落地的消息队列主题,下游从这里订阅消费。
交付语义保障
快递配送有三种承诺等级。第一种“至多一次”:快递员只送一次,路上丢了就不管了,绝不上门补送——对应消息传递中的 at-most-once。第二种“至少一次”:快递可能重复送两次甚至三次,但绝不让你收不到——对应 at-least-once,Kafka 的默认语义。第三种“精确一次”:既不会丢件也不会重复,每个快递恰好被签收一次——对应 exactly-once。
在真实的 CDC 管道中,消费者可能崩溃、网络可能分区、Kafka 重平衡可能发生。如果消费方在“处理完但还没提交偏移量”时崩溃,重启后就会重新消费同一批日志,造成重复处理。至少一次语义保证了不丢数据,代价是可能重复;要升级到精确一次,单靠消息队列还不够,需要两件事:生产者端用幂等写入,消费者端用事务或去重表,保证每条事件即使被处理两遍,最终效果也只生效一遍。
物业向业主承诺“快递必达,但可能重复送”已经算不错了,而“精确一次”则是业主拿着签收单核对:不多送、不漏送。在数据同步场景里,重复的订单消息可能导致金额重复累加,所以越是核心的金融数据管道,越要把至少一次升级为精确一次。
- 至少一次投递次数由于重发机制,每条消息至少被消费一次,可能更多。
- 精确一次投递次数通过事务和幂等,每条消息恰好生效一次。
Schema演进
快递面单改版了!以前是手写三联单,现在改成打印的二维码面单。抄写员必须能同时读懂新旧两种面单,还要保证业主群里的通知格式大家都看得懂——这就是 Schema 演进 的日常。数据库表结构不会一成不变:加字段、删字段、改类型、重命名字段,每一样都会改变 CDC 事件的结构。
按照兼容性,字段变更分为两类。向后兼容:新 Schema 的消费者能够读取用旧 Schema 写入的数据,比如新增一个可选字段,老事件里没有这个字段,新消费者补个默认值就行。向前兼容:旧 Schema 的消费者能读取新 Schema 写入的数据,比如新增字段带默认值,老消费者忽略它也能解析。真正危险的是破坏性变更:删除字段、重命名字段、把字符串改成整数——这些会让旧消费者直接反序列化失败。业界用 Schema Registry 统一管理 Avro 协议,在发布新 Schema 之前自动做兼容性校验。
面单再怎么改版,只要二维码里还保留收件人姓名和地址,老式扫码枪也能扫出关键信息,这就是兼容性的价值。Schema 演进管理得好,表结构变化就能像快递面单升级一样平稳过渡;管理不好,下游消费链路会瞬间瘫痪。
- 新消费者读旧数据升级后的消费方解析历史事件,需要能容忍缺失字段。
- 旧消费者读新数据未升级的消费方解析新事件,需要能忽略多余字段。
乱序处理
业主们发现:同一个收件人的三个快递,从北京、上海、广州三个仓同时发出,到达小区的时间却是打乱的。登记本上的顺序是“送达顺序”,而包裹的真实下单顺序可能完全不同。CDC 事件也一样——分布式环境下,事件乱序是常态而非异常。
乱序有三大来源:第一,多分片并行消费,不同分片的处理速度不同;第二,Debezium 本身可能用多个线程读取不同表的日志;第三,Kafka 分区重平衡时,消费者会临时暂停或重放。要解决乱序,最核心的手段是给事件带上全局可比较的排序键——数据库事务时间戳或自增序列号。下游拿到带排序键的事件后,可以按主键分组、按时间戳重新排列,或者干脆用“幂等写入”兜底:不管顺序如何,最终 UPSERT 的结果是一致的。
快递到了,业主可以先拆别的包裹,等最后一个到达再一起核对——只要知道正确的先后顺序,乱序并不影响最终结果。CDC 管道也是如此:时间戳和序列号就是快递单上的“下单时间”,有了它,乱序也能最终收敛到一致状态。
- 源库时间戳比较先到先得,时间戳小的事件排在前面。
- 序列号次比较同一时刻提交的并发事务,用日志序列号决出先后。
工程实践挑战
再专业的物业团队也躲不开日常琐事:业主投诉快递放错柜、夜班保安交接时漏记了一行、梅雨天登记本受潮字迹模糊。CDC 在生产环境落地,同样要面对一系列“现实摩擦”。最典型的是事务一致性边界问题:CDC 是异步读取日志的,源库事务提交和下游可见之间存在一个窗口期。如果源库刚提交事务、CDC 还没来得及读取,此时下游查数会看到“旧值”,这就是所谓的延迟窗口。
第二个挑战是监控与运维。连接器是否在正常运行?Kafka 里积压了多少未消费的事件?Debezium 提供了 JMX 指标(如 MilliSecondsBehindSource),用来度量从源库变更到下游读取的时延。一旦发现延迟持续上涨,就要检查是源库日志量突增、还是下游消费能力不足。第三个挑战是生态集成:单打独斗的 CDC 没有意义,它必须与 Kafka、Flink、Spark Streaming 组合成完整的实时数仓链路,而这要求工程团队对整条管道的吞吐、容错和成本有全局把控。
物业把琐事管好了,业主才能长期安心。CDC 的工程化也一样:想清楚一致性边界、盯紧延迟指标、选好生态组件,这套“增量搬运”的体系才能真正在业务中稳定运转,成为数据仓库持久流动的活水。
- 当前时间 t\_now下游消费者读取事件时的系统时间。
- 源库变更时间 t\_source事件里携带的源数据库事务时间戳。
- 延迟 lag两者之差,是 CDC 健康度的核心指标,过大说明管道出现瓶颈。
AI 应用场景
实时数仓同步
阶段:数据集成阶段
解决的问题:传统离线 ETL 每天凌晨全量抽取业务库,运营要看当天的实时 GMV 只能等 T+1。CDC 将变更秒级流入数仓,让数据延迟从“天”降到“秒”。
具体案例:某电商平台的订单表在 MySQL 中,业务方用 Debezium 监听 binlog,订单每次插入或更新都会生成事件进入 Kafka;Flink 消费 Kafka 流,实时清洗聚合后写入 ClickHouse 大宽表。运营后台的 GMV 大屏从此可以实时跳动,大促期间还能按分钟追踪库存扣减。
微服务数据同步
阶段:数据分发阶段
解决的问题:微服务架构下,订单服务和库存服务各自拥有独立数据库,跨库更新如果靠应用双写,容易产生分布式事务不一致。CDC 把订单变化发布成事件,让库存服务订阅后自行更新,避免强耦合。
具体案例:某出行平台的订单服务完成支付后,订单表新增一条记录;CDC 监听订单库的日志并把“已支付”事件推送到 Kafka,库存服务、积分服务、消息推送服务各自订阅自己关心的表,互不阻塞地完成后续业务动作。
Python 演示
"""基于日志的 CDC 管道最小演示"""from collections import defaultdict class LogEntry: """一条数据库事务日志""" def __init__(self, table, op, data, seq): self.table = table self.op = op self.data = data self.seq = seq class DatabaseLog: """模拟数据库的追加式事务日志""" def __init__(self): self.entries = [] self._seq = 0 def append(self, table, op, data): self._seq += 1 self.entries.append(LogEntry(table, op, data, self._seq)) def read_since(self, seq): """只返回 seq 之后的新日志,模拟增量读取""" return [entry for entry in self.entries if entry.seq > seq] class CDCPipeline: """CDC 管道:消费日志,按表聚合到下游仓库""" def __init__(self, db_log): self.db_log = db_log self.offset = 0 self.sink = defaultdict(list) self.seen_seq = set() def poll(self): """执行一轮增量拉取,并做幂等去重""" for entry in self.db_log.read_since(self.offset): if entry.seq in self.seen_seq: continue self.seen_seq.add(entry.seq) self.sink[entry.table].append((entry.op, entry.data)) self.offset = entry.seq def show_changes(self): for table, changes in self.sink.items(): for op, data in changes: print(f"{table}: {op} -> {data}") def main(): log = DatabaseLog() log.append("orders", "insert", {"id": 1, "amount": 199}) log.append("orders", "insert", {"id": 2, "amount": 399}) log.append("orders", "update", {"id": 1, "amount": 299}) pipeline = CDCPipeline(log) pipeline.poll() print("第一次拉取:") pipeline.show_changes() print("第二次拉取(新日志 + 模拟重复消费):") log.append("orders", "delete", {"id": 2}) pipeline.poll() pipeline.show_changes() if __name__ == "__main__": main()
class DatabaseLog模拟数据库日志文件,所有日志只能追加,天然有序。def read_since(self, seq)只返回偏移量之后的日志,这正是“增量捕获”的精髓。self.offset记录“上次读到第几条日志”,相当于物业的本子看到哪一行。self.seen_seq = set()去重集合,模拟精确一次语义中“已处理事件”的记录。if entry.seq in self.seen_seq遇到重复日志直接跳过,保证重复投递不重复生效。pipeline.poll()触发一轮增量拉取,模拟 Kafka Consumer 定期 poll 消息。
常见误区
误区“CDC 不就是给每张表加个触发器吗?触发器也能记录数据变化,效果差不多。”
正解触发器和基于日志的 CDC 有本质区别。触发器运行在业务数据库内部,每次 INSERT/UPDATE/DELETE 都会额外执行一段逻辑,增加写路径延迟,而且触发器代码出错可能影响核心业务。基于日志的 CDC 是“旁路”方案——它异步读取数据库已经写好的日志,业务 SQL 完全无感知,无论加多少张表、多少连接器,源库写性能几乎不受影响。二者的差别就像:让住户每天出门时自己登记快递 vs 门卫在收发室顺手记一笔。
为何常见:两者看起来都是“数据变化时自动触发动作”,初学者容易把“触发”当作同一种机制,忽略了“侵入性”这个关键维度。
误区“binlog 和 WAL 记录了所有变更,我只要持续读日志,数据就永远不会丢。”
正解日志本身记录得很完整,但“读日志”不等于“不丢数据”。管道可能崩溃、偏移量可能没有持久化、binlog 可能因为保留期到期被清理(MySQL 默认可能只保留几天)。如果消费者长时间宕机,重启时日志已经被清掉,那部分变更就永久丢失了。工程上必须把偏移量同步持久化到外部存储,并配合至少一次语义和告警监控,才能把丢失概率降到最低。
为何常见:人们倾向于把“数据源存在”等同于“数据必定抵达”,却忽略了中间管道同样有故障和边界。
误区“只要保证每条数据都能送到,结果自然就是精确的,两个说法意思差不多。”
正解至少一次(at-least-once)保证“不丢”,但允许重复;精确一次(exactly-once)要求“既不丢也不重”。在 Kafka 等消息系统中,消费者处理完消息后如果还没来得及提交偏移量就崩溃,重启后会重新消费,导致重复处理。精确一次必须额外引入幂等写入、事务性输出或去重表,把重复的消费变成“无副作用”的操作。
为何常见:直觉上“多送一次”感觉没什么大不了,但在累加金额、统计计数等场景下,重复数据会直接污染结果。
误区“业务表加一列很常见,下游多一个字段又不会崩,没必要大惊小怪。”
正解新增可空字段确实是温和变更,但删除字段、重命名字段、修改字段类型都是破坏性变更。下游消费方如果还在用旧 Schema 反序列化,遇到未知字段还好(忽略即可),遇到缺失字段或类型不匹配就会直接报错。生产环境必须依赖 Schema Registry 做兼容性校验,发布新 Schema 前自动检查消费者兼容性。
为何常见:开发者在单库单表环境里改字段太顺手了,容易忽略“消息是持久化的、消费者是异构的”这一分布式现实。
误区“日志里的事件乱序没关系,我在下游把事件缓存得久一点,再排序就能解决。”
正解增大缓存只能扩大排序窗口,无法根治乱序。乱序可能跨分片、跨连接器、跨长时间窗口,缓存再大也有边界;而且缓存本身会增加延迟和内存成本。真正的解法是让事件携带全局可比较的排序键(时间戳+序列号),或者让下游写入对顺序不敏感——按主键 UPSERT、状态机幂等,从而容忍乱序。
为何常见:单机场景下“先攒着再排序”是直觉解法,但分布式数据流的乱序是无界的,靠堆资源永远追不上。
面试题
Q1. CDC 技术中的英文全称是什么?
- A.Change Data Capture← 正确答案
- B.Continuous Data Center
- C.Central Database Cache
- D.Common Data Computation
答案:A。CDC 是 Change Data Capture 的缩写,意思是“变更数据捕获”。B、C、D 都是干扰项,把“变更”(Change)和“捕获”(Capture)替换成了数据中心、缓存、计算等字眼。就像“快递登记本”真正的名字是“收发登记”,不是“包裹存放处”。
Q2. 相比全量轮询,基于日志的 CDC 最大优势是什么?
- A.完全不需要数据库
- B.开销低且延迟小← 正确答案
- C.能存储更多数据
- D.免去所有网络传输
答案:B。全量轮询每次要扫描整张表,开销随数据量线性增长,且延迟取决于轮询间隔;CDC 只读取日志中的增量变更,开销与变化量成正比,延迟可以降到秒级甚至毫秒级。A、C、D 都不是 CDC 的固有特性。
Q3. Debezium 中负责从数据库读取日志的组件是什么?
- A.自定义序列化器
- B.消息转换处理器
- C.源连接器组件← 正确答案
- D.分布式调度器
答案:C。Debezium 基于 Kafka Connect,其中 Source Connector(源连接器)负责连接数据库、读取 binlog/WAL 并生成变更事件。序列化器和转换器处理的是“事件之后”的环节,调度器更是与日志读取无关。可以理解为:连接器是物业和登记本之间的“门禁授权卡”。
Q4. “至少一次”语义可能会带来以下哪种现象?
- A.部分数据直接丢失
- B.同一数据重复出现← 正确答案
- C.数据全局完全乱序
- D.管道连接永久中断
答案:B。至少一次语义保证每条消息都能送达,但消费者崩溃后未提交偏移量时会重启重放,导致同一事件被消费两次,产生重复数据。它不会丢数据,也不必然导致永久中断。就像承诺“快递必达”的配送,可能因为签收流程异常而多送一次。
Q5. 下列哪种 Schema 变更属于破坏性变更?
- A.新增一个可空字段
- B.直接删除业务字段← 正确答案
- C.修改字段注释信息
- D.为表增加一个索引
答案:B。删除字段会让旧消费者在反序列化时找不到对应属性,属于典型的破坏性变更。新增可空字段、改注释、加索引都不影响既有数据的可解析性。正如快递面单可以多加一行“备注”,但不能把“收件人”一栏撕掉。
Q6. 处理 CDC 事件乱序时,最可靠的排序依据是什么?
- A.随机数生成顺序
- B.源库时间戳与序列号← 正确答案
- C.Kafka 分区编号大小
- D.下游到达先后顺序
答案:B。源库时间戳(事务提交时间)加上日志序列号,能构造出全局可比较的全序关系。Kafka 分区编号和下游到达顺序都是“传输顺序”而非“业务顺序”,随机数更是与顺序无关。就像按快递面单上的下单时间排序,而不是按快递员送到的先后排序。
Q7. 从“至少一次”升级到“精确一次”,通常需要结合哪两种机制?
- A.幂等写入与事务去重← 正确答案
- B.数据压缩与格式转换
- C.分区分片与负载均衡
- D.全量备份与增量恢复
答案:A。至少一次到精确一次的跨越,关键是“消化重复”:生产者端用幂等写入保证重复发送不重复生效,消费者端用事务或去重表保证重复消费不重复落地。压缩、分片、备份都与此无关。这就好比给每个快递打印唯一签收码,重复送货时扫码即知已签收。
Q8. 以下哪些属于基于日志的 CDC 的优势?
- A.对源数据库侵入很小← 正确答案
- B.实时性比轮询更高← 正确答案
- C.完全不需要额外存储
- D.要求改造应用代码
答案:AB。基于日志的 CDC 读取的是数据库本身的日志,业务表无需加时间戳、业务代码无需改造,侵入性极小;同时它实时读取日志,比定时轮询的延迟低得多。实际上 CDC 需要额外的消息队列和偏移量存储,D 更是与“无侵入”的特性相悖。
Q9. Debezium 连接器的核心职责包括哪些?
- A.读取数据库变更日志← 正确答案
- B.生成结构化变更事件← 正确答案
- C.替代应用业务逻辑
- D.将事件投递到消息队列← 正确答案
答案:ABD。连接器做三件事:读日志、生成事件、通过 Kafka Connect 把事件投递到 Kafka。它完全不介入也不替代业务逻辑,C 是明显错误。可以类比为:物业记录员只负责抄登记本并发到业主群,不会替业主决定拆不拆快递。
Q10. 应对 Schema 演进的常用策略有哪些?
- A.Schema 注册表兼容校验← 正确答案
- B.新旧字段映射与默认值← 正确答案
- C.禁止任何表结构变更
- D.完全重放全量历史数据
答案:AB。生产环境通过 Schema Registry 自动校验发布的新 Schema 是否兼容,并对新增字段提供默认值或映射关系,让新旧消费者都能解析。禁止变更和全量重放都是极端方案:前者阻碍业务迭代,后者成本高且不能解决实时问题。