分佈式專題-分佈式消息通信之RabbitMQ01-初識RabbitMQ

前言

前面的章節我們分析了《Kafka使用以及原理分析》《ActiveMQ的使用以及原理分析》,從這一節開始,來說說RabbitMQ。
我們知道,消息中間件的特點有:削峯異步解耦,區別點在於語言支持、吞吐量、持久化等差異

關於RabbitMQ,我會圍繞以下幾點進行展開

  1. 典型應用場景
  2. 基本介紹
  3. Java API編程
  4. 進階知識
  5. UI管理界面
  6. Spring配置方式集成RabbitMQ
  7. Spring Boot集成RabbitMQ

共計分爲兩小節進行闡述:

Tips:本文末有福利哦,不要錯過!另本節的所有演示代碼已上傳GitHub,地址我也在文末一併送出~

本人鄭重承諾不玩微信公衆號,不接任何廣告,無任何收費以及廣告傾向,只爲打造CSDN文化淨土,一心知識分享,天道酬勤,讓我們技術人攜手共進,風雨同舟,開創互聯網的美好未來,從今天的你我做起~

典型應用場景

  1. 跨系統的異步通信 人民銀行二代支付系統,使用重量級消息隊列 IBM MQ,異步,解耦,削峯都有體現。

  2. 應用內的同步變成異步秒殺:自己發送給自己

  3. 基於Pub/Sub模型實現的事件驅動 放款失敗通知、提貨通知、購買碎屏保 系統間同步數據 摒棄ELT(比如全量同步商戶數據); 摒棄API(比如定時增量獲取用戶、獲取產品,變成增量廣播)。

  4. 利用RabbitMQ實現事務的最終一致性

基本介紹

關於RabbitMQ的安裝,文末後記有相關安裝的文章鏈接,筆者親自試用沒有問題,包括Windows與Linux的版本,這裏就不細說了,直接上乾貨~

AMQP協議

我們說爲什麼要有設計模式?其實最重要的在與規範化,大家達成一致纔好幹活,而AMQP就是爲解決各種消息中間件的多語言,多平臺的不統一的一個協議,它制訂了一套消息通信的規範,使得軟件開發變得甜甜的。

AMQP,即Advanced Message Queuing Protocol,一個提供統一消息服務的應用層標準高級消息隊列協議,是應用層協議的一個開放標準,爲面向消息的中間件設計。基於此協議的客戶端與消息中間件可傳遞消息,並不受客戶端/中間件同產品、不同的開發語言等條件的限制。

AMQP的實現有:RabbitMQ、OpenAMQ、Apache Qpid、Redhat Enterprise MRG、AMQP Infrastructure、ØMQ、Zyre等。

RabbitMQ的特性

RabbitMQ使用Erlang語言編寫,使用Mnesia數據庫存儲消息。

  • 可靠性(Reliability) RabbitMQ 使用一些機制來保證可靠性,如持久化、傳輸確認、發佈確認。

  • 靈活的路由(Flexible Routing) 在消息進入隊列之前,通過 Exchange 來路由消息的。對於典型的路由功能,RabbitMQ 已經提供了一些內置的 Exchange 來實現。針對更復雜的路由功能,可以將多個 Exchange 綁定在一起,也通過插件機制實現自己的 Exchange 。

  • 消息集羣(Clustering) 多個 RabbitMQ 服務器可以組成一個集羣,形成一個邏輯 Broker 。

  • 高可用(Highly Available Queues) 隊列可以在集羣中的機器上進行鏡像,使得在部分節點出問題的情況下隊列仍然可用。

  • 多種協議(Multi-protocol) RabbitMQ 支持多種消息隊列協議,比如 AMQP、STOMP、MQTT 等等。

  • 多語言客戶端(Many Clients) RabbitMQ 幾乎支持所有常用語言,比如 Java、.NET、Ruby、PHP、C#、 JavaScript 等等。

  • 管理界面(Management UI) RabbitMQ 提供了一個易用的用戶界面,使得用戶可以監控和管理消息、集羣中的節點。

  • 插件機制(Plugin System)RabbitMQ提供了許多插件,以實現從多方面擴展,當然也可以編寫自己的插件。

工作模型

在這裏插入圖片描述
這裏解釋一下圖上的幾個概念:

概念 解釋
Broker 即RabbitMQ的實體服務器。提供一種傳輸服務,維護一條從生產者到消費者的傳輸線路,保證消息數據能按照指定的方式傳輸。
Exchange 消息交換機。指定消息按照什麼規則路由到哪個隊列Queue。
Queue 消息隊列。消息的載體,每條消息都會被投送到一個或多個隊列中。
Binding 綁定。作用就是將Exchange和Queue按照某種路由規則綁定起來。
Routing 路由關鍵字。Exchange根據Routing Key進行消息投遞。定義綁定時指定的關鍵字稱爲Key Binding Key。
Vhost 虛擬主機。一個Broker可以有多個虛擬主機,用作不同用戶的權限分離。一個虛擬主機持有一組Exchange、Queue和Binding。
Producer 消息生產者。主要將消息投遞到對應的Exchange上面。一般是獨立的程序。
Consumer 消息消費者。消息的接收者,一般是獨立的程序。
Connection Producer 和 Consumer 與Broker之間的TCP長連接。
Channel 消息通道,也稱信道。在客戶端的每個連接裏可以建立多個Channel,每個Channel代表一個會話任務。在RabbitMQ Java Client API中,channel上定義了大量的編程接口。

三種主要的交換機

Direct Exchange直連交換機

定義:直連類型的交換機與一個隊列綁定時,需要指定一個明確的binding key。

路由規則:發送消息到直連類型的交換機時,只有routing key跟binding key完全匹配時,綁定的隊列才能收到消息。

例如:

//只有隊列1能收到消息
channel.basicPublish("MY_DIRECT_EXCHANGE", "key1", null, msg.getBytes());

在這裏插入圖片描述

Topic Exchange主題交換機

定義:主題類型的交換機與一個隊列綁定時,可以指定按模式匹配的routing key。

通配符有兩個,*代表匹配一個單詞。#代表匹配零個或者多個單詞。單詞與單詞之間用 . 隔開。

路由規則:發送消息到主題類型的交換機時,routing key符合binding key的模式時,綁定的隊列才能收到消息。

例如:

//只有隊列1能收到消息
channel.basicPublish("MY_TOPIC_EXCHANGE", "sh.abc", null, msg.getBytes());

//隊列2和隊列3能收到消息
channel.basicPublish("MY_TOPIC_EXCHANGE", "bj.book", null, msg.getBytes());

//只有隊列4能收到消息
channel.basicPublish("MY_TOPIC_EXCHANGE", "abc.def.food", null, msg.getBytes());

在這裏插入圖片描述

Fanout Exchange廣播交換機

定義:廣播類型的交換機與一個隊列綁定時,不需要指定binding key。

路由規則:當消息發送到廣播類型的交換機時,不需要指定routing key,所有與之綁定的隊列都能收到消息。

例如:

//3個隊列都會收到消息
channel.basicPublish("MY_FANOUT_EXCHANGE", "", null, msg.getBytes());

在這裏插入圖片描述

Java API編程

創建Maven工程,pom.xml引入依賴

<dependency>
   <groupId>com.rabbitmq</groupId>
   <artifactId>amqp-client</artifactId>
   <version>4.1.0</version>
</dependency>

生產者

public class MyProducer {
    private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 連接IP
        factory.setHost("192.168.200.111");
        // 連接端口
        factory.setPort(5672);
        // 虛擬機
        factory.setVirtualHost("/");
        // 用戶
        factory.setUsername("guest");
        factory.setPassword("guest");

        // 建立連接
        Connection conn = factory.newConnection();
        // 創建消息通道
        Channel channel = conn.createChannel();

        // 發送消息
        String msg = "Hello world, Rabbit MQ";

        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish(EXCHANGE_NAME, "best", null, msg.getBytes());

        channel.close();
        conn.close();
    }
}

消費者

public class MyConsumer {
    private final static String EXCHANGE_NAME = "SIMPLE_EXCHANGE";
    private final static String QUEUE_NAME = "SIMPLE_QUEUE";

    public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        // 連接IP
        factory.setHost("192.168.200.111");
        // 默認監聽端口
        factory.setPort(5672);
        // 虛擬機
        factory.setVirtualHost("/");

        // 設置訪問的用戶
        factory.setUsername("guest");
        factory.setPassword("guest");
        // 建立連接
        Connection conn = factory.newConnection();
        // 創建消息通道
        Channel channel = conn.createChannel();

        // 聲明交換機
        // String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
        channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

        // 聲明隊列
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println(" Waiting for message....");

        // 綁定隊列和交換機
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"best");

        // 創建消費者
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");
                System.out.println("consumerTag : " + consumerTag );
                System.out.println("deliveryTag : " + envelope.getDeliveryTag() );
            }
        };

        // 開始獲取消息
        // String queue, boolean autoAck, Consumer callback
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}

測試用例

slf4j警告可以忽略,不影響測試~
消費者:
在這裏插入圖片描述
生產者:
在這裏插入圖片描述
Web頁面:
在這裏插入圖片描述

參數說明

如上面代碼所示,我們看看配置通信的過程中參數的設置

聲明交換機的參數

// 聲明交換機
// String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

String type:交換機的類型,direct, topic, fanout中的一種。

boolean durable:是否持久化,代表交換機在服務器重啓後是否還存在。

Channel.queueDeclare方法的構造函數

聲明隊列的參數

// 聲明隊列
// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);

boolean durable:是否持久化,代表隊列在服務器重啓後是否還存在。

boolean exclusive:是否排他性隊列。排他性隊列只能在聲明它的Connection中使用,連接斷開時自動刪除。

boolean autoDelete:是否自動刪除。如果爲true,至少有一個消費者連接到這個隊列,之後所有與這個隊列連接的消費者都斷開時,隊列會自動刪除。

Map<String, Object> arguments:隊列的其他屬性,例如x-message-ttl、x-expires、x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-dead-letter-routing-key、x-max-priority。

消息屬性BasicProperties

消息的全部屬性有14個,以下列舉了一些主要的參數:

參數 釋義
Map<String,Object> headers 消息的其他自定義參數
Integer deliveryMode 2持久化,其他:瞬態
Integer priority 消息的優先級
String correlationId 關聯ID,方便RPC相應與請求關聯
String replyTo 回調隊列
String expiration TTL,消息過期時間,單位毫秒

RabbitMQ的進階知識

  1. 怎麼自動刪除沒人消費的消息?
  2. 無法路由的消息,去了哪裏?
  3. 可以讓消息優先得到消費嗎?
  4. 如何實現延遲發送消息?
  5. MQ怎麼實現RPC?
  6. RabbitMQ流量控制怎麼做?設置隊列大小有用嗎?

TTL

TTL: time to live

  • 消息的過期時間
  • 隊列的過期時間

消息的過期時間

 public static void main(String[] args) throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUri(ResourceUtil.getKey("rabbitmq.uri"));

        // 建立連接
        Connection conn = factory.newConnection();
        // 創建消息通道
        Channel channel = conn.createChannel();

        String msg = "Hello world, Rabbit MQ, DLX MSG";

        // 通過隊列屬性設置消息過期時間
        Map<String, Object> argss = new HashMap<String, Object>();
        argss.put("x-message-ttl",6000);

        // 聲明隊列(默認交換機AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare("TEST_TTL_QUEUE", false, false, false, argss);

        // 對每條消息設置過期時間
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) // 持久化消息
                .contentEncoding("UTF-8")
                .expiration("10000") // TTL
                .build();

        // 此處兩種方式設置消息過期時間的方式都使用了,將以較小的數值爲準

        // 發送消息
        channel.basicPublish("", "TEST_DLX_QUEUE", properties, msg.getBytes());

        channel.close();
        conn.close();
    }

死信隊列

死信隊列: 生產者生產一條消息後,但此消息沒有路由到任何一個隊列的時候,這個消息就變成了死信,如果我們沒有設置一些比如備份交換機、死信交換機這種策略的話,那麼這樣的消息就會被直接刪除,但是有種方法可以將死信消息堆積起來,就是DLX。

有三種情況消息會進入DLX(Dead Letter Exchange)死信交換機。

  1. (NACK || Reject ) && requeue == false
   // 創建消費者,並接收消息
        Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                String msg = new String(body, "UTF-8");
                System.out.println("Received message : '" + msg + "'");

                if (msg.contains("拒收")){
                    // 拒絕消息
                    // requeue:是否重新入隊列,true:是;false:直接丟棄,相當於告訴隊列可以直接刪除掉
                    // TODO 如果只有這一個消費者,requeue 爲true 的時候會造成消息重複消費
                    channel.basicReject(envelope.getDeliveryTag(), false);
                } else if (msg.contains("異常")){
                    // 批量拒絕
                    // requeue:是否重新入隊列
                    // TODO 如果只有這一個消費者,requeue 爲true 的時候會造成消息重複消費
                    channel.basicNack(envelope.getDeliveryTag(), true, false);
                } else {
                    // 手工應答
                    // 如果不應答,隊列中的消息會一直存在,重新連接的時候會重複消費
                    channel.basicAck(envelope.getDeliveryTag(), true);
                }
            }
        };
  1. 消息過期

前面提到,生產者生產一條消息後,但此消息沒有路由到任何一個隊列的時候,這個消息就變成了死信~

        // 指定隊列的死信交換機
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
        arguments.put("x-expires","9000"); // 設置隊列的TTL
  1. 隊列達到最大長度(先入隊的消息會被髮送到DLX)
        // 指定隊列的死信交換機
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
        arguments.put("x-max-length", 4); // 如果設置了隊列的最大長度,超過長度時,先入隊的消息會被髮送到DLX

那麼怎麼獲取死信的消息呢?

可以設置一個死信隊列(Dead Letter Queue)與DLX綁定,即可以存儲Dead Letter,消費者可以監聽這個隊列取走消息。

        // 指定隊列的死信交換機
        Map<String,Object> arguments = new HashMap<String,Object>();
        arguments.put("x-dead-letter-exchange","DLX_EXCHANGE");
        // 聲明隊列(默認交換機AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare("TEST_DLX_QUEUE", false, false, false, arguments);

        // 聲明死信交換機
        channel.exchangeDeclare("DLX_EXCHANGE","topic", false, false, false, null);
        // 聲明死信隊列
        channel.queueDeclare("DLX_QUEUE", false, false, false, null);
        // 綁定,此處 Dead letter routing key 設置爲 #
        channel.queueBind("DLX_QUEUE","DLX_EXCHANGE","#");
        System.out.println(" Waiting for message....");

流程如是:
在這裏插入圖片描述

演示代碼參考:rabbitmq-demo/rabbitmq-javaapi/com.test.ack

優先級隊列

設置一個隊列的最大優先級:

      Map<String, Object> headers = new HashMap<String, Object>();
        headers.put("name", "gupao");
        headers.put("level", "top");
        // 聲明隊列(默認交換機AMQP default,Direct)
        // String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

發送消息時指定消息當前的優先級:

        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2)   // 2代表持久化
                .contentEncoding("UTF-8")  // 編碼
                .expiration("10000")  // TTL,過期時間
                .headers(headers) // 自定義屬性
                .priority(5) // 優先級,默認爲5,配合隊列的 x-max-priority 屬性使用
                .messageId(String.valueOf(UUID.randomUUID()))
                .build();
      // 發送消息
        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish("", QUEUE_NAME, properties, msg.getBytes());

優先級高的消息可以優先被消費,但是:只有消息堆積(消息的發送速度大於消費者的消費速度)的情況下優先級纔有意義。

演示代碼參考:rabbitmq-demo/rabbitmq-javaapi/com.test.message

延遲隊列

RabbitMQ本身不支持延遲隊列。可以使用TTL結合DLX的方式來實現消息的延遲投遞,即把DLX跟某個隊列綁定,到了指定時間,消息過期後,就會從DLX路由到這個隊列,消費者可以從這個隊列取走消息。

另一種方式是使用rabbitmq-delayed-message-exchange插件。

當然,將需要發送的信息保存在數據庫,使用任務調度系統掃描然後發送也是可以實現的。

演示代碼參考:rabbitmq-demo/rabbitmq-javaapi/com.test.dlx

PRC

RabbitMQ實現RPC的原理:服務端處理消息後,把響應消息發送到一個響應隊列,客戶端再從響應隊列取到結果。

其中的問題:Client收到消息後,怎麼知道應答消息是回覆哪一條消息的?所以必須有一個唯一ID來關聯,就是correlationId。
在這裏插入圖片描述

演示代碼參考:rabbitmq-demo/rabbitmq-javaapi/com.test.rpc

服務端流控(Flow Control)

RabbitMQ 會在啓動時檢測機器的物理內存數值。默認當 MQ 佔用 40% 以上內存時,MQ 會主動拋出一個內存警告並阻塞所有連接(Connections)。可以通過修改 rabbitmq.config 文件來調整內存閾值,默認值是 0.4,如下所示: [{rabbit, [{vm_memory_high_watermark, 0.4}]}].

默認情況,如果剩餘磁盤空間在 1GB 以下,RabbitMQ 主動阻塞所有的生產者。這個閾值也是可調的。

注意隊列長度只在消息堆積的情況下有意義,而且會刪除先入隊的消息,不能實現服務端限流。

消費端限流

在AutoACK爲false的情況下,如果一定數目的消息(通過基於consumer或者channel設置Qos的值)未被確認前,不進行消費新的消息。

        //非自動確認消息的前提下,如果一定數目的消息(通過基於consume或者channel設置Qos的值)未被確認前,不進行消費新的消息。
        // 因爲Consumer2的處理速率很慢,收到兩條消息後都沒有發送ACK,隊列不會再發送消息給Consumer2
        channel.basicQos(2);
        channel.basicConsume(QUEUE_NAME, false, consumer);

演示代碼參考:rabbitmq-demo/rabbitmq-javaapi/com.test.limit

UI管理界面的使用

管理插件提供了更簡單的管理方式。

啓用管理插件

  • Windows啓用管理插件

cd C:\Program Files\RabbitMQ Server\rabbitmq_server-3.6.6\sbin
rabbitmq-plugins.bat enable rabbitmq_management

  • Linux啓用管理插件

cd /data/program/rabbitmq_server-3.7.16/sbin
./rabbitmq-plugins enable rabbitmq_management

在這裏插入圖片描述

管理界面訪問端口
在這裏插入圖片描述

默認端口是15672,默認用戶guest,密碼guest。guest用戶默認只能在本機訪問。

在這裏插入圖片描述
登陸後:
在這裏插入圖片描述

Linux 創建RabbitMQ用戶

例如創建用戶admin,密碼admin,授權訪問所有的Vhost

firewall-cmd --permanent --add-port=15672/tcp
firewall-cmd --reload
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator rabbitmqctl set_permissions -p / admin “." ".” “.*”

Spring配置方式集成RabbitMQ

步驟

1、創建Maven工程,pom.xml引入依賴

        <!--rabbitmq依賴 -->
        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>1.3.5.RELEASE</version>
        </dependency>

2、src/main/resouces目錄,創建rabbitMQ.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans
     http://www.springframework.org/schema/beans/spring-beans-3.0.xsd
     http://www.springframework.org/schema/rabbit
     http://www.springframework.org/schema/rabbit/spring-rabbit-1.2.xsd">

    <!--配置connection-factory,指定連接rabbit server參數 -->
    <rabbit:connection-factory id="connectionFactory" virtual-host="/" username="guest" password="guest" host="192.168.200.111" port="5672" />

    <!--通過指定下面的admin信息,當前producer中的exchange和queue會在rabbitmq服務器上自動生成 -->
    <rabbit:admin id="connectAdmin" connection-factory="connectionFactory" />

    <!--######分隔線######-->
    <!--定義queue -->
    <rabbit:queue name="MY_FIRST_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!--定義direct exchange,綁定MY_FIRST_QUEUE -->
    <rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_FIRST_QUEUE" key="FirstKey">
            </rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--定義rabbit template用於數據的接收和發送 -->
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" exchange="MY_DIRECT_EXCHANGE" />

    <!--消息接收者 -->
    <bean id="messageReceiver" class="com.test.consumer.FirstConsumer"></bean>

    <!--queue listener 觀察 監聽模式 當有消息到達時會通知監聽在對應的隊列上的監聽對象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_FIRST_QUEUE" ref="messageReceiver" />
    </rabbit:listener-container>

    <!--定義queue -->
    <rabbit:queue name="MY_SECOND_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 將已經定義的Exchange綁定到MY_SECOND_QUEUE,注意關鍵詞是key -->
    <rabbit:direct-exchange name="MY_DIRECT_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_SECOND_QUEUE" key="SecondKey"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!-- 消息接收者 -->
    <bean id="receiverSecond" class="com.test.consumer.SecondConsumer"></bean>

    <!-- queue litener 觀察 監聽模式 當有消息到達時會通知監聽在對應的隊列上的監聽對象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_SECOND_QUEUE" ref="receiverSecond" />
    </rabbit:listener-container>

    <!--######分隔線######-->
    <!--定義queue -->
    <rabbit:queue name="MY_THIRD_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 定義topic exchange,綁定MY_THIRD_QUEUE,注意關鍵詞是pattern -->
    <rabbit:topic-exchange name="MY_TOPIC_EXCHANGE" durable="true" auto-delete="false" declared-by="connectAdmin">
        <rabbit:bindings>
            <rabbit:binding queue="MY_THIRD_QUEUE" pattern="#.Third.#"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:topic-exchange>

    <!--定義rabbit template用於數據的接收和發送 -->
    <rabbit:template id="amqpTemplate2" connection-factory="connectionFactory" exchange="MY_TOPIC_EXCHANGE" />

    <!-- 消息接收者 -->
    <bean id="receiverThird" class="com.test.consumer.ThirdConsumer"></bean>

    <!-- queue litener 觀察 監聽模式 當有消息到達時會通知監聽在對應的隊列上的監聽對象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_THIRD_QUEUE" ref="receiverThird" />
    </rabbit:listener-container>

    <!--######分隔線######-->
    <!--定義queue -->
    <rabbit:queue name="MY_FOURTH_QUEUE" durable="true" auto-delete="false" exclusive="false" declared-by="connectAdmin" />

    <!-- 定義fanout exchange,綁定MY_FIRST_QUEUE 和 MY_FOURTH_QUEUE -->
    <rabbit:fanout-exchange name="MY_FANOUT_EXCHANGE" auto-delete="false" durable="true" declared-by="connectAdmin" >
        <rabbit:bindings>
            <rabbit:binding queue="MY_FIRST_QUEUE"></rabbit:binding>
            <rabbit:binding queue="MY_FOURTH_QUEUE"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!-- 消息接收者 -->
    <bean id="receiverFourth" class="com.test.consumer.FourthConsumer"></bean>

    <!-- queue litener 觀察 監聽模式 當有消息到達時會通知監聽在對應的隊列上的監聽對象 -->
    <rabbit:listener-container connection-factory="connectionFactory">
        <rabbit:listener queues="MY_FOURTH_QUEUE" ref="receiverFourth" />
    </rabbit:listener-container>
</beans>

整體流程如下:
在這裏插入圖片描述
3、配置applicationContext.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-3.1.xsd
    http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-3.1.xsd">

    <import resource="classpath*:rabbitMQ.xml" />

    <!-- 掃描指定package下所有帶有如 @Controller,@Service,@Resource 並把所註釋的註冊爲Spring Beans -->
    <context:component-scan base-package="com.test.*" />

    <!-- 激活annotation功能 -->
    <context:annotation-config />

    <!-- 激活annotation功能 -->
    <context:spring-configured />
</beans>

4、src/main/resouces目錄,log4j.properties
在這裏插入圖片描述

5、編寫生產者

@Service
public class MessageProducer {
    private Logger logger = LoggerFactory.getLogger(MessageProducer.class);

    @Autowired
    @Qualifier("amqpTemplate")
    private AmqpTemplate amqpTemplate;

    @Autowired
    @Qualifier("amqpTemplate2")
    private AmqpTemplate amqpTemplate2;

    /**
     * 演示三種交換機的使用
     *
     * @param message
     */
    public void sendMessage(Object message) {
        logger.info("Send message:" + message);

        // amqpTemplate 默認交換機 MY_DIRECT_EXCHANGE
        // amqpTemplate2 默認交換機 MY_TOPIC_EXCHANGE

        // Exchange 爲 direct 模式,直接指定routingKey
        amqpTemplate.convertAndSend("FirstKey", "[Direct,FirstKey] "+message);
        amqpTemplate.convertAndSend("SecondKey", "[Direct,SecondKey] "+message);

        // Exchange模式爲topic,通過topic匹配關心該主題的隊列
        amqpTemplate2.convertAndSend("msg.Third.send","[Topic,msg.Third.send] "+message);

        // 廣播消息,與Exchange綁定的所有隊列都會收到消息,routingKey爲空
        amqpTemplate2.convertAndSend("MY_FANOUT_EXCHANGE",null,"[Fanout] "+message);
    }
}

6、編寫4個消費者

public class FirstConsumer implements MessageListener {
    private Logger logger = LoggerFactory.getLogger(FirstConsumer.class);

    public void onMessage(Message message) {
        logger.info("The first consumer received message : " + message.getBody());
    }
}

類比再寫三個消費者,由於代碼邏輯相同,這裏不貼出來了,有興趣的朋友可以看我的完整代碼:

演示代碼參考:rabbitmq-demo/spring-rabbitmq

代碼測試:
運行測試用例,每個一秒發送一次消息,生產者指定三種交換機,四個消費者根據指定的消費者這100條數據。
在這裏插入圖片描述
日誌打印也證實了實驗的猜想。

Spring Boot集成RabbitMQ

步驟

springBoot封裝了mq的操作,所以springboot集成mq不需要導入依賴,直接使用AmqpTemplate即可。
首先配置交換機與隊列的關係:

@Configuration
public class RabbitConfig {

    // 兩個交換機
    @Bean("topicExchange")
    public TopicExchange getTopicExchange(){
        return new TopicExchange("TOPIC_EXCHANGE");
    }

    @Bean("fanoutExchange")
    public FanoutExchange getFanoutExchange(){
        return  new FanoutExchange("FANOUT_EXCHANGE");
    }

    // 三個隊列
    @Bean("firstQueue")
    public Queue getFirstQueue(){
        Map<String, Object> args = new HashMap<String, Object>();
        args.put("x-message-ttl",6000);
        Queue queue = new Queue("FIRST_QUEUE", false, false, true, args);
        return queue;
    }

    @Bean("secondQueue")
    public Queue getSecondQueue(){
        return new Queue("SECOND_QUEUE");
    }

    @Bean("thirdQueue")
    public Queue getThirdQueue(){
        return new Queue("THIRD_QUEUE");
    }

    // 兩個綁定
    @Bean
    public Binding bindSecond(@Qualifier("secondQueue") Queue queue,@Qualifier("topicExchange") TopicExchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with("#.test.#");
    }

    @Bean
    public Binding bindThird(@Qualifier("thirdQueue") Queue queue,@Qualifier("fanoutExchange") FanoutExchange exchange){
        return BindingBuilder.bind(queue).to(exchange);
    }

}

寫三個生產者類,代碼類似,只貼出一個生產者,完整代碼詳見本文末GItlab地址:

@Component
@RabbitListener(queues = "FIRST_QUEUE")
public class FirstConsumer {

    @RabbitHandler
    public void process(String msg){
        System.out.println(" first queue received msg : " + msg);
    }
}

消費者:

@Component
public class MyProvider {

    @Autowired
    AmqpTemplate amqpTemplate;

    public void send(){
        // 發送4條消息

        amqpTemplate.convertAndSend("","FIRST_QUEUE","-------- a direct msg");

        amqpTemplate.convertAndSend("TOPIC_EXCHANGE","shanghai.test.teacher","-------- a topic msg : shanghai.test.teacher");
        amqpTemplate.convertAndSend("TOPIC_EXCHANGE","changsha.test.student","-------- a topic msg : changsha.test.student");

        amqpTemplate.convertAndSend("FANOUT_EXCHANGE","","-------- a fanout msg");

    }

}

測試用例:


@RunWith(SpringRunner.class)
@SpringBootTest
public class SpringbootMq0904ApplicationTests {

	@Autowired
	MyProvider provider;

	@Test
	public void contextLoads() {
		provider.send();
	}

}

這樣,就簡單的通過springboot集成rabbitmq~

演示代碼參考:rabbitmq-demo/springboot-mq

後記

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章