消息發送確認
生產者發送消息,是先發送消息到Exchange,然後Exchange再路由到Queue。這中間就需要確認兩個事情,第一,消息是否成功發送到Exchange;第二,消息是否正確的通過Exchange路由到Queue。
spring提供了兩個回調函數來處理這兩種消息發送確認。
ConfirmCallback和ReturnCallback
-
實現ConfirmCallback並重寫confirm(CorrelationData correlationData, boolean ack, String cause)回調方法,可以確認消息是否發送到Exchange。
-
實現ReturnCallback並重寫returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey)回調方法,可以確認消息從EXchange路由到Queue失敗。注意:這裏的回調是一個失敗回調,只有消息從Exchange路由到Queue失敗纔會回調這個方法。
-
注意,若需要以上兩個回調函數生效,需要添加配置
配置文件:spring: rabbitmq: publisher-confirms: true publisher-returns: true
配置類:
/** * 配置 * * @return */ @Bean public ConnectionFactory connectionFactory() { CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672); connectionFactory.setUsername("admin"); connectionFactory.setPassword("123456"); connectionFactory.setPublisherConfirms(true); connectionFactory.setPublisherReturns(true); return connectionFactory; }
回調配置類
直接上代碼,如下,直接實現ConfirmCallback和ReturnCallback
package com.xquant.rabbitmq.send.mq;
import javax.annotation.PostConstruct;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author chunhui.tan
* @version 創建時間:2018年11月19日 下午2:32:37
*
*/
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback, ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);// 指定 ConfirmCallback
rabbitTemplate.setReturnCallback(this);// 指定 ReturnCallback
}
/**
* 確認消息是否發送到Exchange
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息發送成功:" + correlationData);
} else {
System.out.println("消息發送失敗:" + cause);
}
}
/**
* 消息發送到Queue失敗後的回調
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(message.getMessageProperties().getCorrelationIdString() + " 發送失敗");
System.out.println("消息主體 message : " + message);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交換器 exchange : " + exchange);
System.out.println("消息使用的路由鍵 routing : " + routingKey);
}
}
測試1,消息發送到Exchange的回調
配置類,還是和之前一樣,兩個Queue,分別通過不同的bindingKey綁定同一個Exchange。
package com.xquant.rabbitmq.send.mq;
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.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @author chunhui.tan
* @version 創建時間:2018年11月14日 下午1:29:21
*
*/
@Configuration
public class DirectMqConfig {
/**
* 交換機名稱
*/
public static final String DIRECT_EXCHANGE_NAME = "direct_exchange";
/**
* 綁定key,交換機綁定隊列時需要指定
*/
public static final String BINGDING_KEY_TEST1 = "direct_key1";
public static final String BINGDING_KEY_TEST2 = "direct_key2";
/**
* 隊列名稱
*/
public static final String QUEUE_TEST1 = "test1";
public static final String QUEUE_TEST2 = "test2";
/**
* 構建DirectExchange交換機
*
* @return
*/
@Bean
public DirectExchange directExchange() {
// 支持持久化,長期不用補刪除
return new DirectExchange(DIRECT_EXCHANGE_NAME, true, false);
}
/**
* 構建序列
*
* @return
*/
@Bean
public Queue test1Queue() {
// 支持持久化
return new Queue(QUEUE_TEST1, true);
}
@Bean
public Queue test2Queue() {
// 支持持久化
return new Queue(QUEUE_TEST2, true);
}
/**
* 綁定交交換機和
*
* @return
*/
@Bean
public Binding test1Binding() {
return BindingBuilder.bind(test1Queue()).to(directExchange()).with(BINGDING_KEY_TEST1);
}
@Bean
public Binding test2Binding() {
return BindingBuilder.bind(test2Queue()).to(directExchange()).with(BINGDING_KEY_TEST2);
}
/**
* 配置
*
* @return
*/
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("123456");
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
return connectionFactory;
}
/**
* 實例化操作模板
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
return new RabbitTemplate(connectionFactory);
}
}
生產者,方便起見,還是用接口
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 給test1隊列發消息
*
* @return
*/
@GetMapping("/sendMessage1")
public Object sendMessage1() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentEncoding("UTF-8");
Message message = new Message("你好,這是發給test1隊列的消息".getBytes(), messageProperties);
rabbitTemplate.send("testhuidiao", DirectMqConfig.BINGDING_KEY_TEST1, message);
return "ok";
}
/**
* 給test2隊列發消息
*
* @return
*/
@GetMapping("/sendMessage2")
public Object sendMessage2() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentEncoding("UTF-8");
Message message = new Message("你好,這是發給test2隊列的消息".getBytes(), messageProperties);
rabbitTemplate.send(DirectMqConfig.DIRECT_EXCHANGE_NAME, DirectMqConfig.BINGDING_KEY_TEST2, message);
return "ok";
}
消費者代碼就不貼出了,就是分別監聽兩個消息隊列的監聽函數。
接下來測試,首先啓動項目,觀察rabbitweb頁面如下:
沒有問題,然後我們調用sendMessage1接口,控制檯打印如下:
2018-11-19 15:41:42.286 INFO 9292 --- [nio-8080-exec-2] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-19 15:41:42.286 INFO 9292 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-19 15:41:42.301 INFO 9292 --- [nio-8080-exec-2] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 15 ms
消息發送失敗:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'testhuidiao' in vhost '/', class-id=60, method-id=40)
2018-11-19 15:41:42.440 ERROR 9292 --- [110.34.160:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'testhuidiao' in vhost '/', class-id=60, method-id=40)
可以看到觸發了回調方法confirm(CorrelationData correlationData, boolean ack, String cause),而且發送消息到Exchange失敗,ask==false。
調用sendMessage2接口,控制檯打印如下:
2018-11-19 15:45:16.158 INFO 10804 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-19 15:45:16.159 INFO 10804 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-19 15:45:16.173 INFO 10804 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 14 ms
消息發送成功:null
這是監聽test2得到的消息:======你好,這是發給test2隊列的消息
可以看到觸發了回調方法confirm(CorrelationData correlationData, boolean ack, String cause),而且發送消息到Exchange成功,ask==true。
我們在rabbit的Web頁面刪掉Exchange,如下圖:
然後調用sendMessage2方法,控制檯打印如下,觸發失敗回調
消息發送失敗:channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'direct_exchange' in vhost '/', class-id=60, method-id=40)
2018-11-19 15:49:06.089 ERROR 10804 --- [110.34.160:5672] o.s.a.r.c.CachingConnectionFactory : Channel shutdown: channel error; protocol method: #method<channel.close>(reply-code=404, reply-text=NOT_FOUND - no exchange 'direct_exchange' in vhost '/', class-id=60, method-id=40)
測試2,消息從Exchange路由到Queue失敗的回調
配置類和消費者代碼都不變,然後我們修改生產者的代碼如下,其實只是修改了sendMessage1接口中routingKey的名稱爲testadmin,而實際上是沒有一個queue通過這個routingKey綁定到我們定義的Exchange上的
@RestController
public class ProducerController {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 給test1隊列發消息
*
* @return
*/
@GetMapping("/sendMessage1")
public Object sendMessage1() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentEncoding("UTF-8");
Message message = new Message("你好,這是發給test1隊列的消息".getBytes(), messageProperties);
rabbitTemplate.send(DirectMqConfig.DIRECT_EXCHANGE_NAME, "testadmin", message);
return "ok";
}
/**
* 給test2隊列發消息
*
* @return
*/
@GetMapping("/sendMessage2")
public Object sendMessage2() {
MessageProperties messageProperties = new MessageProperties();
messageProperties.setContentEncoding("UTF-8");
Message message = new Message("你好,這是發給test2隊列的消息".getBytes(), messageProperties);
rabbitTemplate.send(DirectMqConfig.DIRECT_EXCHANGE_NAME, DirectMqConfig.BINGDING_KEY_TEST2, message);
return "ok";
}
啓動工程測試,調用sendMessage1接口,
2018-11-19 16:33:52.061 INFO 12300 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-19 16:33:52.062 INFO 12300 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-19 16:33:52.075 INFO 12300 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 13 ms
消息發送成功:null
很奇怪,明明沒有路由到queue,但是從Exchange到Queue的失敗回調還是沒有觸發。
通過百度找到原因:沒有設置mandatory=true
關於mandatory的作用:
當mandatory標誌位設置爲true時,如果exchange根據自身類型和消息routingKey無法找到一個合適的queue存儲消息,那麼broker會調用basic.return方法將消息返還給生產者;當mandatory設置爲false時,出現上述情況broker會直接將消息丟棄;通俗的講,mandatory標誌告訴broker代理服務器至少將消息route到一個隊列中,否則就將消息return給發送者
總之,如果要觸發returnedMessage回調函數,我們必須設置mandatory=true,否則Exchange沒有找到Queue就會丟棄掉消息,而不會觸發回調。
具體設置方法:
spring:
rabbitmq:
template:
mandatory: true
或者:
/**
* 實例化操作模板
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
改爲以上配置後我們測試sendMessage1接口,控制檯打印如下:
2018-11-19 16:46:45.832 INFO 9376 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/] : Initializing Spring FrameworkServlet 'dispatcherServlet'
2018-11-19 16:46:45.832 INFO 9376 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization started
2018-11-19 16:46:45.846 INFO 9376 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet : FrameworkServlet 'dispatcherServlet': initialization completed in 14 ms
null 發送失敗
消息主體 message : (Body:'[B@38005986(byte[39])' MessageProperties [headers={}, contentType=application/octet-stream, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, deliveryTag=0])
描述:NO_ROUTE
消息使用的交換器 exchange : direct_exchange
消息使用的路由鍵 routing : adadasaa
可以看到描述信息是NO_ROUTE,沒有路由。
總結
關於RabbitMQ消息發送確認,主要是從兩個回調函數來確認。
首先配置如下信息:
yml文件:
spring:
rabbitmq:
addresses: 127.0.0.1
port: 5672
username: admin
password: 123456
publisher-confirms: true #開啓confirms回調函數
publisher-returns: true #開啓returnedMessage回調函數
template:
mandatory: true #必須爲true,否則無法觸發returnedMessage回調,消息丟失
配置類:
/**
* 配置
*
* @return
*/
@Bean
public ConnectionFactory connectionFactory() {
CachingConnectionFactory connectionFactory = new CachingConnectionFactory("127.0.0.1", 5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("123456");
//開啓confirms回調函數
connectionFactory.setPublisherConfirms(true);
//開啓returnedMessage回調函數
connectionFactory.setPublisherReturns(true);
return connectionFactory;
}
/**
* 實例化操作模板
*
* @param connectionFactory
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//必須爲true,否則無法觸發returnedMessage回調,消息丟失
rabbitTemplate.setMandatory(true);
return rabbitTemplate;
}
然後編寫一個類,實現ConfirmCallback, ReturnCallback這兩個接口,在confirm(CorrelationData correlationData, boolean ack, String cause)和returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) 寫對應的邏輯
具體如下:
package com.xquant.rabbitmq.send.mq;
import javax.annotation.PostConstruct;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate.ReturnCallback;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
/**
* @author chunhui.tan
* @version 創建時間:2018年11月19日 下午2:32:37
*
*/
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback, ReturnCallback {
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);// 指定 ConfirmCallback
rabbitTemplate.setReturnCallback(this);// 指定 ReturnCallback
}
/**
* 確認消息是否發送到Exchange
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息發送成功:" + correlationData);
} else {
System.out.println("消息發送失敗:" + cause);
}
}
/**
* 消息發送到Queue失敗後的回調
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
System.out.println(message.getMessageProperties().getCorrelationIdString() + " 發送失敗");
System.out.println("消息主體 message : " + message);
System.out.println("描述:" + replyText);
System.out.println("消息使用的交換器 exchange : " + exchange);
System.out.println("消息使用的路由鍵 routing : " + routingKey);
}
}