RabbitMQ如何保证消息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发送的消息即使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模式也是持久化。






浙公网安备 33010602011771号