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

142 lines
6.0 KiB
Markdown
Raw Blame History

This file contains ambiguous Unicode characters
This file contains Unicode characters that might be confused with other characters. If you think that this is intentional, you can safely ignore this warning. Use the Escape button to reveal them.
---
tags:
- MQ
- CDC
- Kafka
- 数据同步
create time: 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、另一个数据库)。
两者配合,就形成了一条完整的数据管道:
```mermaid
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 事件
```go
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 的反序列化、错误重试、死信队列等问题。
## 关联笔记
- [[30-MQ-核心概念与选型]]
- [[33-MQ-Kafka-架构与核心机制]]
- [[36-MQ-监控指标与告警]]
- [[38-MQ-消息轨迹与链路追踪]]