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

相关推荐
AD钙奶-lalala3 小时前
RabbitMQ在Mac OS上的安装和启动
分布式·rabbitmq
七夜zippoe9 小时前
事务方案选型全景图:金融与电商场景的实战差异与落地指南
java·分布式·事务
葵野寺13 小时前
【RelayMQ】基于 Java 实现轻量级消息队列(七)
java·开发语言·网络·rabbitmq·java-rabbitmq
在未来等你13 小时前
Kafka面试精讲 Day 12:副本同步与数据一致性
大数据·分布式·面试·kafka·消息队列
蒋士峰DBA修行之路15 小时前
实验九 合理定义分布列实现性能优化-分布式聚集
分布式
echoyu.16 小时前
消息队列-kafka完结
java·分布式·kafka
七夜zippoe16 小时前
分布式事务性能优化:从故障现场到方案落地的实战手记(二)
java·分布式·性能优化
胚芽鞘68119 小时前
我对rabbitmq的理解(第一次)
分布式·rabbitmq
KIDAKN19 小时前
RabbitMQ 幂等性, 顺序性 和 消息积压
分布式·rabbitmq