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

相关推荐
让我上个超影吧1 天前
消息队列——RabbitMQ(高级)
java·rabbitmq
yumgpkpm1 天前
AI视频生成:Wan 2.2(阿里通义万相)在华为昇腾下的部署?
人工智能·hadoop·elasticsearch·zookeeper·flink·kafka·cloudera
塔中妖1 天前
Windows 安装 RabbitMQ 详细教程(含 Erlang 环境配置)
windows·rabbitmq·erlang
Ronin3052 天前
信道管理模块和异步线程模块
开发语言·c++·rabbitmq·异步线程·信道管理
后季暖2 天前
flink火焰图使用
大数据·flink
weixin_395448912 天前
cursor日志0224
eureka·flink·etcd
代码匠心2 天前
从零开始学Flink:Flink SQL 元数据持久化实战
大数据·flink·flink sql·大数据处理
Hello.Reader2 天前
Flink Metrics 实战自定义指标、系统指标、排障观测一把梭
大数据·flink
忙碌5442 天前
OpenTelemetry实战指南:构建云原生全链路可观测性体系
ios·flink·apache·iphone