手动ACK不能防止重复消费,需配合幂等性设计;常用方案包括Redis SETNX校验、数据库唯一约束兜底、状态机条件更新。

手动 ACK 本身不能防止重复消费,它只是让消息不被提前删除、为重试留出空间。真正防止重复消费,靠的是在手动 ACK 基础上叠加幂等性设计。RabbitMQ 的语义是 at-least-once(至少一次),ACK 只控制“消息是否从队列移除”,不控制“业务是否只执行一次”。所以必须两者配合。
手动 ACK 必须正确启用和使用
这是整个机制的前提,否则连重试机会都没有,更谈不上防重复。
- Spring Boot 中配置
acknowledge-mode: manual,禁用自动 ACK - 消费者处理逻辑必须包裹在 try-catch 中:成功则调用
channel.basicAck(deliveryTag, false);失败且需重试则调用channel.basicNack(deliveryTag, false, true) - 切忌在业务执行前就 ACK——这等于告诉 RabbitMQ “已处理”,实际还没做,会导致消息丢失
- 避免忽略异常或吞掉 ACK 调用:比如 log.error 后没 rethrow,也没显式 NACK,会导致消息长期 unacked,最终阻塞队列
用唯一消息 ID + Redis 实现轻量幂等校验
这是高并发场景下最常用、性能最好的方案,适合订单创建、积分发放等对吞吐敏感的业务。
- 生产者发送时,生成全局唯一 ID(如 UUID 或雪花 ID),写入消息 header:
messageProperties.setMessageId("msg_abc123") - 消费者拿到消息后,先提取
messageId,用 Redis 的SETNX尝试写入 key(如consumed:msg_abc123),并设置过期时间(建议 ≥ 最大重试窗口,例如 2 小时) - 若 SETNX 返回 1,说明首次消费,继续执行业务逻辑;若返回 0,直接
basicAck并跳过处理 - 注意:Redis 连接异常时应降级为记录日志+告警,不能因 Redis 不可用而阻塞消费
用数据库唯一约束兜底强一致性
适用于金融、账务等不允许任何偏差的场景,牺牲一点性能换取绝对可靠。
立即学习“Java免费学习笔记(深入)”;
- 建一张
t_msg_log表,主键或唯一索引为msg_id(VARCHAR 或 CHAR) - 消费逻辑第一步:尝试插入该
msg_id,不带业务数据,仅占位 - 如果插入成功(影响行数 = 1),说明未处理过,执行后续业务并提交事务;如果抛出
SQLIntegrityConstraintViolationException,捕获后直接 ACK,不再执行业务 - 无需额外清理逻辑——只要表结构合理、索引有效,数据库自身就能保证幂等
结合业务状态机做条件更新
适合本身就有明确状态流转的业务,比如订单从「待支付」→「已支付」,天然具备判断依据。
- 不在开头查表或写 Redis,而是在执行核心更新 SQL 时带上前置状态条件
- 例如:
UPDATE t_order SET status = 'PAID', paid_at = NOW() WHERE order_no = ? AND status = 'UNPAID' - 如果重复消费,WHERE 条件不成立,SQL 影响行数为 0,业务代码根据返回值跳过后续动作即可
- 优点是零外部依赖、无额外存储开销;缺点是只适用于有清晰状态跃迁路径的场景


















