如何保证消息的可靠性的投递
如何保证消息的可靠性的投递
在本项目中,添加员工会发送入职邮件,利用RabbitMQ的队列发送入职邮件。这部分只是实现发送邮件的功能,RabbitMQ它有它的优点就是异步、解耦、流量削峰。RabbitMQ在我们的邮件发送中扮演的角色,就相当于一个中转站。
就好比,我们平时的驿站,取快递的这个操作,大家都很熟悉了吧。生产者,就是我们的商家,驿站,相当于我们的队列,而我们就是消费者。
详细的RabbitMQ的概念:RabbitMQ(一) - BeaBrick0 - 博客园 (cnblogs.com)
但是,它也有缺点。在实际的开发中,需要考虑到如何确保消息的可靠性投递?
我们一个个来解决,之前面试的过程中,面试官,对我说的一句话,记忆犹新,看一个知识点或者问题,要知道为什么是这样、为什么会这样、怎么可以避免。接下来,我们一个一个来。
在什么场景下消息可能会丢失

如图所示,RabbitMQ丢失消息的情况可以发生在任何一个节点上。
消息投递的可靠性方案
针对上面可能会造成消息丢失的节点,我们来做分析:
生产者没有成功把消息发送到MQ
丢失的原因:因为网络传输的不稳定性,当生产者在向MQ发送消息的过程中,MQ没有成功接收到消息,但是生产者却以为MQ成功接收到了消息,不会再次重复发送该消息,从而导致消息的丢失。
解决办法: 有两个解决办法:事务机制和confirm机制,最常用的是confirm机制。
事务机制
RabbitMQ 提供了事务功能,生产者发送数据之前开启 RabbitMQ 事务channel.txSelect,然后发送消息,如果消息没有成功被 RabbitMQ 接收到,那么生产者会收到异常报错,此时就可以回滚事务channel.txRollback,然后重试发送消息;如果收到了消息,那么可以提交事务channel.txCommit。
//伪代码
//开启事务
channel.txSelect
try{
//在这里发送消息
.........
}catch(Exception e){
//如果捕获到异常
channel.txRollBack;
}
//提交事务
channel.txCommit;
Confirm机制
RabbitMQ可以开启 confirm 模式,在生产者那里设置开启 confirm 模式之后,生产者每次写的消息都会分配一个唯一的 id,如果消息成功写入 RabbitMQ 中,RabbitMQ 会给生产者回传一个 ack 消息,告诉你说这个消息 ok 了。
如果 RabbitMQ 没能处理这个消息,会回调你的一个 nack 接口,告诉你这个消息接收失败,生产者可以发送。而且你可以结合这个机制自己在内存里维护每个消息 id 的状态,如果超过一定时间还没接收到这个消息的回调,那么可以重发。
注意:RabbitMQ的事务机制是同步的,很耗型能,会降低RabbitMQ的吞吐量。confirm机制是异步的,生成者发送完一个消息之后,不需要等待RabbitMQ的回调,就可以发送下一个消息,当RabbitMQ成功接收到消息之后会自动异步的回调生产者的一个接口返回成功与否的消息。
RabbitMQ接收到消息之后丢失了消息
丢失的原因:RabbitMQ接收到生产者发送过来的消息,是存在内存中的,如果没有被消费完,此时RabbitMQ宕机了,那么再次启动的时候,原来内存中的那些消息都丢失了。
解决办法:开启RabbitMQ的持久化。当生产者把消息成功写入RabbitMQ之后,RabbitMQ就把消息持久化到磁盘。结合上面的说到的confirm机制,只有当消息成功持久化磁盘之后,才会回调生产者的接口返回ack消息,否则都算失败,生产者会重新发送。存入磁盘的消息不会丢失,就算RabbitMQ挂掉了,重启之后,他会读取磁盘中的消息,不会导致消息的丢失。
持久化的配置
第一点是创建 queue 的时候将其设置为持久化,这样就可以保证 RabbitMQ 持久化 queue 的元数据,但是它是不会持久化 queue 里的数据的。
第二个是发送消息的时候将消息的 deliveryMode 设置为 2,就是将消息设置为持久化的,此时 RabbitMQ 就会将消息持久化到磁盘上去。
注意:持久化要起作用必须同时设置这两个持久化才行,RabbitMQ 哪怕是挂了,再次重启,也会从磁盘上重启恢复 queue,恢复这个 queue 里的数据。
消费者弄丢了消息
丢失的原因:如果RabbitMQ成功的把消息发送给了消费者,那么RabbitMQ的ack机制会自动的返回成功,表明发送消息成功,下次就不会发送这个消息。但如果就在此时,消费者还没处理完该消息,然后宕机了,那么这个消息就丢失了。
b、解决的办法:简单来说,就是必须关闭 RabbitMQ 的自动 ack,可以通过一个 api 来调用就行,然后每次在自己代码里确保处理完的时候,再在程序里 ack 一把。这样的话,如果你还没处理完,不就没有 ack了?那 RabbitMQ 就认为你还没处理完,这个时候 RabbitMQ 会把这个消费分配给别的 consumer 去处理,消息是不会丢的。
本项目的针对消息的可靠性的解决方案
消息发送
第一种方案:消息落库,对消息进行状态打标。

- Step1:进行订单数据入业务库,同时插入一条消息数据入消息库,此时数据状态status为0
- Step2:发送消息
- Step3:服务器发送消息确认
- Step4:生产者监听器监听到了消息确认,这时候更改消息库数据状态status为1
- Step5:分布式定时任务会定时查询消息库状态status为0的数据(说明数据未可靠发送,发送失败或者接收确认失败)
- Step6:将状态status为0的数据进行二次重试发送,每重试一次次数加1
- Step7:如果重试次数超过3次,则将消息库数据状态status改为2,数据状态status为2的数据只能通过人工去排查问题
当然,这种要一直去数据库查询,我们的消息的状态和重试次数。
其实很明显,高并发场景下是有性能瓶颈的,每次消息的推送都需要多一次的入库(数据入消息库),带来多一次的磁盘IO消耗
。由于有性能的限制,我们来看第二种方案,消息的延迟投递,做二次确认,回调检查。
第二种方案:消息延迟投递,做二次确认,回调检查。
- Step1:首先对业务订单数据落库,再发送消息到消息服务器
- Step2:第一次发送消息几分钟之后重新发送该消息,目的是做confirm消息确认检查(延迟投递)
- Step3:下游服务监听消息,进行消费
- Step4:接收到消息之后会发送一个confirm确认消息到服务器
- Step5:回调服务会监听下游服务发送的确认消息,监听到之后会落入消息库
- Step6:几分钟之后,Step2发送了消息确认数据,回调服务接收到了该消息,会和消息库中的消息进行一个比对检查,假如消息库中没有该消息的确认,回调服务就会发起一个ReSend Command命令,通知上游服务重新发送一次消息。
在本项目中,我们选择第一种方案,来取保消息的可靠性投递。
项目的实现流程:
- 发送消息时,将当前消息数据存入数据库中,投递状态改为正在投递中
- 开启消息的回调机制,确认成功,更新投递状态为消息的投递状态
- 开启定时任务,重新投递失败。重试超过3次,更新投递状态为投递失败。
作者:BearBrick0
出处:https://www.cnblogs.com/bearbrick0/p/16147949.html
版权:本作品采用「署名-非商业性使用-相同方式共享 4.0 国际」许可协议进行许可。
【推荐】国内首个AI IDE,深度理解中文开发场景,立即下载体验Trae
【推荐】编程新体验,更懂你的AI,立即体验豆包MarsCode编程助手
【推荐】抖音旗下AI助手豆包,你的智能百科全书,全免费不限次数
【推荐】轻量又高性能的 SSH 工具 IShell:AI 加持,快人一步
· 无需6万激活码!GitHub神秘组织3小时极速复刻Manus,手把手教你使用OpenManus搭建本
· Manus爆火,是硬核还是营销?
· 终于写完轮子一部分:tcp代理 了,记录一下
· 别再用vector<bool>了!Google高级工程师:这可能是STL最大的设计失误
· 单元测试从入门到精通