vault backup: 2026-05-24 20:51:06

This commit is contained in:
hhs
2026-05-24 20:51:06 +08:00
parent 910d16682b
commit 686c0a5bd5
52 changed files with 9246 additions and 0 deletions
@@ -0,0 +1,141 @@
---
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-消息轨迹与链路追踪]]