2、CDC核心原理:三种主流方案深度拆解

CDC(Change Data Capture)说白了就是「怎么把数据库里的变化抓出来」。我在不同项目里用过三种主流方案:基于日志、基于轮询、基于触发器。每种方案都有自己的脾气,选错了后面会很难受。

今天我把这三种方案的原理、优缺点、适用场景掰开揉碎了讲。你想想看,一个每天几千万笔交易的电商系统,跟一个只有几百条配置数据的后台管理工具,能用同一种CDC方案吗?显然不能。

2.1 基于日志的CDC(Log-based CDC)

这是目前生产环境里用得最多的方案。核心思路很简单:数据库每次写操作都会记日志(比如MySQL的binlog、PostgreSQL的WAL),我们直接去读这些日志就行了。

工作原理:

  • 数据库引擎在提交事务时,把变更记录写入事务日志
  • CDC工具(如Canal、Debezium)伪装成从库,拉取日志流
  • 解析日志中的行变更、事务边界、DDL操作
  • 把解析结果推送到消息队列或直接写入目标系统

核心优势:对源库零侵入,不改变业务代码,不增加查询压力。

我在项目中遇到过一件事:某金融系统要求CDC延迟不能超过200毫秒,而且不能影响主库性能。当时选了Log-based方案,用Debezium监听MySQL binlog,配合Kafka做缓冲,延迟稳定在50毫秒以内。如果换成轮询方案,光查询压力就能把从库打挂。

个人经验:Log-based方案最怕两件事——日志清理策略和DDL变更。我曾经因为binlog过期时间设得太短,导致全量重跑了一次。建议binlog保留至少24小时,DDL变更前先暂停CDC消费。

适用场景:

  • 高并发、低延迟要求的在线业务系统
  • 源库不能承受额外查询压力
  • 需要保证数据变更的完整性和顺序性

2.2 基于轮询的CDC(Polling-based CDC)

轮询方案是最「笨」但也是最通用的方法。说白了就是定时执行SQL查询,把「变了的数据」捞出来。

常见实现方式:

  1. 时间戳轮询:在表里加一个last_modified字段,每次查大于上次轮询时间的记录
  2. 版本号轮询:维护一个递增的版本号字段,每次查版本号大于上次记录的记录
  3. 全表比对:每次拉全表数据,跟内存里的快照做diff(不推荐,性能极差)
-- 时间戳轮询示例(每5秒执行一次)
SELECT * FROM orders 
WHERE last_modified > '2024-01-15 10:00:00'
  AND last_modified <= '2024-01-15 10:00:05'
ORDER BY last_modified ASC
LIMIT 1000;

注意:时间戳轮询有个坑——如果同一秒内有多条记录,可能会漏掉或重复。我建议把时间戳精度提高到毫秒级,或者配合自增ID做去重。

轮询方案最大的问题是延迟和性能。你想想看,轮询间隔设1秒,延迟至少1秒;设100毫秒,数据库查询压力就上来了。我曾经在一个日活百万的论坛系统里用轮询方案,轮询间隔3秒,结果高峰期数据库CPU直接飙到80%。后来不得不改成Log-based。

适用场景:

  • 数据量小、变更频率低的系统(比如配置中心、字典表)
  • 源库不支持日志解析(比如某些老旧的数据库)
  • 对延迟不敏感的后台分析场景

2.3 基于触发器的CDC(Trigger-based CDC)

触发器方案是在数据库里建触发器,每次INSERT、UPDATE、DELETE操作触发时,把变更记录写到一张专门的变更日志表里。

实现步骤:

  1. 在源表上创建AFTER INSERT/UPDATE/DELETE触发器
  2. 触发器把变更数据(旧值、新值、操作类型、时间戳)写入变更日志表
  3. CDC消费者定期从变更日志表拉取数据
  4. 消费完成后标记或删除已处理记录
-- MySQL触发器示例
CREATE TRIGGER trg_orders_cdc
AFTER INSERT ON orders
FOR EACH ROW
BEGIN
  INSERT INTO cdc_orders_log(
    operation_type, 
    order_id, 
    order_data, 
    created_at
  ) VALUES (
    'INSERT',
    NEW.id,
    JSON_OBJECT('order_no', NEW.order_no, 'amount', NEW.amount),
    NOW()
  );
END;

核心问题:触发器会拖慢源库的写入性能。每个DML操作都要额外执行一次触发器逻辑,在高并发场景下影响明显。

我记得有一次帮客户排查性能问题,他们的订单写入接口平均耗时从50毫秒涨到了200毫秒。查了半天,发现是CDC触发器里做了JSON序列化和跨表写入。后来把触发器改成只记录主键和操作类型,其他信息由消费者通过主键回查补齐,性能才恢复正常。

适用场景:

  • 对实时性要求不高、写入并发低的小型系统
  • 数据库不支持日志解析,但又需要比轮询更可靠的变更捕获
  • 需要捕获「谁在什么时候改了什么」的审计需求

2.4 三种方案对比

下面这张表是我在实际选型时常用的对比维度,你直接拿去用就行。

对比维度 Log-based Polling-based Trigger-based
对源库影响 无影响 有查询压力 影响写入性能
实时性 毫秒级 秒级(取决于轮询间隔) 秒级(取决于消费频率)
数据完整性 高(事务级别) 中(可能漏数据) 高(触发器保证)
实现复杂度 高(需要解析日志) 低(SQL查询即可) 中(需要管理触发器)
维护成本 中(关注日志清理) 低 高(触发器影响排查难)
数据库兼容性 依赖数据库日志格式 所有数据库都支持 所有支持触发器的数据库
典型延迟 10-100ms 1-30s 1-10s

我的选型建议:能用Log-based就别用其他两种。如果数据库不支持日志解析,优先考虑轮询方案,触发器方案尽量少用——它就像在业务代码里埋雷,排查问题的时候特别痛苦。

2.5 三种方案核心逻辑对比图

下面这张SVG图把三种方案的流程画清楚了,你可以直观看到它们的区别。

三种CDC方案核心流程对比 三种CDC方案核心流程对比 Log-based CDC 业务写入操作 数据库写入事务日志 CDC工具解析日志 推送至消息队列 Polling-based CDC 业务写入操作 数据写入业务表 定时SQL轮询查询 比对并获取变更数据 Trigger-based CDC 业务写入操作 触发器自动触发 写入变更日志表 消费者拉取日志表 关键差异总结 • Log-based:源库零侵入,延迟最低,但需要解析日志格式 • Polling-based:实现最简单,但会带来查询压力,延迟较高 • Trigger-based:数据完整性好,但会拖慢源库写入性能 • 选型建议:高并发低延迟选Log-based,小数据量选Polling,审计需求选Trigger

2.6 选型决策指南

说了这么多,到底怎么选?我一般按下面这个思路来决策:

  1. 先看数据库支持什么:MySQL 5.7+、PostgreSQL 9.4+、Oracle 10g+都支持日志解析。如果用的是SQLite、H2这种嵌入式数据库,只能选轮询或触发器。
  2. 再看延迟要求:要求秒级以内?必须上Log-based。能接受分钟级?轮询方案更省事。
  3. 最后看团队能力:Log-based方案需要懂日志格式、懂CDC工具配置,团队里没人搞过的话,学习成本不低。

避坑提醒:我曾经在一个项目里混合使用了Log-based和Trigger-based两种方案——Log-based负责核心交易表,Trigger-based负责审计日志表。结果触发器里的逻辑跟日志解析的顺序对不上,导致数据不一致。后来统一改成Log-based才解决。所以我的建议是:一个系统里尽量只用一种CDC方案,别混用。