4、变更捕获机制:数据库日志解析(CDC)、应用层钩子、定时比对快照、事件驱动架构

数据变更捕获,说白了就是回答一个问题:数据到底什么时候变了?

我做了这么多年数据治理,见过太多团队在「找变更」这件事上栽跟头。有的靠人工排查,有的靠业务方通知,结果呢?数据对不上,影响分析全白做。

这一章,我把四种主流的变更捕获机制掰开揉碎讲清楚。每种机制都有它的适用场景,也有它的坑。你想想看,选错了方案,后面所有的影响分析都是空中楼阁。

4.1 数据库日志解析(CDC)

CDC,全称 Change Data Capture,翻译过来就是「变更数据捕获」。它是最主流、最可靠的方式之一。

它的原理很简单:数据库在写入数据时,会记录事务日志(比如 MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 Redo Log)。CDC 工具就盯着这些日志看,一旦有新日志产生,就解析出来,变成我们能理解的数据变更事件。

核心思路:不侵入业务代码,直接从数据库底层捞变更。对业务系统零影响。

我个人习惯用 Debezium + Kafka 这套组合。Debezium 负责读日志,Kafka 负责把变更事件流式传输出去。举个例子,MySQL 的 binlog 格式是这样的:

# at 12345678
#230101 10:00:00 server id 1  end_log_pos 12345999 CRC32 0x12345678
# Table: mydb.users
# Type: UPDATE
# Before: id=1001, name='张三', email='zhangsan@old.com'
# After:  id=1001, name='张三', email='zhangsan@new.com'

Debezium 解析后,会输出一个 JSON 事件:

{
  "op": "u",
  "before": {"id": 1001, "name": "张三", "email": "zhangsan@old.com"},
  "after":  {"id": 1001, "name": "张三", "email": "zhangsan@new.com"},
  "source": {"db": "mydb", "table": "users", "ts_ms": 1672531200000}
}

你看,变更前后的数据、变更时间、变更类型,全都有了。影响分析拿到这个,直接就能追溯。

避坑指南:我曾经在一个高并发项目里,直接让 CDC 工具去读主库的 binlog。结果日志解析压力太大,把主库 IO 打满了。后来我改成读从库的 binlog,问题才解决。记住:CDC 一定要连从库或备库,别碰主库。

CDC 的优点是实时性高、无侵入。但缺点也很明显:它只能捕获数据库层面的变更。如果业务代码在内存里改了数据,没写库,CDC 就抓不到。

4.2 应用层钩子

应用层钩子,就是在业务代码里埋点。每次数据变更,主动发一个通知出来。

这种方式很直接。比如你在 Java 里用 Spring 的 @EventListener,或者在 Python 里用装饰器。数据一改,事件就发出去了。

// 伪代码示例
public class UserService {
    @EventListener
    public void onUserUpdate(UserUpdateEvent event) {
        // 把变更事件发送到消息队列
        messageQueue.send(event);
    }
}

我见过不少团队用这种方式。它的好处是灵活——你能捕获任何你想捕获的变更,包括业务语义层面的变更(比如「用户升级为 VIP」这种逻辑变更)。

但它的坏处也很要命:你得改业务代码。老系统不敢动?新系统忘了埋点?钩子一漏,影响分析就断了。

注意:应用层钩子依赖开发人员的纪律性。我曾经在一个项目里,团队有 30 多个微服务,每个服务都自己埋点。结果埋点格式不统一,有的发 JSON,有的发 XML,有的干脆只发个字符串。最后我们不得不写一个适配层来统一格式。所以我的建议是:如果要用钩子,必须制定统一的埋点规范,并且用代码生成工具强制落地。

4.3 定时比对快照

定时比对快照,是最「笨」但最通用的方法。它的逻辑很简单:每隔一段时间,把当前数据全量导出来,和上一次的快照做对比,找出差异。

你想想看,这种方式不需要读日志,不需要改代码,只要你能拿到数据,就能做。对于老系统、第三方系统、甚至 Excel 表格,它都适用。

具体实现上,我一般会这样做:

  1. 生成快照:把目标表的数据导出成文件(CSV、Parquet 都行),或者直接存到一张快照表里。
  2. 比对差异:用 SQL 的 FULL OUTER JOIN 或者写个 MapReduce 任务,逐行比对。
  3. 输出变更:把新增、修改、删除的记录提取出来,形成变更事件。

举个例子,比对两张快照表的 SQL 大概长这样:

SELECT
    COALESCE(old.id, new.id) AS id,
    CASE
        WHEN old.id IS NULL THEN 'INSERT'
        WHEN new.id IS NULL THEN 'DELETE'
        ELSE 'UPDATE'
    END AS change_type,
    old.*,
    new.*
FROM snapshot_20240101 old
FULL OUTER JOIN snapshot_20240102 new
ON old.id = new.id
WHERE old.id IS NULL OR new.id IS NULL
   OR old.name != new.name
   OR old.email != new.email;

这种方式最大的问题是时效性差。你只能知道「两个时间点之间发生了什么变化」,但不知道「具体什么时候变的」。而且全量比对很耗资源,数据量一大,跑一次可能要几个小时。

适用场景:定时比对快照最适合做「离线影响分析」和「数据对账」。比如每天凌晨跑一次,看看昨天哪些数据变了,然后更新影响分析结果。实时性要求不高的场景,它反而是最稳妥的方案。

4.4 事件驱动架构

事件驱动架构,不是一种具体的捕获技术,而是一种架构模式。它的核心思想是:一切数据变更都是事件,事件驱动整个系统的流转。

在这种架构下,每个服务在修改数据时,都会发布一个领域事件。其他服务订阅这些事件,做出相应的反应。数据变更的捕获,天然就融入了系统设计里。

我参与过一个电商系统的重构,就是用事件驱动架构做的。用户下单、支付、发货,每一步都产生事件。影响分析系统直接订阅这些事件,实时更新数据血缘和影响范围。效果非常好。

事件驱动架构的关键组件包括:

  • 事件总线:Kafka、RabbitMQ、Pulsar 等消息中间件,负责事件的传输和路由。
  • 事件存储:把事件持久化,方便回溯和重放。我一般用 Kafka 自带的日志存储,或者单独存到对象存储里。
  • 事件处理器:消费事件,执行具体的业务逻辑或影响分析计算。

我的经验:事件驱动架构虽然好,但引入的复杂度也不小。事件顺序、幂等性、死信处理,都是要提前想好的。我建议先从 CDC 入手,等系统成熟了,再逐步引入事件驱动架构。别一上来就搞大跃进。

4.5 四种机制对比

说了这么多,我把四种机制的核心差异整理成一张表,方便你选型时参考:

特性 数据库日志解析(CDC) 应用层钩子 定时比对快照 事件驱动架构
实时性 高(秒级) 高(毫秒级) 低(分钟/小时级) 高(取决于事件总线)
侵入性 无 高(需改代码) 低(只读数据) 高(需重构架构)
捕获范围 数据库变更 业务逻辑变更 全量数据 业务事件
资源消耗 中(日志解析) 低 高(全量比对) 中(事件存储)
适用场景 实时数据同步、影响分析 业务语义变更捕获 离线对账、历史回溯 微服务、领域驱动设计

4.6 知识体系总览

下面这张图,把四种变更捕获机制的核心逻辑和关系画出来了。你可以把它当作本章的知识地图:

变更捕获机制知识体系 变更捕获机制 数据库日志解析 (CDC) 应用层钩子 定时比对快照 事件驱动架构 核心原理 解析 binlog/WAL 等事务日志 无侵入,实时性高 推荐:Debezium + Kafka 核心原理 在业务代码中埋点 灵活但侵入性强 需统一埋点规范 核心原理 全量导出 + FULL OUTER JOIN 通用但时效性差 适合离线对账 核心原理 一切变更都是事件 事件总线 + 事件存储 适合微服务架构 选型建议:实时场景用 CDC,离线场景用快照,新系统考虑事件驱动

嗯,四种机制讲完了。你可能会问:「那我到底该用哪一种?」

我的建议是:别只选一种。实际项目中,往往是多种机制组合使用。比如用 CDC 做实时变更捕获,用定时快照做离线对账和校验,用事件驱动架构处理跨服务的业务事件。组合起来,才能覆盖所有变更场景。