java基礎(六):RabbitMQ 入門

從入門到放棄的攻城獅發表於2019-05-05

建議先了解為什麼專案要使用 MQ 訊息佇列,MQ 訊息佇列有什麼優點,如果在業務邏輯上沒有此種需求,建議不要使用中介軟體。中介軟體對系統的效能做優化的同時,同時增加了系統的複雜性也維護難易度;其次,需要了解各種常見的 MQ 訊息佇列有什麼區別,以便在相同的成本下選擇一種最合適本系統的技術。

本文主要討論 RabbitMQ,從3月底接觸一個專案使用了 RabbitMQ,就開始著手學習,主要通過視訊和部落格學習了一個月,基本明白了 RabbitMQ 的應用,其它的 MQ 佇列還不清楚,其底層技術還有待學習,以下是我目前的學習心得。

1.安裝 Erlang

RabbitMQ 是基於 Erlang 語言寫的,所以首先安裝 Erlang,本例是在 Windows 上安裝,也可以選擇在 Linux 上安裝,機器上沒有虛擬機器,直接在 Windows 上操作,建議在 Linux 上安裝。官方下載 Erlang 軟體,我下載最新版本 21.3。安裝過程很簡單,直接 Next 到底。 Linux 安裝自行谷歌。如下圖:

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門
安裝結束後,設定環境變數,如下圖

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門
測試是否安裝成功

java基礎(六):RabbitMQ 入門

2.安裝 RabbitMQ

官方下載,選擇最新版本 3.7。安裝過程很簡單,直接 Next 到底。如下圖:

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門
測試安裝是否成功,進入安裝目錄 sbin,執行 rabbitmq-plugins enable rabbitmq_management 命令,出現下面介面,證明安裝成功(建議以管理員方式開啟 dos)。

java基礎(六):RabbitMQ 入門

執行 rabbitmq-server start 命令,啟動服務。本地登陸並建立使用者,如下圖:

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門
關於tags標籤的解釋:

1、  超級管理員(administrator)

可登陸管理控制檯,可檢視所有的資訊,並且可以對使用者,策略(policy)進行操作。

2、  監控者(monitoring)

可登陸管理控制檯,同時可以檢視rabbitmq節點的相關資訊(程式數,記憶體使用情況,磁碟使用情況等)

3、  策略制定者(policymaker)

可登陸管理控制檯, 同時可以對policy進行管理。但無法檢視節點的相關資訊(上圖紅框標識的部分)。

4、  普通管理者(management)

僅可登陸管理控制檯,無法看到節點資訊,也無法對策略進行管理。

5、  其他

無法登陸管理控制檯,通常就是普通的生產者和消費者。

4.JAVA 操作RabbitMQ

參考 RabbitMQ 官網,一共分為6個模式

java基礎(六):RabbitMQ 入門
RabbitMQ 是一個訊息代理,實際上,它接收生產者產生的訊息,然後將訊息傳遞給消費者。在這個過程中,它可以路由、緩衝、持久化等,在傳輸過程中,主要又三部分組成。

生產者:傳送訊息的一端

java基礎(六):RabbitMQ 入門
佇列:它活動在 RabbitMQ 伺服器中,訊息儲存的地方,佇列本質上是一個緩衝物件,所以儲存的訊息不受限制

java基礎(六):RabbitMQ 入門
消費者:訊息接收端

java基礎(六):RabbitMQ 入門
一般情況下,訊息生產者、消費者和佇列不在同一臺伺服器上,本地做測試,放在一臺伺服器上。 測試專案直接建立一個 maven 格式的專案,沒必要建立網路格式。新建一個專案,如下圖:

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門
首先準備操作 MQ 的環境

(1): 準備必要的 Pom 檔案,匯入相應的 jar 包,

 <!--mq客戶端-->
    <dependency>
      <groupId>com.rabbitmq</groupId>
      <artifactId>amqp-client</artifactId>
      <version>4.5.0</version>
    </dependency>
    <!--日誌-->
    <dependency>
      <groupId>org.slf4j</groupId>
      <artifactId>slf4j-log4j12</artifactId>
      <version>1.7.25</version>
    </dependency>
    <!--工具包-->
    <dependency>
      <groupId>org.apache.commons</groupId>
      <artifactId>commons-lang3</artifactId>
      <version>3.3.2</version>
    </dependency>
    <!--spring整合-->
    <dependency>
      <groupId>org.springframework.amqp</groupId>
      <artifactId>spring-rabbit</artifactId>
      <version>1.7.6.RELEASE</version>
    </dependency>
    <dependency>
      <groupId>org.springframework</groupId>
      <artifactId>spring-test</artifactId>
      <version>4.3.7.RELEASE</version>
    </dependency>
複製程式碼

(2): 建立日誌配置檔案,在 resources 下建立 log4j.properties,便於列印精確的日誌資訊

log4j.rootLogger=DEBUG,A1
log4j.logger.org.mybatis=DEBUG
log4j.appender.A1=org.apache.log4j.ConsoleAppender
log4j.appender.A1.layout=org.apache.log4j.PatternLayout
log4j.appender.A1.layout.ConversionPattern=%-d{yyyy-MM-dd HH:mm:ss,SSS} [%t] [%c]-%m%n
複製程式碼

(3): 編寫一個工具類,主要用於連線 RabbitMQ

package com.edu.util;


import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

/**
 * @ClassName ConnectionUtil
 * @Deccription 穿件連線的工具類
 * @Author DZ
 * @Date 2019/5/4 12:27
 **/
public class ConnectionUtil {
    /**
     * 建立連線工具
     *
     * @return
     * @throws Exception
     */
    public static Connection getConnection() throws Exception {
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("127.0.0.1");//MQ的伺服器
        connectionFactory.setPort(5672);//預設埠號
        connectionFactory.setUsername("test");
        connectionFactory.setPassword("test");
        connectionFactory.setVirtualHost("/test");
        return connectionFactory.newConnection();
    }
}

複製程式碼

專案總體圖如下:

java基礎(六):RabbitMQ 入門

4.1.Hello World模式

此模式非常簡單,一個生產者對應一個消費者

java基礎(六):RabbitMQ 入門
首先我們製造一個訊息生產者,併傳送訊息:

package com.edu.hello;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName Sender
 * @Deccription 建立傳送者
 * @Author DZ
 * @Date 2019/5/4 12:45
 **/
public class Sender {
    private final static String QUEUE = "testhello"; //佇列的名字

    public static void main(String[] srgs) throws Exception {
        //獲取連線
        Connection connection = ConnectionUtil.getConnection();
        //建立連線
        Channel channel = connection.createChannel();
        //宣告佇列
        //引數1:佇列的名字
        //引數2:是否持久化佇列,我們的佇列存在記憶體中,如果mq重啟則丟失。如果為ture,則儲存在erlang的資料庫中,重啟,依舊儲存
        //引數3:是否排外,我們連線關閉後是否自動刪除佇列,是否私有當前佇列,如果私有,其他佇列不能訪問
        //引數4:是否自動刪除
        //引數5:我們傳入的其他引數
        channel.queueDeclare(QUEUE, false, false, false, null);
        //傳送內容
        channel.basicPublish("", QUEUE, null, "要傳送的訊息".getBytes());
        //關閉連線
        channel.close();
        connection.close();
    }
}

複製程式碼

定義一個訊息接受者

package com.edu.hello;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.QueueingConsumer;

/**
 * @ClassName Recver
 * @Deccription 訊息接受者
 * @Author DZ
 * @Date 2019/5/4 12:58
 **/
public class Recver {
    private final static String QUEUE = "testhello";//訊息佇列的名稱

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        QueueingConsumer queueingConsumer = new QueueingConsumer(channel);
        //接受訊息,引數2表示自動確認訊息
        channel.basicConsume(QUEUE, true, queueingConsumer);
        while (true) {
            //獲取訊息
            QueueingConsumer.Delivery delivery = queueingConsumer.nextDelivery();//如果沒有訊息就等待,有訊息就獲取訊息,並銷燬,是一次性的
            String message = new String(delivery.getBody());
            System.out.println(message);
        }
    }
}

複製程式碼

此種模式屬於“點對點”模式,一個生產者、一個佇列、一個消費者,可以運用在聊天室(實際上真實的聊天室比這複雜很多,雖然是“點對點”模式,但是並不是一個生產者,一個佇列,一個消費者)

4.2.work queues

java基礎(六):RabbitMQ 入門
一個生產者對應多個消費者,但是隻有一個消費者獲得訊息

定義訊息製造者:

package com.edu.work;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName Sender
 * @Deccription 建立傳送者
 * @Author DZ
 * @Date 2019/5/4 12:45
 **/
public class Sender {
    private final static String QUEUE = "testhellowork"; //佇列的名字

    public static void main(String[] srgs) throws Exception {
        //獲取連線
        Connection connection = ConnectionUtil.getConnection();
        //建立連線
        Channel channel = connection.createChannel();
        //宣告佇列
        //引數1:佇列的名字
        //引數2:是否持久化佇列,我們的佇列存在記憶體中,如果mq重啟則丟失。如果為ture,則儲存在erlang的資料庫中,重啟,依舊儲存
        //引數3:是否排外,我們連線關閉後是否自動刪除佇列,是否私有當前佇列,如果私有,其他佇列不能訪問
        //引數4:是否自動刪除
        //引數5:我們傳入的其他引數
        channel.queueDeclare(QUEUE, false, false, false, null);
        //傳送內容
        for (int i = 0; i < 100; i++) {
            channel.basicPublish("", QUEUE, null, ("要傳送的訊息" + i).getBytes());
        }
        //關閉連線
        channel.close();
        connection.close();
    }
}

複製程式碼

定義2個訊息消費者

package com.edu.work;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;
import java.util.Queue;

/**
 * @ClassName Recver1
 * @Deccription 訊息接受者
 * @Author DZ
 * @Date 2019/5/4 12:58
 **/
public class Recver1 {
    private final static String QUEUE = "testhellowork";//訊息佇列的名稱

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //channel.basicQos(1);//告訴伺服器,當前訊息沒有確認之前,不要傳送新訊息,合理自動分配資源
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //收到訊息時候呼叫
                System.out.println("消費者1收到的訊息:" + new String(body));
                /*super.handleDelivery(consumerTag, envelope, properties, body);*/
                //確認訊息
                //引數2:false為確認收到訊息,ture為拒絕收到訊息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //註冊消費者
        // 引數2:手動確認,我們收到訊息後,需要手動確認,告訴伺服器,我們收到訊息了
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼
package com.edu.work;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription 訊息接受者
 * @Author DZ
 * @Date 2019/5/4 12:58
 **/
public class Recver2 {
    private final static String QUEUE = "testhellowork";//訊息佇列的名稱

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //channel.basicQos(1);//告訴伺服器,當前訊息沒有確認之前,不要傳送新訊息
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                //收到訊息時候呼叫
                System.out.println("消費者2收到的訊息:" + new String(body));
                /*super.handleDelivery(consumerTag, envelope, properties, body);*/
                //確認訊息
                //引數2:false為確認收到訊息,ture為拒絕收到訊息
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        //註冊消費者
        // 引數2:手動確認,我們收到訊息後,需要手動確認,告訴伺服器,我們收到訊息了
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼

這種模式是最簡單的 work 模式,訊息傳送者,迴圈傳送了100次訊息,列印結果如下:

java基礎(六):RabbitMQ 入門
java基礎(六):RabbitMQ 入門
可以看出,訊息消費者消費到的訊息是替換的,即一個訊息只被消費了一次,且兩個消費者各消費了50條訊息。這裡有個弊端,訊息消費者釋出訊息的時候,無論消費者的消費能力如何(電腦的記憶體等硬體),訊息只會均勻分佈給各個消費者(可以給2個消費者 sleep 下,結果還是這樣)。有沒有什麼方式可以讓訊息自動分配(按照電腦的硬體,能者多勞),答案是可以的,只需要增加 channel.basicQos(1);

java基礎(六):RabbitMQ 入門
此方案可以用來進行負載均衡,搶紅包等場景

4.3.public模式

java基礎(六):RabbitMQ 入門
一個消費者將訊息首先傳送到交換器,交換器繫結到多個佇列,然後被監聽該佇列的消費者所接收並消費。X 表示交換器,在 RabbitMQ 中,交換器主要有四種型別: direct、fanout、topic、headers,這裡的交換器是 fanout,其它型別的交換機自行谷歌,主要區別是交換機的匹配方式發生了變化

定義訊息釋出者

package com.edu.publish;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName Sender
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:43
 **/
public class Sender {
    private final static String EXCHANGE_NAME = "testexchange";//定義交換機名字

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //宣告交換機
        //定義一個交換機,型別為fanout,也就是釋出訂閱者模式
        channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
        //釋出訂閱模式,因為訊息是先發布到交換機中,而交換機是沒有儲存功能的,所以如果沒有消費者,訊息會丟失
        channel.basicPublish(EXCHANGE_NAME, "", null, "釋出訂閱模式的訊息".getBytes());
        channel.close();
        connection.close();
    }
}

複製程式碼

定義2個訊息消費者

package com.edu.publish;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver1 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testexchange";
    private final static String QUEUE = "testpubqueue1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        channel.queueBind(QUEUE, EXCHANGE_NAME, "");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者1:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼
package com.edu.publish;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver2 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testexchange";
    private final static String QUEUE = "testpubqueue2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        channel.queueBind(QUEUE, EXCHANGE_NAME, "");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者2:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼

消費者1 和消費者2 都監聽了被同一個交換器繫結的佇列,因此訊息被同時消費到了。如果訊息傳送到沒有佇列繫結的交換器時,訊息將丟失,因為交換器沒有儲存訊息的能力,訊息只能儲存在佇列中。

應用場景:比如一個商城系統需要在管理員上傳商品新的圖片時,前臺系統必須更新圖片,日誌系統必須記錄相應的日誌,那麼就可以將兩個佇列繫結到圖片上傳交換器上,一個用於前臺系統更新圖片,另一個用於日誌系統記錄日誌。

4.4.routing

java基礎(六):RabbitMQ 入門
生產者將訊息傳送到 direct 交換器,在繫結佇列和交換器的時候有一個路由 key,生產者傳送的訊息會指定一個路由 key,那麼訊息只會傳送到相應 key 相同的佇列,接著監聽該佇列的消費者消費訊息。

定義訊息釋出者

package com.edu.route;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName Sender
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 15:05
 **/
public class Sender {
    private final static String EXCANGE_NAME = "testroute";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //定義路由格式的交換機
        channel.exchangeDeclare(EXCANGE_NAME, "direct");
        channel.basicPublish(EXCANGE_NAME, "key2", null, "路由模式的訊息".getBytes());
        channel.close();
        connection.close();
    }
}

複製程式碼
package com.edu.route;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver1 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testroute";
    private final static String QUEUE = "testroute1queue";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        //引數3:繫結到交換機指定的路由的名字
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key1");
        //如果需要繫結多個路由,再繫結一次即可
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key2");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者1:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼
package com.edu.route;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver2 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testroute";
    private final static String QUEUE = "testroute2queue";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        //引數3:繫結到交換機指定的路由的名字
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key1");
        //如果需要繫結多個路由,再繫結一次即可
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key3");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者2:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼

應用場景:利用消費者能夠有選擇性的接收訊息的特性,比如我們商城系統的後臺管理系統對於商品進行修改、刪除、新增操作都需要更新前臺系統的介面展示,而查詢操作確不需要,那麼這兩個佇列分開接收訊息就比較好。

4.5.Topic

java基礎(六):RabbitMQ 入門
上面的路由模式是根據路由key進行完整的匹配(完全相等才傳送訊息),這裡的萬用字元模式通俗的來講就是模糊匹配。符號 “#” 表示匹配一個或多個詞,符號 “*” 表示匹配一個詞。實際上 Topic 模式是 routing 模式的擴充套件

package com.edu.topic;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName Sender
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 15:19
 **/
public class Sender {
    private final static String EXCANGE_NAME = "testtopexchange";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        channel.exchangeDeclare(EXCANGE_NAME, "topic");
        channel.basicPublish(EXCANGE_NAME, "abc.adb.1", null, "topic模式訊息傳送者:".getBytes());
        channel.close();
        connection.close();
    }
}

複製程式碼
package com.edu.topic;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver1 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testtopexchange";
    private final static String QUEUE = "testtopic1queue";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        //引數3:繫結到交換機指定的路由的名字
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key.*");
        //如果需要繫結多個路由,再繫結一次即可
        channel.queueBind(QUEUE, EXCHANGE_NAME, "abc.*");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者1:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼
package com.edu.topic;

import com.edu.util.ConnectionUtil;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @ClassName Recver1
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 14:49
 **/
public class Recver2 {
    //定義交換機
    private final static String EXCHANGE_NAME = "testtopexchange";
    private final static String QUEUE = "testtopic2queue";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        final Channel channel = connection.createChannel();
        channel.queueDeclare(QUEUE, false, false, false, null);
        //繫結佇列到交換機
        //引數3:繫結到交換機指定的路由的名字
        channel.queueBind(QUEUE, EXCHANGE_NAME, "key.*");
        //如果需要繫結多個路由,再繫結一次即可
        channel.queueBind(QUEUE, EXCHANGE_NAME, "abc.#");
        channel.basicQos(1);
        DefaultConsumer defaultConsumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                /* super.handleDelivery(consumerTag, envelope, properties, body);*/
                System.out.println("消費者2:" + new String(body));
                channel.basicAck(envelope.getDeliveryTag(), false);
            }
        };
        channel.basicConsume(QUEUE, false, defaultConsumer);
    }
}

複製程式碼

第六種模式是將上述的模式整合其它的框架,進行遠端訪問,這裡我們將整合 Spring 實現 RCP 遠端模式的使用

5.Spring 整合 RabbitMQ

5.1.自動整合 Spring

編寫spring的配置,此配置檔案的目的是將 Spring 與 RabbitMQ 進行整合,實際上就是將 MQ 的相關資訊(連線,佇列,交換機……)通過XML配置的方式實現

<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/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit-1.7.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd">
    <!--定義連線工廠-->
    <rabbit:connection-factory id="connectionFactory" host="127.0.0.1" port="5672" username="test" password="test"
                               virtual-host="/test"/>
    <!--
     定義模板
     第三個引數,決定訊息傳送到哪裡,如果為exchange,則傳送到交換機;如果為queue,則傳送到佇列
    -->
    <rabbit:template id="template" connection-factory="connectionFactory" exchange="fanoutExchange"/>
    <rabbit:admin connection-factory="connectionFactory"/>
    <!--定義佇列-->
    <rabbit:queue name="myQueue" auto-declare="true"/>
    <!--定義交換機-->
    <rabbit:fanout-exchange name="fanoutExange" auto-declare="true">
        <!--將訊息繫結到交換機-->
        <rabbit:bindings>
            <rabbit:binding queue="myQueue">

            </rabbit:binding>
        </rabbit:bindings>
    </rabbit:fanout-exchange>
    <!--定義監聽器,收到訊息會執行-->
    <rabbit:listener-container connection-factory="connectionFactory">
       <!-- 定義監聽的類和方法-->
        <rabbit:listener ref="consumer" method="test" queue-names="myQueue"/>
    </rabbit:listener-container>
    <!--定義消費者-->
    <bean id="consumer" class="com.edu.spring.MyConsumer"/>

</beans>
複製程式碼

生產者:

package com.edu.spring;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.ApplicationContext;
import org.springframework.context.support.ClassPathXmlApplicationContext;

/**
 * @ClassName SpringTest
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 18:40
 **/
public class SpringTest {
    public static void main(String[] args) throws Exception {
        ApplicationContext applicationContext = new ClassPathXmlApplicationContext("classpath:applicationContext.xml");
        RabbitTemplate rabbitTemplate = applicationContext.getBean(RabbitTemplate.class);
        rabbitTemplate.convertAndSend("Spring的訊息");
        ((ClassPathXmlApplicationContext) applicationContext).destroy();
    }
}

複製程式碼

消費者

package com.edu.spring;

/**
 * @ClassName MyConsumer
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 18:35
 **/
public class MyConsumer {
    /*用於接收訊息*/
    public void test(String message) {
        System.err.println(message);
    }
}

複製程式碼

整合Spring主要是在xml中實現了佇列和交換機的建立。

java基礎(六):RabbitMQ 入門
最好能理解上面的圖。理解後,以後寫相關的程式碼,直接去網上 copy 一份配置檔案,然後根據自己專案的情況進行修改。如果不能理解,就不知道如何修改出現錯誤後不知道錯誤出現在什麼地方。

5.2.手動模式

手動模式,主要增加MQ的回撥操作,MQ訊息失敗或者成功就有相應的回撥資訊,增強系統的健壯性,一旦產生異常,很快就能定位到異常的位置,所以在實際開發中,一般都這種方式

建立xml配置檔案

<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"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/rabbit
       http://www.springframework.org/schema/rabbit/spring-rabbit-1.7.xsd
       http://www.springframework.org/schema/beans
       http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
       http://www.springframework.org/schema/context
       http://www.springframework.org/schema/context/spring-context-4.3.xsd">
    <context:component-scan base-package="com.edu"/>
    <bean id="jsonMessageConverter" class="org.springframework.amqp.support.converter.Jackson2JsonMessageConverter"/>

    <!--
    定義連線工廠
    publisher-confirms為ture,確認失敗等回撥才會執行
    -->
    <rabbit:connection-factory id="connectionFactory" host="127.0.0.1" port="5672" username="test" password="test"
                               virtual-host="/test" publisher-confirms="true"/>

    <rabbit:admin connection-factory="connectionFactory"/>
    <rabbit:template id="amqpTemplate" connection-factory="connectionFactory" confirm-callback="confirmCallBackListener"
                     return-callback="returnCallBackListener"
                     mandatory="true"/>
    <!--定義佇列-->
    <rabbit:queue name="myQueue" auto-declare="true"/>
    <!--定義交換機-->
    <rabbit:direct-exchange name="DIRECT_EX" id="DIRECT_EX">
        <!--將訊息繫結到交換機-->
        <rabbit:bindings>
            <rabbit:binding queue="myQueue">

            </rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>
    <!--定義監聽器,收到訊息會執行-->
    <rabbit:listener-container connection-factory="connectionFactory" acknowledge="manual">
        <!-- 定義監聽的類和方法-->
        <rabbit:listener queues="myQueue" ref="receiveConfirmTestListener"/>
    </rabbit:listener-container>

</beans>
複製程式碼

建立回撥監聽函式

package com.edu.spring2;

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.rabbit.support.CorrelationData;
import org.springframework.stereotype.Component;

/**
 * @ClassName ConfirmCallBackListener
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 22:26
 **/
@Component("confirmCallBackListener")
public class ConfirmCallBackListener implements RabbitTemplate.ConfirmCallback {

    @Override
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        System.out.println("確認回撥 ack==" + ack + "回撥原因==" + cause);
    }
}

複製程式碼
package com.edu.spring2;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * @ClassName ReceiveConfirmTestListener
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 22:24
 **/
@Component("receiveConfirmTestListener")
public class ReceiveConfirmTestListener implements ChannelAwareMessageListener {
    /**
     * 收到訊息時,執行的監聽
     *
     * @param message
     * @param channel
     * @throws Exception
     */
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println(("消費者收到了訊息" + message));
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
    }
}

複製程式碼
package com.edu.spring2;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.stereotype.Component;

/**
 * @ClassName ReturnCallBackListener
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 22:28
 **/
@Component("returnCallBackListener")
public class ReturnCallBackListener implements RabbitTemplate.ReturnCallback {
    @Override
    public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
        System.out.println("失敗回撥" + message);
    }
}

複製程式碼

回撥函式的配置來自 XML

java基礎(六):RabbitMQ 入門
建立傳送訊息的工具類

package com.edu.spring2;

import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * @ClassName PublicUtil
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 22:30
 **/
@Component("publicUtil")
public class PublicUtil {
    @Autowired
    private AmqpTemplate amqpTemplate;

    public void send(String excange, String routingkey, Object message) {
        amqpTemplate.convertAndSend(excange, routingkey, message);
    }
}

複製程式碼

建立測試類

package com.edu.spring2;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @ClassName TestMain
 * @Deccription TODO
 * @Author DZ
 * @Date 2019/5/4 22:32
 **/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = {"classpath:applicationContext2.xml"})
public class TestMain {
    @Autowired
    private PublicUtil publicUtil;
    private static String exChange = "DIRECT_EX";//交換機
    private static String queue = "myQueue";

    /**
     * exChange和queue均正確
     * confirm會執行,ack = ture
     * 訊息正常接收(接收訊息確認方法正常執行)
     * @throws Exception
     */
    @Test
    public void test1() throws Exception {
        publicUtil.send(exChange, queue, "測試1,佇列和交換機均正確");
    }
    /**
     * exChange錯誤,queue正確
     * confirm執行,ack=false
     * 訊息無法接收(接收訊息確認方法不能執行)
     * @throws Exception
     */
    @Test
    public void test2() throws Exception {
        publicUtil.send(exChange + "1", queue, "測試2,佇列正確,交換機錯誤");
    }
    /**
     * exChange正常,queue錯誤
     * return執行
     * confirm執行,ack=ture
     * @throws Exception
     */
    @Test
    public void test3() throws Exception {
        publicUtil.send(exChange, queue + "1", "測試2,佇列錯誤,交換機正確");
    }

    /**
     * exChange錯誤,queue錯誤
     * confirm執行,ack=false
     * @throws Exception
     */
    @Test
    public void test4() throws Exception {
        publicUtil.send(exChange + "1", queue + "1", "測試2,佇列錯誤,交換機錯誤");
    }
}


複製程式碼

測試結果如下:

  • test1:exChange和queue均正確

    java基礎(六):RabbitMQ 入門
    confirm會執行,ack=ture;能正常收到訊息(接收訊息的方法正常執行)

  • test2:exChange錯誤,queue正確

java基礎(六):RabbitMQ 入門
confirm執行,ack=false;不能正常接收到訊息

  • test3:exChange正確,queue錯誤

java基礎(六):RabbitMQ 入門
confirm執行,ack=ture;return執行;不能接收到訊息

  • test4:exChange和queue均錯誤

java基礎(六):RabbitMQ 入門
confirm執行,ack=false;不能接收訊息

上述結論及程式碼如下圖:

java基礎(六):RabbitMQ 入門

根據上述的測試結果,我們可以根據回撥函式的返回結果,檢視MQ的錯誤出現在那裡。根據上述結論,我們可以對3個回撥函式做如下處理:

  • 類 ReceiveConfirmTestListener 中的onMessage方法主要用於接收從 RabbitMQ 推送過來的訊息,並對訊息做相應的邏輯處理

  • 類 ConfirmCallBackListener 中的 confirm 方法主要用於檢查交換機(exChange),當 ack=false,交換機可能錯誤

  • 類 ReturnCallBackListener 中的 returnedMessage 方法用於檢查佇列(queue),當此方法執行時,佇列可能錯誤

java基礎(六):RabbitMQ 入門
所以3個相應的方法可以做如下調整:

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門

java基礎(六):RabbitMQ 入門

實際上,在真實專案中,上面3個方法也是按照這3個邏輯進行設計的。當然這3個方法中還可以加入更多的日誌訊息,和邏輯處理業務。

6.參考

blog.csdn.net/liu911025/a…

blog.csdn.net/lyhkmm/arti…

blog.csdn.net/vbirdbest/a…

blog.csdn.net/cairuojin/a…

www.rabbitmq.com/getstarted.…