練手項目2筆記之day05

1. 頁面發佈

1. 需求分析

在這裏插入圖片描述

業務流程如下:

  1. 管理員進入管理界面點擊“頁面發佈”,前端請求cms頁面發佈接口
  2. cms頁面發佈接口執行頁面靜態化,並將靜態化頁面(html文件)存儲至GridFS中
  3. 靜態化成功後,向消息隊列發送頁面發佈的消息。
  4. 消息隊列負責將消息發送給各個服務器上部署的Cms Client(Cms客戶端)。
  5. 每個接收到頁面發佈消息的Cms Client從GridFS獲取Html頁面文件,並將Html文件存儲在本地服務器

2. RabbitMQ研究

消息隊列將頁面發佈的消息通知給各個服務器 ,具體見第二部分

2. RabbitMQ研究

1. 介紹

1. RabbitMQ

MQ全稱Message Queue,消息隊列,RabbitMQ由erlang語言開發,基於AMQP(Advanced Message Queue,高級消息隊列協議)協議實現的消息隊列,是一種應用程序之間的通信方法,在分佈式開發中應用廣泛。

應用場景:1. 任務異步處理,提高應用程序的響應時間;2.應用程序解耦合

RabbitMQ的優勢:

  1. 使用簡單,功能強大
  2. 基於AMQP協議
  3. 社區活躍,文檔完善
  4. 高併發性能好,得益於Erlang語言
  5. Spring Boot默認集成RabbitMQ

2. 其他相關知識

AMQP

AMQP是一套公開的消息隊列協議,旨在從協議層定義消息通信數據的標準格式,爲了解決MQ市場上協議不統一的問題。RabbitMQ就是遵循AMQP標準協議開發的MQ服務。

JMS

java提供的一套消息服務API標準,爲了所有的java應用提供統一的消息通信的標準,類似jdbc,和AMQP的區別:jms是java語言專屬的消息服務標準,api層定義標準,只能用於java應用;AMQP是在協議層定義的標準,跨語言。

3. 快速入門

1. RabbitMQ的工作原理

下圖是RabbitMQ的基本結構

在這裏插入圖片描述
組成部分說明:

  • Broker:消息隊列服務進程,包括兩部分Exchange和Queue
  • Exchange: 消息隊列交換機,按一定規則將消息路由轉發到某個隊列,對消息過濾。
  • Queue:消息隊列,存儲消息的隊列,消息到達隊列並轉發給指定的消費方
  • Producer:消息生產者,即生產方客戶端,生產方客戶端將消息發送給MQ
  • Consumer:消息消費者,即消費方客戶端,接收MQ轉發的消息

消息發佈接收流程:

  • 發送消息
    1. 生產者和Broker建立TCP連接
    2. 生產者和Broker建立channel通道
    3. 生產者通過通道消息發送給Broker,由Exchange將消息轉發
    4. Exchange將消息轉發到指定的Queue(隊列)
  • 接收消息
    1. 消費者和Broker建立TCP連接
    2. 消費者和Broker建立channel通道
    3. 消費者監聽指定的Queue(隊列)
    4. 當有消息到達Queue時Broker默認將消息 推送給消費者
    5. 消費者接收到消息

2. 下載安裝

RabbitMQ由Erlang語言開發,Erlang語言用於併發及分佈式系統的開發,在電信領域應用廣泛,OTP(Open Telecom Platform)作爲Erlang語言的一部分,包含了很多基於Erlang開發的中間件及工具庫,安裝RabbitMQ需要安裝Erlang/OTP,並保持版本匹配

本項目使用Erlang/OTP 20.3版本和RabbitMQ3.7.3版本

1. 下載安裝

  1. erlang的下載

erlang安裝完成需要配置erlang環境變量: ERLANG_HOME=D:\Program Files\erl9.3 在path中添加%ERLANG_HOME%\bin;

  1. RabbitMQ 的安裝

2. 啓動

安裝成功後會自動創建RabbitMQ服務並且啓動

  1. 安裝並運行服務 rabbitmq-service.bat install 安裝服務 rabbitmq-service.bat stop 停止服務 rabbitmq-service.bat start 啓動服務
  2. 安裝管理插件 安裝rabbitMQ的管理插件,方便在瀏覽器端管理RabbitMQ 管理員身份運行 rabbitmq-plugins.bat enable rabbitmq_management
  3. 啓動成功 登錄RabbitMQ 進入瀏覽器,輸入:http://localhost:15672 初始賬號和密碼:guest/guest

2. hello world

1. 搭建環境

  1. 創建maven工程 創建生產者工程和消費者工程,分別加入RabbitMQ java client的依賴

生產者和消費者都屬於客戶端,rabbitMQ的java客戶端如下:

test-rabbitmq-producer 生產者工程

test-rabbitmq-consumer :消費者工程

<dependencies>
    <dependency>
        <!--此版本與spring boot 1.5.9版本匹配-->
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>4.0.3</version>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
</dependencies>

2. 生產者

在生產者工程的test中創建測試類

public class Producer01 {

  // 隊列
  private static final String QUEUE = "helloworld";

  public static void main(String[] args) {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 聲明隊列
      // 參數
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 隊列名稱
             *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
             *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
             *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
             *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
             */
      channel.queueDeclare(QUEUE,true,false,false,null);
      // 發送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交換機,如果不指定使用mq的默認交換機
             *      2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
             *      3. props:消息屬性
             *      4. body:消息內容
             */
      String message = "hello world 黑馬程序員";
      channel.basicPublish("",QUEUE,null,message.getBytes());
      System.out.println("send to mq..........");

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先關閉通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}

3. 消費者

消費者工程下的test創建測試類

public class Consumer01 {
  // 隊列
  private static final String QUEUE = "helloworld";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = connectionFactory.newConnection();
    // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 監聽隊列
    // 聲明隊列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 隊列名稱
         *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
         *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
         *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
         *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
         */
    channel.queueDeclare(QUEUE,true,false,false,null);

    // 監聽隊列
    // 實現消費方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法將被調用

      /**
             *
             * @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
             * @param envelope 信封,通過envelope
             * @param properties 消息屬性
             * @param body 消息內容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交換機
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息內容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 參數:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:隊列名稱
         *      2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
         *      3. callback: 消費方法,當消費者接受到消息要執行的方法
         */
    channel.basicConsume(QUEUE,true,defaultConsumer);
  }
}

4. 總結

  • 發送端操作流程
    1. 創建連接
    2. 創建通道
    3. 聲明隊列
    4. 發送消息
  • 接收端
    1. 創建連接
    2. 創建通道
    3. 聲明隊列
    4. 監聽隊列
    5. 接收消息
    6. ack回覆

4. 工作模式

有以下幾種工作模式

  1. Work queues
  2. Publish/Subscribe
  3. Routing
  4. Topics
  5. Header
  6. RPC

1. Work queues

在這裏插入圖片描述
與入門程序相比,多了個消費端,兩個消費端共同消費同一個隊列中的消息

應用場景:對於任務過重或任務較多情況使用工作隊列提高任務處理的速度

測試:啓動多個消費者,生產者發送多個消息

結果:

  1. 一條消息只會被一個消費者接收
  2. rabbitMQ採用輪詢的方式將消息平均發送給消費者
  3. 消費者在處理完某條消息後,纔會收到下一條消息

2. Publish/subscribe

1. 工作模式

在這裏插入圖片描述
發佈訂閱模式:

  1. 每個消費者監聽自己的隊列
  2. 生產者將消息發給broker,由交換機將消息轉發到綁定此交換機的每個隊列,每個綁定交換機的隊列都將收到消息

2. 代碼

案例:用戶充值成功或轉賬完成系統通知用戶,通知方式有短信和微信公衆號等方法。

  1. 生產者
public class Producer02_publish {

  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";

  public static void main(String[] args) {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 聲明隊列
      // 參數
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 隊列名稱
             *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
             *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
             *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
             *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 聲明交換機
      /**
             *  參數:
             *  String exchange 交換機的名稱
             *  String type 交換機的類型
             *      1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 對應的routing工作模式
             *      3. topic: 對應的topics工作模式
             *      4. headers:對應headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_FANOUT_INFORM, BuiltinExchangeType.FANOUT);
      // 交換機和隊列綁定
      /**
             *  參數 String queue, String exchange, String routingKey
             *     1. queue 隊列名稱
             *     2. exchange 交換機名稱
             *     3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_FANOUT_INFORM,"");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_FANOUT_INFORM,"");
      // 發送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交換機,如果不指定使用mq的默認交換機
             *      2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
             *      3. props:消息屬性
             *      4. body:消息內容
             */
      for (int i = 0; i < 5; i++) {
        String message = "send inform message to user";
        channel.basicPublish(EXCHANGE_FANOUT_INFORM,"",null,message.getBytes());
        System.out.println("send to mq..........");
      }


    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先關閉通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 消費者,郵件消費者
public class Consumer02_subscribe_email {
  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_FANOUT_INFORM = "exchange_fanout_inform";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = connectionFactory.newConnection();
    // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 監聽隊列
    // 聲明隊列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 隊列名稱
         *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
         *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
         *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
         *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);

    // 監聽隊列
    // 實現消費方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法將被調用

      /**
             *
             * @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
             * @param envelope 信封,通過envelope
             * @param properties 消息屬性
             * @param body 消息內容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交換機
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息內容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 參數:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:隊列名稱
         *      2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
         *      3. callback: 消費方法,當消費者接受到消息要執行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}
  1. 短信發送消費者,同上面類似

3. 測試

生產者發送的消息,每條都轉發到了各個隊列,每個消費者都接收到消息

4. publish/subscribe 和 work queues的區別

區別:

  1. work queues不用定義交換機,而publish/subscribe需要定義交換機
  2. publish/subscribe的生產方是面向交換機發送消息,work queues的生產方是面向隊列發送消息(底層使用默認交換機)
  3. publish/subscribe需要設置隊列和交換機的綁定,work queues不需要設置,實質上work queues會將隊列綁定到默認的交換機

相同點:

兩者實現的發佈/訂閱的效果是一樣的,多個消費端監聽同一個隊列不會重複消費消息

**建議使用 publish/subscribe,**發佈訂閱模式比工作隊列模式更強大,並且發佈訂閱模式可以指定自己專用的交換機。

3. Routing

1. 工作模式

在這裏插入圖片描述
路由模式:

  1. 每個消費者監聽自己的隊列,並且設置routingkey
  2. 生產者將消息發給交換機,由交換機根據routingkey轉發消息給指定隊列

2. 代碼

  1. 生產者

聲明exchange_routing_inform交換機

聲明兩個隊列並且綁定到此交換機,綁定時需要指定routingkey

發送消息時需要指定routingkey

public class Producer03_routing {

  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
  private static final String ROUTINGKEY_EMAIL = "inform_email";
  private static final String ROUTINGKEY_SMS = "inform_sms";

  public static void main(String[] args) {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 聲明隊列
      // 參數
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 隊列名稱
             *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
             *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
             *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
             *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 聲明交換機
      /**
             *  參數:
             *  String exchange 交換機的名稱
             *  String type 交換機的類型
             *      1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 對應的routing工作模式
             *      3. topic: 對應的topics工作模式
             *      4. headers:對應headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
      // 交換機和隊列綁定
      /**
             *  參數 String queue, String exchange, String routingKey
             *     1. queue 隊列名稱
             *     2. exchange 交換機名稱
             *     3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS);
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_ROUTING_INFORM,"inform");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,"inform");
      // 發送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交換機,如果不指定使用mq的默認交換機
             *      2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
             *      3. props:消息屬性
             *      4. body:消息內容
             */
      /* for (int i = 0; i < 5; i++) {
                String message = "send email inform message to user";
                channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL,null,message.getBytes());
                System.out.println("send to mq..........");
            }
            for (int i = 0; i < 5; i++) {
                String message = "send sms inform message to user";
                channel.basicPublish(EXCHANGE_ROUTING_INFORM,ROUTINGKEY_SMS,null,message.getBytes());
                System.out.println("send to mq..........");
            }*/
      for (int i = 0; i < 5; i++) {
        String message = "send inform message to user";
        channel.basicPublish(EXCHANGE_ROUTING_INFORM,"inform",null,message.getBytes());
        System.out.println("send to mq..........");
      }

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先關閉通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 郵件發送消費者
public class Consumer03_routing_email {
  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_ROUTING_INFORM = "exchange_routing_inform";
  private static final String ROUTINGKEY_EMAIL = "inform_email";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = connectionFactory.newConnection();
    // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 監聽隊列
    // 聲明隊列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 隊列名稱
         *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
         *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
         *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
         *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
    // 聲明交換機
    /**
         *  參數:
         *  String exchange 交換機的名稱
         *  String type 交換機的類型
         *      1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
         *      2. direct: 對應的routing工作模式
         *      3. topic: 對應的topics工作模式
         *      4. headers:對應headers工作模式
         */
    channel.exchangeDeclare(EXCHANGE_ROUTING_INFORM, BuiltinExchangeType.DIRECT);
    // 綁定隊列
    channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_ROUTING_INFORM,ROUTINGKEY_EMAIL);
    // 監聽隊列
    // 實現消費方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法將被調用

      /**
             *
             * @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
             * @param envelope 信封,通過envelope
             * @param properties 消息屬性
             * @param body 消息內容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交換機
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息內容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 參數:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:隊列名稱
         *      2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
         *      3. callback: 消費方法,當消費者接受到消息要執行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}
  1. 短信發送消費者,類似上面郵件的代碼

3. 測試

打開RabbitMQ的管理界面,觀察交換機綁定情況

有routingkey,根據routingkey轉發消息到指定的隊列

4. 思考

Routing模式和Publish/subscibe的區別: Routing模式要求隊列在綁定交換機時要指定routingkey,消息會轉發到符合routingkey的隊列。

4. topics

1. 工作模式

在這裏插入圖片描述
路由模式:

  1. 每個消費者監聽自己的隊列,並且設置帶通配符的routingkey
  2. 生產者將消息發給broker,由交換機根據routingkey轉發消息到指定隊列

2. 代碼

案例:

根據用戶的通知設置去通知用戶,設置接收Email的用戶只接收email,設置接收sms的用戶只接收sms,設置兩種都接收的則兩種都有效。

  1. 生產者

聲明交換機,指定topic類型

public class Producer04_topics {

  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
  private static final String ROUTINGKEY_SMS = "inform.#.sms.#";

  public static void main(String[] args) {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = null;
    Channel channel = null;
    try {
      connection = connectionFactory.newConnection();
      // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
      channel = connection.createChannel();
      // 聲明隊列
      // 參數
      /**
             * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
             *      1. queue: 隊列名稱
             *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
             *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
             *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
             *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
             */
      channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
      channel.queueDeclare(QUEUE_INFORM_SMS,true,false,false,null);
      // 聲明交換機
      /**
             *  參數:
             *  String exchange 交換機的名稱
             *  String type 交換機的類型
             *      1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
             *      2. direct: 對應的routing工作模式
             *      3. topic: 對應的topics工作模式
             *      4. headers:對應headers工作模式
             */
      channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
      // 交換機和隊列綁定
      /**
             *  參數 String queue, String exchange, String routingKey
             *     1. queue 隊列名稱
             *     2. exchange 交換機名稱
             *     3. routingKey 路由key,作用是交換機根據路由key的值將消息轉發到指定的隊列,發佈訂閱莫斯設爲空字符串
             */
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_SMS);
      channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_TOPICS_INFORM,"inform");
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
      channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,"inform");
      // 發送消息
      /**
             *  String exchange, String routingKey, BasicProperties props, byte[] body
             *      1. exchange:交換機,如果不指定使用mq的默認交換機
             *      2. routingKey:路由key,交換機根據路由key將消息轉發到指定的隊列,如果使用默認交換機,routingKey設置爲隊列的名稱
             *      3. props:消息屬性
             *      4. body:消息內容
             */
      for (int i = 0; i < 5; i++) {
        String message = "send email inform message to user";
        channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.email",null,message.getBytes());
        System.out.println("send to mq..........");
      }
      /* for (int i = 0; i < 5; i++) {
                String message = "send sms inform message to user";
                channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms",null,message.getBytes());
                System.out.println("send to mq..........");
            }*/
      for (int i = 0; i < 5; i++) {
        String message = "send sms and email inform message to user";
        channel.basicPublish(EXCHANGE_TOPICS_INFORM,"inform.sms.email",null,message.getBytes());
        System.out.println("send to mq..........");
      }

    } catch (Exception e) {
      e.printStackTrace();
    } finally {

      // 先關閉通道
      try {
        channel.close();
      } catch (IOException e) {
        e.printStackTrace();
      } catch (TimeoutException e) {
        e.printStackTrace();
      }
      try {
        connection.close();
      } catch (IOException e) {
        e.printStackTrace();
      }
    }
  }
}
  1. 消費端

隊列綁定交換機指定通配符

通配符規則:

  • 中間以. 分隔
  • 符號# 可以匹配多個詞,符號* 可以匹配一個詞
public class Consumer04_topics_email {
  // 隊列
  private static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  private static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  private static final String ROUTINGKEY_EMAIL = "inform.#.email.#";

  public static void main(String[] args) throws IOException, TimeoutException {
    // 通過連接工廠創建新的連接和mq建立連接
    ConnectionFactory connectionFactory = new ConnectionFactory();
    connectionFactory.setHost("127.0.0.1");
    connectionFactory.setPort(5672);
    connectionFactory.setUsername("guest");
    connectionFactory.setPassword("guest");
    // 設置虛擬機,一個mq服務可以設置多個虛擬機,每個虛擬機相當於一個獨立的mq
    connectionFactory.setVirtualHost("/");
    // 建立新連接
    Connection connection = connectionFactory.newConnection();
    // 創建會話通道,生產者和mq服務所有通信都在channel通道中完成
    Channel channel = connection.createChannel();
    // 監聽隊列
    // 聲明隊列
    /**
         * String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
         *      1. queue: 隊列名稱
         *      2. durable:是否持久化,如果持久化,mq重啓後隊列還在
         *      3. exclusive:是否獨佔連接,隊列只允許在該鏈接中訪問,如果連接關閉後,隊列自動刪除,可用於臨時隊列
         *      4. autoDelete:自動刪除,隊列不再使用時是否自動刪除
         *      5. arguments:參數,設置一個隊列的擴展參數,如存活時間
         */
    channel.queueDeclare(QUEUE_INFORM_EMAIL,true,false,false,null);
    // 聲明交換機
    /**
         *  參數:
         *  String exchange 交換機的名稱
         *  String type 交換機的類型
         *      1. fanout: 對應的rabbitmq的工作模式是 publish/subscribe
         *      2. direct: 對應的routing工作模式
         *      3. topic: 對應的topics工作模式
         *      4. headers:對應headers工作模式
         */
    channel.exchangeDeclare(EXCHANGE_TOPICS_INFORM, BuiltinExchangeType.TOPIC);
    // 綁定隊列
    channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_TOPICS_INFORM,ROUTINGKEY_EMAIL);
    // 監聽隊列
    // 實現消費方法
    DefaultConsumer defaultConsumer = new DefaultConsumer(channel){
      // 接收到消息,此方法將被調用

      /**
             *
             * @param consumerTag 消費者標籤,標識消費者,在監聽隊列時設置channel.basicConsume
             * @param envelope 信封,通過envelope
             * @param properties 消息屬性
             * @param body 消息內容
             * @throws IOException
             */
      @Override
      public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
        // 交換機
        String exchange = envelope.getExchange();
        // 消息id,mq在channel中用來標識消息的id,可用於確認消息已接收
        long deliveryTag = envelope.getDeliveryTag();
        // 消息內容
        String message = new String(body,"utf-8");
        System.out.println("receive message: "+message);
      }
    };
    // 參數:
    /**
         *  String queue, boolean autoAck, Consumer callback
         *      1. queue:隊列名稱
         *      2. autoAck: 自動回覆,當消費者接收到消息後要告訴mq消息已接收,如果將此參數設置爲true會自動回覆mq,false需要編程實現回覆
         *      3. callback: 消費方法,當消費者接受到消息要執行的方法
         */
    channel.basicConsume(QUEUE_INFORM_EMAIL,true,defaultConsumer);
  }
}

3. 測試

生產者發送若干條消息,交換機根據routekey通配符匹配並轉發到指定的隊列

4. 思考

需求可否由routing工作模式實現?

使用routing模式也可以實現,但是過程比topics複雜

topic模式更加強大,可以實現routing、publish/subscribe模式的功能

5. header模式

header模式取消routingkey,使用header中的key/value匹配隊列

案例:

根據用戶的通知設置去通知用戶,設置接收Email的用戶只接收Email,設置接收sms的用戶只接收sms,設置兩種通知類型都接收的則兩種通知都有效。

代碼:

  1. 生產者

隊列和交換機的綁定的代碼

Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_type", "email");
Map<String, Object> headers_sms = new Hashtable<String, Object>();
headers_sms.put("inform_type", "sms");
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
channel.queueBind(QUEUE_INFORM_SMS,EXCHANGE_HEADERS_INFORM,"",headers_sms);

通知

String message = "email inform to user"+i;
Map<String,Object> headers = new Hashtable<String, Object>();
headers.put("inform_type", "email");//匹配email通知消費者綁定的header
//headers.put("inform_type", "sms");//匹配sms通知消費者綁定的header
AMQP.BasicProperties.Builder properties = new AMQP.BasicProperties.Builder();
properties.headers(headers);
//Email通知
channel.basicPublish(EXCHANGE_HEADERS_INFORM, "", properties.build(), message.getBytes());
  1. 消費者
channel.exchangeDeclare(EXCHANGE_HEADERS_INFORM, BuiltinExchangeType.HEADERS);
Map<String, Object> headers_email = new Hashtable<String, Object>();
headers_email.put("inform_email", "email");
//交換機和隊列綁定
channel.queueBind(QUEUE_INFORM_EMAIL,EXCHANGE_HEADERS_INFORM,"",headers_email);
//指定消費隊列
channel.basicConsume(QUEUE_INFORM_EMAIL, true, consumer);

3. 測試

6. PRC

在這裏插入圖片描述

RPC即客戶端遠程調用服務端的方法 ,使用MQ可以實現RPC的異步調用,基於Direct交換機實現 ,流程:

  1. 客戶端既是生產者也是消費者,向RPC請求隊列發送RPC調用消息,同時監聽RPC響應隊列
  2. 服務端監聽RPC請求隊列的消息,收到消息後執行服務端的方法,得到方法返回的結果
  3. 服務端將RPC方法 的結果發送到RPC響應隊列
  4. 客戶端(RPC調用方)監聽RPC響應隊列,接收到RPC調用結果。

5. spring整合RabbitMQ

1. 搭建springboot環境

基於springboot操作RabbitMQ

<dependencies>
    <!--<dependency>
        &lt;!&ndash;此版本與spring boot 1.5.9版本匹配&ndash;&gt;
        <groupId>com.rabbitmq</groupId>
        <artifactId>amqp-client</artifactId>
        <version>4.0.3</version>
    </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>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-logging</artifactId>
    </dependency>
</dependencies>

2. 配置

1. 配置application.yml

server:
  port: 44000
spring:
  application:
    name: test-rabbitmq-producer
  rabbitmq:
    host: 127.0.0.1
    port: 5672
    username: guest
    password: guest
    virtualHost: /

2. 定義RabbitConfig類

配置Exchange Queue以及綁定交換機

package com.xuecheng.test.rabbitmq.config;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitmqConfig {
  // 隊列
  public static final String QUEUE_INFORM_EMAIL = "queue_inform_email";
  public static final String QUEUE_INFORM_SMS = "queue_inform_sms";
  public static final String EXCHANGE_TOPICS_INFORM = "exchange_topics_inform";
  public static final String ROUTINGKEY_EMAIL = "inform.#.email.#";
  public static final String ROUTINGKEY_SMS = "inform.#.sms.#";

  // 聲明交換機
  @Bean(EXCHANGE_TOPICS_INFORM)
  public Exchange EXCHANGE_TOPICS_INFORM(){
    // durable 持久化
    return ExchangeBuilder.topicExchange(EXCHANGE_TOPICS_INFORM).durable(false).build();
  }

  // 聲明隊列QUEUE_INFORM_EMAIL
  @Bean(QUEUE_INFORM_EMAIL)
  public Queue QUEUE_INFORM_EMAIL(){
    return new Queue(QUEUE_INFORM_EMAIL);
  }

  // 聲明隊列QUEUE_INFORM_SMS
  @Bean(QUEUE_INFORM_SMS)
  public Queue QUEUE_INFORM_SMS(){
    return new Queue(QUEUE_INFORM_SMS);
  }

  // 綁定email交換機和隊列,指定routingkey
  @Bean
  public Binding BINDING_QUEUE_INFORM_EMAIL(@Qualifier(QUEUE_INFORM_EMAIL) Queue queue,
                                            @Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_EMAIL).noargs();
  }

  // 綁定sms交換機和隊列,指定routingkey
  @Bean
  public Binding BINDING_QUEUE_INFORM_SMS(@Qualifier(QUEUE_INFORM_SMS) Queue queue,
                                          @Qualifier(EXCHANGE_TOPICS_INFORM)Exchange exchange){
    return BindingBuilder.bind(queue).to(exchange).with(ROUTINGKEY_SMS).noargs();
  }
}

3. 生產端

使用RabbitTemplate發送消息

@RunWith(SpringRunner.class)
@SpringBootTest
public class Producer05_topics_springboot {
  @Autowired
  private RabbitTemplate rabbitTemplate;

  // 使用rabbittemplate發送消息
  @Test
  public void testSendEmail(){
    String message = "send email message to user";
    /**
         *  參數
         *  1. 交換機名稱
         *  2. routingkey
         *  3. 消息內容
         */
  rabbitTemplate.convertAndSend(RabbitmqConfig.EXCHANGE_TOPICS_INFORM,"inform.email",message);
  }
}

4. 消費端

創建消費端工程,添加依賴,和生產端一樣

使用@RabbitListener 註解監聽隊列

@Component
public class ReceiveHandler {

  @RabbitListener(queues = {RabbitmqConfig.QUEUE_INFORM_EMAIL})
  public void send_email(String msg, Message message, Channel channel){
    System.out.println("receive message is: "+msg);
  }
}

5. 測試

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