RabbitMQ 高可用之如何确保消息成功消费(2)

  • 当 autoAck 为 false 的时候,此时即使消费者已经收到消息了,RabbitMQ 也不会立马将消息移除,而是等待消费者显式的回复确认信号后,才会将消息打上删除标记,然后再删除。

  • 当 autoAck 为 true 的时候,此时消息消费者就会自动把发送出去的消息设置为确认,然后将消息移除(从内存或者磁盘中),即使这些消息并没有到达消费者。

我们来看一张图:

如上图所示,在 RabbitMQ 的 web 管理页面:

  • Ready 表示待消费的消息数量。

  • Unacked 表示已经发送给消费者但是还没收到消费者 ack 的消息数量。

这是我们可以从 UI 层面观察消息的消费情况确认情况。

当我们将 autoAck 设置为 false 的时候,对于 RabbitMQ 而言,消费分成了两个部分:

  • 待消费的消息

  • 已经投递给消费者,但是还没有被消费者确认的消息

换句话说,当设置 autoAck 为 false 的时候,消费者就变得非常从容了,它将有足够的时间去处理这条消息,当消息正常处理完成后,再手动 ack,此时 RabbitMQ 才会认为这条消息消费成功了。如果 RabbitMQ 一直没有收到客户端的反馈,并且此时客户端也已经断开连接了,那么 RabbitMQ 就会将刚刚的消息重新放回队列中,等待下一次被消费。

综上所述,确保消息被成功消费,无非就是手动 Ack 或者自动 Ack,无他。当然,无论这两种中的哪一种,最终都有可能导致消息被重复消费,所以一般来说我们还需要在处理消息时,解决幂等性问题。

3. 消息拒绝


当客户端收到消息时,可以选择消费这条消息,也可以选择拒绝这条消息。我们来看下拒绝的方式:

@Component

public class ConsumerDemo {

@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)

public void handle(Channel channel, Message message) {

//获取消息编号

long deliveryTag = message.getMessageProperties().getDeliveryTag();

try {

//拒绝消息

channel.basicReject(deliveryTag, true);

} catch (IOException e) {

e.printStackTrace();

}

}

}

消费者收到消息之后,可以选择拒绝消费该条消息,拒绝的步骤分两步:

  1. 获取消息编号 deliveryTag。

  2. 调用 basicReject 方法拒绝消息。

调用 basicReject 方法时,第二个参数是 requeue,即是否重新入队。如果第二个参数为 true,则这条被拒绝的消息会重新进入到消息队列中,等待下一次被消费;如果第二个参数为 false,则这条被拒绝的消息就会被丢掉,不会有新的消费者去消费它了。

需要注意的是,basicReject 方法一次只能拒绝一条消息。

4. 消息确认


消息确认分为自动确认和手动确认,我们分别来看。

4.1 自动确认

先来看看自动确认,在 Spring Boot 中,默认情况下,消息消费就是自动确认的。

我们来看如下一个消息消费方法:

@Component

public class ConsumerDemo {

@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)

public void handle2(String msg) {

System.out.println("msg = " + msg);

int i = 1 / 0;

}

}

通过 @Componet 注解将当前类注入到 Spring 容器中,然后通过 @RabbitListener 注解来标记一个消息消费方法,默认情况下,消息消费方法自带事务,即如果该方法在执行过程中抛出异常,那么被消费的消息会重新回到队列中等待下一次被消费,如果该方法正常执行完没有抛出异常,则这条消息就算是被消费了。

4.2 手动确认

手动确认我又把它分为两种:推模式手动确认与拉模式手动确认。

4.2.1 推模式手动确认

要开启手动确认,需要我们首先关闭自动确认,关闭方式如下:

spring.rabbitmq.listener.simple.acknowledge-mode=manual

这个配置表示将消息的确认模式改为手动确认。

接下来我们来看下消费者中的代码:

@RabbitListener(queues = RabbitConfig.JAVABOY_QUEUE_NAME)

public void handle3(Message message,Channel channel) {

long deliveryTag = message.getMessageProperties().getDeliveryTag();

try {

//消息消费的代码写到这里

String s = new String(message.getBody());

System.out.println("s = " + s);

//消费完成后,手动 ack

channel.basicAck(deliveryTag, false);

} catch (Exception e) {

//手动 nack

try {

channel.basicNack(deliveryTag, false, true);

} catch (IOException ex) {

ex.printStackTrace();

}

}

}

将消费者要做的事情放到一个 try..catch 代码块中。

如果消息正常消费成功,则执行 basicAck 完成确认。

如果消息消费失败,则执行 basicNack 方法,告诉 RabbitMQ 消息消费失败。

这里涉及到两个方法:

  • basicAck:这个是手动确认消息已经成功消费,该方法有两个参数:第一个参数表示消息的 id;第二个参数 multiple 如果为 false,表示仅确认当前消息消费成功,如果为 true,则表示当前消息之前所有未被当前消费者确认的消息都消费成功。

  • basicNack:这个是告诉 RabbitMQ 当前消息未被成功消费,该方法有三个参数:第一个参数表示消息的 id;第二个参数 multiple 如果为 false,表示仅拒绝当前消息的消费,如果为 true,则表示拒绝当前消息之前所有未被当前消费者确认的消息;第三个参数 requeue 含义和前面所说的一样,被拒绝的消息是否重新入队。

当 basicNack 中最后一个参数设置为 false 的时候,还涉及到一个死信队列的问题,这个松哥以后再专门写文章和大家细聊。

4.2.2 拉模式手动确认

拉模式手动 ack 比较麻烦一些,在 Spring 中封装的 RabbitTemplate 中并未找到对应的方法,所以我们得用原生的办法,如下:

public void receive2() {

Channel channel = rabbitTemplate.getConnectionFactory().createConnection().createChannel(true);

long deliveryTag = 0L;

try {

GetResponse getResponse = channel.basicGet(RabbitConfig.JAVABOY_QUEUE_NAME, false);

deliveryTag = getResponse.getEnvelope().getDeliveryTag();

System.out.println("o = " + new String((getResponse.getBody()), “UTF-8”));

channel.basicAck(deliveryTag, false);

} catch (IOException e) {

try {

channel.basicNack(deliveryTag, false, true);

} catch (IOException ex) {

ex.printStackTrace();

}

}

}

这里涉及到的 basicAck 和 basicNack 方法跟前面的一样,我就不再赘述。

5. 幂等性问题


最后我们再来说说消息的幂等性问题。

大家设想下面一个场景:

消费者在消费完一条消息后,向 RabbitMQ 发送一个 ack 确认,此时由于网络断开或者其他原因导致 RabbitMQ 并没有收到这个 ack,那么此时 RabbitMQ 并不会将该条消息删除,当重新建立起连接后,消费者还是会再次收到该条消息,这就造成了消息的重复消费。同时,由于类似的原因,消息在发送的时候,同一条消息也可能会发送两次(参见四种策略确保 RabbitMQ 消息发送可靠性!你用哪种?)。种种原因导致我们在消费消息时,一定要处理好幂等性问题。

幂等性问题的处理倒也不难,基本上都是从业务上来处理,我来大概说说思路。

采用 Redis,在消费者消费消息之前,现将消息的 id 放到 Redis 中,存储方式如下:

  • id-0(正在执行业务)

  • id-1(执行业务成功)

自我介绍一下,小编13年上海交大毕业,曾经在小公司待过,也去过华为、OPPO等大厂,18年进入阿里一直到现在。

深知大多数Java工程师,想要提升技能,往往是自己摸索成长或者是报班学习,但对于培训机构动则几千的学费,着实压力不小。自己不成体系的自学效果低效又漫长,而且极易碰到天花板技术停滞不前!

因此收集整理了一份《2024年Java开发全套学习资料》,初衷也很简单,就是希望能够帮助到想自学提升又不知道该从何学起的朋友,同时减轻大家的负担。img

既有适合小白学习的零基础资料,也有适合3年以上经验的小伙伴深入学习提升的进阶课程,基本涵盖了95%以上Java开发知识点,真正体系化!

由于文件比较大,这里只是将部分目录截图出来,每个节点里面都包含大厂面经、学习笔记、源码讲义、实战项目、讲解视频,并且会持续更新!

如果你觉得这些内容对你有帮助,可以扫码获取!!(备注Java获取)

img

我的面试宝典:一线互联网大厂Java核心面试题库

以下是我个人的一些做法,希望可以给各位提供一些帮助:

整理了很长一段时间,拿来复习面试刷题非常合适,其中包括了Java基础、异常、集合、并发编程、JVM、Spring全家桶、MyBatis、Redis、数据库、中间件MQ、Dubbo、Linux、Tomcat、ZooKeeper、Netty等等,且还会持续的更新…可star一下!

image

283页的Java进阶核心pdf文档

Java部分:Java基础,集合,并发,多线程,JVM,设计模式

数据结构算法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

image

还有源码相关的阅读学习

image

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!
法:Java算法,数据结构

开源框架部分:Spring,MyBatis,MVC,netty,tomcat

分布式部分:架构设计,Redis缓存,Zookeeper,kafka,RabbitMQ,负载均衡等

微服务部分:SpringBoot,SpringCloud,Dubbo,Docker

[外链图片转存中…(img-bJFpTLP2-1713373035279)]

还有源码相关的阅读学习

[外链图片转存中…(img-ukJCTFWt-1713373035279)]

《互联网大厂面试真题解析、进阶开发核心学习笔记、全套讲解视频、实战项目源码讲义》点击传送门即可获取!

  • 24
    点赞
  • 14
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值