rabbitmq可靠幂等消费需业务id+外部存储判重+事务一致+手动ack:消息带稳定bizid;用redis或db唯一索引校验;判重、执行、记录须原子;关闭autoack,成功后basicack,失败basicnack并配死信。

在 RabbitMQ 中实现可靠的幂等性消费,核心是让同一条消息无论被投递多少次,业务逻辑只执行一次。这不能只靠消息队列本身保证,而要结合唯一标识、外部存储和业务层协同设计。
消息必须携带全局唯一业务 ID
生产者发送消息前,需为每条业务消息生成一个稳定、可追溯的 业务唯一 ID(如订单号、支付流水号),而非用 RabbitMQ 自动生成的 deliveryTag 或 UUID。该 ID 应由业务系统生成并随消息体(或消息头)一并发送:
- 推荐放在消息 body 的 JSON 字段中(如
"bizId": "ORD20240520001"),便于消费者解析 - 也可设为消息 header(如
message.getMessageProperties().setHeader("biz_id", "ORD20240520001")),避免反序列化失败时仍可提取 - 禁止使用时间戳 + 随机数等不可重放的组合——重发场景下会生成新 ID,导致幂等失效
消费端用分布式存储做“已处理记录”判重
消费者收到消息后,不能仅靠本地内存或单机缓存判断是否处理过,必须借助具备高可用与一致性的外部存储进行原子性校验与记录,常用方案有:
在 Java 中初始化和管理阿里云 SDK客户端。包括单例模式、线程安全、endpoint 与 region 配置、VPC 终端节点、同步与异步等。
-
Redis + Lua 脚本:用
SET key value EX 3600 NX原子写入,成功即未处理过;失败则跳过。注意设置合理过期时间(如 24 小时),避免脏数据长期残留 -
数据库唯一索引:建一张轻量幂等表(如
mq_consume_record(biz_id PK, status TINYINT, create_time)),消费前先INSERT IGNORE或ON CONFLICT DO NOTHING;插入成功再执行业务,失败则直接 return - 不建议仅用本地 ConcurrentHashMap 或 Guava Cache:节点重启或集群扩容会导致状态丢失,无法支撑可靠幂等
确保“判重 → 执行 → 记录”三步具备事务一致性
常见错误是先执行业务再落库判重,或判重与业务更新不在同一事务内,导致中间态异常引发重复。正确做法分两种场景:
- 业务操作支持数据库事务:将业务主表变更与幂等记录插入放在同一个本地事务中(如 Spring @Transactional),利用数据库 ACID 保证原子性
- 跨服务/无事务场景:采用“预占 + 确认”两阶段。例如:先插入幂等记录(status=processing),再调用下游;成功后更新 status=success;失败则留待对账或补偿任务清理
- 务必捕获所有异常(包括 OOM、网络超时、JVM crash),并在 finally 或 @AfterReturning/@AfterThrowing 中确保幂等状态最终一致
配合 RabbitMQ 的手动 ACK 与死信机制兜底
自动 ACK 容易在业务处理中途宕机时丢失消息,必须关闭 autoAck,改为手动确认:
- 只有当幂等校验通过且业务逻辑完整执行完毕(含 DB/Redis 写入成功)后,才调用
channel.basicAck() - 若校验失败或业务抛异常,应调用
channel.basicNack(requeue=false)拒绝并丢弃,避免无限重试 - 为防意外,给队列配置
x-dead-letter-exchange,将反复拒绝的消息转入死信队列,供人工核查或异步补偿
Java免费学习笔记:立即使用
解锁 Java 大师之旅:从入门到精通的终极指南










