实时数据湖 flink CDC + Kafka +Doris 【企业级实战】Checkpoint、Offset、事务与幂等如何闭环 06

一、两代项目的一致性边界

第一版直连

text 复制代码
SQLServer Source → Flink State → Postgres JDBC Sink

Source 与 Sink 在同一个 Job,但普通 JDBC Commit 仍不受 Flink 两阶段提交协调,因此语义是:

text 复制代码
At-least-once + Postgres ON CONFLICT 幂等

第二版 Kafka 架构

text 复制代码
数据库 → Flink CDC → Kafka
Exactly Once(依赖 Checkpoint 与 Kafka 事务)

Kafka → Flink → Postgres / Doris JDBC
At-least-once + target-side idempotence

不能因为前半段 Exactly Once,就把整条链路都描述成 Exactly Once。

二、Checkpoint 保存什么

Checkpoint 保存 Source 位置、Keyed State 和算子状态,使任务重启后能够从一致快照恢复。

两代项目都逐步采用 HDFS 存储生产 Checkpoint。第一版按 Job 创建独立目录,第二版还区分本地与集群环境:本地默认使用文件路径,集群使用 HDFS,并允许命令行覆盖。

需要共同调整的参数包括:

text 复制代码
interval
timeout
min pause
max concurrent checkpoints
tolerable failures
externalized checkpoint retention
restart attempts / delay

三、Source-to-Kafka 的事务协同

第二版 Kafka Sink 使用:

java 复制代码
.setDeliveryGuarantee(DeliveryGuarantee.EXACTLY_ONCE)
.setTransactionalIdPrefix(transactionalIdPrefix)

Checkpoint 成功时提交 Kafka 事务。如果 Checkpoint 失败,本轮未提交事务对 Read Committed 消费者不可见。

这要求:

  • Transactional ID 前缀稳定且不冲突;
  • Kafka 事务超时大于最坏 Checkpoint 时间;
  • Operator UID 稳定;
  • Checkpoint 能持续成功,而不是长期失败。

四、Kafka 消费位置

Kafka Source 关闭自动提交,让 Offset 随 Checkpoint 提交:

java 复制代码
enable.auto.commit=false
commit.offsets.on.checkpoint=true

Consumer Group 决定"谁共享一份消费进度",Startup Mode 决定"没有可恢复状态时从哪里开始"。

Flink 自身从 Checkpoint 恢复时,优先使用 Checkpoint 中的 Source State;Group 的 committed Offset 更多用于没有相应 Flink 状态时的启动边界。

五、JDBC Buffer 为什么必须在 Checkpoint 前 Flush

如果 Checkpoint 成功时 Buffer 仍在内存,Kafka Offset 已前进,但数据尚未落库,重启后这部分记录可能无法再次读取。

因此 AbstractJdbcCdcSink.snapshotState() 会调用:

java 复制代码
@Override
public final void snapshotState(
        FunctionSnapshotContext context) throws Exception {
    runtime.flushGuard.flushForCheckpoint();
}

失败必须向上抛出,使 Checkpoint 失败。

六、一次 JDBC Flush 的事务顺序

java 复制代码
try (Connection conn = targetDs.getConnection()) {
    conn.setAutoCommit(false);
    try {
        for (RouteBatch batch : batches) {
            writer.write(conn, batch.route, batch.records);
        }

        conn.commit();
        clearCommittedBuffers();
    } catch (Exception e) {
        conn.rollback();
        throw e;
    }
}

必须遵守:

text 复制代码
先写全部路由
    ↓
全部成功后 Commit
    ↓
Commit 成功后清 Buffer

如果提前清 Buffer,Rollback 后将没有数据可重试。

七、为什么目标端仍可能重复

text 复制代码
数据库 Commit 成功
    ↓
Checkpoint 尚未完成
    ↓
TaskManager 故障
    ↓
从上一个成功 Checkpoint 重放

这批记录会再次写入。目标端必须保证:

  • Insert / Update 通过业务主键 Upsert;
  • Delete 重复执行不改变最终结果;
  • 主键缺失时明确失败;
  • 数据修正与不可变数据使用不同冲突策略。

八、顺序状态也必须恢复

SQLServer 保序不是无状态过滤。每个业务 Key 的最新 LSN、seqvalcommand_id 都是 Keyed State。

如果这部分状态没有进入 Checkpoint,任务恢复后旧事件可能重新被视为新事件。稳定 Key、稳定 Operator UID 和 Checkpoint 缺一不可。

九、Operator UID 为什么不能只用数字下标

第一版后期已经使用真实表清单 Hash 构建 UID;第二版继续把系统、算子角色、Topic 或表身份纳入 UID。

text 复制代码
错误:source-group-0
更稳妥:job + role + identities hash

UID 的目标不是永远不变,而是在逻辑身份相同的前提下稳定;拓扑身份改变时,应明确暴露状态不兼容。

十、必须做的故障注入

  1. Source 正常读取时强制终止 TaskManager;
  2. Kafka 事务未提交时触发失败;
  3. JDBC 批量执行中途抛错并验证 Rollback;
  4. 数据库 Commit 后、Checkpoint 完成前停止任务;
  5. 从 Checkpoint 和 committed Offset 分别恢复;
  6. 验证目标表无重复业务结果;
  7. 修改表分组后验证 UID 与状态兼容;
  8. HDFS 不可写时确认任务不会伪装成功。

十一、小结

text 复制代码
Checkpoint:保存可恢复状态
Kafka 事务:协调 Source-to-Kafka 消息提交
Offset:描述 Kafka 消费位置
JDBC 事务:保证一次 Flush 原子性
目标端幂等:承受故障后的重复执行

下一篇分析第二版最重要的代码重构:如何把第一版超过千行的 PostgresSink 拆成公共 JDBC 主流程和数据库差异 SPI。


相关推荐
Elastic 中国社区官方博客2 小时前
列式存储并不等同于列式数据库。Columnar 模式为 Elasticsearch 带来了什么
大数据·运维·数据库·elasticsearch·搜索引擎
weixin199701080162 小时前
[特殊字符]️《二手ERP对接电商平台的总体方案:统一数据模型 + 事件驱动 + 灰度上线6原则》(附Python源码)
大数据·python
大大大大晴天3 小时前
从元数据到数据地图:企业数据治理的第一块地基
大数据
科创致远3 小时前
科创致远 eSOP 电子作业指导书系统落地应用指南
大数据·人工智能·汽车·制造·精益工程
人丰4 小时前
AI不是空中楼阁:制造企业智能化转型的底座建设方法论
大数据·人工智能·制造
科创致远5 小时前
成都显示模组场景观察|看板、ESOP、MES,该不该合成一块屏
大数据·人工智能·制造
智慧医养结合软件开源5 小时前
【源码交付】智慧养老系统 · Java + Vue3-系统中台
大数据·人工智能·信息可视化
llilian_166 小时前
北斗授时卡同步解决方案 gnss授时卡 计算机时间同步板卡
大数据·网络·人工智能·功能测试·51单片机
ACP广源盛139246256736 小时前
GSV5600 国产 8K Serdes 视频延长芯片,AI 超高清可视化远距离传输方案解析
大数据·人工智能·ai·硬件架构·国产芯片
Databend6 小时前
Databend 原生数据血缘:追溯指标来源,检查变更影响
大数据·数据库·sql