與RabbitMQ的第一次親密接觸

一、前言

首先說一下RabbitMQ,爲什麼叫RabbitMQ呢?
嘿嘿嘿,我想大部分人沒想過這個問題,Rabbit兔子,作者是想表示可以兔子繁殖起來異常的瘋狂,就像分佈式系統一樣。說到兔子瘋狂繁殖,讓我想到了高中時代做過的很多數學題和生物題。
說完命名問題,那爲何要有RabbitMQ呢?他用來解決什麼問題?適用於何種場景?如何使用?
這就是本文要討論的問題,但由於作者也並未深入的瞭解,很多地方都是淺嘗輒止,或只是說明部分內容。第一次親密接觸嘛,哪有第一次接觸就深入的,是吧。

二、爲何我們要選用RabbitMQ

1、Rabbit的作用

(1)解耦

消息中間件在處理過程中加入了一個隱含的、基於數據的接口層,兩邊的處理過程都需要實現這個接口,這允許你獨立的擴展或修改兩邊的處理過程,只需要保證遵守同樣的接口約束即可。

(2)擴展性

擴展性是基於解耦的特性的,因爲發消息和收消息雙發都是各自處理實現,所有有了很大的擴展性

(3)削峯

可以支撐突發訪問壓力,不會因爲突發的超負荷請求而完全崩潰

(4)可恢復性

當部分組件失效時,不會影響整個系統。消息中間件降低了進程之間的耦合度,一個處理消息的進程掛掉,加入消息中間件的消息,依舊可以在系統恢復後進行處理

這裏閒話一句,前幾天剛讀完《淘寶技術這十年》,裏面講到Notify組件的誕生過程,中間有一個很大的版本改造,就是加了數據庫,消息隊列的所有消息都會存儲到數據庫,避免由於宕機或其他原因,造成服務器關閉,而導致數據丟失的問題。

(5)順序保證

大多數情況下,數據處理的順序非常重要,大部分消息中間件支持一定程度的順序性

(6)緩衝

由於不同的元素,其處理時間不同。寫入消息中間件的處理會儘可能快速,該層有助於控制和優化數據經流系統的速度

(7)異步通信

很多時候,應用不會立即的處理消息。消息中間件提供異步處理機機制,允許應用把一些消息放入消息中間件,不會立即處理它

2、Rabbit的特性

(1)可靠性
(2)靈活的路由

在消息進入隊列之前,通過交換器來路由消息,針對複雜的理由功能,可以將多個交換器綁定在一起,也可以通過插件機制實現自己的交換器

(3)擴展性

多個RabbitMQ節點可以組成一個集羣,也可以根據實際業務動態的擴展集羣中的節點

(4)高可用性
(5)多種協議
(6)多種客戶端
(7)管理界面
(8)插件機制

3、遵循了AMQP協議

AMQP的模型架構和RabbitMQ的模型架構師一樣的,生產者將消息發送給交換器,交換器和隊列綁定,當生產者發送消息是,所寫的的RoutingKey與綁定的BindingKey相匹配是,消息即被存入相對於的隊列,消費者可以定於相應的隊列來獲取消息。
AMQP協議本身包括三層:

  • Modle Layer :協議最高層,主要定義了一些供客戶端調用的命令,客戶端可以利用這些命令實現自己的業務邏輯,例如,客戶端可以使用Queue.Declare命令聲明一個隊列,或者使用Consume訂閱消費一個隊列中的消息。
  • Session Layer:位於中間層,主要負責將客戶端的命令發送 給服務器,再將服務端的應答返回給客戶端,主要爲客戶端與服務器之間的通信提供可靠的同步機制和錯誤處理。
  • Transport Layer: 位於最底層,主要傳輸二進制數據流,提供幀處理、信道複用、錯誤檢測和數據表示等。
    AMQP說到底還是一個通信協議,通信協議都會涉及到報文交互,從low-level舉例來說,AMQP本身是應用層的協議,其填充與TCP協議層的數據部分。而從high-level來說,AMQP是通過協議命令進行交互的。AMQP協議可以看做一系列結構化命令的協議,這裏的命令協議代表一種操作,類似於HTTP中的方法(GET/POST/PUT/DELETE等)

四、RabbitMQ基礎功能實現

1、RabbitConsumer 消費者

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-10 14:05
 */
public class RabbitConsumer {
	//定義隊列名稱
    private static final String QUEUE_NAME="queue_demo";
    //定義IP地址
    private static final String IP_ADDRESS="192.168.1.15";
    //定義端口號,此端口號是Rabbit默認端口號
    private static final int PORT=5672;

    public static void main(String [] args) throws IOException, TimeoutException, InterruptedException {
        Address [] addresses=new Address[]
                {new Address(IP_ADDRESS,PORT)};
        ConnectionFactory factory=new ConnectionFactory();
        factory.setUsername("root");
        factory.setPassword("root");
        //創建連接
        Connection connection=factory.newConnection(addresses);
        //創建通道,通道不同於一般網絡連接,它是雙向傳輸的。
        final Channel channel=connection.createChannel();
        Consumer consumer=new DefaultConsumer(channel){
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("recv msg: "+new String(body));
                try {
                    TimeUnit.SECONDS.sleep(1);
                }catch (Exception e){
                    e.printStackTrace();
                }
                channel.basicAck(envelope.getDeliveryTag(),false);
           }
        };

        channel.basicConsume(QUEUE_NAME,consumer);
        TimeUnit.SECONDS.sleep(60);
        channel.close();
        connection.close();
    }
}

2、RabbitProducer 生產者

public class RabbitProducer {
    private static final String EXCHANGE_NAME="exchange_demo";
    private static final String ROUTING_KEY="routingkey_demo";
    private static final String QUEUE_NAME="queue_demo";
    private static final String IP_ADDRESS="192.168.1.15";
    private static final int PORT=5672;

    public static void main(String [] args) throws Exception{
        ConnectionFactory factory=new ConnectionFactory();
        factory.setHost(IP_ADDRESS);
        factory.setPort(PORT);
        factory.setUsername("root");
        factory.setPassword("root");
         //創建連接
        Connection connection=factory.newConnection();
          //創建通道,通道不同於一般網絡連接,它是雙向傳輸的。
        Channel channel=connection.createChannel();
        //創建一個Direct類型,持久化、非自動刪除的交換器
        channel.exchangeDeclare(EXCHANGE_NAME,"direct",true,false,null);
        //創建一個持久化、非排他,非自動刪除的交換器
        channel.queueDeclare(QUEUE_NAME,true,false,false,null);
        //綁定交換器和隊列,通過路由鍵綁定【值得注意的是這裏是通過路由鍵綁定】
        channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
        String msg="Hello World";
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,
                MessageProperties.PERSISTENT_TEXT_PLAIN,msg.getBytes());
        //close
        channel.close();
        connection.close();
    }

}

3、運行流程

1、服務器建立一個連接(生產者啓動)。然後在這個連接之上創建一個信道、之後創建一個交換器和一個隊列。並通過路由鍵綁定,發送消息,關閉當前資源
2、創建連接,注意創建時必須要指定連接地址。不同於生產者的PORT是綁定在factory上的,消費者的端口是,是在連接創建是綁定。
3、消費者創建信道。設置客戶端最多接受未被ack的消息的個數。
4、注入監聽,等待服務端響應。

五、RabbitMQ在Spring boot中的使用

1、添加依賴

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

2、設置連接屬性

spring.rabbitmq.username=root
spring.rabbitmq.password=root
spring.rabbitmq.host=localhost
spring.rabbitmq.port=5672
#投遞消息間隔時間
spring.kafka.consumer.heartbeat-interval=2000
#投遞消息最大次數
spring.rabbitmq.template.retry.max-attempts=2
#當爲false時,如果沒有收到消費者的ack,會無線投遞,設置爲true
#最多隻會投遞三次
spring.rabbitmq.listener.simple.retry.enabled=true

3、聲明隊列

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:05
 */
@Configuration
public class QueueConfiguration {
    public static final String FIRST_QUEUE="com.mq.first";
    public static final String TWO_QUEUE="com.mq.two";
    public static final String THREE_QUEUE="com.mq.three";
    public static final String REQUEST_QUEUE="REQUEST_QUEUE";

    @Bean(name = FIRST_QUEUE)
    public Queue getFirstQueue(){
        System.out.println("create Queue 1");
        return new Queue(FIRST_QUEUE);
    }

    @Bean(name = TWO_QUEUE)
    public Queue getTowQueue(){
        System.out.println("create Queue 2");

        return new Queue(TWO_QUEUE);
    }

    @Bean(name = THREE_QUEUE)
    public Queue getThreeQueue(){
        System.out.println("create Queue 3");
        return new Queue(THREE_QUEUE);
    }
}

2、交換器


/**
 * fanout路由策略(廣播機制)的交換機注入、Queue與Exchange的綁定注入
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:11
 */
@Configuration
public class TopicExchangeAndBindingConfiguration {
    public static final String TOPIC_EXCHANGE="Topic_EXCHANGE";

    @Bean(name = TOPIC_EXCHANGE)
    TopicExchange  getFanoputExchange(){
        System.out.println("create Exchange");
        return new TopicExchange(TOPIC_EXCHANGE);
    }

    /**
     * 將myFirstQueue對應的Queue綁定到此topicExchange,並指定路由鍵爲"routingKey.#"
     * 即:此Exchange中,路由鍵以"routingKey."開頭的Queue將被匹配到
     * @param queue
     * @param myFanoutExchange
     * @return
     */
    @Bean
    Binding bindingQueueFirst(@Qualifier(QueueConfiguration.FIRST_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueFirst");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("com.#");
    }

    /**
     *將myTwoQueue對應的Queue綁定到此topicExchange,並指定路由鍵爲"#.topic"
     *即:此Exchange中,路由鍵以".topic"結尾的Queue將被匹配到
     * @param queue
     * @param myFanoutExchange
     * @return
     */
    @Bean
    Binding bindingQueueTwo(@Qualifier(QueueConfiguration.TWO_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueTwo");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("#.two");
    }
    /**
     * 將myThreeQueue對應的Queue綁定到此topicExchange,並指定路由鍵爲"#"
     * 即:此topicExchange中,任何Queue都將被匹配到
     */
    @Bean
    Binding bindingQueueThree(@Qualifier(QueueConfiguration.THREE_QUEUE) Queue queue,@Qualifier(TOPIC_EXCHANGE) TopicExchange myFanoutExchange){
        System.out.println("bindingQueueThree");
        return BindingBuilder.bind(queue).to(myFanoutExchange).with("#");
    }
}

3、監聽消息


/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:18
 */
@Component
public class DemoMessageListener {

    @RabbitListener(queues = QueueConfiguration.FIRST_QUEUE)
    public void firstConsumer(String msg){
        System.out.println("I am first queue :"+ msg);
    }

    @RabbitListener(queues = QueueConfiguration.TWO_QUEUE)
    public void twoConsumer(String msg){
        System.out.println("I am two queue :"+ msg);
    }

    @RabbitListener(queues = QueueConfiguration.THREE_QUEUE)
    public void threeConsumer(String msg){
        System.out.println("I am three queue :"+ msg);
    }

4、發送消息

/**
 * @author xuyuanpeng
 * @version 1.0
 * @date 2019-05-17 11:22
 */
@SpringBootTest(classes = StartApplication.class)
@RunWith(SpringRunner.class)
public class TestMQ {
    @Autowired
    private AmqpTemplate amqpTemplate;

    @Test
    public void sendMsg(){
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.FIRST_QUEUE,"第一");
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.TWO_QUEUE,"第二");
        amqpTemplate.convertAndSend(TopicExchangeAndBindingConfiguration.TOPIC_EXCHANGE,QueueConfiguration.THREE_QUEUE,"第三");
    }
}

5、請求流程

執行測試用例的代碼。
發送消息,到指定交換器,再通過RoutingKey匹配BindingKey,如果匹配的上,就發送消息。監聽可以監聽到消息的發送。

6、RoutingKey與BindingKey

RoutingKey是路由key,BindingKey是交換器與隊列綁定的key
請求發送消息後,會根據RoutingKey在交換器上尋找,交換器與隊列連接的BindingKey,如果能匹配上,就將消息發送到隊列。
消費者再到隊列中取值。

六、RabbitMQ中的基礎組件

1、生產者

說明

生產者是投遞消息的一方

消息體

業務邏輯數據,如JSON串

標籤

用來表述這條消息,比如一個交換器名稱與一個路由鍵
生產者吧消息傳給RabbitMQ,RabbitMQ之後會根據標籤把消息發送給感興趣的消費者

2、隊列

說明

在消息路由的過程中,消息的標籤會被丟棄,存入隊列的只有消息的消息體

作用

是RabbitMQ的內部對象,用以存儲消息

擴展

Kafka是將消息存儲是topic上

多個消費者可以訂閱同一個消息隊列,這時隊列的消息會會被輪詢給多個消費者進行處理。而不是每個消費者都收到所有的消息並處理

3、消費者

說明

消費者hi接收消息的一方
消費者連接到RabbitMQ服務器,並訂閱到隊列上

4、Broker

說明

消息中間件的服務街店
對於RabbitMQ 來說,一個RabbitMQ Broker可以簡單的看做一個RabbitMQ服務節點,或者說是RabbitMQ的服務實例。

5、交換器

說明

生產者將消息發送到交換器上,交換器將消息路由到一個或多個隊列上,如果路由不到,或許會返回給生產者,或許直接丟棄

路由類型–RoutingKey路由鍵

生產者將消息發給交換器時,一般會指定一個RoutingKey,用來指定這個消息的路由規則。而這個RoutingKey則需要交換器類型和綁定鍵BindingKey聯合才能最終生效
生產者在發送消息給交換器時可以通過RoutingKey指定消息的流向

路由類型–BindingKey綁定

將交換器與隊列關聯起來。生成一個BingdingKey

交換器類型
  • fanout
    他會把所有發送到該交換器的消息,路由到所有與該交換器綁定的隊列中
  • direct
    會吧消息路由到BindingKey和RoutingKey完全匹配的隊列中
  • topic
    會把消息路由到BindingKey和RoutingKey完全匹配的隊列中
    RoutingKey爲被點號‘.’分割的字符串
    com.rabbitmq.client、com.hidden.client
    BindingKey同上
    可以用*、#進行匹配
    匹配全部

一次請求的流程

  • 生產者處理業務數據
  • 序列化業務數據
  • 指定交換器以及路由Key,即爲消息添加了Label(標籤)
  • 發送消息到Broker中
  • RabbitMQ Broker處理消息
  • 消費者訂閱並接受消息
  • 消費者獲取到消息
  • 反序列化消息
  • 業務處理數據

七、總結

Rabbit作爲消息中間件
在這裏插入圖片描述
在實際應用中的流程圖大致如上:
前置場景是,監聽中心已啓動,監聽中心已經創建了交換器,消息隊列,以及綁定了交換器和消息隊列。聲明瞭監聽,即當隊列中有符合BindingKey的消息時會進入監聽處理業務。
1、業務處理的服務A,要處理一個業務,但是業務耗時過長或由於其他原因,決定使用消息。
2、指定發送的路由鍵,交換器,以及發送的消息內容
3、監聽中心調用其他微服務處理業務或直接處理業務

八、參考

參考書籍《RabbitMQ實戰指南》

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