MQ防止重复消费的四种方法

在 MQ(RocketMQ/RabbitMQ/Kafka 等)中,"至少一次 "投递语义决定了 重复消费必然存在 ,只能 业务侧幂等 来"防"。

把散落在各博客里的方案提炼后,企业级落地 就 4 套组合拳,按 "轻→重" 顺序

一、4 套通用方案(附适用场景)

方案 实现要点 性能 一致性 场景
1. 唯一 ID + Redis setNX + TTL 消息头带唯一 key;SETNX 原子判重,成功才处理 高 最终一致 高并发、非核心
2. 数据库去重表(唯一索引) 本地事务内先插去重表,主键冲突即跳过 中 强一致 订单、支付核心
3. 分布式锁(Redis/ZK) 以 msgId 为锁 key,确保集群同时仅一个节点处理 中高 并发安全 长耗时、多节点竞争
4. 死信队列 + 重试上限 消费失败次数 > N 移到 DLQ,人工兜底 - 防无限重试 所有方案配套

二、Redis 方案 1 行代码

java 复制代码
// 返回 true 表示未处理过,可以继续业务
Boolean first = stringRedisTemplate.opsForValue()
        .setIfAbsent(msgId, "1", Duration.ofMinutes(10));
if (Boolean.TRUE.equals(first)) {
    // 业务逻辑
} else {
    log.warn("重复消费 msgId={}", msgId);
}

10 min TTL 避免 key 无限膨胀;也可 SET msgId 1 NX PX 600000 原生命令。


三、Spring AOP 注解式(零侵入)

  1. 注解
java 复制代码
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface NoMQDuplicate {
    String keyPrefix() default "mq:dup:";
    long timeout() default 600;   // 秒
}
  1. 切面(Lua 脚本保证 get+set 原子)
java 复制代码
@Aspect
@Component
@RequiredArgsConstructor
public class NoMQDuplicateAspect {

    private final StringRedisTemplate redis;

    private static final DefaultRedisScript<String> LUA = new DefaultRedisScript<>(
            "local key=KEYS[1] local val=ARGV[1] local ttl=ARGV[2] " +
            "return redis.call('SET',key,val,'NX','PX',ttl)", String.class);

    @Around("@annotation(noMQDuplicate)")
    public Object around(ProceedingJoinPoint pjp, NoMQDuplicate noMQDuplicate) throws Throwable {
        String msgId = SpELUtil.parseKey(noMQDuplicate.key(), pjp);   // 解析 SpEL 拿 msgId
        String key   = noMQDuplicate.keyPrefix() + msgId;
        String flag  = redis.execute(LUA, List.of(key), "1", String.valueOf(noMQDuplicate.timeout() * 1000));
        if (flag == null) {          // 已存在
            log.warn("重复消费 key={}", key);
            return null;             // 直接返回,不抛异常
        }
        try {
            return pjp.proceed();    // 执行业务
        } catch (Throwable t) {
            redis.delete(key);       // 业务异常→删除 key,允许重试
            throw t;
        }
    }
}
  1. 使用
java 复制代码
@RabbitListener(queues = "order.queue")
@NoMQDuplicate(key = "#message.headers['msgId']", timeout = 600)
public void handle(OrderMessage message) {
    // 正常写业务,无需再关心幂等
}

同理可用于 RocketMQ、Kafka 的 @StreamListener 或 @KafkaListener。


四、数据库去重表(核心场景)

表结构

sql 复制代码
CREATE TABLE mq_dedup (
    msg_id  VARCHAR(64) PRIMARY KEY,
    status  TINYINT DEFAULT 0,
    create_time DATETIME DEFAULT CURRENT_TIMESTAMP
);

消费逻辑(本地事务)

java 复制代码
@Transactional
public void consume(String msgId, Runnable business) {
    try {
        jdbcTemplate.update("INSERT INTO mq_dedup(msg_id) VALUES(?)", msgId);
    } catch (DuplicateKeyException e) {
        log.warn("重复消息 {}", msgId);
        return;
    }
    business.run();          // 执行业务
}

插入与业务在同一线程事务,保证 "记录存在 ⇒ 业务已做完" 原子性。


五、小结

  1. MQ 自身无法杜绝重复,只能业务幂等。

  2. 90% 场景 用「唯一 ID + Redis SETNX + TTL」足够;核心/金钱流程叠加「去重表 + 事务」。

  3. 长耗时/多节点竞争再补「分布式锁」。

  4. 失败重试 设上限并配 DLQ,避免无限死循环

相关推荐
驭渊的小故事4 分钟前
RabbitMQ 环境配置和 DirectRabbitConfig 的 SpringBoot 4.0.X 版本
spring boot·rabbitmq·java-rabbitmq
新思维软件1 小时前
基于无线传输的地衡系统:三节点 LoRa 组网与 MQTT 上云的分布式称重方案
分布式·stm32·单片机·嵌入式硬件·物联网·物联网开发
俊哥大数据1 小时前
Flink1.20.3 实时消费 Kafka 数据并解析入湖 Paimon1.4.2 全流程实战
分布式·flink·kafka·数据湖·paimon
宸津-代码粉碎机8 小时前
OpenAI 连夜迎战 Grok Bot 和 Muse:AI 智能体从 “会聊天” 到 “能办事”,现在入场还来得及吗
java·大数据·人工智能·分布式·python
程序猿乐锅9 小时前
【黑马点评 | 第八篇】Redisson分布式锁
java·数据库·spring boot·redis·分布式·spring·缓存
ly76899 小时前
RabbitMQ 消费确认与预取:manual ack、reject、nack 与 QoS 的取舍
rabbitmq·死信队列·消息可靠性·消费确认·qos 预取
灯澜忆梦10 小时前
【RabbitMQ #7】 | 消息转换器
分布式·rabbitmq·ruby
需要82611 小时前
分布式事务:Seata AT/TCC/SAGA 的取舍与踩坑
java·spring boot·分布式·spring·spring cloud
CS创新实验室13 小时前
为什么三台机器就能选出一个“带头大哥“?——Raft 一致性算法,一次讲透
分布式·操作系统
本人手速666+15 小时前
企微开发API如何设计客户冻结状态?WeComApi 在删除、投诉和异常客户场景中的自动化边界
运维·分布式·自动化·企业微信·企微外部群开发·wecomapi·企业微信二次开发