RabbitMQ(二)Springboot整合及使用

一、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(
            exchange = @Exchange("myOrder"),
            value = @Queue("computerOrder"),

            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

 

案例:

  1. 直接使用@RabbitListener(queues = "myQueue")  不能自動創建隊列
  2. 自動創建隊列 @RabbitListener(queuesToDeclare = @Queue("myQueue"))
  3. bindings :屬性自動創建, Exchange和Queue綁定
  4. ExchangeTypes:可以從這個抽象類裏獲取類型,避免手寫出錯
  5. 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)之間的消息確認
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章