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,避免无限死循环

相关推荐
♛小小小让让1 天前
RabbitMQ (二)参数
笔记·python·rabbitmq
理智的煎蛋1 天前
基于 Celery 的分布式文件监控系统
redis·分布式·python·mysql·mongodb
失散131 天前
分布式专题——22 Kafka集群工作机制详解
java·分布式·云原生·架构·kafka
Lansonli1 天前
大数据Spark(六十五):Transformation转换算子groupByKey和filter
大数据·分布式·spark
拾忆,想起1 天前
RabbitMQ死信交换机:消息的“流放之地“
开发语言·网络·分布式·后端·性能优化·rabbitmq
沐浴露z1 天前
一篇文章入门RabbitMQ:基本概念与Java使用
java·分布式·rabbitmq
失散131 天前
分布式专题——24 Kafka功能扩展
java·分布式·云原生·架构·kafka
从零开始学习人工智能1 天前
分布式 WebSocket 架构设计与实现:跨节点实时通信解决方案
分布式·websocket·网络协议
qyt19885201 天前
Python脚本(Kafka生产者+消费者)
分布式·kafka
caibixyy2 天前
Spring Boot 整合 Redisson 实现分布式锁:实战指南
spring boot·分布式·后端