06-15-A-Kafka生态集成与流处理详解
️ 关键词:Kafka Connect · Kafka Streams · KTable · Flink on Kafka · Schema Registry · CDC · 日志管道 · Exactly-Once 流处理 · Spring Kafka
📌 导读 :Kafka 的护城河从来不只是"一个 MQ",而是围绕日志构建的完整生态 ------Connect(数据进出管道)、Streams(轻量流处理)、Schema Registry(数据契约)、Flink(重型流计算)、CDC(数据库变更捕获)。本篇讲生态集成:Kafka Connect 的 Source/Sink 架构与 Worker 模型、Kafka Streams 的 KStream/KTable 双流模型与状态存储、Flink+Kafka 的端到端一致性、Schema Registry 的 Avro 演进规则、典型日志管道与 CDC 链路架构、Spring Kafka 的工程集成。看完本篇你能做到:被问"Kafka 和 Flink 怎么配合"能讲出 Source 位点+Sink 两阶段提交的 EOS 链路,被问"KTable 是什么"能讲出 compact Topic+物化视图的关系,被问"数据怎么进数仓"能画出 CDC→Kafka→Flink→ClickHouse 的完整管道。
📑 目录
- 06-15-A-Kafka生态集成与流处理详解
-
- [📖 术语速查表(每个词都用人话解释)](#📖 术语速查表(每个词都用人话解释))
- [一、Kafka Connect:配置化的数据管道](#一、Kafka Connect:配置化的数据管道)
-
- [1.1 架构](#1.1 架构)
- [1.2 部署一个 Debezium Source(MySQL → Kafka)](#1.2 部署一个 Debezium Source(MySQL → Kafka))
- [1.3 Connect vs 自研消费者](#1.3 Connect vs 自研消费者)
- [二、Kafka Streams:应用内嵌的流处理](#二、Kafka Streams:应用内嵌的流处理)
-
- [2.1 KStream vs KTable:双流模型](#2.1 KStream vs KTable:双流模型)
- [2.2 代码示例:实时订单统计](#2.2 代码示例:实时订单统计)
- [2.3 Streams vs Flink 选型](#2.3 Streams vs Flink 选型)
- [三、Flink + Kafka:端到端 Exactly-Once](#三、Flink + Kafka:端到端 Exactly-Once)
-
- [3.1 EOS 链路的三段拼接](#3.1 EOS 链路的三段拼接)
- [3.2 典型实时数仓管道](#3.2 典型实时数仓管道)
- [四、Schema Registry:数据契约](#四、Schema Registry:数据契约)
-
- [4.1 为什么需要](#4.1 为什么需要)
- [4.2 工作机制](#4.2 工作机制)
- [五、Spring Kafka 工程集成](#五、Spring Kafka 工程集成)
-
- [5.1 生产配置模板](#5.1 生产配置模板)
- [5.2 消费与异常处理](#5.2 消费与异常处理)
- [六、跑一遍:Connect 管道与 Streams 应用](#六、跑一遍:Connect 管道与 Streams 应用)
-
- [6.1 文件 Source → Kafka → 控制台 Sink(最小 Connect 管道)](#6.1 文件 Source → Kafka → 控制台 Sink(最小 Connect 管道))
- [6.2 验证 compact Topic 的 KTable 语义](#6.2 验证 compact Topic 的 KTable 语义)
- 七、总结
-
- [7.1 一张图回顾全文](#7.1 一张图回顾全文)
- [7.2 核心要点浓缩(十二条)](#7.2 核心要点浓缩(十二条))
📖 术语速查表(每个词都用人话解释)
| 术语 | 一句话白话解释 |
|---|---|
| Kafka Connect | 官方数据管道框架------Source(外部→Kafka)/ Sink(Kafka→外部),配置化搬数据不写代码 |
| Connector / Task | Connector 是"搬数据的定义",Task 是"并行执行的单元"------一个 Connector 拆多个 Task 并行 |
| Worker | 跑 Connector 的进程------standalone(单机调试)/ distributed(集群,任务自动均衡+位点存 Kafka) |
| Kafka Streams | Java 库形态的流处理------不用独立集群,应用内嵌(对比 Flink 要独立部署) |
| KStream | "流水"抽象------每条记录独立(INSERT 语义) |
| KTable | "变更表"抽象------同 Key 只留最新(UPDATE 语义,底层是 compact Topic,12 篇三章) |
| 状态存储(State Store) | Streams 的本地状态(RocksDB)+ changelog Topic 备份------故障恢复靠重放 changelog |
| Schema Registry | Schema 注册中心------Avro/Protobuf 的版本管理与兼容性校验(Confluent 开源) |
| Flink CDC / Debezium | 数据库变更捕获------binlog/WAL → Kafka(Debezium 是 Connect Source 的事实标准) |
| 两阶段提交 Sink | Flink Exactly-Once Sink------预提交随 Checkpoint,Checkpoint 完成才 commit 事务 |
| Spring Kafka | Spring 集成------@KafkaListener + KafkaTemplate(对比 rocketmq-spring 同款封装思想) |
一、Kafka Connect:配置化的数据管道
1.1 架构
#mermaid-svg-bbGzKooeQ87FJv3t{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-bbGzKooeQ87FJv3t .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-bbGzKooeQ87FJv3t .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-bbGzKooeQ87FJv3t .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bbGzKooeQ87FJv3t .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-bbGzKooeQ87FJv3t .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-bbGzKooeQ87FJv3t .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-bbGzKooeQ87FJv3t .marker.cross{stroke:#0b0b0b;}#mermaid-svg-bbGzKooeQ87FJv3t svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-bbGzKooeQ87FJv3t p{margin:0;}#mermaid-svg-bbGzKooeQ87FJv3t .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-bbGzKooeQ87FJv3t .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bbGzKooeQ87FJv3t .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bbGzKooeQ87FJv3t .cluster-label span p{background-color:transparent;}#mermaid-svg-bbGzKooeQ87FJv3t .label text,#mermaid-svg-bbGzKooeQ87FJv3t span{fill:#333;color:#333;}#mermaid-svg-bbGzKooeQ87FJv3t .node rect,#mermaid-svg-bbGzKooeQ87FJv3t .node circle,#mermaid-svg-bbGzKooeQ87FJv3t .node ellipse,#mermaid-svg-bbGzKooeQ87FJv3t .node polygon,#mermaid-svg-bbGzKooeQ87FJv3t .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-bbGzKooeQ87FJv3t .rough-node .label text,#mermaid-svg-bbGzKooeQ87FJv3t .node .label text,#mermaid-svg-bbGzKooeQ87FJv3t .image-shape .label,#mermaid-svg-bbGzKooeQ87FJv3t .icon-shape .label{text-anchor:middle;}#mermaid-svg-bbGzKooeQ87FJv3t .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-bbGzKooeQ87FJv3t .rough-node .label,#mermaid-svg-bbGzKooeQ87FJv3t .node .label,#mermaid-svg-bbGzKooeQ87FJv3t .image-shape .label,#mermaid-svg-bbGzKooeQ87FJv3t .icon-shape .label{text-align:center;}#mermaid-svg-bbGzKooeQ87FJv3t .node.clickable{cursor:pointer;}#mermaid-svg-bbGzKooeQ87FJv3t .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-bbGzKooeQ87FJv3t .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-bbGzKooeQ87FJv3t .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-bbGzKooeQ87FJv3t .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-bbGzKooeQ87FJv3t .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-bbGzKooeQ87FJv3t .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-bbGzKooeQ87FJv3t .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-bbGzKooeQ87FJv3t .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-bbGzKooeQ87FJv3t .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-bbGzKooeQ87FJv3t .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bbGzKooeQ87FJv3t .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-bbGzKooeQ87FJv3t div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-bbGzKooeQ87FJv3t .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-bbGzKooeQ87FJv3t rect.text{fill:none;stroke-width:0;}#mermaid-svg-bbGzKooeQ87FJv3t .icon-shape,#mermaid-svg-bbGzKooeQ87FJv3t .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-bbGzKooeQ87FJv3t .icon-shape p,#mermaid-svg-bbGzKooeQ87FJv3t .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-bbGzKooeQ87FJv3t .icon-shape .label rect,#mermaid-svg-bbGzKooeQ87FJv3t .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-bbGzKooeQ87FJv3t .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-bbGzKooeQ87FJv3t .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-bbGzKooeQ87FJv3t :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Connect 集群
distributed Worker × 3
外部系统
MySQL binlog。
ClickHouse。
Elasticsearch。
Debezium Source Connector。
Task × 2 并行。
Kafka。
dbserver.order.orders。
Kafka。
order-event。
ClickHouse Sink。
Task × 3。
ES Sink。
Task × 2。
distributed 模式的三个内部 Topic :connect-configs(Connector 配置)、connect-offsets(Source 位点,如 binlog 位置)、connect-status(运行状态)------Connect 自己也是"用 Kafka 存自己的元数据"(KRaft/位点存储同款哲学,10 篇 2.2)。
1.2 部署一个 Debezium Source(MySQL → Kafka)
json
// POST /connectors (Connect REST API)
{
"name": "orders-cdc-source",
"config": {
"connector.class": "io.debezium.connector.mysql.MySqlConnector",
"tasks.max": "2",
"database.hostname": "mysql-master",
"database.server.id": "184054",
"topic.prefix": "dbserver1",
"table.include.list": "corp_ceo_order.order_info",
"schema.history.internal.kafka.bootstrap.servers": "b1:9092",
"schema.history.internal.kafka.topic": "schema-history.order"
}
}
产出 :dbserver1.corp_ceo_order.order_info Topic------每条 binlog 变更一个事件(op=c 创建/u 更新/d 删除,含 before/after 镜像):
json
{
"before": null,
"after": {"id": 1001, "status": "PAID", "amount": 99.9},
"op": "c", // c=create u=update d=delete r=snapshot
"ts_ms": 1704067200123,
"source": {"file": "mysql-bin.000042", "pos": 154823} // binlog 位点(存 connect-offsets)
}
价值 :业务代码零改动的数据同步------下游(ES/ClickHouse/缓存刷新)订阅 CDC Topic 即可(对比 RocketMQ 篇 06-07-A 的 3.2 节:视图类同步用 CDC,核心链路用业务双写)。
1.3 Connect vs 自研消费者
| 维度 | Kafka Connect | 自研 Consumer 写 Sink |
|---|---|---|
| 开发量 | 配置化(JSON 声明) | 写代码 |
| 位点/重试/并行 | 框架管(Task 自动均衡) | 自己管 |
| 灵活度 | 受 Connector 能力限制 | 任意逻辑 |
| 适用 | 标准同步(DB↔Kafka↔ES/CK) | 复杂业务转换 |
二、Kafka Streams:应用内嵌的流处理
2.1 KStream vs KTable:双流模型
text
KStream(流水/INSERT 语义):
(k1,v1) (k1,v2) (k1,v3) ------ 每条都是独立事件
订单事件流、点击流
KTable(变更表/UPDATE 语义):
(k1,v1) (k1,v2) (k1,v3) ------ 只有 v3 是"当前值"
用户画像表、商品库存表(底层 compact Topic,12 篇三章)
Stream-Table 对偶性(核心思想):
Stream 聚合 → Table(订单流 groupBy 求和 → 每用户消费总额表)
Table 变更 → Stream(画像表的每次更新 → 变更事件流)
------"流水的物化是表,表的日志是流水"(日志即数据库,12 篇结语)
2.2 代码示例:实时订单统计
java
// 订单事件流 → 每分钟每城市成交额(KStream → KTable → Topic)
StreamsBuilder builder = new StreamsBuilder();
builder.stream("order-event",
Consumed.with(Serdes.String(), orderEventSerde))
.groupByKey() // 按 orderId 聚合去重
.windowedBy(TimeWindows.ofSizeAndGrace(
Duration.ofMinutes(1), Duration.ofSeconds(10))) // 1 分钟窗口 + 10s 宽限
.aggregate(
CityAmount::new, // 初始值
(key, event, agg) -> agg.add(event), // 聚合逻辑
Materialized.with(Serdes.String(), cityAmountSerde)
.withRetention(Duration.ofHours(1))) // 状态保留 1h
.toStream()
.to("order-stats-1m"); // 结果写回 Kafka(可再 Sink 到 CK)
// 状态存储:本地 RocksDB + changelog Topic 备份
// 实例挂了 → 新实例重放 changelog 恢复状态(Exactly-Once 下配合事务)
2.3 Streams vs Flink 选型
| 维度 | Kafka Streams | Flink |
|---|---|---|
| 部署 | Java 库,应用内嵌(无独立集群) | 独立集群(JobManager/TaskManager) |
| 数据源 | 只能 Kafka | 任意(Kafka/DB/文件/MQ) |
| 状态规模 | 中(本地 RocksDB) | 大(分布式状态+增量 Checkpoint) |
| 语义 | 事件时间/窗口/Exactly-Once(Kafka 内闭环) | 全(两阶段提交跨系统 EOS) |
| 运维成本 | 极低(就是个 Spring Boot 应用) | 高(集群+Checkpoint 调优) |
| 适用 | Kafka→Kafka 的轻量 ETL/聚合 | 重计算/多源/大状态 |
一句话 :数据不出 Kafka、逻辑不复杂 → Streams(省一个集群);多源汇聚、大状态、复杂 CEP → Flink。
三、Flink + Kafka:端到端 Exactly-Once
3.1 EOS 链路的三段拼接
下游 Consumer(read_committed) Kafka(结果 Topic) Flink Sink(事务 Producer) Checkpoint 协调器 Flink 计算算子(状态) Flink Source Kafka(源 Topic) 下游 Consumer(read_committed) Kafka(结果 Topic) Flink Sink(事务 Producer) Checkpoint 协调器 Flink 计算算子(状态) Flink Source Kafka(源 Topic) #mermaid-svg-JjTbcIdh6c4SaUx2{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-JjTbcIdh6c4SaUx2 .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-JjTbcIdh6c4SaUx2 .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-JjTbcIdh6c4SaUx2 .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-JjTbcIdh6c4SaUx2 .marker.cross{stroke:#0b0b0b;}#mermaid-svg-JjTbcIdh6c4SaUx2 svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-JjTbcIdh6c4SaUx2 p{margin:0;}#mermaid-svg-JjTbcIdh6c4SaUx2 .actor{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-JjTbcIdh6c4SaUx2 text.actor>tspan{fill:#333;stroke:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .actor-line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .innerArc{stroke-width:1.5;stroke-dasharray:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .messageLine0{stroke-width:1.5;stroke-dasharray:none;stroke:#333;}#mermaid-svg-JjTbcIdh6c4SaUx2 .messageLine1{stroke-width:1.5;stroke-dasharray:2,2;stroke:#333;}#mermaid-svg-JjTbcIdh6c4SaUx2 #arrowhead path{fill:#333;stroke:#333;}#mermaid-svg-JjTbcIdh6c4SaUx2 .sequenceNumber{fill:#f4f4f4;}#mermaid-svg-JjTbcIdh6c4SaUx2 #sequencenumber{fill:#333;}#mermaid-svg-JjTbcIdh6c4SaUx2 #crosshead path{fill:#333;stroke:#333;}#mermaid-svg-JjTbcIdh6c4SaUx2 .messageText{fill:#333;stroke:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .labelBox{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-JjTbcIdh6c4SaUx2 .labelText,#mermaid-svg-JjTbcIdh6c4SaUx2 .labelText>tspan{fill:#333;stroke:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .loopText,#mermaid-svg-JjTbcIdh6c4SaUx2 .loopText>tspan{fill:#333;stroke:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .loopLine{stroke-width:2px;stroke-dasharray:2,2;stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:hsl(40.5882352941, 60%, 83.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .note{stroke:hsl(52.6829268293, 60%, 73.9215686275%);fill:#fff5ad;}#mermaid-svg-JjTbcIdh6c4SaUx2 .noteText,#mermaid-svg-JjTbcIdh6c4SaUx2 .noteText>tspan{fill:#333;stroke:none;}#mermaid-svg-JjTbcIdh6c4SaUx2 .activation0{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .activation1{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .activation2{fill:hsl(-79.4117647059, 100%, 93.3333333333%);stroke:hsl(-79.4117647059, 100%, 83.3333333333%);}#mermaid-svg-JjTbcIdh6c4SaUx2 .actorPopupMenu{position:absolute;}#mermaid-svg-JjTbcIdh6c4SaUx2 .actorPopupMenuPanel{position:absolute;fill:#fff4dd;box-shadow:0px 8px 16px 0px rgba(0,0,0,0.2);filter:drop-shadow(3px 5px 2px rgb(0 0 0 / 0.4));}#mermaid-svg-JjTbcIdh6c4SaUx2 .actor-man line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;}#mermaid-svg-JjTbcIdh6c4SaUx2 .actor-man circle,#mermaid-svg-JjTbcIdh6c4SaUx2 line{stroke:hsl(40.5882352941, 60%, 83.3333333333%);fill:#fff4dd;stroke-width:2px;}#mermaid-svg-JjTbcIdh6c4SaUx2 :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} 故障恢复:从 Checkpoint 位点重放源数据 + 恢复状态 + 未提交事务 abort = 端到端 Exactly-Once(不重不丢) poll 拉取消息 1 返回批次 2 转发数据 3 计算结果 4 beginTransaction + 写入(预提交,下游不可见) 5 触发 Checkpoint(屏障注入) 6 快照当前消费位点 7 快照算子状态(Chandy-Lamport 一致性快照) 8 snapshotState:事务预提交完成 9 Checkpoint 完成通知 10 commitTransaction(提交事务) 11 已提交消息可见(只见完整结果) 12
java
// Flink 写 Kafka 的 EOS Sink
KafkaSink<OrderStats> sink = KafkaSink.<OrderStats>builder()
.setBootstrapServers("b1:9092")
.setRecordSerializer(...)
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE) // 两阶段提交
.setTransactionalIdPrefix("order-stats-") // 事务 ID 前缀
.build();
// 注意:transaction.timeout.ms 要 > Checkpoint 间隔 + 恢复时间(默认 15min 上限要调 Broker)
EOS 的边界 (25-A 篇通用结论的 Kafka 版):"Kafka→Flink→Kafka"闭环才有真 EOS ;Sink 到外部系统(MySQL/ClickHouse)时,EOS 退化为"At Least Once + 目标端幂等"(ClickHouse 的 ReplacingMergeTree / MySQL 的 upsert)。
3.2 典型实时数仓管道
text
业务 MySQL ──Debezium──> Kafka(CDC Topic)
├──Flink 清洗打宽──> Kafka(DWD 明细层)
│ └──Flink 聚合──> Kafka(DWS 汇总层)
│ └──Sink──> ClickHouse(ADS 应用层,19-A 篇)
└──Sink──> Elasticsearch(实时搜索视图,17-A 篇)
分层:ODS(CDC 原始)→ DWD(明细清洗)→ DWS(轻度汇总)→ ADS(应用指标)
------每层都是 Kafka Topic(可回放!),计算都是 Flink 作业
这就是"流式数仓":对比离线数仓(Hive T+1),延迟从小时级到秒级
四、Schema Registry:数据契约
4.1 为什么需要
JSON 自由格式的腐化:上游改字段名/类型,下游解析崩溃------"消息格式靠口口相传"是管道事故的头号来源。
4.2 工作机制
text
① Producer 发送前:Schema 注册到 Registry → 得到全局唯一 schemaId
② 消息只带 5 字节头(magic+schemaId)+ Avro 二进制体(不带 Schema 全文!)
------比 JSON 小得多(Schema 不重复传输),且格式强约束
③ Consumer 收到:按 schemaId 从 Registry 拉 Schema 反序列化(本地缓存)
④ 兼容性校验:新 Schema 注册时 Registry 自动校验
BACKWARD(默认):新 Schema 能读旧数据(可加带默认值的字段/删字段)
FORWARD:旧 Schema 能读新数据
FULL:两者都可
------不兼容的变更直接拒绝注册(把"下游崩溃"拦在发布前)
演进规则速记 (BACKWARD):加字段必须带默认值、删字段随意、改类型禁止------遵守这三条,上下游可以独立发布(Schema 版本化的工程价值)。
五、Spring Kafka 工程集成
5.1 生产配置模板
yaml
spring:
kafka:
bootstrap-servers: b1:9092,b2:9092,b3:9092
producer:
acks: all
retries: 2147483647
properties:
enable.idempotence: true
max.in.flight.requests.per.connection: 5
key-serializer: org.apache.kafka.common.serialization.StringSerializer
value-serializer: org.springframework.kafka.support.serializer.JsonSerializer
consumer:
group-id: ${spring.application.name} # 组名=应用名(规范)
enable-auto-commit: false # 手动提交(10 篇军规)
auto-offset-reset: earliest
max-poll-records: 200
properties:
session.timeout.ms: 45000
max.poll.interval.ms: 300000
listener:
ack-mode: manual_immediate
concurrency: 3 # 每实例 3 个消费线程(≤Partition 数)
5.2 消费与异常处理
java
@Component
public class OrderEventConsumer {
@KafkaListener(topics = "order-event", groupId = "point-service")
public void onMessage(ConsumerRecord<String, OrderEvent> record, Acknowledgment ack) {
try {
pointService.grantIdempotent(record.value()); // 幂等在业务层(25-A 篇)
ack.acknowledge(); // 先业务后提交
} catch (RetryableException e) {
throw e; // 抛给 DefaultErrorHandler → 退避重试 → 超限进 DLT(10 篇 5.2)
}
}
}
// 重试+死信(Spring Kafka 封装,对比 RocketMQ 内核自带)
@Bean
DefaultErrorHandler errorHandler(KafkaTemplate<String, Object> template) {
DeadLetterPublishingRecoverer recoverer = new DeadLetterPublishingRecoverer(template,
(r, e) -> new TopicPartition(r.topic() + ".DLT", r.partition())); // 同 Partition 保序
return new DefaultErrorHandler(recoverer,
new ExponentialBackOff(1000L, 2.0)); // 1s 起指数退避
}
与 rocketmq-spring 集成对比(06-07-A 篇 1.2):
| 维度 | Spring Kafka | rocketmq-spring |
|---|---|---|
| 重试/死信 | 框架层配置(DefaultErrorHandler+DLT) | 内核自带(%RETRY%/%DLQ%) |
| 位点/ACK | 手动 ack(manual_immediate) | 框架自动 ACK(异常=重试) |
| 并发模型 | concurrency 线程 + Partition 约束 | consumeThreadMin/Max + Queue 约束 |
| 顺序 | Partition 内天然有序 + DLT 同 Partition | 顺序消费要 ConsumeMode.ORDERLY |
六、跑一遍:Connect 管道与 Streams 应用
6.1 文件 Source → Kafka → 控制台 Sink(最小 Connect 管道)
shell
# ① 写一个测试文件,配置 standalone Connect(文件 Source + 控制台 Sink)
echo -e "line-1\nline-2" > /tmp/connect-test.txt
cat > connect-standalone.properties <<'EOF'
bootstrap.servers=localhost:9092
key.converter=org.apache.kafka.connect.storage.StringConverter
value.converter=org.apache.kafka.connect.storage.StringConverter
offset.storage.file.filename=/tmp/connect.offsets
EOF
cat > file-source.properties <<'EOF'
name=local-file-source
connector.class=FileStreamSource
tasks.max=1
file=/tmp/connect-test.txt
topic=connect-test
EOF
# ② 启动 Worker(加载 Source 连接器)
bin/connect-standalone.sh connect-standalone.properties file-source.properties &
# ③ 追加一行文件,观察自动进入 Kafka
echo "line-3" >> /tmp/connect-test.txt
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic connect-test --from-beginning --timeout-ms 5000
③ 的输出(文件内容被 Source Task 持续同步进 Topic):
text
line-1
line-2
line-3
Processed 3 messages
观察点 :Source 的位点(文件读到第几字节)存在 /tmp/connect.offsets(distributed 模式存 connect-offsets Topic)------断点续传的原理和 Consumer 位点完全一致(1.1 节"Connect 用 Kafka 管自己")。
6.2 验证 compact Topic 的 KTable 语义
shell
# ④ 12 篇 6.2 的 user-profile compact Topic:用 Streams 的 KTable 读"当前状态"
# (KTable 源码层就是"从 compact Topic 头读一遍,同 Key 覆盖")
bin/kafka-console-consumer.sh --bootstrap-server localhost:9092 \
--topic user-profile --from-beginning \
--property print.key=true --timeout-ms 5000
④ 的输出(compact 后每 Key 一条 = KTable 的"物化视图"):
text
user1002 {"level":1,"city":"上海"}
user1001 {"level":3,"city":"深圳"}
Processed 2 messages
💡 对照理解 :④就是 2.1 节 KTable 的磁盘形态------"compact Topic 从头读一遍 = 表的当前快照" 。Streams 的 KTable 只是把这个过程做成了持续增量的内存/RocksDB 状态;而 ①~③ 的 Connect 演示说明:数据进出 Kafka 的"最后一公里"也位点化了(文件偏移=binlog 位点=消费 Offset,全是"日志+位点"范式,12 篇结语的又一次印证)。
七、总结
7.1 一张图回顾全文
#mermaid-svg-gWg5F0zpfXzp3pSt{font-family:Microsoft YaHei;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-gWg5F0zpfXzp3pSt .error-icon{fill:hsl(220.5882352941, 100%, 98.3333333333%);}#mermaid-svg-gWg5F0zpfXzp3pSt .error-text{fill:rgb(8.5000000002, 5.7500000001, 0);stroke:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-gWg5F0zpfXzp3pSt .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-gWg5F0zpfXzp3pSt .marker{fill:#0b0b0b;stroke:#0b0b0b;}#mermaid-svg-gWg5F0zpfXzp3pSt .marker.cross{stroke:#0b0b0b;}#mermaid-svg-gWg5F0zpfXzp3pSt svg{font-family:Microsoft YaHei;font-size:16px;}#mermaid-svg-gWg5F0zpfXzp3pSt p{margin:0;}#mermaid-svg-gWg5F0zpfXzp3pSt .label{font-family:Microsoft YaHei;color:#333;}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster-label text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster-label span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster-label span p{background-color:transparent;}#mermaid-svg-gWg5F0zpfXzp3pSt .label text,#mermaid-svg-gWg5F0zpfXzp3pSt span{fill:#333;color:#333;}#mermaid-svg-gWg5F0zpfXzp3pSt .node rect,#mermaid-svg-gWg5F0zpfXzp3pSt .node circle,#mermaid-svg-gWg5F0zpfXzp3pSt .node ellipse,#mermaid-svg-gWg5F0zpfXzp3pSt .node polygon,#mermaid-svg-gWg5F0zpfXzp3pSt .node path{fill:#fff4dd;stroke:hsl(40.5882352941, 60%, 83.3333333333%);stroke-width:1px;}#mermaid-svg-gWg5F0zpfXzp3pSt .rough-node .label text,#mermaid-svg-gWg5F0zpfXzp3pSt .node .label text,#mermaid-svg-gWg5F0zpfXzp3pSt .image-shape .label,#mermaid-svg-gWg5F0zpfXzp3pSt .icon-shape .label{text-anchor:middle;}#mermaid-svg-gWg5F0zpfXzp3pSt .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-gWg5F0zpfXzp3pSt .rough-node .label,#mermaid-svg-gWg5F0zpfXzp3pSt .node .label,#mermaid-svg-gWg5F0zpfXzp3pSt .image-shape .label,#mermaid-svg-gWg5F0zpfXzp3pSt .icon-shape .label{text-align:center;}#mermaid-svg-gWg5F0zpfXzp3pSt .node.clickable{cursor:pointer;}#mermaid-svg-gWg5F0zpfXzp3pSt .root .anchor path{fill:#0b0b0b!important;stroke-width:0;stroke:#0b0b0b;}#mermaid-svg-gWg5F0zpfXzp3pSt .arrowheadPath{fill:#0b0b0b;}#mermaid-svg-gWg5F0zpfXzp3pSt .edgePath .path{stroke:#0b0b0b;stroke-width:2.0px;}#mermaid-svg-gWg5F0zpfXzp3pSt .flowchart-link{stroke:#0b0b0b;fill:none;}#mermaid-svg-gWg5F0zpfXzp3pSt .edgeLabel{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-gWg5F0zpfXzp3pSt .edgeLabel p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-gWg5F0zpfXzp3pSt .edgeLabel rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-gWg5F0zpfXzp3pSt .labelBkg{background-color:rgba(243.9999999999, 220.9999999998, 255, 0.5);}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster rect{fill:hsl(220.5882352941, 100%, 98.3333333333%);stroke:hsl(220.5882352941, 60%, 88.3333333333%);stroke-width:1px;}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster text{fill:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-gWg5F0zpfXzp3pSt .cluster span{color:rgb(8.5000000002, 5.7500000001, 0);}#mermaid-svg-gWg5F0zpfXzp3pSt div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:Microsoft YaHei;font-size:12px;background:hsl(220.5882352941, 100%, 98.3333333333%);border:1px solid hsl(220.5882352941, 60%, 88.3333333333%);border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-gWg5F0zpfXzp3pSt .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-gWg5F0zpfXzp3pSt rect.text{fill:none;stroke-width:0;}#mermaid-svg-gWg5F0zpfXzp3pSt .icon-shape,#mermaid-svg-gWg5F0zpfXzp3pSt .image-shape{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);text-align:center;}#mermaid-svg-gWg5F0zpfXzp3pSt .icon-shape p,#mermaid-svg-gWg5F0zpfXzp3pSt .image-shape p{background-color:hsl(-79.4117647059, 100%, 93.3333333333%);padding:2px;}#mermaid-svg-gWg5F0zpfXzp3pSt .icon-shape .label rect,#mermaid-svg-gWg5F0zpfXzp3pSt .image-shape .label rect{opacity:0.5;background-color:hsl(-79.4117647059, 100%, 93.3333333333%);fill:hsl(-79.4117647059, 100%, 93.3333333333%);}#mermaid-svg-gWg5F0zpfXzp3pSt .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-gWg5F0zpfXzp3pSt .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-gWg5F0zpfXzp3pSt :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} Kafka 生态集成
Connect。
Source/Sink 配置化管道
Connector 定义,Task 并行执行
distributed:元数据存内部 Topic
Debezium CDC 是事实标准。
Streams。
KStream 流水 vs KTable 变更表
Stream-Table 对偶(聚合/变更)
状态=RocksDB+changelog Topic
不出 Kafka 的轻量 ETL 选它。
Flink。
EOS 三段:Source 位点入 Checkpoint
+状态快照+Sink 两阶段提交
闭环真 EOS,外部系统靠幂等
流式数仓:ODS→DWD→DWS→ADS 全是 Topic。
契约与工程。
Schema Registry:schemaId 5 字节头
BACKWARD 兼容校验拦在发布前
Spring Kafka:手动 ack+ErrorHandler+DLT
重试死信框架配(对比 RocketMQ 内核带)。
7.2 核心要点浓缩(十二条)
- Connect 模型 :Connector(定义)→ Task(并行单元)→ Worker(进程)------distributed 模式任务自动均衡,元数据存三个内部 Topic(用 Kafka 管自己)。
- Debezium CDC :binlog → Kafka 事件(op=c/u/d + before/after + binlog 位点)------业务零改动的数据同步,视图类场景首选(对比业务双写)。
- Connect vs 自研:标准同步用 Connect(配置化+框架管位点重试),复杂转换才自研 Consumer。
- KStream vs KTable :流水(INSERT 语义)vs 变更表(UPDATE 语义,底层 compact Topic)------Stream-Table 对偶是 Streams 的核心思想。
- KTable 的磁盘形态 :compact Topic 从头读一遍 = 当前快照------"日志即数据库"的生态印证(12 篇)。
- Streams 状态 :本地 RocksDB + changelog Topic 备份------故障重放 changelog 恢复;适用"Kafka→Kafka 轻量 ETL"(省一个 Flink 集群)。
- Streams vs Flink:不出 Kafka 选 Streams(库形态零运维),多源/大状态/复杂 CEP 选 Flink(独立集群)。
- Flink EOS 三段 :Source 位点入 Checkpoint + 状态一致性快照 + Sink 两阶段提交(Kafka 事务)------下游 read_committed 闭环。
- EOS 边界 :Kafka 闭环才是真 EOS------Sink 外部系统退化为 At Least Once + 目标端幂等(ReplacingMergeTree/upsert)。
- 流式数仓分层 :ODS(CDC)→DWD(清洗)→DWS(汇总)→ADS(ClickHouse)------每层都是可回放的 Topic,延迟秒级(对比 Hive T+1)。
- Schema Registry :消息带 5 字节 schemaId(不带 Schema 全文)+ BACKWARD 兼容校验------把"下游解析崩溃"拦在 Schema 注册时;演进三规则:加字段带默认值/删字段随意/改类型禁止。
- Spring Kafka 军规 :手动 ack(manual_immediate)+ DefaultErrorHandler 退避重试 + DLT 死信(同 Partition 保序)------重试死信靠框架配置(RocketMQ 是内核自带,集成心智不同)。
📌 最后一句话 :Kafka 生态的统一范式是"日志 + 位点 + 重放 "------Connect 的位点是文件偏移/binlog 位置,Streams 的状态靠 changelog 重放恢复,Flink 的 EOS 靠 Checkpoint 位点对齐,Schema Registry 让重放的数据永远可解析。一切组件都在回答同一个问题:"从日志的哪个位置、以什么格式、重新算一遍"------这就是"日志即架构"(The Log as Architecture):理解了这一点,Kafka 生态的每个新组件你都能预判它的设计。
📌 配套阅读:
如果这篇文章对你有帮助,欢迎点赞、收藏、关注!