vault backup: 2026-05-24 20:51:06
This commit is contained in:
@@ -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-消息轨迹与链路追踪]]
|
||||
Reference in New Issue
Block a user