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

深度分析 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 类业务(订单、流水、日志)

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

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

优点:

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

缺点:

  • 只适用于插入场景

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

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

建表:

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

逻辑:

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

优点:

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

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

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

messageKey 作为锁标识:

java 复制代码
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:状态机幂等(最优雅、企业最爱)

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

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

消费时:

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

优点:

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

生产环境最推荐方案。


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

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

流程:

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

超高可靠。


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

java 复制代码
@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 不保证消息不重复,但通过合理的幂等设计 ,可以轻松实现:重复消息 = 安全跳过 = 最终一致

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

相关推荐
青云计划9 小时前
知光项目知文发布模块
java·后端·spring·mybatis
Victor3569 小时前
MongoDB(9)什么是MongoDB的副本集(Replica Set)?
后端
Victor3569 小时前
MongoDB(8)什么是聚合(Aggregation)?
后端
yeyeye11110 小时前
Spring Cloud Data Flow 简介
后端·spring·spring cloud
Tony Bai11 小时前
告别 Flaky Tests:Go 官方拟引入 testing/nettest,重塑内存网络测试标准
开发语言·网络·后端·golang·php
牛奶11 小时前
《前端架构设计》:除了写代码,我们还得管点啥
前端·架构·设计
+VX:Fegn089511 小时前
计算机毕业设计|基于springboot + vue鲜花商城系统(源码+数据库+文档)
数据库·vue.js·spring boot·后端·课程设计
程序猿阿伟11 小时前
《GraphQL批处理与全局缓存共享的底层逻辑》
后端·缓存·graphql
小小张说故事12 小时前
SQLAlchemy 技术入门指南
后端·python
识君啊12 小时前
SpringBoot 事务管理解析 - @Transactional 的正确用法与常见坑
java·数据库·spring boot·后端