RabbitMQ + Flink 为什么必然会重复?以及如何用 seq 做稳定去重

文章目录

  • [一、RabbitMQ Source:为什么只能 at-least-once?](#一、RabbitMQ Source:为什么只能 at-least-once?)
  • [二、Flink Checkpoint 恢复:dup 是如何产生的?](#二、Flink Checkpoint 恢复:dup 是如何产生的?)
  • [三、为什么 dup 在流处理中是"不可避免"的?](#三、为什么 dup 在流处理中是“不可避免”的?)
  • [四、解决思路:基于 seq 的幂等去重](#四、解决思路:基于 seq 的幂等去重)
  • [五、为什么强调「采集侧」生成 seq?](#五、为什么强调「采集侧」生成 seq?)
  • [六、Flink 中如何基于 seq 去重?](#六、Flink 中如何基于 seq 去重?)
  • 七、这个方案为什么非常稳?
  • [八、真实业务场景:无人机 / 设备轨迹](#八、真实业务场景:无人机 / 设备轨迹)
  • 九、总结

在使用 RabbitMQ 作为 Flink Source 进行实时流处理时,很多人都会遇到一个问题:
为什么已经处理过的数据,在任务重启后又被处理了一次?

这到底是 Bug,还是设计使然?又该如何优雅解决?

本文从 Flink 容错机制 出发,解释 dup(重复数据)为什么不可避免 ,以及一个工业级、低成本、极其稳定的解决方案:基于 seq 的去重


一、RabbitMQ Source:为什么只能 at-least-once?

在 Flink 中,大多数 RabbitMQ Source 的语义是 at-least-once,而不是 exactly-once。

at-least-once 的含义是:

👉 每条消息至少会被处理一次

👉 但有可能被处理多次

原因在于:

  • RabbitMQ 本身并不原生支持与 Flink checkpoint 深度绑定的事务语义
  • Source 在 checkpoint 未完成前,消息已经被消费并处理

结论:

❗ 使用 RabbitMQ Source,本身就要接受「可能重复」这个现实


Flink 的容错机制基于 Checkpoint

  1. Flink 定期对算子状态做 checkpoint
  2. 任务异常 / 重启
  3. 最近一次成功的 checkpoint 恢复
  4. 重放 checkpoint 之后的一部分数据

这一步非常关键。

举个例子

  • Checkpoint 成功时:消费到 offset / message = 100
  • 实际运行时:已经处理到 105
  • 任务崩溃
  • 恢复后:从 100 重新消费

那么:

text 复制代码
101、102、103、104、105 会被再次处理

👉 这部分数据就成了重复数据(dup)

⚠️ 这是 Flink 的正常行为,不是 Bug


三、为什么 dup 在流处理中是"不可避免"的?

即使你:

  • 配置了 checkpoint
  • 配置了 state backend
  • 消费逻辑完全正确

只要是:

  • at-least-once Source
  • 有失败恢复

👉 dup 就一定会出现

所以正确的工程思路不是「避免 dup」,而是:

接受 dup,然后在业务层消化 dup


四、解决思路:基于 seq 的幂等去重

如果你的数据里已经有 seq(序号)字段,那事情会变得非常简单。

核心思想

每个实体(Entity)维护一个严格递增的 seq

不是全局一个 seq,而是:

text 复制代码
entity_id = A : seq = 1,2,3,4...
entity_id = B : seq = 1,2,3,4...

只要满足:

  • 同一实体内 seq 严格递增
  • 不回退
  • 不重复

那么 dup 问题就可以彻底解决


五、为什么强调「采集侧」生成 seq?

因为:

  • Flink 是下游
  • 一旦数据进入 MQ
  • 下游无法判断「这是新数据,还是历史数据」

而采集侧(设备 / SDK / 服务端):

  • 最清楚数据的真实顺序
  • 最适合生成业务语义上的 seq

👉 这是一个经典的"上游负责唯一性,下游负责幂等"的设计


处理逻辑(按实体维度)

text 复制代码
keyBy(entity_id)
→ 维护 last_seq
→ 如果 seq <= last_seq:丢弃
→ 如果 seq > last_seq:处理并更新状态

示例代码(Java)

java 复制代码
.keyBy(Event::getEntityId)
.process(new KeyedProcessFunction<String, Event, Event>() {

    private ValueState<Long> lastSeq;

    @Override
    public void open(Configuration parameters) {
        lastSeq = getRuntimeContext().getState(
            new ValueStateDescriptor<>("lastSeq", Long.class)
        );
    }

    @Override
    public void processElement(Event e, Context ctx, Collector<Event> out) throws Exception {
        Long last = lastSeq.value();

        if (last == null || e.getSeq() > last) {
            lastSeq.update(e.getSeq());
            out.collect(e);
        }
        // else: 重复数据,直接丢弃
    }
});

七、这个方案为什么非常稳?

这个方案有几个非常重要的工程优势:

✅ 不依赖 MQ 是否 exactly-once

✅ 不依赖 checkpoint 是否精确

✅ 不怕 Flink 重启

✅ 不怕 replay

✅ 不怕消息乱序

✅ 逻辑简单,性能高

✅ 状态小,易于扩展

👉 这是很多实时系统(轨迹、IoT、风控、日志)的标准做法


八、真实业务场景:无人机 / 设备轨迹

例如无人机轨迹上报:

json 复制代码
{
  "drone_id": "UAV_001",
  "seq": 1024,
  "lat": 39.9,
  "lon": 116.3,
  "ts": 1700000000
}
  • 同一架无人机:seq 严格递增
  • Flink 按 drone_id 维度维护 last_seq
  • 重放的数据会被自然丢弃

轨迹不会抖、不回退、不重复绘制


九、总结

RabbitMQ + Flink 的重复数据是设计必然
真正稳定的解决方案,是在采集侧生成"每实体严格递增的 seq",
然后在 Flink 中基于 seq 做幂等去重。

相关推荐
康王有点困3 小时前
Flink简单使用
大数据·flink
bkspiderx20 小时前
RabbitMQ 技术指南(C/C++版)
c语言·c++·rabbitmq
小北方城市网21 小时前
JVM 调优实战指南:从问题排查到参数优化
java·spring boot·python·rabbitmq·java-rabbitmq·数据库架构
信创天地1 天前
信创日志全流程管控:ELK国产化版本与华为日志服务实战应用
运维·安全·elk·华为·rabbitmq·dubbo
工业甲酰苯胺1 天前
【面试题】RabbitMQ 中无法路由的消息会去到哪里?
分布式·rabbitmq
Hello.Reader1 天前
Flink OpenSearch SQL Connector Append/Upsert、动态索引、Exactly-Once 与性能调参
大数据·sql·flink
Knight_AL1 天前
Apache Flink 窗口处理函数全解析(增量 + 全量 + 混合)
大数据·flink·apache
Jackyzhe1 天前
Flink源码阅读:Kafka Connector
大数据·flink·kafka
Knight_AL1 天前
深入理解 Apache Flink 的时间语义、Watermark 与窗口触发机制
大数据·flink