深度分析 RocketMQ 幂等性设计与生产级实现方案
一、前言
在使用 RocketMQ 时,我们必须面对一个事实:RocketMQ 只保证 At Least Once(至少投递一次),不保证 Exactly Once(精确一次)。
这意味着:消息重复是必然的,不是偶然的。如果不做幂等处理,就会出现:
- 重复下单
- 重复扣款
- 重复插入数据
- 数据不一致
本文从重复原因 → 设计原则 → 五大实现方案 → 生产最佳实践,把 RocketMQ 幂等性讲透。
二、为什么 RocketMQ 会出现重复消息?
消息重复主要发生在这几个场景:
- Producer 重试发送网络超时、异常、Broker 响应丢失,都会触发重试。
- Broker 重试投递消费失败、超时、断开连接 → 进入重试队列。
- Rebalance 重平衡扩容、缩容、重启导致队列重新分配。
- Consumer 提前 ACK业务没执行完就返回成功,重启后消息再次投递。
结论:RocketMQ 本身不做去重,去重必须由业务层实现幂等。
三、什么是幂等?
幂等:同一个消息执行一次与执行多次,结果完全一致。
满足:
- 重复消息不报错
- 重复消息不产生脏数据
- 重复消息返回成功
四、RocketMQ 幂等设计核心思路
实现幂等只需要抓住一点:识别这条消息是否已经被处理过。
关键标识:
- msgId:Broker 生成的唯一 ID
- offsetMsgId:基于物理偏移的 ID
- 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 多级防御
适用场景:金融、支付核心链路
流程:
- 用业务唯一号做 Redis 去重
- 用去重表做持久化
- 用状态机保证安全
超高可靠。
六、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;
}
七、幂等设计三大原则
- 先判断是否处理,再执行业务
- 幂等判断必须原子性
- 重复消息直接返回成功,不抛异常
八、经典问题
- RocketMQ 是 At Least Once 还是 Exactly Once?
- 为什么 MQ 不内部实现去重?
- 消息重复的场景有哪些?
- 生产中你用什么方案实现幂等?
- 唯一索引、Redis、状态机三种方案对比?
- 如何保证幂等高可用?
九、生产最佳实践总结
- 所有消费端必须做幂等,不要抱有侥幸
- 优先使用业务唯一键(keys)
- 插入用唯一索引,更新用状态机,高并发用 Redis
- 异常时返回 RECONSUME_LATER,不要吞异常
- 做好重试、死信、堆积监控
- 幂等是架构问题,不是 MQ 问题
十、结语
RocketMQ 不保证消息不重复,但通过合理的幂等设计 ,可以轻松实现:重复消息 = 安全跳过 = 最终一致。
幂等是分布式系统高可用的基础,也是面试与生产的必考点。