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

相关推荐
OxYGC4 小时前
[RabbitMQ] 最新版本深度解析:4.0+ 新特性、性能飞跃与生产实践(2025 年更新)
分布式·rabbitmq
Super Rookie4 小时前
RabbitMQ 自动化脚本安装方案
运维·自动化·rabbitmq
武子康4 小时前
Java-154 深入浅出 MongoDB 用Java访问 MongoDB 数据库 从环境搭建到CRUD完整示例
java·数据库·分布式·sql·mongodb·性能优化·nosql
Q飞了10 小时前
分布式存储Ceph与OpenStack、RAID的关系
分布式·ceph·openstack
回家路上绕了弯10 小时前
深入浅出:如何设计一个可靠的分布式 ID 生成器
分布式·后端
阿什么名字不会重复呢12 小时前
Hadoop报错 Couldn‘t find datanode to read file from. Forbidden
大数据·hadoop·分布式
兜兜风d'12 小时前
Spring Boot 整合 RabbitMQ :四大核心模式解析
spring boot·rabbitmq·java-rabbitmq
在未来等你15 小时前
Kafka面试精讲 Day 25:Kafka与大数据生态集成
大数据·分布式·面试·kafka·消息队列
武子康16 小时前
大数据-134 ClickHouse 单机+集群节点落地手册 | 安装配置 | systemd 管理 / config.d
大数据·分布式·后端