Files
cs-note/hhs/MQ/09-流处理与事件驱动/35-MQ-与-CDC.md
T
2026-05-24 20:51:06 +08:00

6.0 KiB
Raw Blame History

tags, create time
tags create time
MQ
CDC
Kafka
数据同步
2026-05-24 19:52

MQ 与 CDC

概述

CDC(Change Data Capture)是一种捕获数据库变更事件并将其发布到消息队列的技术模式。它解耦了数据变更的感知与消费,是构建实时数据管道的基石。

正文

什么是 CDC

传统架构中,当一个服务写入数据库后,其他系统想拿到这份数据,往往依赖定时轮询或者应用层双写。这两种方式要么延迟高,要么耦合重。CDC 的思路完全不同——它直接从数据库的变更日志中"读"出发生了什么变化,然后以事件的形式推送到消息队列。

简单来说,CDC 让数据库的每一次 INSERT、UPDATE、DELETE 都变成一条消息,下游系统按需消费即可。

CDC 的三种实现方式

基于触发器(Trigger-based):在数据库上创建触发器,每次数据变更时触发逻辑将变更写入一张中间表,再由外部程序读取。优点是实现简单,缺点是触发器会增加写操作的延迟,对高写入量的系统不太友好。

基于日志解析(Log-based):直接读取数据库的 WAL(Write-Ahead Log)或 binlog。这是目前最主流的方式,对源库几乎没有侵入性。MySQL 的 binlog、PostgreSQL 的 WAL、Oracle 的 redo log 都可以被解析。缺点是需要处理日志格式的兼容性问题,不同数据库版本可能有差异。

基于时间戳(Timestamp-based):每张表维护一个 updated_at 字段,定期查询变更数据。这种方式最简单,但只能捕获新增和更新,无法捕获删除操作,而且存在时间窗口内的延迟。

[!question] 这三种方式各有取舍。如果你的系统对延迟要求在秒级,而且不能接受对源库有任何性能影响,你会选择哪种?如果还需要捕获 DELETE 操作呢?

Debezium + Kafka Connect

Debezium 是目前最流行的开源 CDC 框架,底层基于日志解析。它以 Kafka Connect 插件的形式运行,支持 MySQL、PostgreSQL、MongoDB、SQL Server、Oracle 等主流数据库。

Kafka Connect 的架构分为两种角色:

  • Source Connector:负责从外部系统(如数据库)读取数据,写入 Kafka Topic。Debezium 就是一个 Source Connector。
  • Sink Connector:负责从 Kafka Topic 读取数据,写入目标系统(如 Elasticsearch、Redis、另一个数据库)。

两者配合,就形成了一条完整的数据管道:

graph LR
    MySQL["MySQL"] -->|"binlog"| Debezium["Debezium Source Connector"]
    PG["PostgreSQL"] -->|"WAL"| Debezium
    Debezium -->|"变更事件"| Kafka["Kafka"]
    Kafka -->|"消费"| ES["Elasticsearch"]
    Kafka -->|"消费"| DW["Data Warehouse"]
    Kafka -->|"消费"| Cache["Redis Cache"]

这里有个关键设计:Debezium 将每张表的变更发布到独立的 Topic(默认命名规则为 serverName.databaseName.tableName),下游消费者可以按表订阅,互不干扰。

典型应用场景

数据库实时同步到 ES:MySQL 作为主存储,ES 作为搜索索引。通过 CDC 将 MySQL 的变更实时同步到 ES,避免了双写带来的数据一致性问题。

数据仓库实时 ETL:传统 ETL 是 T+1 的批处理模式,引入 CDC 后可以做到分钟级甚至秒级的数据入仓,大幅缩短数据时效性。

缓存一致性:数据库变更时,通过 CDC 事件通知缓存层失效或更新,比应用层主动删除缓存更可靠,因为它不依赖业务代码的正确性。

CDC vs 应用层双写

很多团队在做数据同步时,第一反应是在应用代码里"写完 A 库再写 B 库"。这种双写看似简单,实则问题重重:

  1. 一致性难保证:如果第一个写成功、第二个写失败,数据就不一致了。引入分布式事务又太重。
  2. 代码侵入:每个写操作都要额外写同步逻辑,业务代码和数据同步耦合在一起。
  3. 维护成本:新增一个下游系统,就要改一次业务代码。

CDC 完全没有这些问题。它在数据库层面捕获变更,业务代码完全无感知。新增下游系统只需要加一个消费者,不改一行业务代码。

[!question] CDC 虽然解决了双写的一致性问题,但 CDC 本身的事件投递是"至少一次"(at-least-once)语义,下游消费者如何保证幂等?

Go 代码示例:消费 CDC 事件

package main

import (
	"context"
	"encoding/json"
	"log"

	"github.com/segmentio/kafka-go"
)

// CDCEvent Debezium 产出的 CDC 事件结构
type CDCEvent struct {
	Before map[string]interface{} `json:"before"` // 变更前的数据
	After  map[string]interface{} `json:"after"`  // 变更后的数据
	Source struct {
		Table string `json:"table"` // 源表名
	} `json:"source"`
	Op string `json:"op"` // 操作类型: c=create, u=update, d=delete
}

func main() {
	reader := kafka.NewReader(kafka.ReaderConfig{
		Brokers: []string{"localhost:9092"},
		Topic:   "myserver.mydb.orders", // Debezium 默认 Topic 命名
		GroupID: "cdc-consumer",
	})
	defer reader.Close()

	for {
		msg, err := reader.ReadMessage(context.Background())
		if err != nil {
			log.Printf("read error: %v", err)
			continue
		}

		var event CDCEvent
		if err := json.Unmarshal(msg.Value, &event); err != nil {
			log.Printf("unmarshal error: %v", err)
			continue
		}

		// 根据操作类型分发处理
		switch event.Op {
		case "c", "u": // 创建或更新 → 同步到 ES
			log.Printf("sync to ES: table=%s, id=%v", event.Source.Table, event.After["id"])
		case "d": // 删除 → 清理缓存
			log.Printf("invalidate cache: table=%s, id=%v", event.Source.Table, event.Before["id"])
		}
	}
}

这段代码消费 Debezium 产出的 CDC 事件,根据操作类型(create/update/delete)分发到不同的处理逻辑。实际生产中,你还需要处理 Schema Registry 的反序列化、错误重试、死信队列等问题。

关联笔记