一、RabbitMQ的重要概念
RabbitMQ是一種基於amq協議的消息隊列,本文主要記錄一下使用 spring-boot-starter-amqp 操作 rabbitmq。
a) 虛擬主機(vhost)
虛擬主機:一個虛擬主機持有一組交換機、隊列和綁定。虛擬主機的作用在於進行權限管控,rabbitmq默認有一個虛擬主機"/"。可以使用rabbitmqctl add_vhost
命令添加虛擬主機,然後使用rabbitmqctl set_permissions
命令設置指定用戶在指定虛擬主機下的權限,以此達到權限管控的目的。
b) 消息通道(channel)
消息通道: 在客戶端的每個連接裏,可建立多個channel,每個channel代表一個會話任務。
c) 交換機(exchange)
交換機: exchange的功能是用於消息分發,它負責接收消息並轉發到與之綁定的隊列,exchange不存儲消息,如果一個exchange沒有binding任何Queue,那麼當它會丟棄生產者發送過來的消息,在啓用ACK機制後,如果exchange找不到隊列,則會返回錯誤。一個exchange可以和多個Queue進行綁定。
交換機有四種類型:
- 路由模式(Direct):默認
direct 是 rabbitmq 的默認交換機類型。根據 routingKey 完全匹配
direct 類型的行爲是"先匹配, 再投送"。即在綁定時設定一個 routing_key, 消息的routing_key 匹配時, 纔會被交換器投送到綁定的隊列中去。
- 主題模式\通配符模式(Topic):
根據綁定關鍵字通配符規則匹配、比較靈活
類似路由模式,但是 routing_key 支持模糊匹配,按規則轉發消息(最靈活)。符號“#”匹配一個或多個詞,符號“*”匹配不多不少一個詞。
- 發佈訂閱模式(Fanout):
不需要指定 routingkey,相當於羣發
轉發消息到所有綁定隊列,忽略 routing_key。
- Headers:
不太常用,可以自定義匹配規則
設置header attribute參數類型的交換機。相較於 direct 和 topic 固定地使用 routing_key , headers 則是一個自定義匹配規則的類型,忽略routing_key。在隊列與交換器綁定時, 會設定一組鍵值對規則, 消息中也包括一組鍵值對( headers 屬性), 當這些鍵值對有一對, 或全部匹配時, 消息被投送到對應隊列。
在綁定Queue與Exchange時指定一組鍵值對,當消息發送到RabbitMQ時會取到該消息的headers與Exchange綁定時指定的鍵值對進行匹配。如果完全匹配則消息會路由到該隊列,否則不會路由到該隊列。headers屬性是一個鍵值對,可以是Hashtable,鍵值對的值可以是任何類型。
二、依賴與配置
1、添加Maven依賴
spring-boot-starter-amqp
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
2、添加相關配置
在 application.yml 中添加 rabbitmq 連接信息
spring:
application:
name: homepage-rabbitMQ
rabbitmq:
host: localhost
port: 5672
username: springcloud
password: 123456
virtual-host: /spring_cloud
server:
port: 8400
eureka:
client:
service-url:
#將自己註冊進下面這個地址的服務註冊中心
defaultZone: http://admin:admin@localhost:8000/eureka/
三、spring操作rabbitMQ的對象(重要)
spring-boot-starter-amqp依賴爲我們提供了兩個jar
- spring-amqp.jar
- spring-rabbit.jar
操作對象 | 類型 | 描述 |
---|---|---|
AmqpTemplate | interface |
所屬jar包:spring-amqp.jar --》org.springframework.amqp.core |
AmqpAdmin | interface | 所屬jar包:spring-amqp.jar --》org.springframework.amqp.core |
RabbitTemplate | class |
實現了 AmqpTemplate 接口 所屬jar包:spring-rabbit.jar --》org.springframework.amqp.rabbit.core |
RabbitAdmin | class |
實現了 AmqpAdmin 接口 所屬jar包:spring-rabbit.jar --》org.springframework.amqp.rabbit.core |
問:rabbitTemplate 和 amqpTemplate 有什麼關係?
答:源碼中會發現 rabbitTemplate 實現自 amqpTemplate 接口,使用起來並無區別,需引入spring-boot-starter-amqp依賴
下面文字來自官方文檔:
與Spring框架和相關項目提供的許多其他高級抽象一樣,Spring AMQP提供了一個“template”,它扮演着核心角色。定義主要操作的接口稱爲 AmqpTemplate。這些操作涵蓋了發送和接收消息的一般行爲。換句話說,它們對於任何實現都不是惟一的,因此名稱中有“AMQP”。另一方面,該接口的一些實現與AMQP協議的實現綁定在一起。與JMS本身是接口級API不同,AMQP是一個線級協議。該協議的實現提供了自己的客戶機庫,因此模板接口的每個實現都依賴於特定的客戶機庫。目前,只有一個實現:RabbitTemplate。在接下來的示例中,您將經常看到“AmqpTemplate”的用法,但是當您查看配置示例,或者調用模板實例化和/或setter的任何代碼摘錄時,您將看到實現類型(例如,“RabbitTemplate”)。
1、RabbitAdmin
該類封裝了對 RabbitMQ 的管理操作
主要用於在Java代碼中對理隊和隊列進行管理,用於創建、綁定、刪除隊列與交換機,發送消息等
@Bean
public RabbitAdmin rabbitAdmin(ConnectionFactory connectionFactory){
return new RabbitAdmin(connectionFactory);
}
@Autowired
private RabbitAdmin rabbitAdmin;
Exchange 操作
//創建四種類型的 Exchange,均爲持久化,不自動刪除
rabbitAdmin.declareExchange(new DirectExchange("direct.exchange",true,false));
rabbitAdmin.declareExchange(new TopicExchange("topic.exchange",true,false));
rabbitAdmin.declareExchange(new FanoutExchange("fanout.exchange",true,false));
rabbitAdmin.declareExchange(new HeadersExchange("header.exchange",true,false));
//刪除 Exchange
rabbitAdmin.deleteExchange("header.exchange");
Queue 操作
//定義隊列,均爲持久化
rabbitAdmin.declareQueue(new Queue("debug",true));
rabbitAdmin.declareQueue(new Queue("info",true));
rabbitAdmin.declareQueue(new Queue("error",true));
//刪除隊列
rabbitAdmin.deleteQueue("debug");
//將隊列中的消息全消費掉
rabbitAdmin.purgeQueue("info",false);
Binding 綁定
//綁定隊列到交換器,通過路由鍵
rabbitAdmin.declareBinding(new Binding("debug",Binding.DestinationType.QUEUE,
"direct.exchange","key.1",new HashMap()));
rabbitAdmin.declareBinding(new Binding("info",Binding.DestinationType.QUEUE,
"direct.exchange","key.2",new HashMap()));
rabbitAdmin.declareBinding(new Binding("error",Binding.DestinationType.QUEUE,
"direct.exchange","key.3",new HashMap()));
//進行解綁
rabbitAdmin.removeBinding(BindingBuilder.bind(new Queue("info")).
to(new TopicExchange("direct.exchange")).with("key.2"));
//使用BindingBuilder進行綁定
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("info")).
to(new TopicExchange("topic.exchange")).with("key.#"));
//聲明topic類型的exchange
rabbitAdmin.declareExchange(new TopicExchange("exchange1",true,false));
rabbitAdmin.declareExchange(new TopicExchange("exchange2",true,false));
//exchange與exchange綁定
rabbitAdmin.declareBinding(new Binding("exchange1",Binding.DestinationType.EXCHANGE,
"exchange2","key.4",new HashMap()));
個人案例:
package com.mq;
import homepage.ApplicationMQ;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.core.RabbitAdmin;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* RabbitAdmin用於創建、綁定、刪除隊列與交換機,發送消息等
*/
@RunWith(SpringJUnit4ClassRunner.class)
@SpringBootTest(classes = ApplicationMQ.class)
public class RabbitAdminTest {
@Autowired
private RabbitAdmin rabbitAdmin;
/**
* 創建綁定Direct路由模式
* routingKey 完全匹配
* Binding(String destination, Binding.DestinationType destinationType, String exchange, String routingKey, Map<String, Object> arguments)
* Binding(目的地, 目的地類型, exchange, routingKey, 參數)
*/
@Test
public void testDirect() {
//切記命名不能重複復
final String QUEUE_NAME="test.direct.queue";
final String EXCHANGE_NAME="test.direct";
//創建隊列
Queue directQueue=new Queue(QUEUE_NAME);
rabbitAdmin.declareQueue(directQueue);
//創建Direct交換機
DirectExchange directExchange=new DirectExchange(EXCHANGE_NAME);
rabbitAdmin.declareExchange(directExchange);
//綁定交換機和隊列(注意:綁定的時候,一定要確認綁定的雙方都是存在的,否則會報IO異常,NOT_FOUND)
Binding directBinding=new Binding(QUEUE_NAME, Binding.DestinationType.QUEUE, EXCHANGE_NAME, "mq.direct", null);
rabbitAdmin.declareBinding(directBinding);
}
/**
* 創建綁定Topic主題模式\通配符模式
* routingKey 模糊匹配
* BindingBuilder.bind(queue).to(exchange).with(routingKey)
*/
@Test
public void testTopic() {
rabbitAdmin.declareQueue(new Queue("test.topic.queue", true, false, false));
rabbitAdmin.declareExchange(new TopicExchange("test.topic", true, false));
//如果註釋掉上面兩句實現聲明,直接進行下面的綁定竟然不行,該版本amqp-client採用的是5.1.2,將上面兩行代碼放開,則運行成功
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("test.topic.queue", true, false, false)).to(new TopicExchange("test.topic", true, false)).with("mq.topic"));
}
/**
* 創建綁定Fanout發佈訂閱模式
* BindingBuilder.bind(queue).to(FanoutExchange)
*/
@Test
public void testFanout() {
rabbitAdmin.declareQueue(new Queue("test.fanout.queue", true, false, false, null));
rabbitAdmin.declareExchange(new FanoutExchange("test.fanout", true, false, null));
rabbitAdmin.declareBinding(BindingBuilder.bind(new Queue("test.fanout.queue", true, false, false)).to(new FanoutExchange("test.fanout", true, false)));
rabbitAdmin.purgeQueue("test.direct.queue", false);//清空隊列消息
}
}
2、RabbitTemplate
Spring AMQP 提供了 RabbitTemplate 來簡化 RabbitMQ 發送和接收消息操作
RabbitTemplate 初始化
設置 RabbitTemplate 的默認交換器、默認路由鍵、默認隊列
send | 數據以Message類型傳入,自定義消息 Message |
---|---|
convertAndSend | 數據以Object類型傳入,自動將 Java 對象序列化包裝成 Message 對象,Java 對象需要實現 Serializable 序列化接口 |
receive | 數據以Message類型返回,返回 Message 對象 |
receiveAndConvert | 數據以Object類型返回,會自動將返回的 Message 反序列化轉換成 Java 對象 |
發送消息
(1)send (自定義消息 Message)
Message message = new Message("hello".getBytes(),new MessageProperties());
// 發送消息到默認的交換器,默認的路由鍵
rabbitTemplate.send(message);
// 發送消息到指定的交換器,指定的路由鍵
rabbitTemplate.send("direct.exchange","key.1",message);
// 發送消息到指定的交換器,指定的路由鍵
rabbitTemplate.send("direct.exchange","key.1",message,new CorrelationData(UUID.randomUUID().toString()));
(2)convertAndSend(自動 Java 對象包裝成 Message 對象,Java 對象需要實現 Serializable 序列化接口)
User user = new User("linyuan");
// 發送消息到默認的交換器,默認的路由鍵
rabbitTemplate.convertAndSend(user);
// 發送消息到指定的交換器,指定的路由鍵,設置消息 ID
rabbitTemplate.convertAndSend("direct.exchange","key.1",user,new CorrelationData(UUID.randomUUID().toString()));
// 發送消息到指定的交換器,指定的路由鍵,在消息轉換完成後,通過 MessagePostProcessor 來添加屬性
rabbitTemplate.convertAndSend("direct.exchange","key.1",user,mes -> {
mes.getMessageProperties().setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);
return mes;
});
接收消息
(1)receive(返回 Message 對象)
// 接收來自指定隊列的消息,並設置超時時間
Message msg = rabbitTemplate.receive("debug",2000l);
(2)receiveAndConvert(將返回 Message 轉換成 Java 對象)
User user = (User) rabbitTemplate.receiveAndConvert();
四、消息生產者
1、創建消息隊列、交換機
我們在發送消息之前需要做一些準備,比如我們需要保證發送到的消息隊列、交換機是存在的,不然我們發送給誰?
如果不存在我們就需要創建這些,爲了保證肯定存在,我們也可以每次執行前都進行創建(已經存在的重複創建似乎沒有影響具體我也沒有詳細研究過)
創建發送的對象有兩種方式
(1)@Configuration和@Bean配置隊列conf,指定
rabbitConfig.properties:
learn.direct.queue=learn.direct.queue
learn.topic.queue=learn.topic.queue
learn.fanout.queue=learn.fanout.queue
learn.direct.exchange=learn.direct.exchange
learn.topic.exchange=learn.topic.exchange
learn.fanout.exchange=learn.fanout.exchange
RabbitConfig :
package com.marvin.demo.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.PropertySource;
@Configuration
@PropertySource("classpath:rabbitConfig.properties")
public class RabbitConfig {
@Value("${learn.direct.queue}")
private String directQueue;
@Value("${learn.topic.queue}")
private String topicQueue;
@Value("${learn.fanout.queue}")
private String fanoutQueue;
@Value("${learn.direct.exchange}")
private String directExchange;
@Value("${learn.topic.exchange}")
private String topicExchange;
@Value("${learn.fanout.exchange}")
private String fanoutExchange;
//創建隊列
@Bean("vipDirectQueue")
public Queue getDirectQueue(){
return new Queue(directQueue);
}
@Bean("vipTopicQueue")
public Queue getTopicQueue(){
return new Queue(topicQueue);
}
@Bean("vipFanoutQueue")
public Queue getFanoutQueue(){
return new Queue(fanoutQueue);
}
//創建交換機
@Bean("vipDirectExchange")
public DirectExchange getDirectExchange(){
return new DirectExchange(directExchange);
}
@Bean("vipTopicExchange")
public TopicExchange getTopicExchange(){
return new TopicExchange(topicExchange);
}
@Bean("vipFanoutExchange")
public FanoutExchange getFanoutExchange(){
return new FanoutExchange(fanoutExchange);
}
//綁定
@Bean
public Binding bindingDirectQueue(@Qualifier("vipDirectQueue") Queue queue, @Qualifier("vipDirectExchange")DirectExchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("test");
}
}
springboot啓動類:
package com.marvin.demo;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ApplicationConsumer {
public static void main(String[] args) {
SpringApplication.run(ApplicationConsumer.class);
}
}
(2)RabbitMQ管理界面手動添加
2、生產消息併發送
AmqpTemplate : spring 封裝的MQ的模版,直接調用即可
- Send():一般傳遞數據是Message類型
- convertAndSend():一般傳遞數據是Object類型,會自動序列化後傳遞
MessageConvert
- 涉及網絡傳輸的應用序列化不可避免,發送端以某種規則將消息轉成 byte 數組進行發送,接收端則以約定的規則進行 byte[] 數組的解析
- RabbitMQ 的序列化是指 Message 的 body 屬性,即我們真正需要傳輸的內容,RabbitMQ 抽象出一個 MessageConvert 接口處理消息的序列化,其實現有 SimpleMessageConverter(默認)、Jackson2JsonMessageConverter 等
- 當調用了 convertAndSend 方法時會使用 MessageConvert 進行消息的序列化
- SimpleMessageConverter 對於要發送的消息體 body 爲 byte[] 時不進行處理,如果是 String 則轉成字節數組,如果是 Java 對象,則使用 jdk 序列化將消息轉成字節數組,轉出來的結果較大,含class類名,類相應方法等信息。因此性能較差
- 當使用 RabbitMQ 作爲中間件時,數據量比較大,此時就要考慮使用類似 Jackson2JsonMessageConverter 等序列化形式以此提高性能
pojo對象
package com.marvin.demo.entity;
import java.io.Serializable;
public class UserBean implements Serializable {
private Integer id;
private String username;
private String pwd;
public UserBean(Integer id, String username, String pwd) {
this.id = id;
this.username = username;
this.pwd = pwd;
}
//此處省略get、set方法。。。
@Override
public String toString() {
return "UserBean{" +
"id=" + id +
", username='" + username + '\'' +
", pwd='" + pwd + '\'' +
'}';
}
}
convertAndSend類
public class HelloSender {
// spring boot 爲我們提供的包裝類,此處個人就不寫SET方法
@Autowired
private AmqpTemplate amqpTemplate;
/**
* 直接使用convertAndSend,會序列化對象,在接值的一方參數類型一定要一致
*/
public void testDirect3() {
UserBean userBean=new UserBean(3,"cc","cc");
// 調用 發送消息的方法
amqpTemplate.convertAndSend("learn_annotation_DirectExchange","directQueue3",userBean);
}
}
監聽接收類
/**
* 使用convertAndSend傳參時會自動序列化
* 監聽接值的方法參數一定要一致纔會自動轉換
* @param userBean
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "learn_annotation_DirectQueue3"),
exchange = @Exchange(value = "learn_annotation_DirectExchange",type = "direct"),
key = "directQueue3"
))
public void processDirect3(UserBean userBean){
log.info("enter ConsumerDirect-->processDirect3()~~~~~~~~~~~~~~~~~~~");
//接收參數自動轉換反序列化
System.out.println("ConsumerDirect queue3 msg:"+userBean);
System.out.println("ConsumerDirect Object3 UserBean:"+userBean.toString());
}
結果:
其他例子
@Autowired
private AmqpTemplate amqpTemplate; 注入模板
/**
* 封裝發送到消息隊列的方法
*
* @param id
* @param type 發送消息的類型
*/
private void sendMessage(Long id, String type) {
log.info("發送消息到mq");
try {
amqpTemplate.convertAndSend("item." + type, id);
} catch (Exception e) {
log.error("{}商品消息發送異常,商品ID:{}", type, id, e);
}
}
五、消息消費者
添加 @RabbitListener 註解來指定某方法作爲消息消費的方法,例如監聽某 Queue 裏面的消息
註解 | 描述 |
---|---|
@RabbitListener |
該註解指定目標方法來作爲消費消息的方法 通過註解參數指定所監聽的隊列或者Binding 也可以標註在類上面,但需配合 @RabbitHandler 註解一起使用 |
@QueueBinding |
將交換機和隊列綁定 例:@QueueBinding( key = "computer" |
@Queue |
聲明隊列 (durable = "true" 表示持久化的) 例:@Queue(name = "ly.search.insert.queue", durable = "true") |
@Exchange |
聲明交換機(type = ExchangeTypes.TOPIC 表示交換機類型) 例:@Exchange(name = "ly.item.exchange", type = ExchangeTypes.TOPIC) |
@RabbitHandler |
@RabbitListener 標註在類上面表示當有收到消息的時候,就交給 @RabbitHandler 的方法處理,具體使用哪個方法處理,根據 MessageConverter 轉換後的參數類型 |
@Payload |
|
@Headers、@Header |
案例:
- 直接使用@RabbitListener(queues = "myQueue") 不能自動創建隊列
- 自動創建隊列 @RabbitListener(queuesToDeclare = @Queue("myQueue"))
- bindings :屬性自動創建, Exchange和Queue綁定
- ExchangeTypes:可以從這個抽象類裏獲取類型,避免手寫出錯
- key:是String[]數組,可以設置多個key
//1. @RabbitListener(queues = "myQueue") // 不能自動創建隊列
//2. 自動創建隊列 @RabbitListener(queuesToDeclare = @Queue("myQueue"))
//3. 自動創建, Exchange和Queue綁定
@RabbitListener(bindings = @QueueBinding(
value = @Queue("myQueue"),
exchange = @Exchange("myExchange")
))
public void process(String message) {
log.info("MqReceiver: {}", message);
}
/**
* 數碼供應商服務 接收消息
* @param message
*/
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange("myOrder"),
key = "computer",
value = @Queue("computerOrder")
))
public void processComputer(String message) {
log.info("computer MqReceiver: {}", message);
}
/**
* 水果供應商服務 接收消息
* @param message
*/
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange("myOrder"),
key = "fruit",
value = @Queue("fruitOrder")
))
public void processFruit(String message) {
log.info("fruit MqReceiver: {}", message);
}
/**
* 1、ExchangeTypes:可以從這個抽象類裏獲取類型,避免手寫出錯
* 2、key:是String[]數組,可以設置多個key
*/
@RabbitListener(bindings = @QueueBinding(
value = @Queue(name = "ly.search.insert.queue", durable = "true"),
exchange = @Exchange(name = "ly.item.exchange", type = ExchangeTypes.TOPIC, ignoreDeclarationExceptions = "true"),
key = {"item.insert", "item.update"}
))
public void process1(Long id) {
//需要做的動作。。。
}
@RabbitListener(
bindings = @QueueBinding(
value = @Queue(value = "order-queue", durable = "true"),
exchange = @Exchange(value = "order-exchange", durable = "true", type = "topic"),
key = "order.*"
)
)
public void process2(String message) {
//需要做的動作。。。
}
1、@RabbitListener
下面是註解的源碼
@Target({ElementType.TYPE, ElementType.METHOD, ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@MessageMapping
@Documented
@Repeatable(RabbitListeners.class)
public @interface RabbitListener {
String id() default "";
String containerFactory() default "";
String[] queues() default {};
Queue[] queuesToDeclare() default {};
boolean exclusive() default false;
String priority() default "";
String admin() default "";
QueueBinding[] bindings() default {};
String group() default "";
String returnExceptions() default "";
String errorHandler() default "";
String concurrency() default "";
String autoStartup() default "";
}
六、消息發送確認 (重要)
默認情況下如果一個 Message 被消費者所正確接收則會被從 Queue 中移除
如果一個 Queue 沒被任何消費者訂閱,那麼這個 Queue 中的消息會被 Cache(緩存),當有消費者訂閱時則會立即發送,當 Message 被消費者正確接收時,就會被從 Queue 中移除
通過 ConfirmCallback 和 ReturnCallback 來保證消息發送成功
區別:
ConfirmCallback :保證生產者到 Exchange 的發送
ReturnCallback :保證 Exchange 到 Queue 的發送
使用場景:
- 如果消息沒有到 exchange ,則 confirm 回調, ack = false
- 如果消息到達 exchange ,則 confirm 回調, ack = true
- exchange 到 queue 成功,則不回調 return
- exchange 到 queue 失敗,則回調 return (需設置mandatory=true,否則不會回調,消息就丟了)
問:發送的消息怎麼樣纔算失敗或成功?如何確認?
答:當消息無法路由到隊列時,確認消息路由失敗。消息成功路由時,當需要發送的隊列都發送成功後,進行確認消息,對於持久化隊列意味着寫入磁盤,對於鏡像隊列意味着所有鏡像接收成功
1、ConfirmCallback
判斷消息發送到 Exchange 是否成功
通過實現 ConfirmCallback 接口,消息發送到 Broker 後觸發回調,確認消息是否到達 Broker 服務器,也就是隻確認是否正確到達 Exchange 中
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ConfirmCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setConfirmCallback(this); //指定 ConfirmCallback
}
/**
* 當消息發送到交換機(exchange)時,該方法被調用.
* 1.如果消息沒有到exchange,則 ack=false
* 2.如果消息到達exchange,則 ack=true
* @param correlationData:唯一標識
* @param ack:確認結果
* @param cause:引起原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("消息唯一標識:"+correlationData);
System.out.println("確認結果:"+ack);
System.out.println("引起原因:"+cause);
if(ack){
//如果confirm返回成功 則進行更新
System.out.println("confirm消息確認成功");
} else {
//(nack)失敗則進行具體的後續操作:重試 或者補償等手段
System.out.println("confirm消息確認失敗,異常處理...");
}
}
}
還需要在配置文件添加配置
spring:
rabbitmq:
publisher-confirms: true
2、ReturnCallback
判斷消息從 Exchange 發送到 Queue 是否成功,失敗調用該方法(成功不調用)
通過實現 ReturnCallback 接口,啓動消息失敗返回,比如路由不到隊列時觸發回調
@Component
public class RabbitTemplateConfig implements RabbitTemplate.ReturnCallback{
@Autowired
private RabbitTemplate rabbitTemplate;
@PostConstruct
public void init(){
rabbitTemplate.setReturnCallback(this); //指定 ReturnCallback
}
/**
* 當消息從交換機到隊列失敗時,該方法被調用。(若成功,則不調用)
* 需要注意的是:該方法調用後,MsgSendConfirmCallBack中的confirm方法也會被調用,且ack = true
* @param message:傳遞的消息主體
* @param replyCode:問題狀態碼
* @param replyText:問題描述
* @param exchange:使用的交換器
* @param routingKey:使用的路由鍵
*/
@Override
public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
//失敗則進行具體的後續操作:重試 或者補償等手段。。。
System.out.println("消息主體 message : "+message);
System.out.println("問題狀態碼 : "+replyCode);
System.out.println("問題描述:"+replyText);
System.out.println("消息使用的交換器 exchange : "+exchange);
System.out.println("消息使用的路由鍵 routing : "+routingKey);
}
}
還需要在配置文件添加配置
spring:
rabbitmq:
publisher-returns: true
七、消息接收確認(重要)
消息消費者如何通知 Rabbit 消息消費成功?
答:
- 消息通過 ACK 確認是否被正確接收,每個 Message 都要被確認(acknowledged),可以手動去 ACK 或自動 ACK
- 自動確認會在消息發送給消費者後立即確認,但存在丟失消息的可能,如果消費端消費邏輯拋出異常,也就是消費端沒有處理成功這條消息,那麼就相當於丟失了消息
- 如果消息已經被處理,但後續代碼拋出異常,使用 Spring 進行管理的話消費端業務邏輯會進行回滾,這也同樣造成了實際意義的消息丟失
- 如果手動確認則當消費者調用 ack、nack、reject 幾種方法進行確認,手動確認可以在業務失敗後進行一些操作,如果消息未被 ACK 則會發送到下一個消費者
- 如果某個服務忘記 ACK 了,則 RabbitMQ 不會再發送數據給它,因爲 RabbitMQ 認爲該服務的處理能力有限
- ACK 機制還可以起到限流作用,比如在接收到某條消息時休眠幾秒鐘
- 消息確認模式有:
- AcknowledgeMode.NONE:自動確認
- AcknowledgeMode.AUTO:根據情況確認
- AcknowledgeMode.MANUAL:手動確認
1、確認消息(局部方法處理消息)
默認情況下消息消費者是自動 ack (確認)消息的,如果要手動 ack(確認)則需要修改確認模式爲 manual
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
或在 RabbitListenerContainerFactory 中進行開啓手動 ack
@Bean
public RabbitListenerContainerFactory<?> rabbitListenerContainerFactory(ConnectionFactory connectionFactory){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(new Jackson2JsonMessageConverter());
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL); //開啓手動 ack
return factory;
}
1.1、確認消息
@RabbitHandler
public void processMessage2(String message,Channel channel,@Header(AmqpHeaders.DELIVERY_TAG) long tag) {
System.out.println(message);
try {
channel.basicAck(tag,false); // 確認消息
} catch (IOException e) {
e.printStackTrace();
}
}
需要注意的 basicAck 方法需要傳遞兩個參數
- deliveryTag(唯一標識 ID):當一個消費者向 RabbitMQ 註冊後,會建立起一個 Channel ,RabbitMQ 會用 basic.deliver 方法向消費者推送消息,這個方法攜帶了一個 delivery tag, 它代表了 RabbitMQ 向該 Channel 投遞的這條消息的唯一標識 ID,是一個單調遞增的正整數,delivery tag 的範圍僅限於 Channel
- multiple:爲了減少網絡流量,手動確認可以被批處理,當該參數爲 true 時,則可以一次性確認 delivery_tag 小於等於傳入值的所有消息
1.2、手動否認、拒絕消息
發送一個 header 中包含 error 屬性的消息
消費者獲取消息時檢查到頭部包含 error 則 nack 消息
@RabbitHandler
public void processMessage2(String message, Channel channel,@Headers Map<String,Object> map) {
System.out.println(message);
if (map.get("error")!= null){
System.out.println("錯誤的消息");
try {
channel.basicNack((Long)map.get(AmqpHeaders.DELIVERY_TAG),false,true); //否認消息
return;
} catch (IOException e) {
e.printStackTrace();
}
}
try {
channel.basicAck((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //確認消息
} catch (IOException e) {
e.printStackTrace();
}
}
此時控制檯重複打印,說明該消息被 nack 後一直重新入隊列然後一直重新消費
hello
錯誤的消息
hello
錯誤的消息
hello
錯誤的消息
hello
錯誤的消息
也可以拒絕該消息,消息會被丟棄,不會重回隊列
channel.basicReject((Long)map.get(AmqpHeaders.DELIVERY_TAG),false); //拒絕消息
2、確認消息(全局處理消息)
自動確認涉及到一個問題就是如果在處理消息的時候拋出異常,消息處理失敗,但是因爲自動確認而導致 Rabbit 將該消息刪除了,造成消息丟失
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("consumer_queue"); // 監聽的隊列
container.setAcknowledgeMode(AcknowledgeMode.NONE); // NONE 代表自動確認
container.setMessageListener((MessageListener) message -> { //消息監聽處理
System.out.println("====接收到消息=====");
System.out.println(new String(message.getBody()));
//相當於自己的一些消費邏輯拋錯誤
throw new NullPointerException("consumer fail");
});
return container;
}
手動確認消息
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("consumer_queue"); // 監聽的隊列
container.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 手動確認
container.setMessageListener((ChannelAwareMessageListener) (message, channel) -> { //消息處理
System.out.println("====接收到消息=====");
System.out.println(new String(message.getBody()));
if(message.getMessageProperties().getHeaders().get("error") == null){
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("消息已經確認");
}else {
//channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,false);
channel.basicReject(message.getMessageProperties().getDeliveryTag(),false);
System.out.println("消息拒絕");
}
});
return container;
}
AcknowledgeMode 除了 NONE 和 MANUAL 之外還有 AUTO ,它會根據方法的執行情況來決定是否確認還是拒絕(是否重新入queue)
- 如果消息成功被消費(成功的意思是在消費的過程中沒有拋出異常),則自動確認
- 當拋出 AmqpRejectAndDontRequeueException 異常的時候,則消息會被拒絕,且 requeue = false(不重新入隊列)
- 當拋出 ImmediateAcknowledgeAmqpException 異常,則消費者會被確認
- 其他的異常,則消息會被拒絕,且 requeue = true(如果此時只有一個消費者監聽該隊列,則有發生死循環的風險,多消費端也會造成資源的極大浪費,這個在開發過程中一定要避免的)。可以通過 setDefaultRequeueRejected(默認是true)去設置
@Bean
public SimpleMessageListenerContainer messageListenerContainer(ConnectionFactory connectionFactory){
SimpleMessageListenerContainer container = new SimpleMessageListenerContainer();
container.setConnectionFactory(connectionFactory);
container.setQueueNames("consumer_queue"); // 監聽的隊列
container.setAcknowledgeMode(AcknowledgeMode.AUTO); // 根據情況確認消息
container.setMessageListener((MessageListener) (message) -> {
System.out.println("====接收到消息=====");
System.out.println(new String(message.getBody()));
//拋出NullPointerException異常則重新入隊列
//throw new NullPointerException("消息消費失敗");
//當拋出的異常是AmqpRejectAndDontRequeueException異常的時候,則消息會被拒絕,且requeue=false
//throw new AmqpRejectAndDontRequeueException("消息消費失敗");
//當拋出ImmediateAcknowledgeAmqpException異常,則消費者會被確認
throw new ImmediateAcknowledgeAmqpException("消息消費失敗");
});
return container;
}
消息可靠總結
- 持久化
- exchange要持久化
- queue要持久化
- message要持久化
- 消息確認
- 啓動消費返回(@ReturnList註解,生產者就可以知道哪些消息沒有發出去)
- 生產者和Server(broker)之間的消息確認
- 消費者和Server(broker)之間的消息確認