一.概念
1.1消息的確認(Confirm):
是指生產者投遞消息後,如果 Broker 收到消息,則會給我們生產者一個應答。生 產者進行接收應答,用來確定這條消息是否正常的發送到 Broker ,這種方式也是消息的可靠性投遞的核心保障!
Confirm 確認機制流程圖:
1.2 Return 消息機制
用於處理一些不可路 由的消息!
消息生產者,通過指定一個 Exchange 和 Routingkey,把消息送達到某一個隊列中去,然後我們的消費者監聽隊列,進行消費處理操作!但是在某些情況下,如果我們在發送消息的時候,當前的 exchange 不存在或者指定的路由 key 路由不到,這個時候如果我們需要監聽這種不可達的消息,就要使用 Return !
Return 消息機制流程圖:
二.rabbitmq-produce的改動
項目使用上一篇中的項目 rabbitmq-produce、rabbitmq-consumer
2.1 修改配置文件application.yml
代碼如下:
server:
port: 8783
spring:
#配置程序名爲rabbitmq-produce-learn
application:
name: rabbitmq-produce-learn
#配置rabbitMq 服務器
rabbitmq:
host: 127.0.0.1
port: 5672
username: guest
password: guest
#虛擬host 可以不設置,使用server默認host
#virtual-host: JCcccHost
#支持發佈確認
publisher-confirms: true
#支持發佈返回
publisher-returns: true
template:
mandatory: true
listener:
direct:
prefetch: 0
#採用手動應答
acknowledge-mode: manual
simple:
prefetch: 0 #RabbitMQ 將消息順序發送給多個消費者有兩種模式(公平分發、輪詢模式),
#區別在於公平分發的prefetch默認是1,如果設置爲0就是輪詢模式。
#確認模式
#採用手動應答
#none:不確認,不會發送任何ack
#manual:手動確認,發送端和客戶端都需要手動確認
#auto:自動確認,就是自動發ack,除非拋異常。
acknowledge-mode: manual
2.2 在rabbitmq-produce中,新增一個RabbitComfirmAndReturnConfig配置類
RabbitComfirmAndReturnConfig的代碼如下:
package com.example.rabbitmqproduce.config;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* rabbitmq
* comfirm 消息確認機制 和 Return 消息機制 配置類
* 消息的確認,是指生產者投遞消息後,如果 Broker 收到消息,則會給我們生產者一個應答。生
* 產者進行接收應答,用來確定這條消息是否正常的發送到 Broker ,這種方式也是消息的可靠性投遞的核心保障!
*
*
* Return 消息機制 用於處理一些不可路 由的消息!
*消息生產者,通過指定一個 Exchange 和 Routingkey,把消息送達到某一個隊列中去,然後我們的消費者監聽隊列,進行消費處理操作!
*但是在某些情況下,如果我們在發送消息的時候,當前的 exchange 不存在或者指定的路由 key 路由不到,這個時候如果我們需要監聽這種不可達的消息,就要使用 Return !
*在基礎API中有一個關鍵的配置項:Mandatory:如果爲 true,則監聽器會接收到路由不可達的消息,然後進行後續處理,如果爲 false,那麼 broker 端自動刪除該消息!
*/
@Configuration
public class RabbitComfirmAndReturnConfig {
private Logger logger = LoggerFactory.getLogger(RabbitComfirmAndReturnConfig.class);
@Autowired
private CachingConnectionFactory connectionFactory;
public final static String ackQueueName = "ackQueue";
public static final String ackExchangeName = "ackExchange";
private static final String ackBindingKey = "ackRouting";
@Bean
public Queue ackQueue() {
return new Queue(ackQueueName);
}
@Bean
public DirectExchange ackExchange() {
return new DirectExchange(ackExchangeName);
}
@Bean
public Binding bindingExchangeAckMessage() {
return BindingBuilder.bind(ackQueue()).to(ackExchange()).with(ackBindingKey);
}
/**
* 定製化 RabbitTemplate 模版
* @return
*/
@Bean
public RabbitTemplate rabbitTemplate() {
//若使用confirm-callback或return-callback,必須在配置文件yml 配置publisherConfirms或publisherReturns爲true
connectionFactory.setPublisherConfirms(true);
connectionFactory.setPublisherReturns(true);
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
//必須設置爲 true,不然當 發送到交換器成功,但是沒有匹配的隊列,不會觸發 ReturnCallback 回調
//而且 ReturnCallback 比 ConfirmCallback 先回調,意思就是 ReturnCallback 執行完了纔會執行 ConfirmCallback
rabbitTemplate.setMandatory(true);
//設置 ConfirmCallback 回調 配置文件yml需要配置 publisher-confirms: true
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
// 如果發送到交換器都沒有成功(比如說刪除了交換器),ack 返回值爲 false
// 如果發送到交換器成功,但是沒有匹配的隊列(比如說取消了綁定),ack 返回值爲還是 true (這是一個坑,需要注意)
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
String messageId = correlationData.getId();
if (ack) {
logger.info("confirm:"+messageId);
logger.info("消息發送成功:correlationData({}),ack({}),cause({})", correlationData, ack, cause);
}else {
logger.info("confirm:"+messageId);
logger.info("消息發送失敗:correlationData({}),ack({}),cause({})", correlationData, ack, cause);
}
}
});
//設置 ReturnCallback 回調 配置文件yml需要配置 publisher-returns: true
//如果發送到交換器成功,但是沒有匹配的隊列,就會觸發這個回調
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
logger.info("message:"+message);
logger.warn("消息丟失:exchange({}),route({}),replyCode({}),replyText({}),message:{}", exchange, routingKey, replyCode, replyText, message);
}
});
return rabbitTemplate;
}
}
2.3 新建消息生產者DirectExchangeProduce
DirectExchangeProduce的代碼如下:
package com.example.rabbitmqproduce.produce;
import com.alibaba.fastjson.JSON;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
import java.util.UUID;
/**
* 發送信息確認ack
* @Component 注入到Spring容器中
*/
@Component
public class RabbitAckProduce {
//注入一個 RabbitTemplate 來發布消息
@Autowired
private RabbitTemplate rabbitTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitAckProduce.class);
private static final String ackRouteKey = "ackRouting";
private static final String ackExchangeName = "ackExchange";
/**
* 發送消息
*/
public void sendMessage() {
String messageId = String.valueOf(UUID.randomUUID());
String messageData = "hello!亞索 面對疾風吧";
String createTime = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format(new Date());
Map<String,Object> map=new HashMap<>();
map.put("messageId",messageId);
map.put("messageData",messageData);
map.put("createTime",createTime);
//CorrelationData用於confirm機制裏的回調確認
CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
//將消息攜帶綁定鍵值:directRouting 發送到交換機directExchange
rabbitTemplate.convertAndSend(ackExchangeName,ackRouteKey, map,correlationData);
logger.info("mq消息發送結束==》{}", map.toString());
}
}
2.3 寫個測試的TestController類
代碼如下:
package com.example.rabbitmqproduce.controller;
import com.example.rabbitmqproduce.produce.*;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
@RestController
@RequestMapping("TestController")
public class TestController {
private static final Logger logger = LoggerFactory.getLogger(TestController.class);
@Autowired
private RabbitMqProduce rabbitMqProduce;
@Autowired
private DirectExchangeProduce directExchangeProduce;
@Autowired
private FanoutExchangeProduce fanoutExchangeProduce;
@Autowired
private TopicExchangeProduce topicExchangeProduce;
@Autowired
private RabbitAckProduce rabbitAckProduce;
/**
* 測試基本消息模型(簡單隊列)
*/
@RequestMapping(value = "/testSimpleQueue", method = RequestMethod.POST)
public void testSimpleQueue() {
logger.info("測試基本消息模型(簡單隊列)SimpleQueue---開始");
for (int i = 0; i < 10; i++) {
rabbitMqProduce.sendMessage();
}
logger.info("測試基本消息模型(簡單隊列)SimpleQueue---結束");
}
/**
* 測試 Direct-exchange模式
*/
@RequestMapping(value = "/directExchangeTest", method = RequestMethod.POST)
public void directExchangeTest() {
logger.info("測試 Direct-exchange模式 隊列名爲directQueue---開始");
for (int i = 0; i < 10; i++) {
directExchangeProduce.sendMessage();
}
logger.info("測試 Direct-exchange模式 隊列名爲directQueue---結束");
}
/**
* 測試 Fanout-exchange模式
*/
@RequestMapping(value = "/fanoutExchangeTest", method = RequestMethod.POST)
public void fanoutExchangeTest() {
logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---開始");
fanoutExchangeProduce.sendMessage();
logger.info("測試 fanout-exchange模式 隊列名爲fanoutQueue---結束");
}
/**
* 測試 Topic-exchange模式 topicA 和 topicB
*/
@RequestMapping(value = "/topictExchangeTest", method = RequestMethod.POST)
public void topictExchangeTest() {
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---開始");
topicExchangeProduce.sendMessageTopicA();
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameA---結束");
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---開始");
topicExchangeProduce.sendMessageTopicB();
logger.info("測試 topict-exchange模式 隊列名爲topictQueueNameB---結束");
}
/**
* 測試 ack模式
*/
@RequestMapping(value = "/ackTest", method = RequestMethod.POST)
public void ackTest() {
logger.info("測試 ack 隊列名爲ackQueue---開始");
rabbitAckProduce.sendMessage();
logger.info("測試 ack 隊列名爲ackQueue---結束");
}
}
三.rabbitmq-consumer的改動
3.1 新建消息消費者RabbitAckConsumer類
RabbitAckConsumer代碼如下:
package com.example.rabbitmqconsumer.consumer;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.io.IOException;
import java.util.Map;
/**
* 收到信息確認ack
* @RabbitListener(queues = "ackQueue") 監聽名爲ackQueue的隊列
*/
@Component
@RabbitListener(queues = "ackQueue")
public class RabbitAckConsumer {
@Autowired
private RabbitTemplate rabbitTemplate;
private Logger logger = LoggerFactory.getLogger(RabbitAckConsumer.class);
/**
* 消費消息
* @RabbitHandler 代表此方法爲接受到消息後的處理方法
*/
@RabbitHandler
public void receiveMessage(Map msg , Message message , Channel channel) throws IOException {
// 採用手動應答模式, 手動確認應答更爲安全穩定
channel.basicAck(message.getMessageProperties().getDeliveryTag(), true);
logger.info("接收到的消息:---->" + msg.toString());
}
}
四.測試
首先啓動生產者rabbitmq-produce項目。在postman或瀏覽器上訪問:
http://localhost:8783/TestController/ackTest POST請求
這時可以在rabbitmq-produce的控制檯可以看到
然後再啓動消費者rabbitmq-consumer工程,在rabbitmq-consumer可以看到: