欢迎光临
我们一直在努力

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

文章目录

  • 一、RabbitMQ Source:为什么只能 at-least-once?
  • 二、Flink Checkpoint 恢复:dup 是如何产生的?
  • 三、为什么 dup 在流处理中是“不可避免”的?
  • 四、解决思路:基于 seq 的幂等去重
  • 五、为什么强调「采集侧」生成 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 恢复:dup 是如何产生的?

Flink 的容错机制基于 Checkpoint:

  • Flink 定期对算子状态做 checkpoint
  • 任务异常 / 重启
  • 从最近一次成功的 checkpoint 恢复
  • 重放 checkpoint 之后的一部分数据
  • 这一步非常关键。

    举个例子

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

    那么:

    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,而是:

    entity_id = A : seq = 1,2,3,4…
    entity_id = B : seq = 1,2,3,4…

    只要满足:

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

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


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

    因为:

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

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

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

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


    六、Flink 中如何基于 seq 去重?

    处理逻辑(按实体维度)

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


    示例代码(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、风控、日志)的标准做法


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

    例如无人机轨迹上报:

    {
    "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 做幂等去重。

    赞(0)
    未经允许不得转载:171主机测评 » RabbitMQ + Flink 为什么必然会重复?以及如何用 seq 做稳定去重
    分享到: 更多 (0)

    评论 抢沙发

    • 昵称 (必填)
    • 邮箱 (必填)
    • 网址