想象你住在一个有上千户人家的小区里,物业想统计“每户今天是否收到了新快递”。最笨的办法是什么?是工作人员挨家挨户敲门,问完一千家才能得到答案——这就是“全量轮询”:每查一次都扫描整张表,住户越多成本越高,快递到了还得等半天才被发现。聪明的物业会怎么做?它盯紧收发室那本“快递登记本”——每来一个快递,门卫就按时间顺序记一行,物业只需翻看“上次看到第几行之后”的新内容,就能瞬间知道哪些户有变化。这本“登记本”,就是数据库的事务日志(如 MySQL 的 binlog、PostgreSQL 的 WAL),而那个“翻看新增记录”的聪明办法,就是今天的主角——CDC(Change Data Capture,变更数据捕获)。 CDC 是 ETL 数据集成中最重要的一项技术。它通过读取数据库日志,实时捕获表的插入、更新、删除操作,把“变化”本身变成一条条事件流,推送给下游数据仓库、消息队列或另一个业务系统。相比传统全量抽取,CDC 只搬变化不搬全表,既有秒级延迟的优势,又对源库几乎零侵入。当然,现实世界的日志也不是永远整齐的:事件可能重复、可能乱序、表结构还会悄悄改版——这些工程挑战,正是这篇故事的后半部分。

岗位关联

学会 CDC,就等于掌握了实时数仓与数据同步的“引擎”:无论是搭建 Flink/Spark 实时管道、做微服务事件驱动,还是维护跨系统数据一致性,CDC 都是 AI 工程师和数据工程师的基本功。

目录
  1. 可视化演示
  2. CDC基础认知
  3. 数据库日志原理
  4. 基于日志的CDC
  5. Debezium架构
  6. 交付语义保障
  7. Schema演进
  8. 乱序处理
  9. 工程实践挑战
  10. AI 应用场景
  11. Python 演示
  12. 常见误区
  13. 面试题

可视化演示

展示系统处理流水线、实时性能指标曲线及多方案对比分析,包含数据流转、吞吐延迟监控与方案评估。

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

系统可视化 处理流程:输入→处理→模型→输出 性能监控:实时指标 P=L×T

CDC基础认知

物业最开始尝试过“每天深夜挨家敲门问快递”,这就是全量抽取:把整张业务表完整复制一份到数据仓库。一千户人家就要敲一千次门,一万户就要敲一万次——轮询成本随着数据量线性上涨,而且快递上午到、晚上才被统计,延迟高达数小时甚至一天。CDC 的思路完全不同:它不打扰任何住户,只在物业门口盯住登记本,谁有新变化就记谁。增量捕获,就是“只读上次检查点之后新增的日志记录”。

在数据库世界里,全量轮询意味着频繁执行 SELECT * FROM orders 这类大查询。当表有千万行时,每次轮询都会消耗大量 IO 和 CPU,还可能影响线上业务。而 CDC 把“全表扫描”变成了“日志消费”:业务表的数据量再大,只要单位时间内变化的行数是有限的,CDC 的开销就只跟变化量成正比。这也是为什么实时数仓几乎都选 CDC 作为数据入口。

回到快递的比喻:住户不会因为“物业半夜敲门”而生气,但如果每晚都被吵醒一次,谁都会投诉。CDC 的价值就在于——它让数据集成从“打扰千家万户”变成“安静地翻登记本”,这也是它成为数据管道标配的根本原因。

Costpolling=N×Q≫Costcdc=M\text{Cost}_{\text{polling}} = N \times Q \quad\gg\quad \text{Cost}_{\text{cdc}} = M
其中 N 是全表行数,Q 是轮询次数,M 是真实发生的变更事件数。全量轮询的开销等于“全表行数 × 查询次数”,而 CDC 的开销只等于“变更条数”——就像敲门问询每个住户 vs 只看登记本上的新增行。
  1. 全表行数 N整张业务表的数据总量。轮询每次查询都要触碰全部 N 行,表越大单次开销越高。
    NN
  2. 轮询查询次数 Q单位时间内发起的全表查询次数。轮询间隔越短,Q 越大,开销也越大。
    QQ
  3. 变更事件数 M真正发生变化的行数。CDC 只搬运这 M 个事件,通常远小于 N × Q。
    MM

数据库日志原理

物业的登记本有个特点:门卫只能往后追加新记录,绝不允许撕页或涂改,而且每一行都盖上时间戳。这个设计保证了“流水账”的完整性和顺序性。数据库的事务日志也是如此——MySQL 的 binlog(二进制日志)和 PostgreSQL 的 WAL(预写日志)都是追加写的文件,记录着每一次提交的事务对数据行的修改。

binlog 是 MySQL 在存储引擎层之上产生的逻辑日志。它有三种格式:STATEMENT 记录原始 SQL 语句,ROW 记录每一行变更前后的镜像,MIXED 是两者的混合。对于 CDC 来说,ROW 格式最为关键:它不关心 SQL 怎么写的,只忠实记录“哪张表的哪一行,从什么值变成了什么值”。PostgreSQL 的 WAL 则位于存储引擎底层,记录的是数据页的物理修改,Debezium 通过逻辑解码插件(如 pgoutput)把它翻译成行级变更事件。

登记本上的每一行都像这条日志:哪户、什么快递、送来前有没有旧件、送来后是什么样、几点签收的。正因为数据库把账记得这么细,CDC 才有“抄作业”的资本——这是它区别于轮询和触发器的天然优势。

LogEntryROW=⟨table,op,before,after,ts⟩\text{LogEntry}_{\text{ROW}} = \langle \text{table}, \text{op}, \text{before}, \text{after}, \text{ts} \rangle
一条 ROW 格式的日志条目包含:表名、操作类型、变更前镜像、变更后镜像、时间戳。有了“前后镜像”,下游不仅能知道数据变了,还能知道具体怎么变的。
  1. 表名 table发生变更的数据表,如 orders。
    table\text{table}
  2. 操作类型 opINSERT / UPDATE / DELETE 三种之一。
    op\text{op}
  3. 前后镜像 before 与 after变更前的值和变更后的值。DELETE 只有 before,INSERT 只有 after。
    before,after\text{before}, \text{after}
  4. 时间戳 ts事务提交的时间,用于后续排序与延迟计算。
    ts\text{ts}

基于日志的CDC

物业请来一位专职“抄写员”,他的工作只有一个:每隔几秒翻一次登记本,把新增的记录抄到业主群通知里。他既不敲门,也不进住户家,更不需要住户配合装什么设备——这就是基于日志的 CDC 的运作方式。与之相对的“查询式 CDC”则是在业务表上维护一个 update_time 字段,靠定时查询“最近五分钟改过的行”来发现变化。

日志读取流程可以拆成四步:第一步,定位当前日志偏移量(相当于记住“上次抄到第几行”);第二步,从偏移量开始顺序读取新日志;第三步,解析日志内容,还原出结构化的变更事件;第四步,把事件发送到下游管道(如 Kafka)。这套流程天然是异步的、无侵入的——业务数据库完全无感知。而查询式 CDC 有三个瓶颈:要求表里有时间戳字段、无法感知物理删除、高频率查询仍然会给数据库增加负担。

抄写员从不惊动住户,却把每一条新快递都通知到位。基于日志的 CDC 也是一样:它把“数据如何变化”从数据库的内务变成了可消费的事件流——这正是现代数据集成最优雅的地方。

ChangeEvent=⟨lsn,table,op,before,after,txn_id,ts⟩\text{ChangeEvent} = \langle \text{lsn}, \text{table}, \text{op}, \text{before}, \text{after}, \text{txn\_id}, t_s \rangle
一个完整的 CDC 变更事件,除了日志本身的字段,还要带上日志序列号(lsn)和事务 ID(txn_id),用于后续精确排序和幂等去重。
  1. 日志序列号 lsn日志中的单调递增位置,是“读到哪了”的标记。
    lsn\text{lsn}
  2. 表名与操作 table, op说明哪张表的什么操作。
    table,op\text{table}, \text{op}
  3. 前后镜像 before, after行变更前后的完整数据快照。
    before,after\text{before}, \text{after}
  4. 事务 ID 与时间戳 txn\_id, t\_s标识变更所属事务和发生时间,是乱序处理和延迟监控的基础。
    txn_id,ts\text{txn\_id}, t_s

Debezium架构

如果物业想把“抄写员”能力标准化,干脆外包给一家专业的“物业数据服务公司”——Debezium 就是这样的角色。它本身不生产数据,而是基于 Kafka Connect 框架,通过一个个连接器(Connector) 接入不同的数据库源(MySQL、PostgreSQL、MongoDB 等),把数据库日志自动翻译成统一格式的变更事件,发布到 Kafka 的主题(Topic)里。

Debezium 的工作分为三个阶段。首先是初始快照:首次部署时,连接器会对存量数据做一次一致性快照,把历史数据先送一遍;接着记录当时的日志偏移量,再无缝切换到增量模式,保证存量与增量不重不漏。其次是事件生成:连接器内部通过解析 binlog 或 WAL,把每一行变更包装成结构化事件。最后是序列化:事件通常以 Avro 格式写入 Schema Registry,这样下游消费方可以在表结构变化时仍然正确解析数据。

找专业公司外包之后,物业只需要签好合同(配置连接器),剩下的事一概不用操心。Debezium 的成熟之处也在这里:你不用自己写日志解析代码,只需配置好源和目标,它就能源源不断地把变更“翻译”成标准事件流。

DB Log→Source ConnectorChangeEvent→ConverterAvro/JSON→Kafka ConnectKafka Topic\text{DB Log} \xrightarrow{\text{Source Connector}} \text{ChangeEvent} \xrightarrow{\text{Converter}} \text{Avro/JSON} \xrightarrow{\text{Kafka Connect}} \text{Kafka Topic}
这条链路就是 Debezium 的核心工作流:从日志到事件,从事件到序列化消息,最终落到 Kafka。每一步都有对应的组件负责,像流水线上的工位一样各司其职。
  1. Source Connector连接数据库、读取日志的“门户”,一个连接器对应一个数据源。
    Source Connector\text{Source Connector}
  2. ChangeEvent连接器产出的标准变更事件,包含前后镜像和元信息。
    ChangeEvent\text{ChangeEvent}
  3. Converter把事件转换成 Avro 或 JSON 字节流,方便在 Kafka 中传输。
    Converter\text{Converter}
  4. Kafka Topic事件最终落地的消息队列主题,下游从这里订阅消费。
    Kafka Topic\text{Kafka Topic}

交付语义保障

快递配送有三种承诺等级。第一种“至多一次”:快递员只送一次,路上丢了就不管了,绝不上门补送——对应消息传递中的 at-most-once。第二种“至少一次”:快递可能重复送两次甚至三次,但绝不让你收不到——对应 at-least-once,Kafka 的默认语义。第三种“精确一次”:既不会丢件也不会重复,每个快递恰好被签收一次——对应 exactly-once。

在真实的 CDC 管道中,消费者可能崩溃、网络可能分区、Kafka 重平衡可能发生。如果消费方在“处理完但还没提交偏移量”时崩溃,重启后就会重新消费同一批日志,造成重复处理。至少一次语义保证了不丢数据,代价是可能重复;要升级到精确一次,单靠消息队列还不够,需要两件事:生产者端用幂等写入,消费者端用事务或去重表,保证每条事件即使被处理两遍,最终效果也只生效一遍。

物业向业主承诺“快递必达,但可能重复送”已经算不错了,而“精确一次”则是业主拿着签收单核对:不多送、不漏送。在数据同步场景里,重复的订单消息可能导致金额重复累加,所以越是核心的金融数据管道,越要把至少一次升级为精确一次。

#deliveryat-least-once≥1,#deliveryexactly-once=1\#\text{delivery}_{\text{at-least-once}} \geq 1, \qquad \#\text{delivery}_{\text{exactly-once}} = 1
数学上,“至少一次”保证投递次数大于等于 1,“精确一次”要求投递次数恰好等于 1。从前者到后者,必须靠端到端的幂等与去重机制来“消化”重复。
  1. 至少一次投递次数由于重发机制,每条消息至少被消费一次,可能更多。
    #deliveryat-least-once≥1\#\text{delivery}_{\text{at-least-once}} \geq 1
  2. 精确一次投递次数通过事务和幂等,每条消息恰好生效一次。
    #deliveryexactly-once=1\#\text{delivery}_{\text{exactly-once}} = 1

Schema演进

快递面单改版了!以前是手写三联单,现在改成打印的二维码面单。抄写员必须能同时读懂新旧两种面单,还要保证业主群里的通知格式大家都看得懂——这就是 Schema 演进 的日常。数据库表结构不会一成不变:加字段、删字段、改类型、重命名字段,每一样都会改变 CDC 事件的结构。

按照兼容性,字段变更分为两类。向后兼容:新 Schema 的消费者能够读取用旧 Schema 写入的数据,比如新增一个可选字段,老事件里没有这个字段,新消费者补个默认值就行。向前兼容:旧 Schema 的消费者能读取新 Schema 写入的数据,比如新增字段带默认值,老消费者忽略它也能解析。真正危险的是破坏性变更:删除字段、重命名字段、把字符串改成整数——这些会让旧消费者直接反序列化失败。业界用 Schema Registry 统一管理 Avro 协议,在发布新 Schema 之前自动做兼容性校验。

面单再怎么改版,只要二维码里还保留收件人姓名和地址,老式扫码枪也能扫出关键信息,这就是兼容性的价值。Schema 演进管理得好,表结构变化就能像快递面单升级一样平稳过渡;管理不好,下游消费链路会瞬间瘫痪。

backward:Readernew(dataold) OK,forward:Readerold(datanew) OK\text{backward}: \text{Reader}_{\text{new}}(\text{data}_{\text{old}}) \ \text{OK}, \qquad \text{forward}: \text{Reader}_{\text{old}}(\text{data}_{\text{new}}) \ \text{OK}
向后兼容考验“新代码读老数据”,向前兼容考验“老代码读新数据”。两者的交集越宽,Schema 演进的下游影响就越小。
  1. 新消费者读旧数据升级后的消费方解析历史事件,需要能容忍缺失字段。
    Readernew(dataold)\text{Reader}_{\text{new}}(\text{data}_{\text{old}})
  2. 旧消费者读新数据未升级的消费方解析新事件,需要能忽略多余字段。
    Readerold(datanew)\text{Reader}_{\text{old}}(\text{data}_{\text{new}})

乱序处理

业主们发现:同一个收件人的三个快递,从北京、上海、广州三个仓同时发出,到达小区的时间却是打乱的。登记本上的顺序是“送达顺序”,而包裹的真实下单顺序可能完全不同。CDC 事件也一样——分布式环境下,事件乱序是常态而非异常。

乱序有三大来源:第一,多分片并行消费,不同分片的处理速度不同;第二,Debezium 本身可能用多个线程读取不同表的日志;第三,Kafka 分区重平衡时,消费者会临时暂停或重放。要解决乱序,最核心的手段是给事件带上全局可比较的排序键——数据库事务时间戳或自增序列号。下游拿到带排序键的事件后,可以按主键分组、按时间戳重新排列,或者干脆用“幂等写入”兜底:不管顺序如何,最终 UPSERT 的结果是一致的。

快递到了,业主可以先拆别的包裹,等最后一个到达再一起核对——只要知道正确的先后顺序,乱序并不影响最终结果。CDC 管道也是如此:时间戳和序列号就是快递单上的“下单时间”,有了它,乱序也能最终收敛到一致状态。

ei≺ej  ⟺  (ts,i<ts,j) ∨ (ts,i=ts,j∧seqi<seqj)e_i \prec e_j \iff (t_{s,i} < t_{s,j}) \ \lor\ (t_{s,i} = t_{s,j} \land \text{seq}_i < \text{seq}_j)
两条事件谁先谁后,先比源库时间戳;时间戳相同,再比日志序列号。有了这个全序关系,下游就能把乱序事件重新排好。
  1. 源库时间戳比较先到先得,时间戳小的事件排在前面。
    ts,i<ts,jt_{s,i} < t_{s,j}
  2. 序列号次比较同一时刻提交的并发事务,用日志序列号决出先后。
    seqi<seqj\text{seq}_i < \text{seq}_j

工程实践挑战

再专业的物业团队也躲不开日常琐事:业主投诉快递放错柜、夜班保安交接时漏记了一行、梅雨天登记本受潮字迹模糊。CDC 在生产环境落地,同样要面对一系列“现实摩擦”。最典型的是事务一致性边界问题:CDC 是异步读取日志的,源库事务提交和下游可见之间存在一个窗口期。如果源库刚提交事务、CDC 还没来得及读取,此时下游查数会看到“旧值”,这就是所谓的延迟窗口。

第二个挑战是监控与运维。连接器是否在正常运行?Kafka 里积压了多少未消费的事件?Debezium 提供了 JMX 指标(如 MilliSecondsBehindSource),用来度量从源库变更到下游读取的时延。一旦发现延迟持续上涨,就要检查是源库日志量突增、还是下游消费能力不足。第三个挑战是生态集成:单打独斗的 CDC 没有意义,它必须与 Kafka、Flink、Spark Streaming 组合成完整的实时数仓链路,而这要求工程团队对整条管道的吞吐、容错和成本有全局把控。

物业把琐事管好了,业主才能长期安心。CDC 的工程化也一样:想清楚一致性边界、盯紧延迟指标、选好生态组件,这套“增量搬运”的体系才能真正在业务中稳定运转,成为数据仓库持久流动的活水。

lagCDC=tnow−tsource\text{lag}_{\text{CDC}} = t_{\text{now}} - t_{\text{source}}
这条公式衡量 CDC 管道有多“新鲜”:当前时间减去源库变更事件的发生时间,差值越小代表数据实时性越高。
  1. 当前时间 t\_now下游消费者读取事件时的系统时间。
    tnowt_{\text{now}}
  2. 源库变更时间 t\_source事件里携带的源数据库事务时间戳。
    tsourcet_{\text{source}}
  3. 延迟 lag两者之差,是 CDC 健康度的核心指标,过大说明管道出现瓶颈。
    lagCDC\text{lag}_{\text{CDC}}

AI 应用场景

实时数仓同步

阶段:数据集成阶段

解决的问题:传统离线 ETL 每天凌晨全量抽取业务库,运营要看当天的实时 GMV 只能等 T+1。CDC 将变更秒级流入数仓,让数据延迟从“天”降到“秒”。

具体案例:某电商平台的订单表在 MySQL 中,业务方用 Debezium 监听 binlog,订单每次插入或更新都会生成事件进入 Kafka;Flink 消费 Kafka 流,实时清洗聚合后写入 ClickHouse 大宽表。运营后台的 GMV 大屏从此可以实时跳动,大促期间还能按分钟追踪库存扣减。

微服务数据同步

阶段:数据分发阶段

解决的问题:微服务架构下,订单服务和库存服务各自拥有独立数据库,跨库更新如果靠应用双写,容易产生分布式事务不一致。CDC 把订单变化发布成事件,让库存服务订阅后自行更新,避免强耦合。

具体案例:某出行平台的订单服务完成支付后,订单表新增一条记录;CDC 监听订单库的日志并把“已支付”事件推送到 Kafka,库存服务、积分服务、消息推送服务各自订阅自己关心的表,互不阻塞地完成后续业务动作。

Python 演示

python73 行
"""基于日志的 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()
目的:演示 CDC 管道的核心逻辑——维护偏移量、只读增量日志、用集合去重实现“至少一次”语义下的幂等消费。
  1. class DatabaseLog模拟数据库日志文件,所有日志只能追加,天然有序。
  2. def read_since(self, seq)只返回偏移量之后的日志,这正是“增量捕获”的精髓。
  3. self.offset记录“上次读到第几条日志”,相当于物业的本子看到哪一行。
  4. self.seen_seq = set()去重集合,模拟精确一次语义中“已处理事件”的记录。
  5. if entry.seq in self.seen_seq遇到重复日志直接跳过,保证重复投递不重复生效。
  6. 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 技术中的英文全称是什么?

单选题 · easy

  • 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 最大优势是什么?

单选题 · easy

  • A.完全不需要数据库
  • B.开销低且延迟小← 正确答案
  • C.能存储更多数据
  • D.免去所有网络传输

答案:B。全量轮询每次要扫描整张表,开销随数据量线性增长,且延迟取决于轮询间隔;CDC 只读取日志中的增量变更,开销与变化量成正比,延迟可以降到秒级甚至毫秒级。A、C、D 都不是 CDC 的固有特性。

Q3. Debezium 中负责从数据库读取日志的组件是什么?

单选题 · easy

  • A.自定义序列化器
  • B.消息转换处理器
  • C.源连接器组件← 正确答案
  • D.分布式调度器

答案:C。Debezium 基于 Kafka Connect,其中 Source Connector(源连接器)负责连接数据库、读取 binlog/WAL 并生成变更事件。序列化器和转换器处理的是“事件之后”的环节,调度器更是与日志读取无关。可以理解为:连接器是物业和登记本之间的“门禁授权卡”。

Q4. “至少一次”语义可能会带来以下哪种现象?

单选题 · mid

  • A.部分数据直接丢失
  • B.同一数据重复出现← 正确答案
  • C.数据全局完全乱序
  • D.管道连接永久中断

答案:B。至少一次语义保证每条消息都能送达,但消费者崩溃后未提交偏移量时会重启重放,导致同一事件被消费两次,产生重复数据。它不会丢数据,也不必然导致永久中断。就像承诺“快递必达”的配送,可能因为签收流程异常而多送一次。

Q5. 下列哪种 Schema 变更属于破坏性变更?

单选题 · mid

  • A.新增一个可空字段
  • B.直接删除业务字段← 正确答案
  • C.修改字段注释信息
  • D.为表增加一个索引

答案:B。删除字段会让旧消费者在反序列化时找不到对应属性,属于典型的破坏性变更。新增可空字段、改注释、加索引都不影响既有数据的可解析性。正如快递面单可以多加一行“备注”,但不能把“收件人”一栏撕掉。

Q6. 处理 CDC 事件乱序时,最可靠的排序依据是什么?

单选题 · mid

  • A.随机数生成顺序
  • B.源库时间戳与序列号← 正确答案
  • C.Kafka 分区编号大小
  • D.下游到达先后顺序

答案:B。源库时间戳(事务提交时间)加上日志序列号,能构造出全局可比较的全序关系。Kafka 分区编号和下游到达顺序都是“传输顺序”而非“业务顺序”,随机数更是与顺序无关。就像按快递面单上的下单时间排序,而不是按快递员送到的先后排序。

Q7. 从“至少一次”升级到“精确一次”,通常需要结合哪两种机制?

单选题 · hard

  • A.幂等写入与事务去重← 正确答案
  • B.数据压缩与格式转换
  • C.分区分片与负载均衡
  • D.全量备份与增量恢复

答案:A。至少一次到精确一次的跨越,关键是“消化重复”:生产者端用幂等写入保证重复发送不重复生效,消费者端用事务或去重表保证重复消费不重复落地。压缩、分片、备份都与此无关。这就好比给每个快递打印唯一签收码,重复送货时扫码即知已签收。

Q8. 以下哪些属于基于日志的 CDC 的优势?

多选题 · mid

  • A.对源数据库侵入很小← 正确答案
  • B.实时性比轮询更高← 正确答案
  • C.完全不需要额外存储
  • D.要求改造应用代码

答案:AB。基于日志的 CDC 读取的是数据库本身的日志,业务表无需加时间戳、业务代码无需改造,侵入性极小;同时它实时读取日志,比定时轮询的延迟低得多。实际上 CDC 需要额外的消息队列和偏移量存储,D 更是与“无侵入”的特性相悖。

Q9. Debezium 连接器的核心职责包括哪些?

多选题 · hard

  • A.读取数据库变更日志← 正确答案
  • B.生成结构化变更事件← 正确答案
  • C.替代应用业务逻辑
  • D.将事件投递到消息队列← 正确答案

答案:ABD。连接器做三件事:读日志、生成事件、通过 Kafka Connect 把事件投递到 Kafka。它完全不介入也不替代业务逻辑,C 是明显错误。可以类比为:物业记录员只负责抄登记本并发到业主群,不会替业主决定拆不拆快递。

Q10. 应对 Schema 演进的常用策略有哪些?

多选题 · extreme

  • A.Schema 注册表兼容校验← 正确答案
  • B.新旧字段映射与默认值← 正确答案
  • C.禁止任何表结构变更
  • D.完全重放全量历史数据

答案:AB。生产环境通过 Schema Registry 自动校验发布的新 Schema 是否兼容,并对新增字段提供默认值或映射关系,让新旧消费者都能解析。禁止变更和全量重放都是极端方案:前者阻碍业务迭代,后者成本高且不能解决实时问题。