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 IGNORE或ON 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,否则重试会不断触发重复消费;建议改用死信队列 + 人工干预
# 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配置与性能优化方法
阿里云网站搭建费用解析:服务器价格与建站成本优化指南


