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

相关推荐
qq_12498707531 天前
基于Hadoop的信贷风险评估的数据可视化分析与预测系统的设计与实现(源码+论文+部署+安装)
大数据·人工智能·hadoop·分布式·信息可视化·毕业设计·计算机毕业设计
洛豳枭薰1 天前
消息队列关键问题描述
kafka·rabbitmq·rocketmq
Coder_Boy_1 天前
基于Spring AI的分布式在线考试系统-事件处理架构实现方案
人工智能·spring boot·分布式·spring
袁煦丞 cpolar内网穿透实验室1 天前
远程调试内网 Kafka 不再求运维!cpolar 内网穿透实验室第 791 个成功挑战
运维·分布式·kafka·远程工作·内网穿透·cpolar
人间打气筒(Ada)1 天前
GlusterFS实现KVM高可用及热迁移
分布式·虚拟化·kvm·高可用·glusterfs·热迁移
xu_yule1 天前
Redis存储(15)Redis的应用_分布式锁_Lua脚本/Redlock算法
数据库·redis·分布式
難釋懷1 天前
分布式锁的原子性问题
分布式
ai_xiaogui1 天前
【开源前瞻】从“咸鱼”到“超级个体”:谈谈 Panelai 分布式子服务器管理系统的设计架构与 UI 演进
服务器·分布式·架构·分布式架构·panelai·开源面板·ai工具开发
凯子坚持 c1 天前
如何基于 CANN 原生能力,构建一个支持 QoS 感知的 LLM 推理调度器
分布式