序言
- 在文章的开始,首先引出几个问题:
你是否还在为两个(多个)系统间需要通过定时任务来同步某些数据而苦恼?
你是否在为异构系统的不同进程间相互调用、通讯的问题而苦恼、挣扎?
如果是,那么恭喜你,消息服务让你可以很轻松地解决这些问题。
消息服务擅长于解决多系统、异构系统间的数据交换(消息通知/通讯)问题,你也可以把它用于系统间服务的相互调用(RPC)。 - 本文将要介绍的RabbitMQ就是当前最主流的消息中间件之一。主要通过概念的阐述和示例的结合来简单的介绍RabbitMQ,希望能有所帮助。
基本概念
RabbitMQ简介
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。
AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。
RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持多种客户端,如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP等,支持AJAX。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。ConnectionFactory、Connection、Channel
ConnectionFactory、Connection、Channel都是RabbitMQ对外提供的API中最基本的对象。Connection是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑。ConnectionFactory为Connection的制造工厂。Channel是我们与RabbitMQ打交道的最重要的一个接口
,我们大部分的业务操作是在Channel这个接口中完成的,包括定义Queue、定义Exchange、绑定Queue与Exchange、发布消息等。Queue
Queue(队列)是RabbitMQ的内部对象,用于存储消息。RabbitMQ中的消息都只能存储在Queue中
,生产者(下图中的P)生产消息并最终投递到Queue中,消费者(下图中的C1和C2)可以从Queue中获取消息并消费。并且多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
Exchange
在上面的队列中可以看到生产者将消息投递到Queue中,实际上这在RabbitMQ中这种事情永远都不会发生。实际的情况是,生产者将消息发送到Exchange(交换器,下图中的Exchange),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。
Exchange是按照什么逻辑将消息路由到Queue的?这个将在Binding中介绍。
RabbitMQ中的Exchange有四种类型,不同的类型有着不同的路由策略,这将在Exchange Types中介绍。routing key
生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。
在Exchange Type与binding key固定的情况下,我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。Binding
RabbitMQ中通过Binding将Exchange与Queue关联起来,这样RabbitMQ就知道如何正确地将消息路由到指定的Queue了Binding key
在绑定(Binding)Exchange与Queue的同时,一般会指定一个binding key;消费者将消息发送给Exchange时,一般会指定一个routing key;当binding key与routing key相匹配时,消息将会被路由到对应的Queue中。在绑定多个Queue到同一个Exchange的时候,这些Binding允许使用相同的binding key。binding key 并不是在所有情况下都生效,它依赖于Exchange Type,比如fanout类型的Exchange就会无视binding key,而是将消息路由到所有绑定到该Exchange的Queue。Exchange Types (交换器类型)
RabbitMQ常用的Exchange Type有 Fanout、 Direct、 Topic、 Headers 这四种。在本文章中主要围绕前三种进行示例总结。
(1)direct
直连型交换机,根据消息携带的路由键将消息投递给对应队列。就是有一个队列绑定到一个直连交换机上,同时赋予一个路由键 routing key 。然后当一个消息携带着路由值为X,这个消息通过生产者发送给交换机时,交换机就会根据这个路由值X去寻找绑定值也是X的队列。
在上图中,我们可以看到两个队列Q1、Q2直接绑定到了交换器E上。 第一个队列用绑定key(firstKey)绑定,第二个队列用绑定key(secondKey)绑定。在这种交换机中,通过路由键firstKey发布到交换器的消息将被路由到队列Q1,路由键secondKey发布到交换器的消息将被路由到队列Q2,所有其他消息将被丢弃。
(2)Topic
这种类型的交换机的路由规则支持 binding key 和 routing key 的模糊匹配,会把消息路由到满足条件的Queue。 binding key 中可以存在两种特殊字符 与 #,用于做模糊匹配,其中 * 用于匹配一个单词,# 用于匹配0个或多个单词,单词以符号“.”为分隔符。
以上图中,routingKey=”one.firstKey.two”的消息会路由到Q1,routingKey=”secondKey.one”的消息会路由到Q2。简单来说, (星号) 用来表示一个单词 (必须出现的),# (井号) 用来表示任意数量(零个或多个)单词。
(3)Fanout
这种类型的交换机路由规则非常简单,它会把所有发送到该交换机的消息路由到所有与它绑定的Queue中,这时 Routing key 不起作用。
(4)Headers
这种类型的交换机不依赖于 routing key 与 binding key 的匹配规则来路由消息,而是根据发送的消息内容中的 headers 属性进行匹配。Message acknowledgment(ack 消息的确认)
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(ack)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
注意:
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑。Message durability(消息的持久化)
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失。但依然解决不了小概率丢失事件的发生(比如RabbitMQ服务器已经接收到生产者的消息,但还没来得及持久化该消息时RabbitMQ服务器就断电了),如果我们需要对这种小概率事件也要管理起来,那么我们要用到事务。Prefetch count(每次向消费者发送消息的总数)
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
使用docker安装运行RabbitMQ
- 在Linux中安装运行命令:
//安装带管理页面的RabbitMQ docker pull rabbitmq:3-management //运行rabbitMQ docker run -d -p 5672:5672 -p 15672:15672 --name myrabbitmq 3658aa401173 //-d 后台运行 -p 暴露端口 //5672 是客户端和RabbitMQ通信的端口 //15672是管理界面访问web页面的端口 //3658aa401173 镜像id //在执行前我们还要去服务器添加两个端口的安全组
- 访问rabbitmq的管理页面:IP:15672 账号密码都是guest
实战
- 在这个模块我会通过编写代码来演示provider消息推送实例,consumer消息消费实例,Direct、Topic、Fanout的使用,消息回调、手动确认等。
-这时我们需要创建项目,首先展示一下整体项目的结构:
Direct Exchange
- 创建SpringBoot项目direct-provider,关于RabbitMQ的依赖我们可以在创建项目的时候就选择好,下面创建项目对于依赖就不会一一阐述。
- 编写配置文件application.yml:
server: port: 8001 spring: #项目名字 application: name: direct-provider #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest
- 创建
DirectRabbitMqConfig.java
(对于队列和交换机持久化以及连接使用设置,在注释里有说明,后面的不同交换机的配置就不做同样说明了):package com.twy.directprovider.config; import org.springframework.amqp.core.Binding; import org.springframework.amqp.core.BindingBuilder; import org.springframework.amqp.core.DirectExchange; import org.springframework.amqp.core.Queue; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; /** * 自连型交换机:生产者 * @Author twy * @CreateTime 2020/11/02 */ @Configuration public class DirectRabbitMqConfig { /** * 测试自连型交换机:TestDirectExchange * @return */ @Bean public DirectExchange TestDirectExchange(){ return new DirectExchange("TestDirectExchange",true,false); } /** * 测试队列:TestDirectQueue * @return */ @Bean public Queue TestDirectQueue(){ // 总共4个参数 // durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效 // exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable // autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。 // return new Queue("TestDirectQueue",true,true,false); //一般我们设置一下队列的持久化就好,其余两个就是默认false return new Queue("TestDirectQueue",true); } /** * 将队列和交换机绑定, 并设置用于匹配键:TestDirectBinding * @return */ @Bean public Binding TestDirectBinding(){ return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectBinding"); } }
- 然后在controller层写一个接口进行消息推送
DirectSendMessageController.java
:/** * 简单测试:消息推送 */ @RestController public class DirectSendMessageController { /** * 使用RabbitTemplate,提供了接收/发送等等方法 */ @Autowired private RabbitTemplate rabbitTemplate; @RequestMapping("/sendDirectMessage") public String sendDirectMessage() { String messageId = String.valueOf(UUID.randomUUID()); String messageData = "send direct message !!!!"; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String,Object> map=new HashMap<>(); map.put("messageId",messageId); map.put("messageData",messageData); map.put("createTime",createTime); //将消息推送到服务器 //将消息携带绑定键值:TestDirectBinding 发送到交换机TestDirectExchange rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectBinding", map); return "ok"; } }
- 运行项目,通过postman来测试接口:
- 因为我们还没有创建消费者,所以我们可以看到消息没有被消费,在下图的RabbitMQ管理页面可以看到:
- 接下来我们开始创建消费项目direct-consumer,配置application.yml配置文件:
server: port: 8002 spring: #项目名字 application: name: direct-consumer #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest
- 然后和生产者一样,创建
DirectRabbitMqConfig.java
(消费者单纯的使用,其实可以不用添加这个配置,直接建后面的监听就好,使用注解来让监听器监听对应的队列即可。配置上了的话,其实消费者也是生产者的身份,也能推送该消息):/** * 自连型交换机:消费者 * 消费者单纯的使用,其实可以不用添加这个配置,直接建后面的监听就好,使用注解来让监听器监听对应的队列即可。 * 但是配置上了这个配置的话,其实消费者也是生成者的身份,也能推送该消息。 * @Author twy * @CreateTime 2020/11/02 */ @Configuration public class DirectRabbitMqConfig { /** * 测试自连型交换机:TestDirectExchange * @return */ @Bean public DirectExchange TestDirectExchange(){ return new DirectExchange("TestDirectExchange",true,false); } /** * 测试队列:TestDirectQueue * @return */ @Bean public Queue TestDirectQueue(){ //一般我们设置一下队列的持久化就好,其余两个就是默认false return new Queue("TestDirectQueue",true); } /** * 将队列和交换机绑定, 并设置用于匹配键:TestDirectBinding * @return */ @Bean public Binding TestDirectBinding(){ return BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectBinding"); } }
- 创建消息接收监听类,
DirectConsumerListener.java
:/** * 消费者消息接收监听类 */ @Component @RabbitListener(queues = "TestDirectQueue") //监听的队列名称,之前在配置文件中设置的 public class DirectConsumerListener { @RabbitHandler public void receive(Map message){ System.out.println("Direct Consumer 监听到的消息:" + message.toString()); } }
- 最后将项目运行起来,可以看到把之前推送的那条消息消费下来了:
Topic Exchange
- 创建项目topic-provider,编写配置文件:
server: port: 8011 spring: #项目名字 application: name: topic-provider #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest
- 创建
TopicRabbitMqConfig.java
:/** * 主题交换机配置类:生产者 */ @Configuration public class TopicRabbitMqConfig { /** * 定义绑定键 */ public final static String FirstRounting = "topic.first"; public final static String SecondRounting = "topic.second"; /** * 测试主题交换机 * @return */ @Bean public TopicExchange TestTopicExchange(){ return new TopicExchange("TestTopicExchange"); } /** * 主题测试队列一 * @return */ @Bean public Queue TestFirstQueue(){ return new Queue("TestFirstQueue"); } /** * 主题测试队列二 * @return */ @Bean public Queue TestSecondQueue(){ return new Queue("TestSecondQueue"); } /** * 测试队列一绑定主题交换机 * 将TestFirstQueue和TestTopicExchange绑定,而且绑定的键值为topic.first * 这样只要是消息携带的路由键是topic.first,才会分发到该队列 * @return */ @Bean public Binding firstQueueBindingTopicExchange(){ return BindingBuilder.bind(TestFirstQueue()).to(TestTopicExchange()).with(FirstRounting); } /** * 测试队列二绑定主题交换机 * 将TestSecondQueue和TestTopicExchange绑定,而且绑定的键值为用上通配路由键规则topic.# * 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列 * @return */ @Bean public Binding secondQueueBindingTopicExchange(){ return BindingBuilder.bind(TestSecondQueue()).to(TestTopicExchange()).with("topic.#"); } }
- 创建接口TopicSendMessageController.java用于推送消息:
/** * 主题交换机消息推送测试 */ @RestController public class TopicSendMessageController { /** * 使用RabbitTemplate,提供了接收/发送等等方法 */ @Autowired private RabbitTemplate rabbitTemplate; /** * 主题交换机测试接口 * @param routingKey 绑定键值 topic.first * @param message 要发送的消息 主题交换机测试:绑定简直topic.first * @return */ @RequestMapping("/sendTopicMessage") public String sendTopicMessage(@RequestParam("routingKey") String routingKey, @RequestParam("message") String message) { String messageId = String.valueOf(UUID.randomUUID()); String messageData = message; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String,Object> map=new HashMap<>(); map.put("messageId",messageId); map.put("messageData",messageData); map.put("createTime",createTime); //将消息推送到服务器 //将消息携带绑定键值:routingKey 发送到交换机TestTopicExchange rabbitTemplate.convertAndSend("TestTopicExchange", routingKey, map); return "ok"; } }
- 我们紧接着创建topic-consumer,编写配置文件:
server: port: 8012 spring: #项目名字 application: name: topic-consumer #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest
- 创建TopicRabbitMqConfig.java:
/** * 主题交换机配置类:消费者 */ @Configuration public class TopicRabbitMqConfig { /** * 定义绑定键 */ public final static String FirstRounting = "topic.first"; public final static String SecondRounting = "topic.second"; /** * 测试主题交换机 * @return */ @Bean public TopicExchange TestTopicExchange(){ return new TopicExchange("TestTopicExchange"); } /** * 主题测试队列一 * @return */ @Bean public Queue TestFirstQueue(){ return new Queue("TestFirstQueue"); } /** * 主题测试队列二 * @return */ @Bean public Queue TestSecondQueue(){ return new Queue("TestSecondQueue"); } /** * 测试队列一绑定主题交换机 * 将TestFirstQueue和TestTopicExchange绑定,而且绑定的键值为topic.first * 这样只要是消息携带的路由键是topic.first,才会分发到该队列 * @return */ @Bean public Binding firstQueueBindingTopicExchange(){ return BindingBuilder.bind(TestFirstQueue()).to(TestTopicExchange()).with(FirstRounting); } /** * 测试队列二绑定主题交换机 * 将TestSecondQueue和TestTopicExchange绑定,而且绑定的键值为用上通配路由键规则topic.# * 这样只要是消息携带的路由键是以topic.开头,都会分发到该队列 * @return */ @Bean public Binding secondQueueBindingTopicExchange(){ return BindingBuilder.bind(TestSecondQueue()).to(TestTopicExchange()).with("topic.#"); } }
- 创建两个监听类TopicFirstQueueListener和TopicSecondQueueListener:
/** * 只要是消息携带的路由键是topic.first,才会分发到该队列 */ @Component @RabbitListener(queues = "TestFirstQueue") public class TopicFirstQueueListener { @RabbitHandler public void process(Map message) { System.out.println("Topic First Queue Listener 消费者收到消息 : " + message.toString()); } } /** * 只要是消息携带的路由键是以topic.开头,都会分发到该队列 */ @Component @RabbitListener(queues = "TestSecondQueue") public class TopicSecondQueueListener { @RabbitHandler public void process(Map message) { System.out.println("Topic Second Queue Listener 消费者收到消息 : " + message.toString()); } }
- 运行项目进行测试:
从上图可以看出我们给的Routingkey是topic.first,因此两个队列都会收到消息:
- 运行消费者:
- 接下来我们再推送一条信息:
推送消息的RoutingKey为topic.#test,这时应该只有第二个队列接受到消息,并且被消费:
Fanout Exchange
-
创建项目fanout-privoder,写配置文件:
server: port: 8021 spring: #项目名字 application: name: fanout-provider #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest
-
创建FanoutRabbitMqConfig.java:
/** * 扇形交换机配置文件 * */ @Configuration public class FanoutRabbitMqConfig { /** * 测试扇形交换机 * @return */ @Bean public FanoutExchange TestFanoutExchange(){ return new FanoutExchange("TestFanoutExchange"); } /** * 队列A * @return */ @Bean public Queue queueA(){ return new Queue("fanout.queue.A"); } /** * 队列B * @return */ @Bean public Queue queueB(){ return new Queue("fanout.queue.B"); } /** * 队列C * @return */ @Bean public Queue queueC(){ return new Queue("fanout.queue.C"); } /** * 队列A绑定扇形交换机 */ @Bean public Binding queueABindingExchange(){ return BindingBuilder.bind(queueA()).to(TestFanoutExchange()); } /** * 队列B绑定扇形交换机 */ @Bean public Binding queueBBindingExchange(){ return BindingBuilder.bind(queueB()).to(TestFanoutExchange()); } /** * 队列C绑定扇形交换机 */ @Bean public Binding queueCBindingExchange(){ return BindingBuilder.bind(queueC()).to(TestFanoutExchange()); } }
-
写一个接口用来测试:
/** - 扇形交换机消息推送测试 */ @RestController public class FanoutSendMessageController { /** * 使用RabbitTemplate,提供了接收/发送等等方法 */ @Autowired private RabbitTemplate rabbitTemplate; /** * 主题交换机测试接口 * @return */ @RequestMapping("/sendFanoutMessage") public String sendTopicMessage() { String messageId = String.valueOf(UUID.randomUUID()); String messageData = "message: test FanoutExchange !!!"; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String,Object> map=new HashMap<>(); map.put("messageId",messageId); map.put("messageData",messageData); map.put("createTime",createTime); //将消息推送到服务器 //将消息携带绑定键值:routingKey 发送到交换机TestFaoutExchange rabbitTemplate.convertAndSend("TestFanoutExchange", null, map); return "ok"; } }
-
紧接着我们创建项目fanout-consumer,写3个监听类,分别监听queueA,B,C:
@Component @RabbitListener(queues = "fanout.queue.A") public class FanoutConsumerQueueA { @RabbitHandler public void process(Map message) { System.out.println("FanoutConsumerQueueA消费者监听队列A,收到消息 : " + message.toString()); } } @Component @RabbitListener(queues = "fanout.queue.B") public class FanoutConsumerQueueB { @RabbitHandler public void process(Map message) { System.out.println("FanoutConsumerQueueB消费者监听队列B,收到消息 : " + message.toString()); } } @Component @RabbitListener(queues = "fanout.queue.C") public class FanoutConsumerQueueC { @RabbitHandler public void process(Map message) { System.out.println("FanoutConsumerQueueC消费者监听队列C,收到消息 : " + message.toString()); } }
-
启动生产者项目调用接口推送消息:
-
可以看到每个队列都有一个消息等待消费,我们启动消费者:
可以看到只要发送到 fanoutExchange 这个扇型交换机的消息, 三个队列都绑定这个交换机,所以三个消息接收类都监听到了这条消息
生产者推送消息的消息确认
-
创建项目
ack-provider
,添加配置文件并加上消息确认的配置:server: port: 8031 spring: #项目名字 application: name: ack-provider #配置rabbitMQ rabbitmq: host: 116.62.155.206 port: 5672 username: guest password: guest #消息确认配置 #确认消息已经发送到交换机 publisher-confirm-type: correlated #确认消息已发送到队列 publisher-returns: true
-
这一步也是我们最重要的一步,创建
ProviderAckRabbitMqConfig.java
,配置相关的消息确认回调函数:/** * 生产者:消息确认配置文件 */ @Configuration public class ProviderAckRabbitMqConfig { /** * 创建一个直接交换机,但是不绑定队列 * * @return */ @Bean public DirectExchange noBindingQueueExchange() { return new DirectExchange("noBindingQueueExchange"); } @Bean public RabbitTemplate creatRabbitTemplate(ConnectionFactory connectionFactory) { RabbitTemplate rabbitTemplate = new RabbitTemplate(); rabbitTemplate.setConnectionFactory(connectionFactory); //设置开启Mandatory,才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数 rabbitTemplate.setMandatory(true); /** * 回调函数:ConfirmCallback */ rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() { @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { System.out.println("ConfirmCallback: " + "相关数据:" + correlationData); System.out.println("ConfirmCallback: " + "确认情况:" + ack); System.out.println("ConfirmCallback: " + "原因:" + cause); } }); /** * 回调函数:ReturnCallback */ rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() { @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { System.out.println("ReturnCallback: " + "消息:" + message); System.out.println("ReturnCallback: " + "回应码:" + replyCode); System.out.println("ReturnCallback: " + "回应信息:" + replyText); System.out.println("ReturnCallback: " + "交换机:" + exchange); System.out.println("ReturnCallback: " + "路由键:" + routingKey); } }); return rabbitTemplate; } }
在配置文件种,只有设置
rabbitTemplate.setMandatory(true);
才能触发回调函数,无论消息推送结果怎么样都强制调用回调函数。我们可以看到上面写了两个回调函数,一个叫 ConfirmCallback ,一个叫 RetrunCallback。 -
那么现在我们要探讨的是这两种回调函数都是在什么情况下被触发呢?
(1)消息推送到了服务器,但是在服务器里找不到交换机
(2)消息推送到了服务器,找到了交换机,但是没有找到队列
(3)消息推送到服务器,交换机和队列都没有找到,这种情况和上面找不到交换机大致上一样
(4)消息推送成功
接下来我会写几个接口分别来测试这几种情况: -
消息推送到了服务器,但是在服务器里找不到交换机:
(1)创建测试接口:/** * (1)消息推送到server,但是在server里找不到交换机 * * @return */ @PostMapping("/TestMessageAck1") public String TestMessageAck1() { String messageId = String.valueOf(UUID.randomUUID()); String messageData = "AckMessage: 消息推送到server,但是在server里找不到交换机!!!"; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String, Object> map = new HashMap<>(); map.put("messageId", messageId); map.put("messageData", messageData); map.put("createTime", createTime); /** * 把消息推送到名为‘non-existent-exchange’的交换机上(这个交换机是没有创建没有配置的) */ rabbitTemplate.convertAndSend("non-existent-exchange", "TestDirectRouting", map); return "ok"; }
(2)在这个接口种把消息推送到
non-existent-exchange
上,这个交换机我是没有创建的,接下来调用接口:
(3)从输出的结果来看,是找不到交换机的意思,从而得出结论:在找不到交换机的情况下调用ConfirmCallback 回调函数
。 -
消息推送到了服务器,找到了交换机,但是没有找到队列
(1)在这种情况下,我们首先在之前的配置文件种创建一个直接型交换机,但是补给它绑定队列:/** * 创建一个直接交换机,但是不绑定队列 * * @return */ @Bean public DirectExchange noBindingQueueExchange() { return new DirectExchange("noBindingQueueExchange"); }
(2)创建测试接口:
/** * (2)消息推送到server,找到交换机了,但是没找到队列 * * @return */ @PostMapping("/TestMessageAck2") public String TestMessageAck2() { String messageId = String.valueOf(UUID.randomUUID()); String messageData = "AckMessage: 消息推送到server,找到交换机了,但是没找到队列!!!"; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String, Object> map = new HashMap<>(); map.put("messageId", messageId); map.put("messageData", messageData); map.put("createTime", createTime); /** * 把消息推送到名为‘noBindingQueueExchange’的交换机上(这个交换机是没有绑定队列) */ rabbitTemplate.convertAndSend("noBindingQueueExchange", "TestDirectRouting", map); return "ok"; }
(3)进行接口测试:
从结果中我们可以看出,消息是推送成功到服务器了的,所以ConfirmCallback
对消息确认情况是true,
而在RetrunCallback回调函数的打印参数里面可以看到,消息是推送到了交换机成功了,但是在路由分发给队列的时候,找不到队列,所以报了错误NO_ROUTE
。
结论:这种情况触发的是 ConfirmCallback和RetrunCallback两个回调函数
。 -
消息推送到服务器,交换机和队列都没有找到,这种情况和第一种情况返回一致,这里就不举例子了。
-
消息推送成功
(1)这个测试就很简单了:/** * (3)推送消息成功 */ @PostMapping("/TestMessageAck3") public String TestMessageAck3() { String messageId = String.valueOf(UUID.randomUUID()); String messageData = "AckMessage: 推送消息成功!!!"; String createTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss")); Map<String, Object> map = new HashMap<>(); map.put("messageId", messageId); map.put("messageData", messageData); map.put("createTime", createTime); /** * 把消息推送到名为‘TestDirectExchange’的交换机上(这个交换机是绑定队列) * BindingBuilder.bind(TestDirectQueue()).to(TestDirectExchange()).with("TestDirectBinding"); */ rabbitTemplate.convertAndSend("TestDirectExchange", "TestDirectBinding", map); return "ok"; }
(2)结果:
结论:这种情况触发的是 ConfirmCallback 回调函数
。
以上是生产者推送消息的消息确认 回调函数的使用,可以在回调函数中加一些业务处理等
消费者接收到消息的消息确认机制
-
在消费者接收消息确认机制中和生产者的消息确认机制不同,因为消息接收本来就是在监听消息,符合条件的消息才会被消费下来。因此,消费者消息接收的确认机制主要存在三种模式:
(1)自动确认,这也是默认的消息确认情况。AcknowledgeMode.NONE。RabbitMQ成功将消息发出(即将消息成功写入TCP Socket)中,立即认为本次投递已经被正确处理,不管消费者端是否成功处理本次投递。所以这种情况如果消费端消费逻辑抛出异常,也就是消费端没有处理成功这条消息,那么就相当于丢失了消息。一般这种情况我们都是使用try catch捕捉异常后,打印日志用于追踪数据,这样找出对应数据再做后续处理。
(2) 手动确认 ,也是我们配置接收消息确认机制时,多数选择的模式。消费者收到消息后,手动调用basic.ack/basic.nack/basic.reject后,RabbitMQ收到这些消息后,才认为本次投递成功。
[1] basic.ack用于肯定确认
[2] basic.nack用于否定确认(注意:这是AMQP 0-9-1的RabbitMQ扩展)
[3] basic.reject用于否定确认,但与basic.nack相比有一个限制:一次只能拒绝单条消息
消费者端以上的3个方法都表示消息已经被正确投递,但是basic.ack表示消息已经被正确处理。
而basic.nack,basic.reject表示没有被正确处理,下面着重讲一下basic.reject和basic.nack。 -
channel.basicReject(deliveryTag, true)
拒绝消费当前消息,第一个参数是当前消息到的数据的唯一id,第二参数如果传入true,就是将数据重新丢回队列里,那么下次还会消费这消息。设置false,就是告诉服务器,我已经知道这条消息数据了,因为一些原因拒绝它,而且服务器也把这个消息丢掉就行, 下次不想再消费这条消息了。 -
channel.basicNack(deliveryTag, false, true)
第一个参数是当前消息到的数据的唯一id,第二个参数是指是否针对多条消息;如果是true,也就是说一次性针对当前通道的消息的tagID小于当前这条消息的,都拒绝确认,第三个参数是指是否重新入列,也就是指不确认的消息是否重新丢回到队列里面去。
注意:
使用不确认后重新入列这个确认模式要谨慎,因为这里也可能因为考虑不周出现消息一直被重新丢回去的情况,导致积压
-
创建项目ack-consumer项目,在项目中添加SimpleMessageListenerContainer.java:
/** * 消费者:消息确认配置文件 */ @Configuration public class ConsumerAckRabbitMqConfig { @Autowired private CachingConnectionFactory connectionFactory; //消息接收处理类 @Autowired private MyAckListener myAckListener; /** * 配置消息接收 --> 手动确认 */ @Bean public SimpleMessageListenerContainer simpleMessageListenerContainer(){ SimpleMessageListenerContainer container = new SimpleMessageListenerContainer(connectionFactory); container.setConcurrentConsumers(1); container.setMaxConcurrentConsumers(1); // RabbitMQ默认是自动确认,这里改为手动确认消息 container.setAcknowledgeMode(AcknowledgeMode.MANUAL); //设置一个队列 container.setQueueNames("TestDirectQueue"); //如果同时设置多个如下: 前提是队列都是必须已经创建存在的 // container.setQueueNames("TestDirectQueue","TestDirectQueue2","TestDirectQueue3"); //另一种设置队列的方法,如果使用这种情况,那么要设置多个,就使用addQueues //container.setQueues(new Queue("TestDirectQueue",true)); //container.addQueues(new Queue("TestDirectQueue2",true)); //container.addQueues(new Queue("TestDirectQueue3",true)); container.setMessageListener(myAckListener); return container; } }
container.setAcknowledgeMode(AcknowledgeMode.MANUAL);
RabbitMQ默认是自动确认,这里改为手动确认消息。container.setQueueNames("TestDirectQueue");
设置队列。container.setMessageListener(myAckListener);
设置手动确认消息监听类。
-
接下来我们写一个手动消息监听类:
MyAckListener.java
(手动确认模式需要实现 ChannelAwareMessageListener):/** * 手动监听消息类 */ @Component public class MyAckListener implements ChannelAwareMessageListener { @Override public void onMessage(Message message, Channel channel) throws Exception { long deliveryTag = message.getMessageProperties().getDeliveryTag(); try { //因为传递消息的时候用的map传递,所以将Map从Message内取出需要做些处理 String msg = message.toString(); if ("TestDirectQueue".equals(message.getMessageProperties().getConsumerQueue())) { System.out.println("消费的消息来自的队列名为:" + message.getMessageProperties().getConsumerQueue()); System.out.println("MyAckListener(手动监听消息类):" + msg); System.out.println("消费消息来自:" + message.getMessageProperties().getConsumerQueue()); /** * 处理业务 */ System.out.println("执行TestDirectQueue中的消息的业务处理流程......"); } //第二个参数,手动确认可以被批处理,当该参数为 true 时,则可以一次性确认 delivery_tag 小于等于传入值的所有消息 channel.basicAck(deliveryTag, true); //第二个参数,true会重新放回队列,所以需要自己根据业务逻辑判断什么时候使用拒绝 //channel.basicReject(deliveryTag, true); } catch (Exception e) { channel.basicReject(deliveryTag, false); e.printStackTrace(); } } }
-
接下来我们发送一个消息进行监听:
到在这里,文章也基本介绍结束啦,下面贴上项目的地址