RabbitMQ(三)——延迟队列的第二种实现方式(插件)、消息发送可靠性(两种思路、开启事务、发送方确认机制)、自带的重试机制(Spring自带、业务重试——发送失败重试、消费失败重试+幂等性处理)
一、延迟队列的第二种实现方式(插件)
第一种方式在上篇博文中。
接下来讲第二种方式。
1、使用插件
使用插件的方式主要就是在消息头中设置消息的延迟时间。
a、安装和使用
第二种方式就是使用插件:rabbitmq_delayed_message_exchange
首先我们需要下载 rabbitmq_delayed_message_exchange 插件,这是一个 GitHub 上的开源项目,我
们直接下载即可:
https://github.com/rabbitmq/rabbitmq-delayed-message-exchange/releases
选择适合自己的版本,我这里选择最新的 3.9.0 版。
下载完成后在命令行执行如下命令将下载文件拷贝到 Docker 容器中去:
docker cp ./rabbitmq_delayed_message_exchange-3.9.0.ez some-rabbit:/plugins
这里第一个参数是宿主机上的文件地址,第二个参数是拷贝到容器的位置。
接下来再执行如下命令进入到 RabbitMQ 容器中:
docker exec -it some-rabbit /bin/bash
进入到容器之后,执行如下命令启用插件:
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
启用成功之后,还可以通过如下命令查看所有安装的插件,看看是否有我们刚刚安装过的插件,如下:
rabbitmq-plugins list
有 E* 的就是安装的了
b、前期准备
c、代码实现
消费者:
接口:
效果:
可以看到,刚好 5 秒。
二、消息发送可靠性
1、两种消费思路
RabbitMQ 的消息消费,整体上来说有两种不同的思路:
- 推(push):MQ主动将消息推送给消费者,这种方式需要消费者设置一个缓冲区去缓存消息,对于消费者而言,内存中总是有一堆需要处理的消息,所以这种方式的效率比较高,这也是目前大多数应用采用的消费方式。
- 拉(pull):消费者主动从 MQ 拉取消息,这种方式效率并不是很高,不过有的时候如果服务端需要批量拉取消息,倒是可以采用这种方式。
先看推:
当监听的队列中有消息时,就会触发该方法。
再来看拉(pull):
调用 receiveAndConvert 方法,方法参数为队列名称,方法执行完成后,会从 MQ 上拉取一条消息下来,如果该方法返回值为 null,表示该队列上没有消息了。receiveAndConvert 方法有一个重载方法,可以在重载方法中传入一个等待超时时间,例如 3 秒。此时,假设队列中没有消息了,则receiveAndConvert 方法会阻塞 3 秒,3 秒内如果队列中有了新消息就返回,3 秒后如果队列中还是没有新消息,就返回 null,这个等待超时时间要是不设置的话,默认为 0。
这是消息两种不同的消费模式。
如果需要从消息队列中持续获得消息,就可以使用推模式;如果只是单纯的消费一条消息,则使用拉模式即可。切忌将拉模式放到一个死循环中,变相的订阅消息,这会严重影响 RabbitMQ 的性能。
2、 确保消费成功的两种思路
对于消息消费成功,其实官方提供了相关的机制。
为了保证消息能够可靠的到达消息消费者,RabbitMQ 中提供了消息消费确认机制。当消费者去消费消息的时候,可以通过指定 autoAck 参数来表示消息消费的确认方式:
- 当 autoAck 为 false 的时候,此时即使消费者已经收到消息了,RabbitMQ也不会立马将消息移除,而是等待消费者显式的回复确认信号后,才会将消息打上删除标记,然后再删除。
- 当 autoAck 为 true的时候,此时MQ就会自动把发送出去的消息设置为确认,然后将消息移除(从内存或者磁盘中),即使这些消息并没有到达消费者。
来看一张图:
如上图所示,在 RabbitMQ 的 web 管理页面:
- Ready 表示待消费的消息数量。
- Unacked 表示已经发送给消费者但是还没收到消费者 ack 的消息数量。
当我们将 autoAck 设置为 false 的时候,对于 RabbitMQ 而言,消费分成了两个部分:
- 待消费的消息
- 已经投递给消费者,但是还没有被消费者确认的消息
当设置 autoAck 为 false 的时候,消费者就变得非常从容了,它将有足够的时间去处理这条消息,当消息正常处理完成后,再手动 ack,此时 RabbitMQ 才会认为这条消息消费成功了。如果RabbitMQ 一直没有收到客户端的反馈,并且此时客户端也已经断开连接了,那么 RabbitMQ 就会将刚刚的消息重新放回队列中,等待下一次被消费。
综上所述,确保消息被成功消费,无非就是手动 Ack 或者自动 Ack,无他。当然,无论这两种中的哪一种,最终都有可能导致消息被重复消费,所以一般来说我们还需要在处理消息时,解决幂等性问题。
总而言之:只要确保消息到达队列了就算消息发送成功了。因为队列之后的事情不归我们管,是消费者的事情。
如何确保消息成功到达 RabbitMQ?RabbitMQ 给出了两种方案:
- 开启事务机制
- 发送方确认机制
这是两种不同的方案,不可以同时开启,只能选择其中之一,如果两者同时开启,则会报如下错误:
3、开启事务机制(了解即可)
首先需要配置一个事务管理器, 发消息的时候配置一个事务就行。如果这个代码里面出错的话,就会回滚。通过加事务的方式确保这个消息发送成功。
a、代码实现
消息发送:
接口:
在消息发送那一块,通过加注解的方式开启事务,可以自行尝试故意出错,如果出错了这个消息会自动回滚。
消息发送使用了 @Transactional 注解,跟使用 mysql 的时候一样,因为 Spring 已经把事务的用法统一起来了。规则都是一样的。
b、为什么不选择事务方式
为什么事务模式了解就行呢?因为开启事务后会多了四个步骤:
4、发送方确认机制
再次强调,这种方式跟上面的事务方式不能共存,同时开启会报错:
a、代码实现
首先是 config 配置文件,先去掉事务管理:
其他都不变,再加点东西:
发送消息:
到这里代码就搞完了。
可以自行测试 交换机出问题或者队列出问题。比如直接注释掉交换机就行(在 send 方法里注释)。
5、自带的重试机制
失败重试分两种情况,一种是压根没找到 MQ 导致的失败重试(一般是没启动 MQ,或者网络问题)。
另一种是找到 MQ 了,但是消息发送失败了。
a、自带重试机制(Spring 自带,与 MQ 无关)
不仅是 MQ,其他地方也可使用。
b、业务重试
具体的业务重试看下一章!
三、业务重试
1、RabbitMQ 员工入职发送邮件
这里拿了一个 SpringBoot 管理系统的的项目拆分之后,再加了一个发送消息的 SpringBoot 项目,两个项目一个发一个收消息来测试。
这里先只搞一部分需求,需求是员工成功入职了就模拟发送一个邮件,发送到员工的邮箱中去,发送内容:欢迎入职。这里只是模拟这个功能,所以邮件展示在控制台中。接着就是每次发送邮件都会有日志记录这个邮件的信息。
数据库 mapper 层的代码在最后的章节中有展示,前面先展示 MQ 业务相关的代码。 mapper 层没有什么特别的,就是普通的数据库 增删改查之类的。
a、实体类和前置准备
发送邮件项目的注册文件:
这里仅展示部分重要的实体类:
员工类序列化:
邮件类序列化:
发生邮件日志类:
b、模拟添加员工接口:
模拟员工入职添加员工的接口:
c、发送消息
这里说句题外话,如果这里 MQ 有事务,数据库也有事务,那么开启事务的注解是指定哪个事务呢?
一般来说是不可能两个一起存在,只能存在一个,如果真的有两个,可以通过属性 transactionManager = “xx” 来指定事务管理器。默认情况下,开启事务的注解只对数据库事务有效。
d、启动类添加注解
开启定时任务:
e、重复发送邮件,一定次数后设为失败
f、service 的 config
g、消费邮件项目配置文件
h、消费方的 config
i、消费邮件
到此这一部分的代码就结束了。
如果需要测试,就正常的登录后然后访问前面那个接口即可。一个是看数据库的日志,一个是看控制台是否有输出入职的相关信息。
2、消费失败重试
上面的代码还不完整,就比如消费的时候,还没发邮件,或者正在构建邮件的时候,突然有异常,比如这样:
跟上面那样正常测试,此时会看到消费者那边正在无限循环的报错;其实消息已经发出去了,发给消费者,但是消费者还没告诉 MQ 已经消费成功了;造成这样的原因有两点:
1、消费者的的项目的配置文件多写了两行:
这里需要注释掉。
此时再次运行,还是会一直报错。
2、消费者里边已经默认开启事务了。
如果失败,会自动的进行重试,所以会不停的一直打印错误信息。如果抛异常,这个消息会重新回到 MQ 里面等待消费者过来消费。这里可以配置,手动告诉 MQ 是否消费成功:
这个时候再次重试,虽然一开始会报错,但是只会报一次错误,不会像之前那样一直的重复报错。因为已经切换成手动,需要我们自己去手动的告诉 MQ 消费成功还是失败。
a、完善代码,手动告诉 MQ 消费成功或失败
basicAck:确认消息消费成功。
basicNack:确认消息消费失败。
一般是用上面这两个就够了。
消费者:
3、消息的幂等性处理
注意:这个跟 redis 那个不同,redis 处理的是接口幂等性,这个是消息幂等性。
但是上面的代码还不够,可能会存在消息发送重复,所以这里消费的时候还需要作幂等性处理:
解决思路是:如果发过来的消息已经处理过了,就不去管;如果还没处理,再去管。一般都是结合 redis 来做。 看下图:
a、前期准备
添加 redis 依赖,这里一开始是应该添加了。
还有消费者的配置文件:
b、幂等性处理代码
这里直接截图部分代码,红色框框内为新增代码,剩余的跟上面一样(注意看注释,很多重要的点):
到这里代码就完整了。
c、测试
如果要测试,这里这么设置:
MailSendLogScheduled 类:
发送多一条重复的消息进行测试。
4、mapper 层代码展示
想了又想,还是决定贴出 mapper 层的代码供日后查看:
a、EmployeeMapper
b、MailSendLogMapper