RabbitMq 學習筆記

RabbitMq 學習筆記

一、消息中間件介紹&爲什麼要使用消息中間件

我們用java來舉例子, 打個比方 我們客戶端發送一個下單請求給訂單系統(order)訂單系統發送了

一個請求給我們的庫存系統告訴他需要更改庫存了, 我已經下單了, 這裏, 每一個請求我們都可以看作一條消息, 但是 我們客戶端需要等待訂單系統告訴我這條消息的處理結果(我到底有沒有下單成功) 但是 訂單系統不需要知道庫存系統這條消息的處理情況 因爲無論你庫存有沒有改動成功, 我訂單還是下了, 因爲是先下完了訂單(下成功了) 纔去更改庫存, 庫存如果更改出BUG了 那是庫存系統的問題, 這個BUG不會影響訂單系統。如果這裏你能理解的話, 那麼我們就能發現 我們用戶發送的這條消息(下訂單), 是需要同步的(我需要知道結果), 訂單發送給庫存的消息,是可以異步的(我不想知道你庫存到底改了沒, 我只是通知你我這邊成功下了一個訂單)

那麼如果我們還按原來的方式去實現這個需求的話, 那麼結果會是這樣:

那可能有同學說了, 我們訂單系統開闢線程去訪問庫存系統不就好了嗎?

使用線程池解決 確實可以, 但是也有他的缺點, 那麼 到底怎麼來完美解決這個問題呢?

如果這張圖能理解的話, 那麼 這個消息系統, 就是我們的消息中間件。

二、RabbitMq介紹&AMQP介紹

導語:我們剛剛介紹了什麼是消息中間件, 那麼RabbitMq就是對於消息中間件的一種實現,市面上還有很多很多實現, 比如RabbitMq、ActiveMq、ZeroMq、kafka,以及阿里開源的RocketMQ等等 我們這節主要學習RabbitMq 。

2.1 AMQP

這裏引用百度的一句話 再加以我的理解: AMQP 其實和Http一樣 都是一種協議, 只不過 Http是針對網絡傳輸的, 而AMQP是基於消息隊列的

AMQP 協議中的基本概念:

  • Broker: 接收和分發消息的應用,我們在介紹消息中間件的時候所說的消息系統就是Message Broker。

  • Virtual host: 出於多租戶和安全因素設計的,把AMQP的基本組件劃分到一個虛擬的分組中,類似於網絡中的namespace概念。當多個不同的用戶使用同一個RabbitMQ server提供的服務時,可以劃分出多個vhost,每個用戶在自己的vhost創建exchange/queue等。

  • Connection: publisher/consumer和broker之間的TCP連接。斷開連接的操作只會在client端進行,Broker不會斷開連接,除非出現網絡故障或broker服務出現問題。

  • Channel: 如果每一次訪問RabbitMQ都建立一個Connection,在消息量大的時候建立TCP Connection的開銷將是巨大的,效率也較低。Channel是在connection內部建立的邏輯連接,如果應用程序支持多線程,通常每個thread創建單獨的channel進行通訊,AMQP method包含了channel id幫助客戶端和message broker識別channel,所以channel之間是完全隔離的。Channel作爲輕量級的Connection極大減少了操作系統建立TCP connection的開銷。

  • Exchange: message到達broker的第一站,根據分發規則,匹配查詢表中的routing key,分發消息到queue中去。常用的類型有:direct (point-to-point), topic (publish-subscribe) and fanout (multicast)。

  • Queue: 消息最終被送到這裏等待consumer取走。一個message可以被同時拷貝到多個queue中。

  • Binding: exchange和queue之間的虛擬連接,binding中可以包含routing key。Binding信息被保存到exchange中的查詢表中,用於message的分發依據。

2.2 Exchange的類型:

direct :

​ 這種類型的交換機的路由規則是根據一個routingKey的標識,交換機通過一個routingKey與隊列綁定 ,在生產者生產消息的時候 指定一個routingKey 當綁定的隊列的routingKey 與生產者發送的一樣 那麼交換機會吧這個消息發送給對應的隊列。

fanout:

​ 這種類型的交換機路由規則很簡單,只要與他綁定了的隊列, 他就會吧消息發送給對應隊列(與routingKey沒關係)

topic:(因爲*在這個筆記軟件裏面是關鍵字,所以下面就用星替換掉了)

​ 這種類型的交換機路由規則也是和routingKey有關 只不過 topic他可以根據:星,#( 星號代表過濾一單詞,#代表過濾後面所有單詞, 用.隔開)來識別routingKey 我打個比方 假設 我綁定的routingKey 有隊列A和B A的routingKey是:星.user B的routingKey是: #.user

​ 那麼我生產一條消息routingKey 爲: error.user 那麼此時 2個隊列都能接受到, 如果改爲 topic.error.user 那麼這時候 只有B能接受到了

headers:

​ 這個類型的交換機很少用到,他的路由規則 與routingKey無關 而是通過判斷header參數來識別的, 基本上沒有應用場景,因爲上面的三種類型已經能應付了。

2.3 RabbitMQ

MQ: message Queue 顧名思義 消息隊列, 隊列大家都知道, 存放內容的一個東西, 存放的內容先進先出, 消息隊列, 只是裏面存放的內容是消息而已。

RabbitMq 是一個開源的 基於AMQP協議實現的一個完整的企業級消息中間件,服務端語言由Erlang(面向併發編程)語言編寫 對於高併發的處理有着天然的優勢,客戶端支持非常多的語言:

• Python

• Java

• Ruby

• PHP

• C#

• JavaScript

• Go

• Elixir

• Objective-C

• Swift

2.4 主流MQ對比

特性 ActiveMQ RabbitMQ RocketMQ Kafka
單機吞吐量 萬級,比 RocketMQ、Kafka 低一個數量級 同 ActiveMQ 10 萬級,支撐高吞吐 10 萬級,高吞吐,一般配合大數據類的系統來進行實時數據計算、日誌採集等場景
topic 數量對吞吐量的影響 topic 可以達到幾百/幾千的級別,吞吐量會有較小幅度的下降,這是 RocketMQ 的一大優勢,在同等機器下,可以支撐大量的 topic topic 從幾十到幾百個時候,吞吐量會大幅度下降,在同等機器下,Kafka 儘量保證 topic 數量不要過多,如果要支撐大規模的 topic,需要增加更多的機器資源
時效性 毫秒級 微秒級,這是 RabbitMQ 的一大特點,延遲最低 毫秒級 毫秒級
可用性 高,基於主從架構實現高可用 同 ActiveMQ 非常高,分佈式架構 非常高,分佈式,一個數據多個副本,少數機器宕機,不會丟失數據,不會導致不可用
消息可靠性 有較低的概率丟失數據 經過參數優化配置,可以做到 0 丟失 同 RocketMQ
功能支持 MQ 領域的功能極其完備 基於 erlang 開發,併發能力很強,性能極好,延時很低 MQ 功能較爲完善,還是分佈式的,擴展性好 功能較爲簡單,主要支持簡單的 MQ 功能,在大數據領域的實時計算以及日誌採集被大規模使用

三、RabbitMQ服務端部署

在介紹消息中間件的時候所提到的“消息系統” 便是我們這節的主題:RabbitMq 如同redis一樣 他也是採用c/s架構 由服務端 與客戶端組成, 我們現在我們計算機上部署他的服務端

由於我們剛剛介紹過了RabbitMQ服務端是由Erlang語言編寫所以我們這裏先下載Erlang語言的環境

注意:如果是在官網下的RabbitMQ服務端的話 Erlang語言的版本不能太低, 不然要卸載掉舊的去裝新的, 我們這裏下載OTP21.0版本直接從外網下載會很慢, 我這裏直接貼上百度網盤的地址(因爲這個東西還是有點大的)

https://pan.baidu.com/s/1pZJ8l2f3omrgnuCm9a8DVA

我們再去官網下載 他的服務端安裝包

http://www.rabbitmq.com/download.html

根據自己的系統選擇下載即可

注意! 需要先下載Erlang再下載安裝包安裝,不然安裝RabbitMQ服務端的時候會提示你本地沒有Erlang環境

安裝的話, 基本上就是默認的選項不用改

如何看RabbitMq安裝完成了? 在系統-服務中找到如下即可:

包括啓動 停止 重啓 服務等

RabbitMQ安裝會附帶一個管理工具(方便我們能直觀的查看整個RabbitMQ的運行狀態和詳細數據等,有點像Navicat 對應Mysql的關係) 值得一提的是, 管理工具和RabbitMQ是兩碼事 希望同學們不要混稀了。

管理工具啓動方式:

到你們安裝的 RabbitMQ Server\rabbitmq_server-3.7.12\sbin 目錄下面 執行一條cmd命令:

rabbitmq-plugins enable rabbitmq_management

直接複製這條命令即可 , 當然 嫌每次都要去目錄中去執行的麻煩的話, 可以配置一個環境變量 或者在我們的開始菜單欄中找到這個:

輸入完啓動命令後 稍微等一下會有結果返回 然後可以打開瀏覽器 輸入

http://127.0.0.1:15672

訪問管理頁面:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UPwTssCQ-1576739464138)(C:\Users\Administrator\Desktop\md\RabbitMq\p7.png)]

默認賬號密碼都是 guest 即:

username :guest
password:guest

登錄進去之後會看到如下界面(因爲我不小心裝了2次RabbitMq 所以這裏能看到都重複了, 你們自己那不會重複,然後我們剛剛說了 管理工具和rabbitmq 是兩碼事 所以端口也就不一樣)

這個頁面在筆記裏面介紹起來可能比較複雜, 就不一一介紹了, 我這裏講個重點, 就是線上環境下一定要把guest用戶(當然 guest這個用戶只能本機才能登陸)刪掉並且新加一個用戶, 這裏就演示一下這個功能

首先 點擊admin頁籤, 在下面找到Add User

然後輸入賬號 密碼 確認密碼 這個Tags其實是一個用戶權限標籤, 關於他的介紹可以看官方介紹(點旁邊那個小問號就好了,我這裏直接翻譯他的介紹)

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-pD2bn90z-1576739464140)(C:\Users\Administrator\Desktop\md\RabbitMq\p10.png)]
在這裏插入圖片描述

填寫完之後點擊AddUser 就可以添加一個用戶了, 添加完用戶之後還要給這個用戶添加對應的權限(注:Targ不等於權限)

比如說 我剛剛添加了一個jojo角色 !

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-AaZ2LdsY-1576739464141)(C:\Users\Administrator\Desktop\md\RabbitMq\p13.png)]

點擊這個jojo可以進去給他添加權限 這個權限可以是 Virtual host 級別的 也可以是交換機級別的 甚至是細化到某一個讀寫操作 我這裏就給他添加一個Virtual host權限

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-HpmAhu7A-1576739464142)(C:\Users\Administrator\Desktop\md\RabbitMq\p14.png)]

這裏 我們給了他 testhost這個Virtual host的權限 正則匹配都是* 也就是所有權限

然後點擊set添加完畢

那麼管理頁面 我們就講到這裏

四、RabbitMq快速開始

因爲我們這裏是用java來作爲客戶端, 我們首先引入maven依賴:

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

(注意的是, 我這裏引入的是5.x的rabbitmq客戶端版本, 那麼我們jdk的版本最好在8以上,反之, 這裏就建議使用4.x的版本,這裏僅僅討論jdk8 其他的版本不做討論)

首先 我們編寫一個連接的工具類:

package com.test.util;

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

 import java.util.ArrayList;
 import java.util.HashMap;
 import java.util.List;
 import java.util.Map;

 public class ConnectionUtil {

   public static final String QUEUE_NAME = "testQueue";

   public static final String EXCHANGE_NAME = "exchange";


   public static Connection getConnection() throws Exception {
        //創建一個連接工廠
        ConnectionFactory connectionFactory = new ConnectionFactory();
        //設置rabbitmq 服務端所在地址 我這裏在本地就是本地
        connectionFactory.setHost("127.0.0.1");
        //設置端口號,連接用戶名,虛擬地址等
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("jojo");
        connectionFactory.setPassword("jojo");
        connectionFactory.setVirtualHost("testhost");
        return connectionFactory.newConnection();
    }
 }

 

然後我們編寫一個消費者(producer),和一個生產者(consumer):

生產者:

public class Consumer {

  	public static void sendByExchange(String message) throws Exception {

        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //聲明隊列
        channel.queueDeclare(ConnectionUtil.QUEUE_NAME, true, false, false, null);
        // 聲明exchange
        channel.exchangeDeclare(ConnectionUtil.EXCHANGE_NAME, "fanout");
        //交換機和隊列綁定
        channel.queueBind(ConnectionUtil.QUEUE_NAME, ConnectionUtil.EXCHANGE_NAME, "");
        channel.basicPublish(ConnectionUtil.EXCHANGE_NAME, "", null, message.getBytes());
        System.out.println("發送的信息爲:" + message);
        channel.close();
        connection.close();
    }

 }

消費者:

public class Producer {

   public static void getMessage() throws Exception {
        Connection connection = ConnectionUtil.getConnection();
        Channel channel = connection.createChannel();
        //channel.queueDeclare(ConnectionUtil.QUEUE_NAME,true,false,false,null);
        DefaultConsumer deliverCallback = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println(new String(body, "UTF-8"));
            }
        };
        channel.basicConsume(ConnectionUtil.QUEUE_NAME, deliverCallback);
    }
 }  

 

這裏, 我們演示綁定fanout的類型的交換機, 所以不需要routingKey 就可以路由只需要綁定即可

(可能有同學要問了, 如果沒有綁定交換機怎麼辦呢? 沒有綁定交換機的話, 消息會發給rabbitmq默認的交換機裏面 默認的交換機隱式的綁定了所有的隊列,默認的交換機類型是direct 路由建就是隊列的名字)

基本上這樣子的話就已經進行一個快速入門了, 由於我們現在做項目基本上都是用spring boot(就算沒用spring boot也用spring 吧) 所以後面我們直接基於spring boot來講解(rabbitmq的特性,實戰等)

五、spring boot 整合rabbitmq

spring boot的環境怎麼搭建這邊就不提了, 這裏引入spring boot -AMQP的依賴:

<dependency>
   <groupId>org.springframework.boot</groupId>
   <artifactId>spring-boot-starter-amqp</artifactId>
 </dependency>

配置連接,創建交換機,隊列

配置連接

首先 我和上面一樣 先要配置連接信息這裏 可以選用yml的方式 也可以選用javaConfig的方式 這裏兩種方式我都貼出來 你們自己選

yml: 參數什麼意思剛剛介紹過了 這裏吧你自己的參數填進去就好了

spring:
  rabbitmq:
   host:
   port:
   username:
   password:
   virtual-host: 

這樣 spring boot 會幫你把rabbitmq其他相關的東西都自動裝備好,

javaConfig:

 @Bean
    public ConnectionFactory connectionFactory() {
        CachingConnectionFactory connectionFactory = new 		    CachingConnectionFactory("localhost", 5672);
        //我這裏直接在構造方法傳入了
        //    connectionFactory.setHost();
        //    connectionFactory.setPort();
        connectionFactory.setUsername("admin");
        connectionFactory.setPassword("admin");
        connectionFactory.setVirtualHost("testhost");
        //是否開啓消息確認機制
        //connectionFactory.setPublisherConfirms(true);
        return connectionFactory;
    }

配置完連接之後 我們就可以開始發送消息和接收消息了(因爲我們上面剛剛測試rabbitmq的時候創建過隊列和交換機等等這種東西了 當然 spring boot也可以創建)

spring boot創建交換機 隊列 並綁定:


    @Bean
    public DirectExchange defaultExchange() {
        return new DirectExchange("directExchange");
    }

    @Bean
    public Queue queue() {
        //名字 是否持久化
        return new Queue("testQueue", true);
    }

    @Bean
    public Binding binding() {
        //綁定一個隊列 to: 綁定到哪個交換機上面 with:綁定的路由建(routingKey)
        return BindingBuilder.bind(queue()).to(defaultExchange()).with("direct.key");
    }

發送消息:

發送消息比較簡單, spring 提供了一個RabbitTemplate 來幫助我們完成發送消息的操作

如果想對於RabbitTemplate 這個類進行一些配置(至於有哪些配置我們後面會講到) 我們可以在config類中 把他作爲Bean new出來並配置

 @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        //注意 這個ConnectionFactory 是使用javaconfig方式配置連接的時候才需要傳入的 如果是yml配置的連接的話是不需要的
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        return template;
    }
 @Component
    public class TestSend {
        @Autowired
        RabbitTemplate rabbitTemplate;

        public void testSend() {
            //至於爲什麼調用這個API 後面會解釋
            //參數介紹: 交換機名字,路由建, 消息內容
            rabbitTemplate.convertAndSend("directExchange", "direct.key", "hello");
        }
    }

我們只需要寫一個類 然後交給spring 管理 在類裏面注入RabbitTemplate 就可以直接調用api來發送消息了

接收消息:

這裏我新建了一個項目(最好新建一個 當然 不新建也沒關係) 來接收信息(之前的配置這裏就不貼出來了 和上面基本一樣),

@Component
 public class TestListener {

 @RabbitListener(queues = "testQueue")
   public void get(String message) throws Exception{
     System.out.println(message);
   }
 }

不出意外的話運行起來能看見效果, 這裏就不貼效果圖了

那麼至此rabbitmq的一個快速開始 以及和spring boot整合 就完畢了, 下面開始會講一些rabbitmq的一些高級特性 以及原理等

六、RabbitMq特性

6.1 如何確保消息一定發送到Rabbitmq了?

我們剛剛所講過的例子 在正常情況下 是沒問題的, 但是 實際開發中 我們往往要考慮一些非正常的情況, 我們從消息的發送開始:

默認情況下,我們不知道我們的消息到底有沒有發送到rabbitmq當中, 這肯定是不可取的, 假設我們是一個電商項目的話 用戶下了訂單 訂單發送消息給庫存 結果這個消息沒發送到rabbitmq當中 但是訂單還是下了,這時候 因爲沒有消息 庫存不會去減少庫存, 這種問題是非常嚴重的, 所以 接下來就講一種解決方案: 失敗回調

失敗回調, 顧名思義 就是消息發送失敗的時候會調用我們事先準備好的回調函數,並且把失敗的消息 和失敗原因等 返回過來。

具體操作:

注意 使用失敗回調也需要開啓發送方確認模式 開啓方式在下文

更改RabbitmqTemplate:

 @Bean
    public RabbitTemplate rabbitTemplate(ConnectionFactory connectionFactory) {
        RabbitTemplate template = new RabbitTemplate(connectionFactory);
        //開啓mandatory模式(開啓失敗回調)
        template.setMandatory(true);
        //指定失敗回調接口的實現類
        template.setReturnCallback(new MyReturnCallback());
        return template;
    }

回調接口的實現類:

實現RabbitTemplate.ReturnCallback裏面的returnedMessage方法即可 他會吧相關的參數都傳給你

public class MyReturnCallback implements RabbitTemplate.ReturnCallback {

   @Override
   public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
     System.out.println(message);
     System.out.println(replyCode);
     System.out.println(replyText);
     System.out.println(exchange);
     System.out.println(routingKey);
   }
 }

這裏模擬一個失敗的發送 : 當指定的交換機不能吧消息路由到隊列時(沒有指定路由建或者指定的路右鍵沒有綁定對應的隊列 或者壓根就沒有綁定隊列都會失敗) 消息就會發送失敗 效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-UAAOckkg-1576739464143)(C:\Users\Administrator\Desktop\md\RabbitMq\p15.png)]

分別打印的是錯誤狀態碼,錯誤原因(這裏的原因是不能路由) 交換機名字 和路由建 (還有個參數是你發送出去的消息 因爲太長了就沒截圖了.)

可能有些同學想到了一個答案------事物

沒錯事物的確能解決這個問題, 而且 恰巧rabbitmq剛好也支持事物, 但是! 事物非常影響rabbitmq的性能 有多嚴重? 據我所查到的資料 (當然 只是我所瞭解的 同學們也可以自己去嘗試測試結果) 開啓rabbitmq事物的話 對性能的影響超過100倍之多 也就是說 開啓事物後處理一條消息的時間 不開事物能處理100條(姑且這樣認爲吧), 那麼 這樣是非常不合理的, 因爲消息中間件的性能其實非常關鍵的(參考雙11) 如果這樣子做的話 雖然能確保消息100%投遞成功 但是代價太大了!

那麼除了事物還有什麼解決方案嗎?

rabbitmq其實還提供了一種解決方案, 叫:發送方確認模式 這種方式 對性能的影響非常小 而且也能確定消息是否發送成功

而且 發送方確認模式一般也會和失敗回調一起使用 這樣 就能確保消息100%投遞了

發送方確認開啓:

其實代碼在上面配置連接的時候已經放出來了 就是在連接工廠那被註釋的一行代碼 :

 connectionFactory.setPublisherConfirms(true);

如果是yml配置的話:

spring:
  rabbitmq:
   publisher-confirms: true

和失敗回調一樣 實現一個接口:

public class MyConfirmCallback implements RabbitTemplate.ConfirmCallback{

   @Override
   public void confirm(CorrelationData correlationData, boolean ack, String cause) {
     System.out.println(correlationData);
     System.out.println(ack);
     System.out.println(cause);
   }
 }

在RabbitmqTemplate 設置一下

template.setConfirmCallback(new MyConfirmCallback());

而且我們可以在發送消息的時候附帶一個CorrelationData參數 這個對象可以設置一個id,可以是你的業務id 方便進行對應的操作

CorrelationData correlationData = new CorrelationData(UUID.randomUUID().toString());
 rabbitTemplate.convertAndSend("directExchange", "direct.key123123", "hello",correlationData);

效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-JtxVWTc3-1576739464143)(C:\Users\Administrator\Desktop\md\RabbitMq\p16.png)]

這裏會吧我們傳入的那個業務id 以及ack(是否發送成功) 以及原因 返回回來

但是 要注意的是 confirm模式的發送成功 的意思是發送到RabbitMq(Broker)成功 而不是發送到隊列成功

所以纔有了上面我所說的那句 要和失敗回調結合使用 這樣才能確認消息投遞成功了

可能這裏有點繞, 簡單的總結一下就是 confirm機制是確認我們的消息是否投遞到了 RabbitMq(Broker)上面 而mandatory是在我們的消息進入隊列失敗時候不會被遺棄(讓我們自己進行處理)

那麼上面 就是rabbitmq在發送消息時我們可以做的一些處理, 接下來我們會講到rabbitmq在接收(消費)消息時的一些特性

6.2 消費者如何確認消費?

爲什麼要確認消費? 默認情況下 消費者在拿到rabbitmq的消息時 已經自動確認這條消息已經消費了, 講白話就是rabbitmq的隊列裏就會刪除這條消息了, 但是 我們實際開發中 難免會遇到這種情況, 比如說 拿到這條消息 發現我處理不了 比如說 參數不對, 又比如說 我當前這個系統出問題了, 暫時不能處理這個消息, 但是 這個消息已經被你消費掉了 rabbitmq的隊列裏也刪除掉了, 你自己這邊又處理不了, 那麼 ,這個消息就被遺棄了。 這種情況在實際開發中是不合理的, rabbitmq提供瞭解決這個問題的方案, 也就是我們上面所說的confirm模式 只是我們剛剛講的是發送方的 這次我們來講消費方的。

首先 我們在消費者這邊(再強調一遍 我這裏建議大家消費者和生產者分兩個項目來做,包括我自己就是這樣的, 雖然一個項目也可以,我覺得分開的話容易理解一點)

設置一下消息確認爲手動確認:

當然 我們要對我們的消費者監聽器進行一定的配置的話, 我們需要先實例一個監聽器的Container 也就是容器, 那麼我們的監聽器(一個消費者裏面可以實例多個監聽器) 可以指定這個容器 那麼我們只需要對這個Container(容器) 進行配置就可以了

首先得聲明一個容器並且在容器裏面指定消息確認爲手動確認:

@Bean
    public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory) {
        SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
                new SimpleRabbitListenerContainerFactory();
        //這個connectionFactory就是我們自己配置的連接工廠直接注入進來
        simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
        //這邊設置消息確認方式由自動確認變爲手動確認
        simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
        return simpleRabbitListenerContainerFactory;
    }

AcknowledgeMode關於這個類 就是一個簡單的枚舉類 我們來看看:

public enum AcknowledgeMode {
    NONE,
    MANUAL,
    AUTO;

    private AcknowledgeMode() {
    }

    public boolean isTransactionAllowed() {
        return this == AUTO || this == MANUAL;
    }

    public boolean isAutoAck() {
        return this == NONE;
    }

    public boolean isManual() {
        return this == MANUAL;
    }
}

3個狀態 不確認 手動確認 自動確認

我們剛剛配置的就是中間那個 手動確認

既然現在是手動確認了 那麼我們在處理完這條消息之後 得使這條消息確認:

@Component
    public class TestListener {

        //containerFactory:指定我們剛剛配置的容器
        @RabbitListener(queues = "testQueue", containerFactory = "simpleRabbitListenerContainerFactory")
        public void getMessage(Message message, Channel channel) throws Exception {
            System.out.println(new String(message.getBody(), "UTF-8"));
            System.out.println(message.getBody());
            //這裏我們調用了一個下單方法如果下單成功了那麼這條消息就可以確認被消費了 
            boolean f = placeAnOrder();
            if (f) {
                //傳入這條消息的標識, 這個標識由rabbitmq來維護 我們只需要從message中拿出來就可以 
                //第二個boolean參數指定是不是批量處理的 什麼是批量處理我們待會兒會講到 
                channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
            } else {
                //當然 如果這個訂單處理失敗了 我們也需要告訴rabbitmq 告訴他這條消息處理失敗了 可以退回 也可以遺棄 要注意的是 無論這條消息成功與否 一定要通知 就算失敗了 如果不通知的話 rabbitmq端會顯示這條消息一直處於未確認狀態,那麼這條消息就會一直堆積在rabbitmq端 除非與rabbitmq斷開連接 那麼他就會把這條消息重新發給別人 所以 一定要記得通知! 
                //前兩個參數 和上面的意義一樣, 最後一個參數 就是這條消息是返回到原隊列 還是這條消息作廢 就是不退回了。 
                channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
                //其實 這個API也可以去告訴rabbitmq這條消息失敗了 與basicNack不同之處 就是 他不能批量處理消息結果 只能處理單條消息  其實basicNack作爲basicReject的擴展開發出來的 
                //channel.basicReject(message.getMessageProperties().getDeliveryTag(),true); 
            }
        }
    }

正常情況下的效果, 我就不演示給大家看了, 這裏給大家看一個如果忘記退回消息的效果:

這裏 我把消息確認的代碼註釋掉:

//      channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);*

然後調用生產者發送一條消息 我們來看管理頁面:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-1poFGSec-1576739464144)(C:\Users\Administrator\Desktop\md\RabbitMq\p18.png)]

這裏能看到 有一條消息在rabbitmq當中 而且狀態是ready

然後我們使用消費者來消費掉他 注意 這裏我們故意沒有告訴rabbitmq我們消費成功了 來看看效果

這裏 消費的結果打印就不截圖了 還是來看管理頁面:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-v1DG6eGN-1576739464144)(C:\Users\Administrator\Desktop\md\RabbitMq\p19.png)]

就算我們消費端消費了下次 但是能看到 這條消息還是會在rabbitmq當中 只是他的狀態爲 unacked 就是未確認

這就是我們剛剛說的那種情況 無論消費成功與否 一定要通知rabbitmq 不然就會這樣 一直囤積在rabbitmq當中 直到連接斷開爲止.

6.3 消息預取

扯完消息確認 我們來講一下剛剛所說的批量處理的問題

什麼情況下回遇到批量處理的問題呢?

在這裏 就要先扯一下rabbitmq的消息發放機制了

rabbitmq 默認 他會最快 以輪詢的機制吧隊列所有的消息發送給所有客戶端 (如果消息沒確認的話 他會添加一個Unacked的標識上圖已經看過了)

那麼 這種機制會有什麼問題呢, 對於Rabbitmq來講 這樣子能最快速的使自己不會囤積消息而對性能造成影響, 但是 對於我們整個系統來講, 這種機制會帶來很多問題, 比如說 我一個隊列有2個人同時在消費,而且他們處理能力不同, 我打個最簡單的比方 有100個訂單消息需要處理(消費) 現在有消費者A 和消費者B , 消費者A消費一條消息的速度是 10ms 消費者B 消費一條消息的速度是15ms ( 當然 這裏只是打比方) 那麼 rabbitmq 會默認給消費者A B 一人50條消息讓他們消費 但是 消費者A 他500ms 就可以消費完所有的消息 並且處於空閒狀態 而 消費者B需要750ms 才能消費完 如果從性能上來考慮的話 這100條消息消費完的時間一共是750ms(因爲2個人同時在消費) 但是如果 在消費者A消費完的時候 能把這個空閒的性能用來和B一起消費剩下的信息的話, 那麼這處理速度就會快非常多。

這個例子可能有點抽象, 我們通過代碼來演示一下

我往Rabbitmq生產100條消息 由2個消費者來消費 其中我們讓一個消費者在消費的時候休眠0.5秒(模擬處理業務的延遲) 另外一個消費者正常消費 我們來看看效果:

正常的那個消費者會一瞬間吧所有消息(50條)全部消費完(因爲我們計算機處理速度非常快) 下圖是加了延遲的消費者:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-hKtk91HG-1576739464145)(file:///C:\Users\ADMINI~1\AppData\Local\Temp\msohtmlclip1\01\clip_image039.gif)]

可能我筆記裏面你看不出效果,這個你自己測試就會發現 其中一個消費者很快就處理完自己的消息了 另外一個消費者還在慢慢的處理 其實 這樣嚴重影響了我們的性能了。

其實講了這麼多 那如何來解決這個問題呢?

我剛剛解釋過了 造成這個原因的根本就是rabbitmq消息的發放機制導致的, 那麼我們現在來講一下解決方案: 消息預取

什麼是消息預取? 講白了以前是rabbitmq一股腦吧所有消息都均發給所有的消費者(不管你受不受得了) 而現在 是在我消費者消費之前 先告訴rabbitmq 我一次能消費多少數據 等我消費完了之後告訴rabbitmqrabbitmq再給我發送數據

在代碼中如何體現?

在使用消息預取前 要注意一定要設置爲手動確認消息, 原因參考上面劃重點的那句話。

因爲我們剛剛設置過了 這裏就不貼代碼了, 完了之後設置一下我們預取消息的數量 一樣 是在容器(Container)裏面設置:

@Bean
 public SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory(ConnectionFactory connectionFactory){
   SimpleRabbitListenerContainerFactory simpleRabbitListenerContainerFactory =
       new SimpleRabbitListenerContainerFactory();
   simpleRabbitListenerContainerFactory.setConnectionFactory(connectionFactory);
   //手動確認消息
   simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
   //設置消息預取的數量
   simpleRabbitListenerContainerFactory.setPrefetchCount(1);
   return simpleRabbitListenerContainerFactory;
 }

 

那麼設置完之後是什麼效果呢? 還是剛剛那個例子 還是2個消費者 因爲會在消費者返回消息的確認之後 rabbitmq纔會繼續發送消息給客戶端 而且客戶端的消息累計量不會超過我們剛剛設置預取的數量, 所以我們再運行同樣的例子的話 會發現 A消費者消費完99條消息了 B消費者才消費1條 (因爲B消費者休眠了0.5秒才消費完{返回消息確認} 但是0.5秒之內A消費者就已經把所有消息消費完畢了 當然 如果計算機處理速度較慢這個結果可能會有差異,效果大概就是A消費者會處理大量消息)

我這裏的效果就是B消費者只消費一條消息 A消費者就消費完了, 效果圖就不發了 這裏同學們儘量自己測試一下 或者改變一下參數看看效果。

關於這個預取的數量如何設置呢? 我們發現 如果設置爲1 能極大的利用客戶端的性能(我消費完了就可以趕緊消費下一條 不會導致忙的很忙 閒的很閒) 但是, 我們每消費一條消息 就要通知一次rabbitmq 然後再取出新的消息, 這樣對於rabbitmq的性能來講 是非常不合理的 所以這個參數要根據業務情況設置

我根據我查閱到的資料然後加以測試, 這個數值的大小與性能成正比 但是有上限,與數據可靠性,以及我們剛剛所說的客戶端的利用率成反比 大概如下圖:

那麼批量確認, 就是對於我們預取的消息,進行統一的確認。

6.4 死信交換機

我們來看一段代碼:

channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);

我們上面解釋過 這個代碼是消息處理失敗的確認 然後第三個參數我有解釋過是消息是否返回到原隊列, 那麼問題來了,如果 沒有返回給原隊列 那麼這條消息就被作廢了?

rabbitmq考慮到了這個問題提供瞭解決方案: 死信交換機(有些人可能叫作垃圾回收器,垃圾交換機等)

死信交換機有什麼用呢? 在創建隊列的時候 可以給這個隊列附帶一個交換機, 那麼這個隊列作廢的消息就會被重新發到附帶的交換機,然後讓這個交換機重新路由這條消息

理論是這樣, 代碼如下:

 @Bean
    public Queue queue() {
        Map<String,Object> map = new HashMap<>();
        //設置消息的過期時間 單位毫秒 
        map.put("x-message-ttl",10000);
        //設置附帶的死信交換機 
        map.put("x-dead-letter-exchange","exchange.dlx");
        //指定重定向的路由建 消息作廢之後可以決定需不需要更改他的路由建 如果需要 就在這裏指定 
        map.put("x-dead-letter-routing-key","dead.order");
        return new Queue("testQueue", true,false,false,map);
    }

大概是這樣的一個效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-bDPXVUBE-1576739464146)(C:\Users\Administrator\Desktop\md\RabbitMq\p22.png)]

其實我們剛剛發現 所謂死信交換機, 只是對應的隊列設置了對應的交換機是死信交換機, 對於交換機來講, 他還是一個普通的交換機 。

下面會列出rabbitmq的常用配置:

隊列配置:

參數名 配置作用
x-dead-letter-exchange 死信交換機
x-dead-letter-routing-key 死信消息重定向路由鍵
x-expires 隊列在指定毫秒數後被刪除
x-ha-policy 創建HA隊列
x-ha-nodes HA隊列的分佈節點
x-max-length 隊列的最大消息數
x-message-ttl 毫秒爲單位的消息過期時間,隊列級別
x-max-priority 最大優先值爲255的隊列優先排序功能

消息配置:

參數名 配置作用
content-type 消息體的MIME類型,如application/json
content-encoding 消息的編碼類型
message-id 消息的唯一性標識,由應用進行設置
correlation-id 一般用做關聯消息的message-id,常用於消息的響應
timestamp 消息的創建時刻,整形,精確到秒
expiration 消息的過期時刻, 字符串,但是呈現格式爲整型,精確到秒
delivery-mode 消息的持久化類型,1爲非持久化,2爲持久化,性能影響巨大
app-id 應用程序的類型和版本號
user-id 標識已登錄用戶,極少使用
type 消息類型名稱,完全由應用決定如何使用該字段
reply-to 構建回覆消息的私有響應隊列
headers 鍵/值對錶,用戶自定義任意的鍵和值
priority 指定隊列中消息的優先級

七、Rabbitmq linux安裝&集羣高可用

7.1 rabbitmq linux下安裝

這裏考慮到可能有同學沒了解過linux 或者不太熟悉linux 所以下載地址之類的東西我這裏直接貼現成的, 也就是說 只要按照我的步驟走下去基本上都沒問題.

在安裝(搭建集羣)之前 確定兩個點 1:防火牆關掉2:打開網絡

關閉防火牆

systemctl stop firewalld.service

禁止開機自啓

systemctl disable firewalld.service

首先 還是安裝erlang

  • 下載erlang
wget http://www.rabbitmq.com/releases/erlang/erlang-18.2-1.el6.x8664.rpm 
  • 安裝erlang
  rpm -ihv http://www.rabbitmq.com/releases/erlang/erlang-18.2-1.el6.x86*64.rpm 

安裝完erlang之後 開始rabbitmq 還是再提醒一下 先裝erlang 再裝rabbitmq

裝Rabbitmq之前 先裝一個公鑰 :

rpm --import https://dl.bintray.com/rabbitmq/Keys/rabbitmq-release-signing-key.asc

裝好公鑰之後 下載Rabbitmq:

wget http://www.rabbitmq.com/releases/rabbitmq-server/v3.6.6/rabbitmq-server-3.6.6-1.el7.noarch.rpm

安裝:

rpm -ihv rabbitmq-server-3.6.6-1.el7.noarch.rpm

安裝中途可能會提示你需要一個叫socat的插件

如果提示了 就先安裝socat 再裝rabbitmq

安裝socat:

yum install socat

至此 就裝好了Rabbitmq了 可以執行以下命令啓動Rabbitmq:

service rabbitmq-server start

和windows環境下一樣 rabbitmq對於linux也提供了他的管理插件

安裝rabbitmq管理插件:

rabbitmq-plugins enable rabbitmq_management

安裝完管理插件之後 如果有裝了瀏覽器的話 比如火狐 可以和windows一樣 訪問 一下 localhost:15672 可以看到一個熟悉的頁面:

當然 如果你和我一樣 是用虛擬機搭建的linux的話 可以用主機訪問一下也是沒問題的

那麼 安裝 我們就講到這 後面我們來講集羣環境的搭建 以及一些問題

7.2 rabbitmq集羣搭建,配置

rabbbitmq由於是由erlang語言開發的 天生就支持分佈式

rabbitmq 的集羣分兩種模式 一種是默認模式 一種是鏡像模式

當然 所謂的鏡像模式是基於默認模式加上一定的配置來的

在rabbitmq集羣當中 所有的節點(一個rabbitmq服務器) 會被歸爲兩類 一類是磁盤節點 一類是內存節點

磁盤節點會把集羣的所有信息(比如交換機,隊列等信息)持久化到磁盤當中,而內存節點只會將這些信息保存到內存當中 講白了 重啓一遍就沒了。

爲了可用性考慮 rabbitmq官方強調集羣環境至少需要有一個磁盤節點, 而且爲了高可用的話, 必須至少要有2個磁盤節點, 因爲如果只有一個磁盤節點 而剛好這唯一的磁盤節點宕機了的話, 集羣雖然還是可以運作, 但是不能對集羣進行任何的修改操作(比如 隊列添加,交換機添加,增加/移除 新的節點等)

具體想讓rabbitmq實現集羣, 我們首先需要改一下系統的hostname (因爲rabbitmq集羣節點名稱是讀取hostname的)

這裏 我們模擬3個節點 :

rabbitmq1

rabbitmq2

rabbitmq3

linux修改hostname命令:

hostnamectl set-hostname [name]

修改後重啓一下 讓rabbitmq重新讀取節點名字

然後 我們需要讓每個節點通過hostname能ping通(記得關閉防火牆) 這裏 我們可以修改修改一下hosts文件

關閉防火牆

systemctl stop firewalld.service

禁止開機自啓

systemctl disable firewalld.service

接下來,我們需要將各個節點的.erlang.cookie文件內容保持一致(文件路徑/var/lib/rabbitmq/.erlang.cookie)

因爲我是採用虛擬機的方式來模擬集羣環境, 所以如果像我一樣是克隆的虛擬機的話 同步.erlang.cookie文件這個操作在克隆的時候就已經完成了。

上面這些步驟完成之後 我們就可以開始來構建集羣 了

我們先讓rabbitmq2 加入 rabbitmq1與他構建爲一個集羣

執行命令( ram:使rabbitmq2成爲一個內存節點 默認爲:disk 磁盤節點):

rabbitmqctl stopapp rabbitmqctl joincluster rabbit@rabbitmq1 --ram rabbitmqctl start_app

在構建的時候 我們需要先停掉rabbitmqctl服務才能構建 等構建完畢之後再啓動

我們吧rabbitmq2添加完之後在rabbitmq3節點上也執行同樣的代碼 使他也加入進去 當然 我們也可以讓rabbitmq3也作爲一個磁盤節點

當執行完操作以後我們來看看效果:

[外鏈圖片轉存失敗,源站可能有防盜鏈機制,建議將圖片保存下來直接上傳(img-dPnhRV3O-1576739464146)(C:\Users\Administrator\Desktop\md\RabbitMq\p26.png)]

隨便在哪個節點打開管理頁面都能看到集羣環境各節點的信息;

有關集羣的其他命令:

 rabbitmq-server -detached 啓動RabbitMQ節點
 rabbitmqctl startapp 啓動RabbitMQ應用,而不是節點 
 rabbitmqctl stopapp 停止
 rabbitmqctl status 查看狀態
 rabbitmqctl adduser mq 123456 rabbitmqctl setusertags mq administrator 新增賬戶 
 rabbitmq-plugins enable rabbitmqmanagement 啓用RabbitMQManagement 
 rabbitmqctl clusterstatus 集羣狀態
 rabbitmqctl forgetclusternode rabbit@[nodeName] 節點摘除
 rabbitmqctl reset application 重置

普通模式的rabbitmq集羣搭建好後, 我們來說一下鏡像模式

在普通模式下的rabbitmq集羣 他會吧所有節點的交換機信息 和隊列的元數據(隊列數據分爲兩種 一種爲隊列裏面的消息, 另外一種是隊列本身的信息 比如隊列的最大容量,隊列的名稱,等等配置信息, 後者稱之爲元數據) 進行復制 確保所有節點都有一份。

而鏡像模式,則是吧所有的隊列數據完全同步(當然 對性能肯定會有一定影響) 當對數據可靠性要求高時 可以使用鏡像模式

實現鏡像模式也非常簡單 有2種方式 一種是直接在管理臺控制, 另外一種是在聲明隊列的時候控制

聲明隊列的時候可以加入鏡像隊列參數 在上方的參數列表當中有解釋 我們來講一下管理臺控制

鏡像隊列配置命令解釋:

rabbitmqctl set_policy [-p Vhost] Name Pattern Definition [Priority]
-p Vhost: 可選參數,針對指定vhost下的queue進行設置  
Name: policy的名稱            
Pattern: queue的匹配模式(正則表達式) 
Definition:鏡像定義,包括三個部分ha-mode, ha-params, ha-sync-mode      
     ha-mode:指明鏡像隊列的模式,有效值爲 all/exactly/nodes                 
        all:表示在集羣中所有的節點上進行鏡像      
        exactly:表示在指定個數的節點上進行鏡像,節點的個數由ha-params指定            
        nodes:表示在指定的節點上進行鏡像,節點名稱通過ha-params指定 
     ha-params:ha-mode模式需要用到的參數  
     ha-sync-mode:進行隊列中消息的同步方式,有效值爲automatic和manual

這裏舉個例子 如果想配置所有名字開頭爲 policy的隊列進行鏡像 鏡像數量爲1那麼命令如下:

rabbitmqctl setpolicy hapolicy "^policy_" '{"ha-mode":"exactly","ha-params":1,"ha-sync-mode":"automatic"}'
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章