RabbitMQ-如何保证消息在99.99%的情况下不丢失
1. 简介
MQ虽然帮我们解决了很多问题,但是也带来了很多问题,其中最麻烦的就是:如何保证消息的可靠性传输。
我们在聊如何保证消息的可靠性传输之前,先考虑下哪些情况下会出现消息丢失的情况。
首先,上图中完整的展示了消息从生产到被消费的完整链路,我们通过图列举下各种情况。
Producer
在把Message
发送到Broker
的过程中,因为网络不可靠的原因,可能会出现Message
还未发送到Broker
就丢失,或者Message
发送到了Broker
,但是由于某种原因,消息未保存到Broker。Broker
接收到Message
数据存储在内存,Consumer
还没消费,Broker
宕机了。Consumer
接收到了Message
,Message
相关业务还没来得及处理,程序报错或者宕机了,Broker
会认为Consunmer
消息正常消费了,就把当前消息从队列中移除了。这种情况也算是消息丢失。
从上述的问题中我们可以总结出想要消息被正常消费,就得保证:
- 消息成功被
Broker
接收到。 - 消息可以被
Broker
持久化。 - 消息成功被
Consumer
接收并且当消费失败时,消息可以重回队列。 - 要有相应的补偿机制。(当任何一个环节出错时,可以进行消息 补偿)。
2. 消息的可靠投递
我们在使用MQ的时候,为了避免消息丢失或者投递失败。RabbitMQ为我们提供了两种方式来控制消息的投递可靠性。
- confirm 确认模式
- return 退回模式
如图所示:
消息从 producer 到 exchange 则会返回一个confirmCallback 。
消息从 exchange 到 queue 投递失败则会返回一个 ReturnsCallback 信息,其内容为ReturnedMessage实例信息。
我们将利用这两个 callback 控制消息的可靠性投递。
2.1 confirm
2.1.1 引入所需依赖
2.1.2 application.yaml
2.1.3 ConfirmCallBack
2.1.3 RabbitConfig
2.1.4 测试方法
这里两个测试方法,sentMsg()
使用默认的Exchange
,而sentMsg2()
设置一个不存在的Exchange
测试失败情况。
2.1.5 启动测试
sendMsg()
方法日志如下:
sendMsg2()
方法日志如下:
2.1.6 小结
Confirm 确认模式
是从Producer
到Exchange
。Producer
发送的消息正常或失败时都会进入Confirm Callback
方法。Producer
发送消息的Exchange
不存在时,Confirm Callback
中的Ack
为false且Cause
为发送失败原因。
2.2 return
2.2.1 application.yaml
2.2.2 ReturnCallback
这里注意下,网上很多提到的ReturnCallback
(少了个s)接口已经弃用,注释中也提到了,弃用是为了更好的使用ReturnedMessage
类,因为对象的方式可以更好的支持lambda
表达式。
2.2.3 RabbitConfig
将RabbitReturnCallback
设置到RabbitTemplate
中。
2.2.4 测试方法
2.2.5 启动测试
2.2.6 小节
Return 退回模式
是从Exchange
到Queue
。Return
给了Producer
。Producer
发送的消息Exchange
和Routing Key
都不正确时,当Exchange
接收失败后直接触发Confirm Callback
,不会进入到Return Callback
,因为还没到Exchange
。- 当
Exchange
正确接收消息,但是Routing Key
设置错误, 触发Return Callback
方法。
3. 消息的可靠消费
上文中我们提到了一种消息丢失的情况,即 Consumer
接收到了Message
,Message
相关业务还没来得及处理,程序报错或者宕机了,Broker
会认为Consunmer
消息正常消费了,就把当前消息从队列中移除了。这种情况也算是消息丢失。
那能不能消息消费成功后再将消息从queue中移除呢?
答案肯定是可以的。
3.1 ACK确认机制
ACK指Acknowledge,确认。 表示消费端收到消息后的确认方式。
- 作用:
- 确认消息是否被消费者消费,消息通过ACK机制确认是否被正确接收,每个消息都要被确认。
- 默认情况下,一个消息被消费者正确消费就会从队列中移除
- ACK确认模式
- AcknowledgeMode.NONE :不确认
- 默认所有消息消费成功,会不断的向消费者推送消息。
- 因为RabbitMQ认为所有推送的消息已被成功消费,所以推送出去的消息不会暂存在
broker
,消息存在丢失的危险。
- AcknowledgeMode.AUTO:自动确认
- 由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到
broker
。 - 使用自动确认模式时,需要考虑的另一件事是消费者过载,因为
broker
会暂存没有收到ack
的消息,等消费端ack
后才会丢掉;如果收到消费端的nack
(消费失败的标识)或connection
断开没收到反馈,会将消息放回到原队列头部,导致消费者反复的在消费这条消息。
- 由spring-rabbit依据消息处理逻辑是否抛出异常自动发送ack(无异常)或nack(异常)到
- AcknowledgeMode.MANUAL:手动确认
- 手动确认则当消费者调用
ack
、nack
、reject
几种方法进行确认,手动确认可以在业务失败后进行一些操作,如果消息未被 ACK 则会发送到下一个消费者。 - 手动确认模式可以使用 prefetch,限制通道上未完成的(“正在进行中的”)发送的数量。也就是
Consumer
一次可以从Broker
取几条消息。 - 如果忘记进行ACK确认
忘记通过basicAck返回确认信息是常见的错误。这个错误非常严重,将导致消费者客户端退出或者关闭后,消息会被退回RabbitMQ服务器,这会使RabbitMQ服务器内存爆满,而且RabbitMQ也不会主动删除这些被退回的消息。只要程序还在运行,没确认的消息就一直是 Unacked 状态,无法被 RabbitMQ 重新投递。更厉害的是,RabbitMQ 消息消费并没有超时机制,也就是说,程序不重启,消息就永远是 Unacked 状态。处理运维事件时不要忘了这些 Unacked 状态的消息。当程序关闭时(实际只要 消费者 关闭就行),消息会恢复为 Ready 状态。
- 手动确认则当消费者调用
3.2 配置application.yaml
3.3 Consumer
消费消息有三种回执方法,接下来先看下每个方法参数的含义。
3.3.1 basicAck
deliveryTag
:消息投递的标签号,每次消费消息或者消息重新投递后,deliveryTag
都会增加。手动消息确认模式下,我们可以对指定deliveryTag
的消息进行ack
、nack
、reject
等操作。
multiple
:是否批量确认,值为 true
则会一次性 ack
所有小于当前消息 deliveryTag
的消息。
举个栗子: 假设我先发送三条消息deliveryTag
分别是5、6、7,可它们都没有被确认,当我发第四条消息此时deliveryTag
为8,multiple
设置为 true,会将5、6、7、8的消息全部进行确认。
3.3.2 basicNack
deliveryTag
:表示消息投递序号。
multiple
:是否批量确认。
requeue
:值为 true
消息将重新入队列。
3.3.3 basicReject
basicNack
:表示失败确认,一般在消费消息业务异常时用到此方法,可以将消息重新投递入队列。
deliveryTag
:表示消息投递序号。
requeue
:值为 true
消息将重新入队列。
3.4 启动测试
在这里我们执行sentMsg()
方法,输出日志如下:
从日志信息中我们可以看出,消息已成功被消费,并且当第一次消费失败后消息被重新放回了队列,并进行了再此消费,当再次失败后则放弃该条消息。
3.5 小节
消费方的ACK机制可以有效的解决消息从Broker
到Consumer
丢失的问题。但也要注意一点:消息的无限消费。
3.6 消息无限消费
如果消费端代码就像下边这样写的,思路很简单:处理完业务逻辑后确认消息, int a = 1 / 0
发生异常后将消息重新投入队列。
但是有个问题是,业务代码一旦出现 bug
99.9%的情况是不会自动修复,一条消息会被无限投递进队列,消费端无限执行,导致了死循环,CPU被瞬间打满了,而且rabbitmq management
只有一条未被确认的消息。
经过测试分析发现,当消息重新投递到消息队列时,这条消息不会回到队列尾部,仍是在队列头部。
消费者会立刻消费这条消息,业务处理再抛出异常,消息再重新入队,如此反复进行。导致消息队列处理出现阻塞,导致正常消息也无法运行,那该怎么处理呢?
第一种方法:是根据异常类型来选择是否重新放入队列。
第二种方法: 先将消息进行应答,此时消息队列会删除该条消息,然后通过channel.basicPublish()重新发布这个消息,异常消息就放在了消息队列尾部,,进而不会影响已经进入队列的消息处理。
但这种方法并没有解决根本问题,错误消息还是会时不时报错,后面优化设置了消息重试次数,达到了重试上限以后,手动确认,队列删除此消息,并将消息持久化入MySQL
并推送报警,进行人工处理和定时任务做补偿。
4. 总结
4.1 持久化
- Exchange 要持久化 通过
durable
属性控制,true:持久化, 缺省:true。 - queue 要持久化 通过
durable
属性控制,true:持久化, 缺省:true。 - message 要持久化
在springboot环境下,message模式也是持久化。
4.2 生产方确认Confirm
4.3 消费方确认Ack
4.4 Broker 高可用
__EOF__

本文链接:https://www.cnblogs.com/ludangxin/p/15257853.html
关于博主:评论和私信会在第一时间回复。或者直接私信我。
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!
声援博主:如果您觉得文章对您有帮助,可以点击文章右下角【推荐】一下。您的鼓励是博主的最大动力!
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 记一次.NET内存居高不下排查解决与启示
· 探究高空视频全景AR技术的实现原理
· 理解Rust引用及其生命周期标识(上)
· 浏览器原生「磁吸」效果!Anchor Positioning 锚点定位神器解析
· 没有源码,如何修改代码逻辑?
· 分享4款.NET开源、免费、实用的商城系统
· 全程不用写代码,我用AI程序员写了一个飞机大战
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· 白话解读 Dapr 1.15:你的「微服务管家」又秀新绝活了
· 记一次.NET内存居高不下排查解决与启示