实时数据湖 Flink CDC + Kafka +Doris 【企业级实战】之Kafka 事件缓冲层 【附核心源码】 04

一、Kafka 层承担四种职责

在当前项目中,Kafka 不只是消息队列,而是:

  1. 事件缓冲层:吸收下游短时故障和速度波动;
  2. 数据契约层:统一 MySQL、SQLServer CDC 事件;
  3. 分发层:让 Postgres、Doris 独立消费;
  4. 重放边界:在保留期内按 Topic、Partition、Offset 重新消费。

这四种职责决定了 Kafka 层不能只写一句 producer.send(json)

二、KafkaRecord 应包含什么

项目把 Kafka 消息封装为统一记录,核心概念包括:

text 复制代码
key:稳定业务键
value:CDC 事件 JSON
topic:事件所属数据流
partition:消费后可追踪分区
offset:消费后可追踪位置

Key 的重要性远高于普通日志消息。它直接影响:

  • 同一业务对象是否进入同一 Partition;
  • SQLServer 消费端能否按 Key 保存顺序状态;
  • 下游是否能建立幂等主键;
  • Delete 是否能与之前的 Insert / Update 对应。

三、进入 Kafka 前的 Enrichment

Source 输出的原始 JSON 先经过 CdcKafkaRecordEnrichmentFunction

java 复制代码
SingleOutputStreamOperator<KafkaRecord> kafkaRecords = stream
        .flatMap(new CdcKafkaRecordEnrichmentFunction(
                runtime.systemName,
                runtime.redisName,
                runtime.redisHost,
                runtime.redisPort,
                runtime.redisDatabase,
                runtime.redisPassword))
        .name("cdc-kafka-record-enrichment-" + suffix)
        .uid("cdc-kafka-record-enrichment-"
                + runtime.systemName + "-" + suffix);

这一步通常需要完成:

  • 识别或补齐 op
  • before / after 提取主键;
  • 查询 Redis 主键元数据;
  • 构建稳定 Kafka Key;
  • 保留源系统、源表和顺序字段。

如果 Enrichment 无法获得关键主键,不应随便退化成随机 Key。那会把"启动失败"变成更难发现的数据乱序和脏数据。

四、Exactly-Once Kafka Sink

当前 Kafka Sink 使用 Flink 新 Sink API:

java 复制代码
KafkaSink<KafkaRecord> sink = KafkaSink.<KafkaRecord>builder()
        .setBootstrapServers(bootstrapServers)
        .setKafkaProducerConfig(producerProps)
        .setRecordSerializer(
                new CdcKafkaRecordSerializationSchema(topic))
        .setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
        .setTransactionalIdPrefix(transactionalIdPrefix)
        .build();

配套 Producer 参数包括:

text 复制代码
acks=all
enable.idempotence=true
retries>=3
max.in.flight.requests.per.connection=5
transaction.timeout.ms=...

Flink 在 Checkpoint 成功时提交 Kafka 事务,使 Source 状态和本次写入 Kafka 的消息保持一致。

五、Transactional ID 为什么必须稳定且唯一

项目为不同 Source 类型和系统生成独立前缀,例如:

text 复制代码
flink-cdc-mysql-{system}-{topic}-{operator}-
flink-cdc-{sqlserver-system}-{topic}-{operator}-

前缀需要满足两点:

  • 稳定:同一逻辑算子重启后仍能识别和清理遗留事务;
  • 唯一:不同 Job、不同 Source 组不能使用相同前缀互相 Fence。

常见错误包括:

  • 使用随机 UUID,导致重启后事务身份变化;
  • 多个环境共用同一前缀;
  • 复制 Job 后忘记修改系统或 Topic 部分;
  • 前缀含 Kafka 不接受的特殊字符。

六、Topic 自动创建后的元数据延迟

项目启动时会检查 Topic,不存在则自动创建。但 Admin Client 返回创建成功,不代表所有 Broker 已立即完成元数据传播。

典型时间线:

text 复制代码
CreateTopics 返回成功
    ↓
立刻 DescribeTopics
    ↓
UnknownTopicOrPartitionException

正确处理是:

  • 对元数据未就绪异常做有限次数重试;
  • 每次重试打印 Topic 和次数;
  • 认证、网络、权限等其他错误不应被误判为传播延迟;
  • 最终失败时保留原始异常链。

七、Kafka Source 的 Offset 语义

Kafka 消费端关闭自动提交:

java 复制代码
props.put(ConsumerConfig.ENABLE_AUTO_COMMIT_CONFIG, "false");
props.put("commit.offsets.on.checkpoint", "true");

Source 支持三种启动模式:

java 复制代码
switch (startupMode) {
    case LATEST:
        return OffsetsInitializer.latest();
    case COMMITTED_OFFSETS:
        return OffsetsInitializer.committedOffsets(
                OffsetResetStrategy.EARLIEST);
    case EARLIEST:
    default:
        return OffsetsInitializer.earliest();
}

选择含义:

模式 适合场景 风险
Earliest 新目标端需要消费保留期内全部数据 数据量大,可能触发长时间追赶
Latest 只关心启动后的新事件 会跳过已有消息
Committed Offsets 稳定生产任务重启 首次没有 Offset 时需要明确回退策略

八、Consumer Group 为什么按目标端拆分

假设 Postgres 和 Doris 都使用:

text 复制代码
group.id=cdc-erp

Kafka 会把它们当作同一消费组中的两个消费者,共同分摊 Partition。结果不是"两边各一份",而是"两边合起来一份"。

正确设计:

text 复制代码
cdc-erp-postgres
cdc-erp-doris

Consumer Group 应由:

text 复制代码
environment + system + target type + target instance

共同构成,既避免冲突,也方便从名称识别用途。

九、Kafka 只能保证什么顺序

Kafka 能保证单 Partition 内的记录顺序,不能保证:

  • 不同 Partition 全局有序;
  • Key 不稳定时同一业务对象有序;
  • 数据库事务顺序自动等同于 Kafka Offset;
  • 下游批量执行不会重排操作。

因此顺序需要端到端共同保证:

text 复制代码
稳定业务 Key
    ↓
同 Key 进入同一 Partition
    ↓
消费端按 Key 分组
    ↓
结合源端顺序字段过滤旧事件
    ↓
Sink 批量不改变操作顺序

十、SQLServer 消费端保序

当前项目只对启用配置的 SQLServer 链路增加保序算子:

java 复制代码
DataStream<KafkaRecord> ordered = stream
        .keyBy(new CdcKafkaRecordKeySelector())
        .process(new SqlServerKafkaRecordOrderProcessFunction())
        .uid(CdcJobSupport.operatorUid(
                systemName,
                "sqlserver-cdc-order-" + downstream,
                Collections.singletonList(topic)));

顺序状态会记录:

text 复制代码
startLsn
seqval
commandId
eventTs
kafkaOffset

新事件到达时与已保存状态比较,旧事件可以被识别并过滤。这里的状态也必须随 Checkpoint 保存,否则重启后保序逻辑会失忆。

十一、Source-to-Kafka 与 Kafka-to-Sink 的语义不要混淆

text 复制代码
Source → Kafka
Exactly Once(在配置与 Checkpoint 正确的条件下)

Kafka → JDBC Target
At-least-once + target-side idempotence

Kafka 中没有重复,不代表 JDBC 目标端不会因为 Checkpoint 恢复而重复执行。每一段都要单独分析提交点。

十二、Kafka 模块验收清单

  • Topic 创建和元数据重试经过验证
  • Partition 数与 Source / Consumer 并行度匹配
  • Kafka Key 对同一业务主键保持稳定
  • Delete 事件可以生成正确 Key
  • Transactional ID 前缀稳定且跨任务唯一
  • Kafka 事务超时覆盖最坏 Checkpoint 时长
  • 每个目标端使用独立 Consumer Group
  • Startup Mode 和首次启动策略明确
  • SQLServer 顺序状态可随 Checkpoint 恢复
  • Lag、失败事务和消息重试可观测

十三、小结

Kafka 层真正增加的不是一个 Topic,而是一套必须长期稳定的数据契约和状态边界。

下一篇将转向数据之外的控制面:Pipeline YAML、资源配置、Postgres 映射表和 Redis 元数据如何共同驱动一条链路,以及配置错误为什么比代码异常更容易造成静默丢数。

相关推荐
AI人工智能+电脑小能手14 分钟前
大白话说Java设计模式-41-备忘录模式(源码剖析篇)
java·设计模式·备忘录模式·源码分析·serializable·undo log·spring statemachine
lv__pf15 分钟前
Sentinel【TL微服务10、11】
java·微服务·sentinel
Elastic 中国社区官方博客24 分钟前
驯服 PUNKs:ES|QL 如何查询 Elasticsearch 从未被告知的字段
大数据·sql·elasticsearch·搜索引擎·全文检索
Escalating_xu32 分钟前
【C++ string 上篇】从字符串基础到容量管理、迭代器与经典算法题
java·c++·算法
拓人间精准客38 分钟前
ToB 销售获客避坑指南:如何用全维度大数据实现降本增效
大数据·数据结构·单例模式
TDengine (老段)42 分钟前
TDengine 应用案例 — 能源与电力监控
大数据·数据库·物联网·能源·时序数据库·tdengine·涛思数据
电商API_180079052471 小时前
自动化获取淘宝评论数据技术分享之API调用示例
大数据·运维·人工智能·自动化·网络爬虫
数智启示录1 小时前
实时数据湖 flink CDC + Kafka +Doris 【企业级实战】性能调优与生产运维闭环 09
运维·flink·kafka
福建佰胜张工1 小时前
西门子 ULTRAMAT23 分析仪 7MB2337‑0NH10‑3PW1 原理、调试、故障处理与现场运维全记录
大数据·运维·人工智能·自动化