RabbitMQ如何保证消息的可靠投递?

开发 前端
总而言之,在生产环境中,我们一般都是单条手动ack,消费失败后不会重新入队(因为很大概率还会再次失败),而是将消息重新投递到死信队列,方便以后排查问题。

 

Spring Boot整合RabbitMQ

github地址:

https://github.com/erlieStar/rabbitmq-examples

Spring有三种配置方式

  1. 基于XML
  2. 基于JavaConfig
  3. 基于注解

当然现在已经很少使用XML来做配置了,只介绍一下用JavaConfig和注解的配置方式

RabbitMQ整合Spring Boot,我们只需要增加对应的starter即可

  1. <dependency> 
  2.   <groupId>org.springframework.boot</groupId> 
  3.   <artifactId>spring-boot-starter-amqp</artifactId> 
  4. </dependency> 

基于注解

在application.yaml的配置如下

  1. spring: 
  2.   rabbitmq: 
  3.     host: myhost 
  4.     port: 5672 
  5.     username: guest 
  6.     password: guest 
  7.     virtual-host: / 
  8.  
  9. log: 
  10.   exchange: log.exchange 
  11.   info: 
  12.     queue: info.log.queue 
  13.     binding-key: info.log.key 
  14.   error: 
  15.     queue: error.log.queue 
  16.     binding-key: error.log.key 
  17.   all
  18.     queue: all.log.queue 
  19.     binding-key'*.log.key' 

消费者代码如下

  1. @Slf4j 
  2. @Component 
  3. public class LogReceiverListener { 
  4.  
  5.     /** 
  6.      * 接收info级别的日志 
  7.      */ 
  8.     @RabbitListener( 
  9.             bindings = @QueueBinding( 
  10.                     value = @Queue(value = "${log.info.queue}", durable = "true"), 
  11.                     exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC), 
  12.                     key = "${log.info.binding-key}" 
  13.             ) 
  14.     ) 
  15.     public void infoLog(Message message) { 
  16.         String msg = new String(message.getBody()); 
  17.         log.info("infoLogQueue 收到的消息为: {}", msg); 
  18.     } 
  19.  
  20.     /** 
  21.      * 接收所有的日志 
  22.      */ 
  23.     @RabbitListener( 
  24.             bindings = @QueueBinding( 
  25.                     value = @Queue(value = "${log.all.queue}", durable = "true"), 
  26.                     exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC), 
  27.                     key = "${log.all.binding-key}" 
  28.             ) 
  29.     ) 
  30.     public void allLog(Message message) { 
  31.         String msg = new String(message.getBody()); 
  32.         log.info("allLogQueue 收到的消息为: {}", msg); 
  33.     } 

生产者如下

  1. @RunWith(SpringRunner.class) 
  2. @SpringBootTest 
  3. public class MsgProducerTest { 
  4.  
  5.     @Autowired 
  6.     private AmqpTemplate amqpTemplate; 
  7.     @Value("${log.exchange}"
  8.     private String exchange; 
  9.     @Value("${log.info.binding-key}"
  10.     private String routingKey; 
  11.  
  12.     @SneakyThrows 
  13.     @Test 
  14.     public void sendMsg() { 
  15.         for (int i = 0; i < 5; i++) { 
  16.             String message = "this is info message " + i; 
  17.             amqpTemplate.convertAndSend(exchange, routingKey, message); 
  18.         } 
  19.  
  20.         System.in.read(); 
  21.     } 

Spring Boot针对消息ack的方式和原生api针对消息ack的方式有点不同

原生api消息ack的方式

消息的确认方式有2种

自动确认(autoAck=true)

手动确认(autoAck=false)

消费者在消费消息的时候,可以指定autoAck参数

String basicConsume(String queue, boolean autoAck, Consumer callback)

autoAck=false: RabbitMQ会等待消费者显示回复确认消息后才从内存(或者磁盘)中移出消息

autoAck=true: RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费了这些消息

手动确认的方法如下,有2个参数

basicAck(long deliveryTag, boolean multiple)

deliveryTag: 用来标识信道中投递的消息。RabbitMQ 推送消息给Consumer时,会附带一个deliveryTag,以便Consumer可以在消息确认时告诉RabbitMQ到底是哪条消息被确认了。

RabbitMQ保证在每个信道中,每条消息的deliveryTag从1开始递增

multiple=true: 消息id<=deliveryTag的消息,都会被确认

myltiple=false: 消息id=deliveryTag的消息,都会被确认

消息一直不确认会发生啥?

如果队列中的消息发送到消费者后,消费者不对消息进行确认,那么消息会一直留在队列中,直到确认才会删除。

如果发送到A消费者的消息一直不确认,只有等到A消费者与rabbitmq的连接中断,rabbitmq才会考虑将A消费者未确认的消息重新投递给另一个消费者

Spring Boot中针对消息ack的方式

有三种方式,定义在AcknowledgeMode枚举类中

方式 解释
NONE 没有ack,等价于原生api中的autoAck=true
MANUAL 用户需要手动发送ack或者nack
AUTO 方法正常结束,spring boot 框架返回ack,发生异常spring boot框架返回nack

spring boot针对消息默认的ack的方式为AUTO。

在实际场景中,我们一般都是手动ack。

application.yaml的配置改为如下

  1. spring: 
  2.   rabbitmq: 
  3.     host: myhost 
  4.     port: 5672 
  5.     username: guest 
  6.     password: guest 
  7.     virtual-host: / 
  8.     listener: 
  9.       simple: 
  10.         acknowledge-mode: manual # 手动ack,默认为auto 

相应的消费者代码改为

  1. @Slf4j 
  2. @Component 
  3. public class LogListenerManual { 
  4.  
  5.     /** 
  6.      * 接收info级别的日志 
  7.      */ 
  8.     @RabbitListener( 
  9.             bindings = @QueueBinding( 
  10.                     value = @Queue(value = "${log.info.queue}", durable = "true"), 
  11.                     exchange = @Exchange(value = "${log.exchange}", type = ExchangeTypes.TOPIC), 
  12.                     key = "${log.info.binding-key}" 
  13.             ) 
  14.     ) 
  15.     public void infoLog(Message message, Channel channel) throws Exception { 
  16.         String msg = new String(message.getBody()); 
  17.         log.info("infoLogQueue 收到的消息为: {}", msg); 
  18.         try { 
  19.             // 这里写各种业务逻辑 
  20.             channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); 
  21.         } catch (Exception e) { 
  22.             channel.basicNack(message.getMessageProperties().getDeliveryTag(), falsefalse); 
  23.         } 
  24.     } 

我们上面用到的注解,作用如下

注解 作用
RabbitListener 消费消息,可以定义在类上,方法上,当定义在类上时需要和RabbitHandler配合使用
QueueBinding 定义绑定关系
Queue 定义队列
Exchange 定义交换机
RabbitHandler RabbitListener定义在类上时,需要用RabbitHandler指定处理的方法

基于JavaConfig

既然用注解这么方便,为啥还需要JavaConfig的方式呢?

JavaConfig方便自定义各种属性,比如同时配置多个virtual host等

具体代码看GitHub把

RabbitMQ如何保证消息的可靠投递

一个消息往往会经历如下几个阶段

在这里插入图片描述

所以要保证消息的可靠投递,只需要保证这3个阶段的可靠投递即可

生产阶段

这个阶段的可靠投递主要靠ConfirmListener(发布者确认)和ReturnListener(失败通知)

前面已经介绍过了,一条消息在RabbitMQ中的流转过程为

producer -> rabbitmq broker cluster -> exchange -> queue -> consumer

ConfirmListener可以获取消息是否从producer发送到broker

ReturnListener可以获取从exchange路由不到queue的消息

我用Spring Boot Starter 的api来演示一下效果

application.yaml

  1. spring: 
  2.   rabbitmq: 
  3.     host: myhost 
  4.     port: 5672 
  5.     username: guest 
  6.     password: guest 
  7.     virtual-host: / 
  8.     listener: 
  9.       simple: 
  10.         acknowledge-mode: manual # 手动ack,默认为auto 
  11.  
  12. log: 
  13.   exchange: log.exchange 
  14.   info: 
  15.     queue: info.log.queue 
  16.     binding-key: info.log.key 

发布者确认回调

  1. @Component 
  2. public class ConfirmCallback implements RabbitTemplate.ConfirmCallback { 
  3.  
  4.     @Autowired 
  5.     private MessageSender messageSender; 
  6.  
  7.     @Override 
  8.     public void confirm(CorrelationData correlationData, boolean ack, String cause) { 
  9.         String msgId = correlationData.getId(); 
  10.         String msg = messageSender.dequeueUnAckMsg(msgId); 
  11.         if (ack) { 
  12.             System.out.println(String.format("消息 {%s} 成功发送给mq", msg)); 
  13.         } else { 
  14.             // 可以加一些重试的逻辑 
  15.             System.out.println(String.format("消息 {%s} 发送mq失败", msg)); 
  16.         } 
  17.     } 

失败通知回调

  1. @Component 
  2. public class ReturnCallback implements RabbitTemplate.ReturnCallback { 
  3.  
  4.     @Override 
  5.     public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { 
  6.         String msg = new String(message.getBody()); 
  7.         System.out.println(String.format("消息 {%s} 不能被正确路由,routingKey为 {%s}", msg, routingKey)); 
  8.     } 
  1. @Configuration 
  2. public class RabbitMqConfig { 
  3.  
  4.     @Bean 
  5.     public ConnectionFactory connectionFactory( 
  6.             @Value("${spring.rabbitmq.host}") String host, 
  7.             @Value("${spring.rabbitmq.port}"int port, 
  8.             @Value("${spring.rabbitmq.username}") String username, 
  9.             @Value("${spring.rabbitmq.password}") String password
  10.             @Value("${spring.rabbitmq.virtual-host}") String vhost) { 
  11.         CachingConnectionFactory connectionFactory = new CachingConnectionFactory(host); 
  12.         connectionFactory.setPort(port); 
  13.         connectionFactory.setUsername(username); 
  14.         connectionFactory.setPassword(password); 
  15.         connectionFactory.setVirtualHost(vhost); 
  16.         connectionFactory.setPublisherConfirms(true); 
  17.         connectionFactory.setPublisherReturns(true); 
  18.         return connectionFactory; 
  19.     } 
  20.  
  21.     @Bean 
  22.     public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory, 
  23.                                          ReturnCallback returnCallback, ConfirmCallback confirmCallback) { 
  24.         RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory); 
  25.         rabbitTemplate.setReturnCallback(returnCallback); 
  26.         rabbitTemplate.setConfirmCallback(confirmCallback); 
  27.         // 要想使 returnCallback 生效,必须设置为true 
  28.         rabbitTemplate.setMandatory(true); 
  29.         return rabbitTemplate; 
  30.     } 

这里我对RabbitTemplate做了一下包装,主要就是发送的时候增加消息id,并且保存消息id和消息的对应关系,因为RabbitTemplate.ConfirmCallback只能拿到消息id,并不能拿到消息内容,所以需要我们自己保存这种映射关系。在一些可靠性要求比较高的系统中,你可以将这种映射关系存到数据库中,成功发送删除映射关系,失败则一直发送

  1. @Component 
  2. public class MessageSender { 
  3.  
  4.     @Autowired 
  5.     private RabbitTemplate rabbitTemplate; 
  6.  
  7.     public final Map<String, String> unAckMsgQueue = new ConcurrentHashMap<>(); 
  8.  
  9.     public void convertAndSend(String exchange, String routingKey, String message) { 
  10.         String msgId = UUID.randomUUID().toString(); 
  11.         CorrelationData correlationData = new CorrelationData(); 
  12.         correlationData.setId(msgId); 
  13.         rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData); 
  14.         unAckMsgQueue.put(msgId, message); 
  15.     } 
  16.  
  17.     public String dequeueUnAckMsg(String msgId) { 
  18.         return unAckMsgQueue.remove(msgId); 
  19.     } 
  20.  

测试代码为

  1. @RunWith(SpringRunner.class) 
  2. @SpringBootTest 
  3. public class MsgProducerTest { 
  4.  
  5.     @Autowired 
  6.     private MessageSender messageSender; 
  7.     @Value("${log.exchange}"
  8.     private String exchange; 
  9.     @Value("${log.info.binding-key}"
  10.     private String routingKey; 
  11.  
  12.     /** 
  13.      * 测试失败通知 
  14.      */ 
  15.     @SneakyThrows 
  16.     @Test 
  17.     public void sendErrorMsg() { 
  18.         for (int i = 0; i < 3; i++) { 
  19.             String message = "this is error message " + i; 
  20.             messageSender.convertAndSend(exchange, "test", message); 
  21.         } 
  22.         System.in.read(); 
  23.     } 
  24.  
  25.     /** 
  26.      * 测试发布者确认 
  27.      */ 
  28.     @SneakyThrows 
  29.     @Test 
  30.     public void sendInfoMsg() { 
  31.         for (int i = 0; i < 3; i++) { 
  32.             String message = "this is info message " + i; 
  33.             messageSender.convertAndSend(exchange, routingKey, message); 
  34.         } 
  35.         System.in.read(); 
  36.     } 

先来测试失败者通知

输出为

  1. 消息 {this is error message 0} 不能被正确路由,routingKey为 {test} 
  2. 消息 {this is error message 0} 成功发送给mq 
  3. 消息 {this is error message 2} 不能被正确路由,routingKey为 {test} 
  4. 消息 {this is error message 2} 成功发送给mq 
  5. 消息 {this is error message 1} 不能被正确路由,routingKey为 {test} 
  6. 消息 {this is error message 1} 成功发送给mq 

消息都成功发送到broker,但是并没有被路由到queue中

再来测试发布者确认

输出为

  1. 消息 {this is info message 0} 成功发送给mq 
  2. infoLogQueue 收到的消息为: {this is info message 0} 
  3. infoLogQueue 收到的消息为: {this is info message 1} 
  4. 消息 {this is info message 1} 成功发送给mq 
  5. infoLogQueue 收到的消息为: {this is info message 2} 
  6. 消息 {this is info message 2} 成功发送给mq 

消息都成功发送到broker,也成功被路由到queue中

存储阶段

这个阶段的高可用还真没研究过,毕竟集群都是运维搭建的,后续有时间的话会把这快的内容补充一下

消费阶段

消费阶段的可靠投递主要靠ack来保证。

总而言之,在生产环境中,我们一般都是单条手动ack,消费失败后不会重新入队(因为很大概率还会再次失败),而是将消息重新投递到死信队列,方便以后排查问题

总结一下各种情况

  1. ack后消息从broker中删除
  2. nack或者reject后,分为如下2种情况

(1) reque=true,则消息会被重新放入队列

(2) reque=fasle,消息会被直接丢弃,如果指定了死信队列的话,会被投递到死信队列

本文转载自微信公众号「Java识堂」,可以通过以下二维码关注。转载本文请联系Java识堂公众号。

 

责任编辑:武晓燕 来源: Java识堂
相关推荐

2023-03-06 08:16:04

SpringRabbitMQ

2021-04-27 07:52:18

RocketMQ消息投递

2021-02-02 11:01:31

RocketMQ消息分布式

2020-09-27 07:44:08

RabbitMQ投递消息

2023-12-04 09:23:49

分布式消息

2023-11-30 18:03:02

TCP传输

2024-02-20 11:30:23

光纤

2022-07-26 20:00:35

场景RabbitMQMQ

2022-08-02 11:27:25

RabbitMQ消息路由

2023-11-27 17:29:43

Kafka全局顺序性

2023-10-17 16:30:00

TCP

2020-10-18 07:25:55

MQ消息幂等架构

2021-03-08 10:19:59

MQ消息磁盘

2013-08-04 22:14:52

DevOpsDevOps实施DevOps实践

2021-08-10 09:59:15

RabbitMQ消息微服务

2020-10-26 09:19:11

线程池消息

2017-08-21 08:51:22

CAN网络通讯

2021-10-22 08:37:13

消息不丢失rocketmq消息队列

2021-09-13 07:23:53

KafkaGo语言

2009-08-27 10:01:27

ibmdw云计算
点赞
收藏

51CTO技术栈公众号