Spring Boot(十三)RabbitMQ安裝與整合

王磊的部落格發表於2019-01-19

一、前言

RabbitMQ是一個開源的訊息代理軟體(面向訊息的中介軟體),它的核心作用就是建立訊息佇列,非同步接收和傳送訊息,MQ的全程是:Message Queue中文的意思是訊息佇列。

<!–more–>

1.1 使用場景

  • 削峰填谷:用於應對間歇性流量提升對於系統的“破壞”,比如秒殺活動,可以把請求先傳送到訊息佇列在平滑的交由系統去處理,當訪問量大於一定數量的時候,還可以直接遮蔽後續操作,給前臺的使用者友好的顯示;
  • 延遲處理:可以進行事件後置,比如訂單超時業務,使用者下單30分鐘未支付取消訂單;
  • 系統解耦:訊息佇列也可以幫開發人員完成業務的解耦,比如使用者上傳頭像的功能,最初的設計是使用者上傳完之後才能發帖,後面有增加了經驗系統,需要在上傳頭像之後增加經驗值,到後來又上線了金幣系統,上傳頭像之後可以增加金幣,像這種需求的不斷升級,如果在業務程式碼裡面寫死每次該業務程式碼是很不優雅的,這個時候如果使用訊息佇列,那麼只需要增加一個訂閱器用於介紹使用者上傳頭像的訊息,再執行經驗的增加和金幣的增加是非常簡單的,並且在不改動業務模組業務程式碼的基礎上可以輕鬆實現,如果後期需要撤銷某個模組了,只需要刪除訂閱器即可,就這樣就降低了系統開發的耦合性;

1.2 為什麼使用RabbitMQ?

現在市面上比較主流的訊息佇列還有Kafka、RocketMQ、RabbitMQ,它們的介紹和區別如下:

  • Kafka是LinkedIn開源的分散式釋出-訂閱訊息系統,目前歸屬於Apache定級專案。Kafka主要特點是基於Pull的模式來處理訊息消費,追求高吞吐量,一開始的目的就是用於日誌收集和傳輸。0.8版本開始支援複製,對訊息的重複、丟失、錯誤沒有嚴格要求,適合產生大量資料的網際網路服務的資料收集業務。
  • RabbitMQ是使用Erlang語言開發的開源訊息佇列系統,基於AMQP協議來實現。AMQP的主要特徵是面向訊息、佇列、路由(包括點對點和釋出/訂閱)、可靠性、安全。AMQP協議更多用在企業系統內,對資料一致性、穩定性和可靠性要求很高的場景,對效能和吞吐量的要求還在其次。
  • RocketMQ是阿里開源的訊息中介軟體,它是純Java開發,具有高吞吐量、高可用性、適合大規模分散式系統應用的特點。RocketMQ思路起源於Kafka,但並不是Kafka的一個Copy,它對訊息的可靠傳輸及事務性做了優化,目前在阿里集團被廣泛應用於交易、充值、流計算、訊息推送、日誌流式處理、binglog分發等場景。

簡單總結: Kafka的效能最好,適用於對訊息吞吐量達,對訊息丟失不敏感的系統;RocketMQ借鑑了Kafka並提高了訊息的可靠性,修復了Kafka的不足;RabbitMQ效能略低於Kafka,並實現了AMQP(Advanced Message Queuing Protocol)高階訊息佇列協議的標準,有非常好的穩定性。

支援語言對比

  • RocketMQ 支援語言:Java、C++、Golang
  • Kafka 支援語言:Java、Scala
  • RabbitMQ 支援語言:C#、Java、Js/NodeJs、Python、Ruby、Erlang、Perl、Clojure、Golang

1.3 RabbitMQ特點

RabbitMQ的特點是易用、擴充套件性好(叢集訪問)、高可用,具體如下:

  • 可靠性:持久化、訊息確認、事務等保證了訊息的可靠性;
  • 伸縮性:叢集服務,可以很方便的新增伺服器來提高系統的負載;
  • 高可用:叢集狀態下部分節點出現問題依然可以執行;
  • 多語言支援:RabbitMQ幾乎支援了所有的語言,比如Java、.Net、Nodejs、Golang等;
  • 易用的管理頁面:RabbitMQ提供了易用了網頁版的管理監控系統,可以很方便的完成RabbitMQ的控制和檢視;
  • 外掛機制:RabbitMQ提供了許多外掛,可以豐富和擴充套件Rabbit的功能,使用者也可編寫自己的外掛;

1.4 RabbitMQ基礎知識

在瞭解訊息通訊之前首先要了解3個概念:生產者、消費者和代理。

生產者:訊息的建立者,負責建立和推送資料到訊息伺服器;

消費者:訊息的接收方,用於處理資料和確認訊息;

代理:就是RabbitMQ本身,用於扮演“快遞”的角色,本身不生產訊息,只是扮演“快遞”的角色。

(一)訊息傳送原理

首先你必須連線到Rabbit才能釋出和消費訊息,那怎麼連線和傳送訊息的呢?

你的應用程式和Rabbit Server之間會建立一個TCP連線,一旦TCP開啟,並通過了認證,認證就是你試圖連線Rabbit之前傳送的Rabbit伺服器連線資訊和使用者名稱和密碼,有點像程式連線資料庫,使用Java有兩種連線認證的方式,後面程式碼會詳細介紹,一旦認證通過你的應用程式和Rabbit就建立了一條AMQP通道(Channel)。

通道是建立在“真實”TCP上的虛擬連線,AMQP命令都是通過通道傳送出去的,每個通道都會有一個唯一的ID,不論是釋出訊息,訂閱佇列或者接收訊息都是通過通道完成的。

(二)為什麼不通過TCP直接傳送命令?

對於作業系統來說建立和銷燬TCP會話是非常昂貴的開銷,假設高峰期每秒有成千上萬條連線,每個連線都要建立一條TCP會話,這就造成了TCP連線的巨大浪費,而且作業系統每秒能建立的TCP也是有限的,因此很快就會遇到系統瓶頸。

如果我們每個請求都使用一條TCP連線,既滿足了效能的需要,又能確保每個連線的私密性,這就是引入通道概念的原因。

(三)RabbitMQ名稱解釋

ConnectionFactory(連線管理器): 應用程式與Rabbit之間建立連線的管理器,程式程式碼中使用;

Channel(通道): 訊息推送使用的通道;

Exchange(交換器): 用於接受、分配訊息;

Queue(佇列): 用於儲存生產者的訊息;

RoutingKey(路由鍵): 用於把生成者的資料分配到交換器上;

BindingKey(繫結鍵): 用於把交換器的訊息繫結到佇列上;

看到上面的解釋,最難理解的路由鍵和繫結鍵了,那麼他們具體怎麼發揮作用的,請看下圖:

1.5 交換器分類

RabbitMQ的Exchange(交換器)分為四類:

  • direct(預設)
  • headers
  • fanout
  • topic

其中headers交換器允許你匹配AMQP訊息的header而非路由鍵,除此之外headers交換器和direct交換器完全一致,但效能卻很差,幾乎用不到,所以我們這裡不做解釋。

1.5.1 direct交換器

direct為預設的交換器型別,也非常的簡單,如果路由鍵匹配的話,訊息就投遞到相應的佇列,如下圖:

1.5.2 fanout交換器

fanout有別於direct交換器,fanout是一種釋出/訂閱模式的交換器,當你傳送一條訊息的時候,交換器會把訊息廣播到所有附加到這個交換器的佇列上。

注意: 對於fanout交換器來說routingKey(路由鍵)是無效的,這個引數是被忽略的。

1.5.3 topic交換器

topic交換器執行和fanout類似,但是可以更靈活的匹配自己想要訂閱的資訊,這個時候routingKey路由鍵就排上用場了,使用路由鍵進行訊息(規則)匹配。

topic路由器的關鍵在於定義路由鍵,定義routingKey名稱不能超過255位元組,使用“.”作為分隔符,例如:com.mq.rabbit.error。

匹配規則

匹配表示式可以用“*”和“#”匹配任何字元,具體規則如下:

  • “*”匹配一個分段(用“.”分割)的內容;
  • “#”匹配所有字元;

例如釋出了一個“cn.mq.rabbit.error”的訊息:

能匹配上的路由鍵:

  • cn.mq.rabbit.*
  • cn.mq.rabbit.#
  • #.error
  • cn.mq.#
  • #

不能匹配上的路由鍵:

  • cn.mq.*
  • *.error
  • *

1.6 訊息持久化

RabbitMQ佇列和交換器有一個不可告人的祕密,就是預設情況下重啟伺服器會導致訊息丟失,那麼怎麼保證Rabbit在重啟的時候不丟失呢?答案就是訊息持久化。

當你把訊息傳送到Rabbit伺服器的時候,你需要選擇你是否要進行持久化,但這並不能保證Rabbit能從崩潰中恢復,想要Rabbit訊息能恢復必須滿足3個條件:

  1. 投遞訊息的時候durable設定為true,訊息持久化,程式碼:channel.queueDeclare(x, true, false, false, null),引數2設定為true持久化;
  2. 設定投遞模式deliveryMode設定為2(持久),程式碼:channel.basicPublish(x, x, MessageProperties.PERSISTENT_TEXT_PLAIN,x),引數3設定為儲存純文字到磁碟;
  3. 訊息已經到達持久化交換器上;
  4. 訊息已經到達持久化的佇列;

持久化工作原理

Rabbit會將你的持久化訊息寫入磁碟上的持久化日誌檔案,等訊息被消費之後,Rabbit會把這條訊息標識為等待垃圾回收。

持久化的缺點

訊息持久化的優點顯而易見,但缺點也很明顯,那就是效能,因為要寫入硬碟要比寫入記憶體效能較低很多,從而降低了伺服器的吞吐量,儘管使用SSD硬碟可以使事情得到緩解,但他仍然吸乾了Rabbit的效能,當訊息成千上萬條要寫入磁碟的時候,效能是很低的。

所以使用者要根據自己的情況,選擇適合自己的方式。

學習更多RabbitMQ知識,訪問:https://gitbook.cn/gitchat/ac…

二、在Docker中安裝RabbitMQ

(1)下載映象

https://hub.docker.com/r/libr…

  • alpine 輕量版
  • management 帶外掛的版本

從映象的大小也可以很直觀的看出來alpine是輕量版。

使用命令:

docker pull rabbitmq:3.7.7-management

下載帶management外掛的版本。

(2)執行RabbitMQ

使用命令:

docker run -d –hostname myrabbit –name rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3.7.7-management

  • -d 後臺執行
  • –hostname 主機名稱
  • –name 容器名稱
  • -p 15672:15672 http訪問埠,對映本地埠到容器埠
  • -p 5672:5672 amqp埠,對映本地埠到容器埠

正常啟動之後,訪問:http://localhost:15672/

登入網頁管理頁面,使用者名稱密碼:guest/guest,登入成功如下圖:

三、RabbitMQ整合

3.1 新增依賴

如果用Idea建立新專案,可以直接在建立Spring Boot的時候,點選“Integration”皮膚,選擇RabbitMQ整合,如下圖:

如果是老Maven專案,直接在pom.xml新增如下程式碼:

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

3.2 配置RabbitMQ資訊

在application.properties設定如下資訊:

spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
spring.rabbitmq.username=test
spring.rabbitmq.password=test

3.3 程式碼

3.3 程式碼實現

本節分別來看三種交換器:direct、fanout、topic的實現程式碼。

3.3.1 Direct Exchange

3.3.1.1 配置佇列

建立DirectConfig.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.DirectExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class DirectConfig {
    final static String QUEUE_NAME = "direct"; //佇列名稱
    final static String EXCHANGE_NAME = "mydirect"; //交換器名稱
    @Bean
    public Queue queue() {
        // 宣告佇列 引數一:佇列名稱;引數二:是否持久化
        return new Queue(DirectConfig.QUEUE_NAME, false);
    }
    // 配置預設的交換機,以下部分都可以不配置,不設定使用預設交換器(AMQP default)
    @Bean
    DirectExchange directExchange() {
        // 引數一:交換器名稱;引數二:是否持久化;引數三:是否自動刪除訊息
        return new DirectExchange(DirectConfig.EXCHANGE_NAME, false, false);
    }
    // 繫結“direct”佇列到上面配置的“mydirect”路由器
    @Bean
    Binding bindingExchangeDirectQueue(Queue directQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(directQueue).to(directExchange).with(DirectConfig.QUEUE_NAME);
    }
}

3.3.1.2 傳送訊息

建立Sender.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.Date;
/**
 * 訊息傳送者-生產訊息
 */
@Component
public class Sender {
    @Autowired
    private AmqpTemplate rabbitTemplate;
    public void driectSend(String message) {
        System.out.println("Direct 傳送訊息:" + message);
        //引數一:交換器名稱,可以省略(省略儲存到AMQP default交換器);引數二:路由鍵名稱(direct模式下路由鍵=佇列名稱);引數三:儲存訊息
        this.rabbitTemplate.convertAndSend("direct", message);
    }
}

注意:

  • 在direct交換器中,路由鍵名稱就是佇列的名稱;
  • 傳送訊息“convertAndSend”的時候,第一個引數為交換器的名稱,非必填可以忽略,如果忽略則會把訊息傳送到預設交換器“AMQP default”;

3.3.1.3 消費訊息

建立Receiver.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

/**
 * 訊息接收者-消費訊息
 */
@Component
@RabbitListener(queues = "direct")
public class Receiver {
    @Autowired
    private AmqpTemplate rabbitTemplate;
    @RabbitHandler
    /**
     * 監聽消費訊息
     */
    public void process(String message) {
        System.out.println("Direct 消費訊息:" + message);
    }
}

3.3.1.4 測試程式碼

使用Spring Boot中的預設測試框架JUnit進行單元測試,不瞭解JUnit的可以參考我的上一篇文章,建立MQTest.java程式碼如下:

package com.example.rabbitmq.mq;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.text.SimpleDateFormat;
import java.util.Date;
import static org.junit.Assert.*;

@RunWith(SpringRunner.class)
@SpringBootTest
public class MQTest {
    @Autowired
    private Sender sender;
    @Test
    public void driectTest() {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
        sender.driectSend("Driect Data:" + sf.format(new Date()));
    }
}

執行之後,效果如下圖:

表示訊息已經被髮送並被消費了。

3.3.2 Fanout Exchange

3.3.2.1 配置佇列

建立FanoutConfig.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.core.*;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class FanoutConfig {
    final static String QUEUE_NAME = "fanout"; //佇列名稱
    final static String QUEUE_NAME2 = "fanout2"; //佇列名稱
    final static String EXCHANGE_NAME = "myfanout"; //交換器名稱
    @Bean
    public Queue queueFanout() {
        return new Queue(FanoutConfig.QUEUE_NAME);
    }
    @Bean
    public Queue queueFanout2() {
        return new Queue(FanoutConfig.QUEUE_NAME2);
    }
    //配置交換器
    @Bean
    FanoutExchange fanoutExchange() {
        return new FanoutExchange(FanoutConfig.EXCHANGE_NAME);
    }
    // 繫結佇列到交換器
    @Bean
    Binding bindingFanoutExchangeQueue(Queue queueFanout, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queueFanout).to(fanoutExchange);
    }
    // 繫結佇列到交換器
    @Bean
    Binding bindingFanoutExchangeQueue2(Queue queueFanout2, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(queueFanout2).to(fanoutExchange);
    }
}

3.3.2.2 傳送訊息

建立FanoutSender.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
public class FanoutSender {
    @Autowired
    private AmqpTemplate rabbitTemplate;
    public void send(String message) {
        System.out.println("傳送訊息:" + message); this.rabbitTemplate.convertAndSend(FanoutConfig.EXCHANGE_NAME,FanoutConfig.QUEUE_NAME, message);
    }
    public void send2(String message) {
        System.out.println("傳送訊息2:" + message); this.rabbitTemplate.convertAndSend(FanoutConfig.EXCHANGE_NAME,FanoutConfig.QUEUE_NAME2, message);
    }
}

3.3.2.3 消費訊息

建立兩個監聽類,第一個FanoutReceiver.java程式碼如下:

package com.example.rabbitmq.mq;
import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;

@Component
@RabbitListener(queues = "fanout")
public class FanoutReceiver {
    @RabbitHandler
    public void process(String msg) {
        System.out.println("Fanout(FanoutReceiver)消費訊息:" + msg);
    }
}

第二個FanoutReceiver2.java程式碼如下:

package com.example.rabbitmq.mq;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
@RabbitListener(queues = "fanout2")
public class FanoutReceiver2 {
    @RabbitHandler
    public void process(String message) {
        System.out.println("Fanout(FanoutReceiver2)消費訊息:" + message);
    }
}

3.3.2.4 測試程式碼

建立FanoutTest.java程式碼如下:

package com.example.rabbitmq.mq;
import com.example.rabbitmq.RabbitmqApplication;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
import java.text.SimpleDateFormat;
import java.util.Date;

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitmqApplication.class)
public class FanoutTest {
    @Autowired
    private FanoutSender sender;

    @Test
    public void Test() throws InterruptedException {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
        sender.send("Time1 => " + sf.format(new Date()));
        sender.send2("Date2 => " + sf.format(new Date()));
    }
}

執行測試程式碼,輸出結果如下:

傳送訊息:Time1 => 2018-09-11
傳送訊息2:Date2 => 2018-09-11
Fanout(FanoutReceiver2)消費訊息:Time1 => 2018-09-11
Fanout(FanoutReceiver2)消費訊息:Date2 => 2018-09-11
Fanout(FanoutReceiver)消費訊息:Time1 => 2018-09-11
Fanout(FanoutReceiver)消費訊息:Date2 => 2018-09-11

總結: 可以看出fanout會把訊息分發到所有訂閱到該交換器的佇列,fanout模式是忽略路由鍵的。

3.3.3 Topic Exchange

3.3.3.1 配置佇列

@Configuration
public class TopicConfig {
    final static String QUEUE_NAME = "log";
    final static String QUEUE_NAME2 = "log.all";
    final static String QUEUE_NAME3 = "log.all.error";
    final static String EXCHANGE_NAME = "topicExchange"; //交換器名稱
    @Bean
    public Queue queuetopic() {
        return new Queue(TopicConfig.QUEUE_NAME);
    }
    @Bean
    public Queue queuetopic2() {
        return new Queue(TopicConfig.QUEUE_NAME2);
    }
    @Bean
    public Queue queuetopic3() {
        return new Queue(TopicConfig.QUEUE_NAME3);
    }
    // 配置交換器
    @Bean
    TopicExchange topicExchange() {
        return new TopicExchange(TopicConfig.EXCHANGE_NAME);
    }
    // 繫結佇列到交換器,並設定路由鍵(log.#)
    @Bean
    Binding bindingtopicExchangeQueue(Queue queuetopic, TopicExchange topicExchange) {
        return BindingBuilder.bind(queuetopic).to(topicExchange).with("log.#");
    }
    // 繫結佇列到交換器,並設定路由鍵(log.*)
    @Bean
    Binding bindingtopicExchangeQueue2(Queue queuetopic2, TopicExchange topicExchange) {
        return BindingBuilder.bind(queuetopic2).to(topicExchange).with("log.*");
    }
    // 繫結佇列到交換器,並設定路由鍵(log.*.error)
    @Bean
    Binding bindingtopicExchangeQueue3(Queue queuetopic3, TopicExchange topicExchange) {
        return BindingBuilder.bind(queuetopic3).to(topicExchange).with("log.*.error");
    }
}

3.3.3.2 釋出訊息

@Component
public class TopicSender {
    @Autowired
    private AmqpTemplate rabbitTemplate;
    public void topicSender(String message) {
        String routingKey = "log.all.error";
        System.out.println(routingKey + " 傳送訊息:" + message);
        this.rabbitTemplate.convertAndSend(TopicConfig.EXCHANGE_NAME, routingKey, message);
    }
}

3.3.3.3 消費訊息

@Component
@RabbitListener(queues = "log")
public class TopicReceiver {
    @RabbitHandler
    public void process(String msg) {
        System.out.println("log.# 消費訊息:" + msg);
    }
}
@Component
@RabbitListener(queues = "log.all")
public class TopicReceiver2 {
    @RabbitHandler
    public void process(String msg) {
        System.out.println("log.* 消費訊息:" + msg);
    }
}
@Component
@RabbitListener(queues = "log.all.error")
public class TopicReceiver3 {
    @RabbitHandler
    public void process(String msg) {
        System.out.println("log.*.error 消費訊息:" + msg);
    }
}

3.3.3.4 測試程式碼

@RunWith(SpringRunner.class)
@SpringBootTest(classes = RabbitmqApplication.class)
public class FanoutTest {
    @Autowired
    private FanoutSender fanoutSender;
    @Test
    public void Test() {
        SimpleDateFormat sf = new SimpleDateFormat("yyyy-MM-dd");
        fanoutSender.send("Time1 => " + sf.format(new Date()));
        fanoutSender.send2("Date2 => " + sf.format(new Date()));
    }
}

輸出結果:

log.all.error 傳送訊息:time => 2018-09-11
log.# 消費訊息:time => 2018-09-11
log.*.error 消費訊息:time => 2018-09-11

總結: 在Topic Exchange中“#”可以匹配所有內容,而“*”則是匹配一個字元段的內容。

以上示例程式碼Github地址:https://github.com/vipstone/s…

參考文件

阿里 RocketMQ 優勢對比:https://juejin.im/entry/5a0ab…

相關文章