深度分析 RocketMQ 幂等性设计与生产级实现方案

6 阅读4分钟

深度分析 RocketMQ 幂等性设计与生产级实现方案

一、前言

在使用 RocketMQ 时,我们必须面对一个事实:RocketMQ 只保证 At Least Once(至少投递一次),不保证 Exactly Once(精确一次)。

这意味着:消息重复是必然的,不是偶然的。如果不做幂等处理,就会出现:

  • 重复下单
  • 重复扣款
  • 重复插入数据
  • 数据不一致

本文从重复原因 → 设计原则 → 五大实现方案 → 生产最佳实践,把 RocketMQ 幂等性讲透。


二、为什么 RocketMQ 会出现重复消息?

消息重复主要发生在这几个场景:

  1. Producer 重试发送网络超时、异常、Broker 响应丢失,都会触发重试。
  2. Broker 重试投递消费失败、超时、断开连接 → 进入重试队列。
  3. Rebalance 重平衡扩容、缩容、重启导致队列重新分配。
  4. Consumer 提前 ACK业务没执行完就返回成功,重启后消息再次投递。

结论:RocketMQ 本身不做去重,去重必须由业务层实现幂等


三、什么是幂等?

幂等:同一个消息执行一次与执行多次,结果完全一致。

满足:

  • 重复消息不报错
  • 重复消息不产生脏数据
  • 重复消息返回成功

四、RocketMQ 幂等设计核心思路

实现幂等只需要抓住一点:识别这条消息是否已经被处理过。

关键标识:

  1. msgId:Broker 生成的唯一 ID
  2. offsetMsgId:基于物理偏移的 ID
  3. keys:业务唯一标识(最推荐,如订单号、流水号)

最佳实践:优先使用业务唯一键。


五、生产级幂等五大方案(从简单到推荐)

方案 1:数据库唯一索引(最简单、最稳定)

适用场景:insert 类业务(订单、流水、日志)

思路:利用数据库唯一索引约束,重复插入会报错,捕获后视为成功。

try {
    orderMapper.insert(order);
} catch (DuplicateKeyException e) {
    log.info("订单已存在,幂等处理:{}", orderNo);
}
return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;

优点:

  • 实现最简单
  • 天然并发安全
  • 无额外中间件

缺点:

  • 只适用于插入场景

方案 2:去重表 + 事务(通用强一致)

适用场景:多表操作、无法加唯一索引

建表:

CREATE TABLE message_idempotent (
    id BIGINT PRIMARY KEY AUTO_INCREMENT,
    message_key VARCHAR(64) NOT NULL UNIQUE,
    create_time DATETIME
);

逻辑:

@Transactional(rollbackFor = Exception.class)
public void handleMessage(String key) {
    // 插入去重表
    int rows = idempotentMapper.insertIgnore(key);
    if (rows == 0) {
        // 已处理
        return;
    }
    // 执行业务逻辑
}

优点:

  • 通用、强一致
  • 与业务事务绑定

方案 3:Redis 分布式锁(高并发首选)

适用场景:高并发、更新操作、多服务实例

messageKey 作为锁标识:

String key = "mq:idempotent:" + messageKey;
Boolean lock = redisTemplate.opsForValue()
        .setIfAbsent(key, "handled", 24, TimeUnit.HOURS);

if (Boolean.FALSE.equals(lock)) {
    return CONSUME_SUCCESS;
}
// 执行业务

优点:

  • 性能极高
  • 适合高并发更新

注意:

  • 过期时间要足够长(≥24h)
  • 不要使用自动续期

方案 4:状态机幂等(最优雅、企业最爱)

适用场景:订单、支付、物流等状态流转

例如订单状态:待支付 → 已支付 → 已完成

消费时:

UPDATE order
SET status = 2
WHERE order_no = ? AND status = 1;
  • 影响行数 = 1 → 第一次处理
  • 影响行数 = 0 → 重复消息,直接返回成功

优点:

  • 无锁、高性能
  • 天然幂等
  • 业务语义清晰

生产环境最推荐方案。


方案 5:业务唯一标识 + 本地表 + Redis 多级防御

适用场景:金融、支付核心链路

流程:

  1. 用业务唯一号做 Redis 去重
  2. 用去重表做持久化
  3. 用状态机保证安全

超高可靠。


六、RocketMQ 消费端幂等编码模板

@Override
public ConsumeConcurrentlyStatus consumeMessage(
        List<MessageExt> msgs, ConsumeConcurrentlyContext context) {
    for (MessageExt msg : msgs) {
        String key = msg.getKeys(); // 业务唯一键
        try {
            // 1. 幂等判断:是否已处理
            if (idempotentService.isProcessed(key)) {
                continue;
            }
            // 2. 业务处理
            businessService.handle(msg);
            // 3. 标记已处理
            idempotentService.markProcessed(key);
        } catch (Exception e) {
            log.error("消费异常", e);
            // 异常 → 重试
            return ConsumeConcurrentlyStatus.RECONSUME_LATER;
        }
    }
    return ConsumeConcurrentlyStatus.CONSUME_SUCCESS;
}

七、幂等设计三大原则

  1. 先判断是否处理,再执行业务
  2. 幂等判断必须原子性
  3. 重复消息直接返回成功,不抛异常

八、经典问题

  1. RocketMQ 是 At Least Once 还是 Exactly Once?
  2. 为什么 MQ 不内部实现去重?
  3. 消息重复的场景有哪些?
  4. 生产中你用什么方案实现幂等?
  5. 唯一索引、Redis、状态机三种方案对比?
  6. 如何保证幂等高可用?

九、生产最佳实践总结

  1. 所有消费端必须做幂等,不要抱有侥幸
  2. 优先使用业务唯一键(keys)
  3. 插入用唯一索引,更新用状态机,高并发用 Redis
  4. 异常时返回 RECONSUME_LATER,不要吞异常
  5. 做好重试、死信、堆积监控
  6. 幂等是架构问题,不是 MQ 问题

十、结语

RocketMQ 不保证消息不重复,但通过合理的幂等设计,可以轻松实现:重复消息 = 安全跳过 = 最终一致

幂等是分布式系统高可用的基础,也是面试与生产的必考点。