RabbitMQ幂等消费需业务bizId+外部存储判重+事务一致+手动ACK:消息带稳定业务ID(如订单号),用Redis或DB唯一索引校验,判重、执行、落库须原子,关闭autoAck,成功后basicAck,失败basicNack配死信。

Java 消息队列中实现幂等消费,关键不是“防止消息重复投递”,而是让重复到达的消息在业务层只生效一次。RabbitMQ、RocketMQ 等主流队列都采用 AT-LEAST-ONCE 投递语义——可靠不丢消息,但可能重复。所以幂等必须由消费者主动承担,不能依赖中间件兜底。
带稳定 bizId 的消息设计
每条消息必须携带一个由业务系统生成的、全局唯一且可重放的标识(如订单号 ORD20260828001),而不是用 UUID 或时间戳+随机数这类不可重现的组合。这个 bizId 要随消息体一起发送,推荐放在 JSON body 中(如 "bizId": "ORD20260828001"),也可设为消息 header(如 message.getMessageProperties().setHeader("biz_id", ...)),确保反序列化失败时仍能提取用于判重。
用 Redis 或 DB 做原子判重
消费者收到消息后,不能靠本地内存或单机缓存判断是否处理过,必须借助高可用外部存储做“已处理”记录校验:
- Redis + Lua:用 SET key value EX 86400 NX 原子写入,key 为 bizId,value 可存时间戳或 traceId;成功即未处理,失败则跳过
- 数据库唯一索引:建一张轻量表(如 mq_processed(biz_id PK, create_time)),消费前执行 INSERT IGNORE 或 ON CONFLICT DO NOTHING;插入成功再执行业务逻辑,失败直接 return
- 避免仅用 ConcurrentHashMap 或 Guava Cache:节点重启或扩容会导致状态丢失,无法支撑分布式幂等
判重、执行、落库三步强一致
常见错误是先执行业务再记录状态,或把判重和业务更新拆在不同事务里。正确做法取决于业务场景:
立即学习“Java免费学习笔记(深入)”;
- 业务操作支持本地事务:将业务主表变更与幂等记录插入放在同一个 @Transactional 方法内,靠数据库 ACID 保证原子性
- 跨服务或无事务场景:采用两阶段预占,例如先插入 status = 'processing' 的幂等记录,再调用下游;成功后更新 status = 'success';失败则留待对账补偿清理
- 所有异常路径(包括 OOM、网络超时、JVM crash)都要捕获,并在 finally 或 AOP 切面中确保幂等状态最终一致
配合 RabbitMQ 手动 ACK 控制消费确认
必须关闭 autoAck,改为手动控制消息确认时机:
- 业务逻辑执行成功、幂等状态落库完成后,调用 channel.basicAck(deliveryTag, false)
- 执行失败或幂等校验失败,调用 channel.basicNack(deliveryTag, false, false) 并配置死信队列,便于人工介入或自动重试
- 不建议简单 requeue(basicNack(..., true)),否则可能陷入无限循环


















