Java面试之消息队列如何解决消息幂等性

发布时间 - 2026-02-03 00:00:00    点击率:
RabbitMQ 重复投递源于“至少一次”语义,未及时 ACK 时会重发;需用 Redis SETNX 实现幂等消费,并辅以 DB 唯一索引兜底,避免事务与 Redis 耦合。

为什么 RabbitMQ 会重复投递消息?

不是 RabbitMQ 故意“发两遍”,而是它默认采用“至少一次”(at-least-once)投递语义。只要消费者没在 channel.basicAck() 前正常返回 ACK,Broker 就认为消费失败,会重新入队或投递给其他消费者。常见触发点包括:网络超时、JVM Full GC 导致消费线程卡住、消费者进程突然 kill -9、手动调用 channel.basicNack(requeue=true)。这些都不是 Bug,是分布式系统为保障可靠性做的权衡。

  • 消费者处理耗时 > consumer_timeout(RabbitMQ 3.8+ 默认 30 分钟),Broker 主动 requeue
  • 消费者在 basicConsume() 后崩溃,未发送任何 ack/nack,消息重回 ready 状态
  • 集群主从切换期间,部分 unacked 消息被误判为“未确认”,触发重发

用 Redis + SETNX 实现最简幂等消费

这是生产环境最常用、落地最快的方式。核心就一条:收到消息后,先用消息 ID 向 Redis 写一个带过期时间的 key;写成功才执行业务逻辑;写失败(key 已存在)直接跳过。不用事务、不查 DB,性能损耗极小。

  • messageId 必须全局唯一且稳定——优先用业务字段(如 order_id),其次用 MessageProperties.getMessageId(),慎用内容哈希(同一业务逻辑多次发相同内容,但语义不同)
  • 必须设过期时间(如 24 小时),否则 Redis 内存无限增长;过期时间要远大于单次业务最大耗时,但不宜超过业务生命周期(比如订单超时关单是 30 分钟,幂等 key 过期设 2 小时足够)
  • StringRedisTemplate.opsForValue().setIfAbsent(key, "1", Duration.ofHours(2)),不要用 hasKey() + set() 两步操作(非原子,竞态失败)
@Autowired
private StringRedisTemplate redisTemplate;

public void handleMessage(Message message) {
    String messageId = extractMessageId(message); // 如:从 headers 取 "x-order-id"
    String dedupKey = "mq:dedup:" + messageId;

    Boolean isFresh = redisTemplate.opsForValue()
        .setIfAbsent(dedupKey, "1", Duration.ofHours(2));
    
    if (Boolean.TRUE.equals(isFresh)) {
        processBusiness(message); // 执行下单、扣库存等真实逻辑
    } else {
        log.warn("Duplicate message ignored: {}", messageId);
    }
}

数据库唯一索引兜底,不是替代方案

有人觉得“我 DB 有唯一索引,插入失败就说明重复”,这只能作为第二道防线,绝不能当主方案。因为:唯一索引冲突是业务异常,会打断正常流程;高并发下大量 insert 失败再 catch,对 MySQL 是无效压力;且无法防止“已插入但事务未提交时重复消费”的中间态问题。

  • 适合场景:最终一致性要求极高、且业务本身天然带唯一键(如支付流水号、退款单号),可配合 INSERT IGNOREON DUPLICATE KEY UPDATE
  • 必须搭配 Redis 去重使用——Redis 判断“该不该进 DB”,DB 索引只拦住漏网之鱼
  • 注意:MySQL 的 INSERT ... SELECT 或自增 ID 不构成幂等保障,别被误导

Spring AMQP 的 @RabbitListener 怎么加幂等?

别在 listener 方法里裸写去重逻辑。Spring AMQP 提供了更干净的切面式接入方式:用 ChannelAwareMessageListener 或自定义 AdviceChain,但最推荐的是封装一个幂等代理 Consumer Bean。

  • 把去重逻辑抽成独立组件(如 DeduplicationService),所有 @RabbitListener 方法第一行调用 deduplicationService.checkAndMark(messageId)
  • 避免在 listener 上加 @Transactional 后再去 Redis——事务和 Redis

    不在一个上下文,回滚不联动;应确保 Redis 操作在事务外完成
  • 若用 Spring Retry,必须关闭 requeue = true,否则重试会不断触发重复消费;建议改用死信队列 + 人工干预
幂等不是“加个判断就行”,本质是承认分布式系统没有银弹。Redis 去重失效、DB 唯一索引被绕过、消息体解析出错导致 ID 提取错误……这些细节比框架选型更容易决定成败。


# mysql  # java  # redis  # ai  # 退款  # 为什么  # red  # asic  # spring  # rabbitmq  # 分布式  # jvm  # 封装  # select  # catch  # 线程  # 并发  # channel  # 数据库  # bug  # 的是  # 重发  # 这是  # 就行  # 这只  # 自定义  # 再去  # 极高  # 更容易  # 两步 


相关栏目: 【 网站优化151355 】 【 网络推广146373 】 【 网络技术251813 】 【 AI营销90571


相关推荐: 昵图网官方站入口 昵图网素材图库官网入口  怎样使用JSON进行数据交换_它有什么限制  活动邀请函制作网站有哪些,活动邀请函文案?  Laravel如何部署到服务器_线上部署Laravel项目的完整流程与步骤  网站制作免费,什么网站能看正片电影?  jquery插件bootstrapValidator表单验证详解  详解jQuery停止动画——stop()方法的使用  iOS发送验证码倒计时应用  java获取注册ip实例  如何用PHP快速搭建高效网站?分步指南  Laravel如何生成URL和重定向?(路由助手函数)  如何在阿里云通过域名搭建网站?  linux写shell需要注意的问题(必看)  javascript中对象的定义、使用以及对象和原型链操作小结  轻松掌握MySQL函数中的last_insert_id()  如何在建站宝盒中设置产品搜索功能?  mc皮肤壁纸制作器,苹果平板怎么设置自己想要的壁纸我的世界?  javascript中数组(Array)对象和字符串(String)对象的常用方法总结  如何用美橙互联一键搭建多站合一网站?  Laravel策略(Policy)如何控制权限_Laravel Gates与Policies实现用户授权  Laravel怎么进行数据库回滚_Laravel Migration数据库版本控制与回滚操作  Laravel如何配置中间件Middleware_Laravel自定义中间件拦截请求与权限校验【步骤】  如何在搬瓦工VPS快速搭建网站?  Laravel的.env文件有什么用_Laravel环境变量配置与管理详解  Laravel如何保护应用免受CSRF攻击?(原理和示例)  logo在线制作免费网站在线制作好吗,DW网页制作时,如何在网页标题前加上logo?  Laravel怎么使用Session存储数据_Laravel会话管理与自定义驱动配置【详解】  如何在阿里云虚拟主机上快速搭建个人网站?  详解免费开源的.NET多类型文件解压缩组件SharpZipLib(.NET组件介绍之七)  惠州网站建设制作推广,惠州市华视达文化传媒有限公司怎么样?  Edge浏览器怎么启用睡眠标签页_节省电脑内存占用优化技巧  如何在服务器上三步完成建站并提升流量?  如何在阿里云高效完成企业建站全流程?  百度浏览器ai对话怎么关 百度浏览器ai聊天窗口隐藏  ,南京靠谱的征婚网站?  悟空浏览器如何设置小说背景色_悟空浏览器背景色设置【方法】  Python结构化数据采集_字段抽取解析【教程】  HTML5建模怎么导出为FBX格式_FBX格式兼容性及导出步骤【指南】  怎么用AI帮你为初创公司进行市场定位分析?  如何为不同团队 ID 动态生成多个非值班状态按钮  如何快速建站并高效导出源代码?  MySQL查询结果复制到新表的方法(更新、插入)  Laravel如何集成第三方登录_Laravel Socialite实现微信QQ微博登录  HTML 中如何正确使用模板变量为元素的 name 属性赋值  Win11怎么恢复误删照片_Win11数据恢复工具使用【推荐】  javascript中的try catch异常捕获机制用法分析  phpredis提高消息队列的实时性方法(推荐)  Laravel怎么多语言本地化设置_Laravel语言包翻译与Locale动态切换【手册】  laravel怎么配置和使用PHP-FPM来优化性能_laravel PHP-FPM配置与性能优化方法  阿里云网站搭建费用解析:服务器价格与建站成本优化指南