Spring Boot消息隊列應用實踐

消息隊列是大型複雜系統解耦利器。本文根據應用廣泛的消息隊列RabbitMQ,介紹Spring Boot應用程序中隊列中間件的開發和應用。

一、RabbitMQ基礎

1、RabbitMQ簡介

RabbitMQ是Spring所在公司Pivotal自己的產品,是基於AMQP高級隊列協議的消息中間件,採用erlang開發,所以你的RabbitMQ隊列服務器需要erlang環境。

可以直接參考官方的說法:RabbitMQ is the most widely deployed open source message broker.言簡意賅,一目瞭然。

2、AMQP

高級消息隊列協議(AMQP)是一個異步消息傳遞所使用的應用層協議規範。作爲線路層協議(AMQP是一個抽象的協議,它不負責處理具體的數據),而不是API(例如Java消息系統JMS),AMQP客戶端能夠無視消息的來源任意發送和接受信息。
AMQP的原始用途只是爲金融界提供一個可以彼此協作的消息協議,而現在的目標則是爲通用消息隊列架構提供通用構建工具。因此,面向消息的中間件(MOM)系統,例如發佈/訂閱隊列,沒有作爲基本元素實現。反而通過發送簡化的AMQ實體,用戶被賦予了構建例如這些實體的能力。這些實體也是規範的一部分,形成了在線路層協議頂端的一個層級:AMQP模型。這個模型統一了消息模式,諸如之前提到的發佈/訂閱,隊列,事務以及流數據,並且添加了額外的特性,例如更易於擴展,基於內容的路由。

擴展閱讀:既然有高級的消息協議,必然有簡單的協議,STOMP(Simple (or Streaming) Text Orientated Messaging Protocol),也就是簡單消息文本協議,猛擊這裏

3、MSMQ

這裏附帶介紹一下MSMQ。.NET開發者接觸最多的可能還是這個消息隊列,我知道有兩個以.NET作爲主要開發語言的公司都選擇MSMQ來開發公共框架如ESB、日誌組件等。

如果你有.NET下MSMQ(微軟消息隊列)開發和使用經驗,一定不會對隊列常用術語陌生。對比一下,對後面RabbitMQ的學習和理解非常有幫助。

邏輯結構如下:

4、基本術語  

安裝好RabbitMQ後,可以啓用插件,打開RabbitMQ自帶的後臺,一圖勝千言,你會看到很多似曾相識的技術術語和名詞。

當然你也可以參考這裏的圖片示例一個一個驗證下面的名詞。

(1)Broker:消息隊列服務器實體。

(2)Producer:生產者。

(3)Consumer:消費者。

(4)Queue(隊列):消息隊列載體,每個消息都會被投入到一個或多個隊列。Queue是 RabbitMQ 的內部對象,用於存儲消息;消費者Consumer就是通過訂閱隊列來獲取消息的,RabbitMQ 中的消息都只能存儲在 Queue 中,生產者Producer生產消息並最終投遞到 Queue 中,消費者可以從 Queue 中獲取消息並消費;多個消費者可以訂閱同一個 Queue。

(5)Connection(連接):Producer 和 Consumer 通過TCP 連接到 RabbitMQ Server。

(6)Channel(信道):基於 Connection創建,數據流動都是在 Channel 中進行。

(7)Exchange(交換器):生產者將消息發送到 Exchange(交換器),由Exchange 將消息路由到一個或多個 Queue 中(或者丟棄);Exchange 並不存儲消息;Exchange Types 常用的有 Fanout、Direct、Topic 和Header四種類型,每種類型對應不同的路由規則:
Direct:完全匹配,消息路由到那些 Routing Key 與 Binding Key 完全匹配的 Queue 中。比如 Routing Key 爲mq_cleint-key,只會轉發mq_cleint-key,不會轉發mq_cleint-key.1,也不會轉發mq_cleint-key.1.2。
Topic:模式匹配,Exchange 會把消息發送到一個或者多個滿足通配符規則的 routing-key 的 Queue。其中*表示匹配一個 word,#匹配多個 word 和路徑,路徑之間通過.隔開。如滿足a.*.c的 routing-key 有a.hello.c;滿足#.hello的 routing-key 有a.b.c.hello。
Fanout:忽略匹配,把所有發送到該 Exchange 的消息路由到所有與它綁定 的Queue 中。

Header:也根據規則匹配,相較於Direct和Topic固定地使用RoutingKey ,Headers 則是一個自定義匹配規則的類型。在隊列與交換器綁定時, 會設定一組鍵值對(Key-Value)規則, 消息中也包括一組鍵值對( Headers 屬性), 當這些鍵值對有一對,,或全部匹配時, 消息被投送到對應隊列。

(8)Binding(綁定):是 Exchange(交換器)將消息路由給 Queue 所需遵循的規則。

(9)Routing Key(路由鍵):消息發送給 Exchange(交換器)時,消息將擁有一個路由鍵(默認爲空), Exchange(交換器)根據這個路由鍵將消息發送到匹配的隊列中。

(10)Binding Key(綁定鍵):指定當前 Exchange(交換器)下,什麼樣的 Routing Key(路由鍵)會被下派到當前綁定的 Queue 中。

5、應用場景

我們使用一個技術或組件或中間件,必須要非常理解它的適用場景,否則很容易誤用。

RabbitMQ的經典應用場景包括:異步處理、應用解耦、流量削峯、日誌處理、消息通訊。

已經有很多人總結了這5種場景下的RabbitMQ實際應用。

推薦閱讀:猛擊這裏

到這裏,RabbitMQ基礎知識介紹結束,下面開始動手實踐。

添加依賴

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

配置RabbitMQ

## RabbitMQ相關配置
spring.application.name=springbootdemo
spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=springbootmq
spring.rabbitmq.password=123456
application.mq.properties

新增RabbitMQConfig類

package com.power.demo.messaging;

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

/**
 * RabbitMQ消息隊列配置類
 * <p>
 * 注意:如果已在配置文件中聲明瞭Queue對象,就不用在RabbitMQ的管理員頁面創建隊列(Queue)了
 */
@Configuration
public class RabbitMQConfig {

    /**
     * 聲明接收字符串的隊列 Hello 默認
     *
     * @return
     */
    @Bean
    public Queue stringQueue() {

        //boolean isDurable = true;//是否持久化
        //boolean isExclusive = false;  //僅創建者可以使用的私有隊列,斷開後自動刪除
        //boolean isAutoDelete = false;  //當所有消費客戶端連接斷開後,是否自動刪除隊列
        //Queue queue = new Queue(MQField.HELLO_STRING_QUEUE, isDurable, isExclusive, isAutoDelete);
        //return  queue;

        //return new Queue(MQField.HELLO_STRING_QUEUE); //默認支持持久化

        return QueueBuilder.durable(MQField.HELLO_STRING_QUEUE)
                //.exclusive()
                //.autoDelete()
                .build();
    }

    /**
     * 聲明接收Goods對象的隊列 Hello  支持持久化
     *
     * @return
     */
    @Bean
    public Queue goodsQueue() {

        return QueueBuilder.durable(MQField.HELLO_GOODS_QUEUE).build();
    }

    /**
     * 聲明WorkQueue隊列 competing consumers pattern,多個消費者不會重複消費隊列的相同消息
     *
     * @return
     */
    @Bean
    public Queue workQueue() {
        return QueueBuilder.durable(MQField.MY_WORKER_QUEUE).build();
    }

    /**
     * 消息隊列中最常見的模式:發佈訂閱模式
     * <p>
     * 聲明發布訂閱模式隊列 Publish/Subscribe
     * <p>
     * exchange類型包括:direct, topic, headers 和 fanout
     **/

    /*fanout(廣播)隊列相關聲明開始*/
    @Bean
    public Queue fanOutAQueue() {
        return QueueBuilder.durable(MQField.MY_FANOUTA_QUEUE).build();
    }

    @Bean
    public Queue fanOutBQueue() {
        return QueueBuilder.durable(MQField.MY_FANOUTB_QUEUE).build();
    }

    @Bean
    FanoutExchange fanoutExchange() {
        return (FanoutExchange) ExchangeBuilder.fanoutExchange(MQField.MY_FANOUT_EXCHANGE).build();

        //return new FanoutExchange(MQField.MY_FANOUT_EXCHANGE);
    }

    @Bean
    Binding bindingExchangeA(Queue fanOutAQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanOutAQueue).to(fanoutExchange);
    }

    @Bean
    Binding bindingExchangeB(Queue fanOutBQueue, FanoutExchange fanoutExchange) {
        return BindingBuilder.bind(fanOutBQueue).to(fanoutExchange);
    }

    /*fanout隊列相關聲明結束*/


    /*topic隊列相關聲明開始*/

    @Bean
    public Queue topicAQueue() {
        return QueueBuilder.durable(MQField.MY_TOPICA_QUEUE).build();
    }

    @Bean
    public Queue topicBQueue() {
        return QueueBuilder.durable(MQField.MY_TOPICB_QUEUE).build();
    }

    @Bean
    TopicExchange topicExchange() {
        return (TopicExchange) ExchangeBuilder.topicExchange(MQField.MY_TOPIC_EXCHANGE).build();
    }

    //綁定時,注意隊列名稱與上述方法名一致
    @Bean
    Binding bindingTopicAExchangeMessage(Queue topicAQueue, TopicExchange topicExchange) {
        return BindingBuilder.bind(topicAQueue).to(topicExchange).with(MQField.MY_TOPIC_ROUTINGKEYA);
    }

    @Bean
    Binding bindingTopicBExchangeMessages(Queue topicBQueue, TopicExchange topicExchange) {

        return BindingBuilder.bind(topicBQueue).to(topicExchange).with(MQField.MY_TOPIC_ROUTINGKEYB);

    }

    /*topic隊列相關聲明結束*/

    /*direct隊列相關聲明開始*/

    @Bean
    public Queue directAQueue() {
        return QueueBuilder.durable(MQField.MY_DIRECTA_QUEUE).build();
    }

    @Bean
    public Queue directBQueue() {
        return QueueBuilder.durable(MQField.MY_DIRECTB_QUEUE).build();
    }

    /**
     * 聲明Direct交換機 支持持久化.
     *
     * @return the exchange
     */
    @Bean
    DirectExchange directExchange() {
        return (DirectExchange) ExchangeBuilder.directExchange(MQField.MY_DIRECT_EXCHANGE).durable(true).build();
    }

    @Bean
    Binding bindingDirectAExchangeMessage(Queue directAQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(directAQueue).to(directExchange).with(MQField.MY_DIRECT_ROUTINGKEYA);
    }

    @Bean
    Binding bindingDirectBExchangeMessage(Queue directBQueue, DirectExchange directExchange) {
        return BindingBuilder.bind(directBQueue).to(directExchange)
                //.with(MQField.MY_DIRECT_ROUTINGKEYB)
                .with(MQField.MY_DIRECT_ROUTINGKEYB);
    }

    /*direct隊列相關聲明結束*/
}
RabbitMQConfig

RabbitMQConfig我將常用到的模式都配置在裏面了,註釋已經寫得很清楚,在詳細介紹模式的地方會用到這裏定義的隊列、綁定和交換器。

持久化配置

在RabbitMQConfig類中尤其注意這幾個參數,包括是否可持久化durable;僅創建者可以使用的私有隊列,斷開後自動刪除exclusive;當所有消費客戶端連接斷開後,是否自動刪除隊列autoDelete。其中durable和autoDelete對隊列和交換器都可以配置。

RabbitMQ支持的消息的持久化(durable),也就是將數據寫在磁盤上,爲了數據安全考慮,絕大多數場景下我們都會選擇持久化,可能記錄一些不是業務必須的日誌稍微例外。
消息隊列持久化包括3個部分:

(1)、隊列持久化,在聲明時指定Queue.durable爲1

(2)、交換器持久化,在聲明時指定Exchange.durable爲1

(3)、消息持久化,在投遞時指定消息的delivery_mode爲2(而1表示非持久化) 參考:這裏

如果Exchange和Queue都是持久化的,那麼它們之間的Binding也是持久化的;如果Exchange和Queue兩者之間有一個持久化,另一個非持久化,就不允許建立綁定。

二、常見模式

在Spring Boot下使用RabbitMQ非常容易,直接調用AmqpTemplate類封裝好的接口即可。

1、hello world

 

P爲生產者,C爲消費者,中間紅色框表示消息隊列。生產者P將消息發送到消息隊列Queue,消費者C對消息進行處理。

生產者:

package com.power.demo.messaging.hello;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

/**
 * Hello消息生產者
 **/
@Component
public class HelloSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean send(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.HELLO_STRING_QUEUE, message);

        isOK = true;

        System.out.println(String.format("HelloSender發送字符串消息結果:%s", isOK));

        return isOK;
    }

    public boolean send(GoodsVO goodsVO) throws Exception {

        boolean isOK = false;

        rabbitTemplate.convertAndSend(MQField.HELLO_GOODS_QUEUE, goodsVO);

        isOK = true;

        System.out.println(String.format("HelloSender發送對象消息結果:%s", isOK));

        return isOK;

    }

}
HelloSender

消費者:

package com.power.demo.messaging.hello;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * Hello消息消費者
 **/
@Component
public class HelloReceiver {

    @RabbitListener(queues = MQField.HELLO_STRING_QUEUE)
    @RabbitHandler
    public void process(String message) {

        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }

        System.out.println("HelloReceiver接收到的字符串消息是 => " + message);
    }


    @RabbitListener(queues = MQField.HELLO_GOODS_QUEUE)
    @RabbitHandler
    public void process(GoodsVO goodsVO) {
        System.out.println("------ 接收實體對象 ------");
        System.out.println("HelloReceiver接收到的實體對象是 => " + SerializeUtil.Serialize(goodsVO));
    }
}
HelloReceiver

這是最簡單的一種模式,這個最簡單示例,可以看到應用場景裏的異步處理的影子。

在Controller中,新增一個接口:

    @RequestMapping(value = "/hello/sendmsg", method = RequestMethod.GET)
    @ApiOperation("簡單字符串消息測試")
    @ApiImplicitParams({
            @ApiImplicitParam(paramType = "query", name = "message", required = true, value = "字符串消息", dataType = "String")
    })
    public String sendMsg(String message) throws Exception {

        boolean isOK = helloSender.send(message);

        return String.valueOf(isOK);
    }
sendmsg

按照傳統方式調用RPC接口,通常都是同步等待接口返回,而使用隊列後,消息生產者直接向RabbitMQ服務器發送一條消息,不需要同步等待這個消息的處理結果。

示例代碼中,消息消費者會刻意等待5秒(Thread.sleep(5000);)後才處理(打印出)消息,但是實際調用這個接口的時候,非常快就返回成功結果了,因爲這個發送消息的動作不需要等待消費者消費消息的結果。

發送的消息,除了簡單消息對象如字符串等,示例裏你還看到有一個發送商品對象的消息,也就是說明RabbitMQ支持自定義的複雜對象消息。

2、work queues

P爲生產者,C1、C2爲消費者,中間紅色框表示消息隊列。生產者P將消息發送到消息隊列Queue,消費者C1和C2對消息進行處理。

這種模式比較容易產生誤解的地方是,多個消費者會不會消費隊列裏的同一條消息。答案是不會。

官方的說明是因爲消費者根據競爭消費模式(competing consumers pattern)分派任務(Distributing tasks among workers (the competing consumers pattern) )。

對於work queues這種模式,同一條消息M1,要麼C1拉取到,要麼C2拉取到,不會出現C1和C2同時拉取到並消費。

當然,這種模式還可以擴展,除了一個生產者,也可以有多個生產者。

生產者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class WorkProducerA {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean send(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_WORKER_QUEUE, message);

        isOK = true;

        System.out.println(String.format("WorkProducerA發送字符串消息結果:%s", isOK));

        return isOK;
    }
}
WorkProducerA

相同隊列下另一個生產者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class WorkProducerB {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean send(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_WORKER_QUEUE, message);

        isOK = true;

        System.out.println(String.format("WorkProducerB發送字符串消息結果:%s", isOK));

        return isOK;
    }
}
WorkProducerB

消費者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.concurrent.atomic.AtomicInteger;

@Component
public class WorkConsumerA {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    @RabbitListener(queues = MQField.MY_WORKER_QUEUE)
    @RabbitHandler
    public void process(String message) throws Exception {

        int index = atomicInteger.getAndIncrement();

        Thread.sleep(2000);

        System.out.println("WorkConsumerA接收到的字符串消息是 => " + message);

        System.out.println("WorkConsumerA自增序號 => " + index);
    }

}
WorkConsumerA

另一個消費者:

package com.power.demo.messaging.workqueues;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.util.concurrent.atomic.AtomicInteger;

@Component
public class WorkConsumerB {

    private static AtomicInteger atomicInteger = new AtomicInteger();

    @RabbitListener(queues = MQField.MY_WORKER_QUEUE)
    @RabbitHandler
    public void process(String message) throws Exception {

        int index = atomicInteger.getAndIncrement();

        Thread.sleep(10);

        System.out.println("WorkConsumerB接收到的字符串消息是 => " + message);

        System.out.println("WorkConsumerB自增序號 => " + index);
    }

}
View Code

pub/sub

應用最廣泛的發佈/訂閱模式。

官方的說法是:發送多個消息到多個消費者(Sending messages to many consumers at once.)

這個模式和work queues模式最明顯的區別是,隊列Queue前加了一層,多了Exchange(交換器)。

 P爲生產者,X爲交換器,C1、C2爲消費者,中間紅色框表示消息隊列。生產者P將消息不是直接發送到隊列Queue,而是發送到交換器X(注意:交換器Exchange並不存儲消息),然後由交換機X發送給兩個隊列,兩個消費者C1和C2各自監聽一個隊列,來消費消息。

根據交換器類型的不同,又可以分爲Fanout、Direct和Topic這三種消費方式,Headers方式實際應用不是非常廣泛,本文暫不討論。

3、fanout

任何發送到Fanout Exchange的消息都會被轉發到與該Exchange綁定(Binding)的所有Queue上。

(1)可以理解爲路由表的模式

(2)這種模式不需要RoutingKey,即使配置了也忽略

(3)這種模式需要提前將Exchange與Queue進行綁定,一個Exchange可以綁定多個Queue,一個Queue可以同多個Exchange進行綁定

(4)如果接受到消息的Exchange沒有與任何Queue綁定,則消息會被拋棄

Fanout廣播模式實現同一個消息被多個消費者消費,而work queues是同一個消息只能有一個消費者(競爭去)消費。

生產者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class FanoutSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean send(GoodsVO goodsVO) throws Exception {

        boolean isOK = false;

        if (goodsVO == null) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_FANOUT_EXCHANGE, "", goodsVO);

        isOK = true;

        System.out.println(String.format("FanoutSender發送對象消息結果:%s", isOK));

        return isOK;

    }

}
FanoutSender

消費者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class FanoutReceiverA {

    @RabbitListener(queues = MQField.MY_FANOUTA_QUEUE)
    @RabbitHandler
    public void process(GoodsVO goodsVO) {
        System.out.println("FanoutReceiverA接收到的商品消息是 => " + SerializeUtil.Serialize(goodsVO));
    }
}
FanoutReceiverA

另一個消費者:

package com.power.demo.messaging.pubsub.fanout;

import com.power.demo.entity.vo.GoodsVO;
import com.power.demo.messaging.MQField;
import com.power.demo.util.SerializeUtil;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class FanoutReceiverB {

    @RabbitListener(queues = MQField.MY_FANOUTB_QUEUE)
    @RabbitHandler
    public void process(GoodsVO goodsVO) {
        System.out.println("FanoutReceiverB接收到的商品消息是 => " + SerializeUtil.Serialize(goodsVO));
    }
}
FanoutReceiverB

4、direct

Fanout是1對多以廣播的方式,發送給所有的消費者。

Direct則是創建消息隊列的時候,指定一個BindingKey。當發送者發送消息的時候,指定對應的RoutingKey,當RoutingKey和消息隊列的BindingKey一致的時候,消息將會被髮送到該消息隊列中。
Direct廣播模式最明顯不同於Fanout模式的地方是,消費者可以進行消息過濾,有選擇的進行接收想要消費的消息,也就是隊列綁定關鍵字,發送者將數據根據關鍵字發送到Exchange,Exchange根據關鍵字判定應該將數據發送(路由)到指定隊列。

任何發送到Direct Exchange的消息都會被轉發到RoutingKey中指定的Queue。

(1)消息傳遞時需要一個“RoutingKey”,可以簡單的理解爲要發送到的隊列名字

(2)如果vhost中不存在RouteKey中指定的隊列名,則該消息會被拋棄

生產者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class DirectSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean sendDirectA(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_DIRECT_EXCHANGE, MQField.MY_DIRECT_ROUTINGKEYA, message);

        isOK = true;

        System.out.println(String.format("DirectSender發送DirectA字符串消息結果:%s", isOK));

        return isOK;
    }

    public boolean sendDirectB(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_DIRECT_EXCHANGE, MQField.MY_DIRECT_ROUTINGKEYB, message);

        isOK = true;

        System.out.println(String.format("DirectSender發送DirectB字符串消息結果:%s", isOK));

        return isOK;
    }

}
DirectSender

消費者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class DirectReceiverA {

    @RabbitListener(queues = MQField.MY_DIRECTA_QUEUE)
    @RabbitHandler
    public void process(String message) {
        System.out.println("DirectReceiverA接收到的字符串消息是 => " + message);
    }

}
DirectReceiverA

另一個消費者:

package com.power.demo.messaging.pubsub.direct;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class DirectReceiverB {

    @RabbitListener(queues = MQField.MY_DIRECTB_QUEUE)
    @RabbitHandler
    public void process(String message) {
        System.out.println("DirectReceiverB接收到的字符串消息是 => " + message);
    }

}
DirectReceiverB

5、topic

Topic轉發信息主要是依據通配符,隊列和交換機的綁定主要是依據一種模式(通配符+字符串),而當發送消息的時候,只有指定的RoutingKey和該模式相匹配的時候,消息纔會被髮送到該消息隊列中。

任何發送到Topic Exchange的消息都會被轉發到所有關心RoutingKey中指定話題的Queue上

(1)每個隊列都有其關心的主題,所有的消息都帶有一個“標題”(RoutingKey),Exchange會將消息轉發到所有關注主題能與RouteKey模糊匹配的隊列

(2)需要RoutingKey,也需要提前綁定Exchange與Queue

(3)在進行綁定時,要提供一個該隊列關心的主題,如“#.log.#”表示該隊列關心所有涉及log的消息(一個RoutingKey爲”mq.log.error”的消息會被轉發到該隊列)

(4)“#”表示0個或若干個關鍵字,“*”表示一個關鍵字。如“log.*”能與“log.warn”匹配,無法與“log.warn.timeout”匹配;但“log.#”能與上述兩者都匹配

(5)如果Exchange沒有發現能夠與RouteKey匹配的Queue,則會拋棄此消息

生產者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.StringUtils;

@Component
public class TopicSender {

    @Autowired
    private AmqpTemplate rabbitTemplate;

    public boolean sendTopicA(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, MQField.MY_TOPIC_ROUTINGKEYA, message);

        isOK = true;

        System.out.println(String.format("TopicSender發送TopicA字符串消息結果:%s", isOK));

        return isOK;
    }

    public boolean sendTopicB(String message) throws Exception {
        boolean isOK = false;

        if (StringUtils.isEmpty(message)) {
            System.out.println("消息爲空");
            return isOK;
        }

        rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, MQField.MY_TOPIC_ROUTINGKEYB, message);

        isOK = true;

        System.out.println(String.format("TopicSender發送TopicB字符串消息結果:%s", isOK));

        return isOK;
    }

    public boolean sendToMatchedTopic() {

        boolean isOK = false;

        String routingKey = "my_topic_routingkeyA.16";//模糊匹配MQField.MY_TOPIC_ROUTINGKEYA

        //String routingKey = "my_topic_routingkeyB.32";//模糊匹配MQField.MY_TOPIC_ROUTINGKEYB

        String matchedKeys = "";
        if (MQField.MY_TOPIC_ROUTINGKEYA.contains(routingKey.split("\\.")[0])) {
            matchedKeys = "TopicReceiverA";
        } else if (MQField.MY_TOPIC_ROUTINGKEYB.contains(routingKey.split("\\.")[0])) {
            matchedKeys = "TopicReceiverB";
        }

        String msg = "message to matched receivers:" + matchedKeys;

        rabbitTemplate.convertAndSend(MQField.MY_TOPIC_EXCHANGE, routingKey, msg);

        isOK = true;

        System.out.println(String.format("TopicSender發送字符串消息結果:%s", isOK));

        return isOK;
    }

}
TopicSender

消費者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

@Component
public class TopicReceiverA {

    @RabbitListener(queues = MQField.MY_TOPICA_QUEUE)
    @RabbitHandler
    public void process(String message) {
        System.out.println("TopicReceiverA接收到的字符串消息是 => " + message);
    }

}
TopicReceiverA

另一個消費者:

package com.power.demo.messaging.pubsub.topic;

import com.power.demo.messaging.MQField;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;


@Component
public class TopicReceiverB {

    @RabbitListener(queues = MQField.MY_TOPICB_QUEUE)
    @RabbitHandler
    public void process(String message) {
        System.out.println("TopicReceiverB接收到的字符串消息是 => " + message);
    }

}
TopicReceiverB

示例代碼中,定義了兩個topic,生產者通過調用sendToMatchedTopic方法,根據RoutingKey模糊匹配,將消息發送到匹配的隊列上。

到這裏,發佈訂閱模式的介紹就結束了。我們再來總結下發布訂閱模式下RabbitMQ消息隊列主要工作流程。以Topic爲例:

生產者
1、獲取一個連接(Connection)
2、從連接(Connection)上獲取一個信道( Channel)
3、聲明一個交換器( Exchange)
4、聲明1個或多個隊列(Queue)
5、把隊列(Queue)綁定到交換器(Exchange)上
6、向指定的交換器(Exchange)發送消息,消息路由到特定隊列(Queue)

消費者

RabbitMQ消費者消費消息,支持推(push)模式和拉(pull)模式,這裏以拉模式說明下流程。

1、創建一個連接(Connection)
2、啓動MainLoop後臺線程,通過連接(Connection)循環拉取消息
3、處理並確認消息被消費

6、rpc

RPC調用流程說明:
(1)當客戶端啓動的時候,它創建一個匿名獨享的回調隊列

(2)在 RPC 請求中,客戶端發送帶有兩個屬性的消息:一個是設置回調隊列的 reply_to 屬性,另一個是設置唯一值的 correlation_id 屬性

(3)將請求發送到一個 rpc_queue 隊列中

(4)服務器等待請求發送到這個隊列中來。當請求出現的時候,它執行他的工作並且將帶有執行結果的消息發送給 reply_to 字段指定的隊列。

(5)客戶端等待回調隊列裏的數據。當有消息出現的時候,它會檢查 correlation_id 屬性。如果此屬性的值與請求匹配,將它返回給應用

Callback queue回調隊列,客戶端向服務器發送請求,服務器端處理請求後,將其處理結果保存在一個存儲體中。而客戶端爲了獲得處理結果,那麼客戶在向服務器發送請求時,同時發送一個回調隊列地址reply_to。
Correlation id關聯標識,客戶端可能會發送多個請求給服務器,當服務器處理完後,客戶端無法辨別在回調隊列中的響應具體和那個請求時對應的。爲了處理這種情況,客戶端在發送每個請求時,同時會附帶一個獨有correlation_id屬性,這樣客戶端在回調隊列中根據correlation_id字段的值就可以分辨此響應屬於哪個請求。

服務端:

package com.power.demo.messaging.rpc;

import com.power.demo.messaging.MQField;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Consumer;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.concurrent.TimeoutException;

public class RPCServer {

    private static int fib(int n) {
        if (n == 0) {
            return 0;
        }
        if (n == 1) {
            return 1;
        }
        return fib(n - 1) + fib(n - 2);
    }

    //直接運行此方法
    public static void main(String[] argv) {
        ConnectionFactory factory = new ConnectionFactory();
        //factory.setHost("localhost");

        Connection connection = null;
        try {
            connection = factory.newConnection();
            final Channel channel = connection.createChannel();

            channel.queueDeclare(MQField.MY_RPC_QUEUE, false, false, false, null);

            channel.basicQos(1);

            System.out.println(" [x] Awaiting RPC requests");

            Consumer consumer = new DefaultConsumer(channel) {
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                    AMQP.BasicProperties replyProps = new AMQP.BasicProperties
                            .Builder()
                            .correlationId(properties.getCorrelationId())
                            .build();

                    String response = "";

                    try {
                        String message = new String(body, "UTF-8");
                        int n = Integer.parseInt(message);

                        System.out.println(" [.] fib(" + message + ")");
                        response += fib(n);

                        System.out.println(String.format("RPCServer計算fib數列應答:%s", response));

                    } catch (RuntimeException e) {
                        System.out.println(" [.] " + e.toString());
                    } finally {
                        channel.basicPublish("", properties.getReplyTo(), replyProps, response.getBytes("UTF-8"));
                        channel.basicAck(envelope.getDeliveryTag(), false);
                        // RabbitMq consumer worker thread notifies the RPC server owner thread
                        synchronized (this) {
                            this.notify();
                        }
                    }
                }
            };

            channel.basicConsume(MQField.MY_RPC_QUEUE, false, consumer);
            // Wait and be prepared to consume the message from RPC client.
            while (true) {
                synchronized (consumer) {
                    try {
                        consumer.wait();
                    } catch (InterruptedException e) {
                        e.printStackTrace();
                    }
                }
            }
        } catch (IOException | TimeoutException e) {
            e.printStackTrace();
        } finally {
            if (connection != null)
                try {
                    connection.close();
                } catch (IOException _ignore) {
                }
        }
    }
}
RPCServer

客戶端:

package com.power.demo.messaging.rpc;

import com.power.demo.messaging.MQField;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Envelope;

import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;

public class RPCClient {

    private Connection connection;
    private Channel channel;
    private String replyQueueName;

    public RPCClient() throws IOException, TimeoutException {
        ConnectionFactory factory = new ConnectionFactory();
        //factory.setHost("localhost");

        connection = factory.newConnection();
        channel = connection.createChannel();

        replyQueueName = channel.queueDeclare().getQueue();
    }

    public String call(String message) throws IOException, InterruptedException {
        final String corrId = UUID.randomUUID().toString();

        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();

        channel.basicPublish("", MQField.MY_RPC_QUEUE, props, message.getBytes("UTF-8"));

        final BlockingQueue<String> response = new ArrayBlockingQueue<String>(1);

        channel.basicConsume(replyQueueName, true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                if (properties.getCorrelationId().equals(corrId)) {
                    response.offer(new String(body, "UTF-8"));
                }
            }
        });

        return response.take();
    }

    public void close() throws IOException {
        connection.close();
    }

    //直接運行此方法
    public static void main(String[] argv) {
        RPCClient fibonacciRpc = null;
        String response = null;
        try {
            fibonacciRpc = new RPCClient();

            System.out.println(" [x] Requesting fib(10)");
            response = fibonacciRpc.call("10");
            System.out.println(" [.] Got '" + response + "'");
            System.out.println(String.format("RPCClient得到計算fib數列應答:%s", response));
        } catch (IOException | TimeoutException | InterruptedException e) {
            e.printStackTrace();
        } finally {
            if (fibonacciRpc != null) {
                try {
                    fibonacciRpc.close();
                } catch (IOException _ignore) {
                }
            }
        }
    }
}
RPCClient

示例代碼我這裏直接改造了一下官方的demo代碼。啓動RPCServer,再運行RPCClient就可以看到RPC調用結果了。

 三、常見問題

1、冪等性

生產環境各種業務系統出現重複消息是不可避免的,因爲不能保證生產者不發送重複消息。

對於讀操作而言,重複消息可能無害,但是對於寫操作,重複消息容易造成業務災難,比如相同消息多次扣減庫存,多次支付請求扣款等。

有一種情況也會造成重複消息,就是RabbitMQ對設置autoAck=false之後沒有被Ack的消息是不會清除掉的,消費者可以多次重複消費。

我個人認爲RabbitMQ只是消息傳遞的載體,要保證冪等性,還是需要在消費者業務邏輯上下功夫。

2、有序消息

我碰到過某廠有一個開發團隊通過Kafka來實現有序隊列,因爲發送的消息有先後依賴關係,需要消費者收到多個消息保存起來最後聚合後一起處理業務邏輯。

但是,其實大部分業務場景下我們都不需要消息有先後依賴關係,因爲有序隊列產生依賴關係,後續消費很容易造成各種處理難題。

歸根結底,我認爲需要有序消息的業務系統在設計上就是不合理的,爭取在設計上規避纔好。當然良好的設計需要豐富的經驗和優化,以及妥協。

3、高可用

RabbitMQ支持集羣,模式主要可分爲三種:單一模式、普通模式和鏡像模式。

RabbitMQ支持彈性部署,在業務高峯期間可通過集羣彈性部署支撐業務系統。

RabbitMQ支持消息持久化,如果隊列服務器出現問題,消息做了持久化,後續恢復正常,消息數據不丟失不會影響正常業務流程。

RabbitMQ還有很多高級特性,比如發佈確認和事務等,雖然可能會降低性能,但是增強了可靠性。

 

參考:

http://www.rabbitmq.com/

https://msdn.microsoft.com/en-us/library/ms711472(v=vs.85).aspx

http://www.cnblogs.com/dubing/p/4017613.html

https://blog.csdn.net/super_rd/article/details/70238869

https://blog.csdn.net/joeyon1985/article/details/39429117

http://www.cnblogs.com/saltlight-wangchao/p/6214334.html

http://www.cnblogs.com/binyue/p/4763766.html

https://my.oschina.net/u/2948566/blog/1624963

https://www.cnblogs.com/rjzheng/p/8994962.html

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