乾貨!基於SpringBoot的RabbitMQ多種模式佇列實戰

Acelin_H 發表於 2021-09-17
Spring RabbitMQ

環境準備


安裝RabbitMQ

由於RabbitMQ的安裝比較簡單,這裡不再贅述。可自行到官網下載http://www.rabbitmq.com/download.html

依賴

SpringBoot專案匯入依賴

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
連線配置

配置檔案新增如下配置(根據自身情況修改配置)

spring.rabbitmq.host=127.0.0.1
spring.rabbitmq.port=5672
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
#spring.rabbitmq.virtual-host=acelin

五種佇列模式實現


1 點對點的佇列

在java配置檔案DirectRabbitConfig中先宣告一個佇列用於接收資訊

public static final String PEER_TO_PEER_QUEUE = "peer-to-peer-queue"; // 點對點佇列

/******************************* Peer-to-peer ******************************/
@Bean
public Queue peer2peerQueue() {
    return new Queue(PEER_TO_PEER_QUEUE,true);
}

建立一個消費者類Peer2PeerConsumers。用@RabbitListener對宣告的佇列進行監聽

@Component
public class Peer2PeerConsumers extends Base {

    @RabbitListener(queues = DirectRabbitConfig.PEER_TO_PEER_QUEUE)
    public void consumer2(Object testMessage) {
        logger.debug("peer-to-peer消費者收到訊息  : " + testMessage.toString());
    }
}

創造一個訊息生產者。在編碼形式上,直接把訊息發傳送給接收的訊息佇列

/**
 * 【點對點模式】
 * @param task 訊息內容
 **/
@PostMapping("/peer-to-peer/{task}")
public String peerToPeer(@PathVariable("task") String task){
    rabbitTemplate.convertAndSend(DirectRabbitConfig.PEER_TO_PEER_QUEUE,task);
    return "ok";

}

啟動專案。佇列繫結到預設交換機

image

呼叫生產者介面產生訊息,可看到的消費者立即接收到資訊

peer-to-peer消費者收到訊息  : (Body:'hi mq' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=peer-to-peer-queue, deliveryTag=1, consumerTag=amq.ctag-vuKWCYLNLn3GwRJKJO5-Mg, consumerQueue=peer-to-peer-queue])

這裡要說明一點的是,點對點模式雖然編碼形式只與佇列互動,但其本質上還是要跟交換機互動的,本質跟下面要介紹的路由模式其實是一樣的。

檢視convertAndSend方法的原始碼,可以看到我們雖然沒有進行交換機和佇列的繫結,傳送訊息是也沒指定交換機,但是程式會為我們繫結預設的交換機。

The default exchange is implicitly bound to every queue, with a routing key equal to the queue name. It is not possible to explicitly bind to, or unbind from the default exchange. It also cannot be deleted.

預設交換機會隱式繫結到每個佇列,路由鍵等於佇列名稱。我們無法明確繫結到預設交換機或從預設交換中解除繫結。它也無法刪除。

且我們第一個引數傳遞的是佇列的名稱,但實際上程式是以這個名字作為路由,將同名佇列跟預設交換機做繫結。所以的訊息會根據該路由資訊,通過預設交換機分發到同名佇列上。(我們通過接收的資訊receivedRoutingKey=peer-to-peer-queueconsumerQueue=peer-to-peer-queue也可以看的出來)


2 工作佇列模式Work Queue

在java配置檔案DirectRabbitConfig中先宣告一個工作佇列

public static final String WORK_QUEUE = "work-queue"; // 工作佇列


/******************************* Work Queue ******************************/
@Bean
public Queue workQueue() {
    return new Queue(WORK_QUEUE,true);
}

建立一個消費者類WorkConsumers。同樣用@RabbitListener對宣告的佇列進行監聽

@Component
public class WorkConsumers extends Base {

    @RabbitListener(queues = DirectRabbitConfig.WORK_QUEUE)
    public void consumer1(Object testMessage) {
        logger.debug("work消費者[1]收到訊息  : " + testMessage.toString());
    }

    @RabbitListener(queues = DirectRabbitConfig.WORK_QUEUE)
    public void consumer2(Object testMessage) {
        logger.debug("work消費者[2]收到訊息  : " + testMessage.toString());
    }
}

創造一個訊息生產者。在編碼形式上,直接把訊息發傳送給接收的訊息佇列

/**
 * 【工作佇列模式】
 * @param task 訊息內容
 **/
@PostMapping("/work/{task}")
public String sendWorkMessage(@PathVariable("task") String task){
	
	rabbitTemplate.convertAndSend(DirectRabbitConfig.WORK_QUEUE,task);
	return "ok";

}

啟動專案,同樣的,工作佇列也是繫結到預設交換機。

image

呼叫生產者介面連續傳送幾次訊息,可看到兩個消費者競爭對佇列訊息進行消費,一條訊息只被一個消費者消費,不會出現重複消費的情況,因此工作佇列模式也被稱為競爭消費者模式。

- work消費者[1]收到訊息  : (Body:'task1' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=work-queue, deliveryTag=1, consumerTag=amq.ctag-PUYjfVq56aEn-7a9DzLNzQ, consumerQueue=work-queue])

- work消費者[2]收到訊息  : (Body:'task2' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=work-queue, deliveryTag=1, consumerTag=amq.ctag-1IVtDalFUCKVvYpFr_GF8A, consumerQueue=work-queue])

- work消費者[1]收到訊息  : (Body:'task3' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=work-queue, deliveryTag=2, consumerTag=amq.ctag-PUYjfVq56aEn-7a9DzLNzQ, consumerQueue=work-queue])

- work消費者[2]收到訊息  : (Body:'task4' MessageProperties [headers={}, contentType=text/plain, contentEncoding=UTF-8, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=, receivedRoutingKey=work-queue, deliveryTag=2, consumerTag=amq.ctag-1IVtDalFUCKVvYpFr_GF8A, consumerQueue=work-queue])

事實上,競爭消費者模式本質就是多個消費者對同一個佇列訊息進行消費。另外,與點對點模式一樣,工作佇列模式的也是用到了預設交換機進行訊息分發。因此於基於的Direct交換機的路由模式的原理本質上都是一樣的,因此,某種程度上,我們也可以用路由模式實現工作佇列模式,這點我們下面介紹路由模式再進行展開


3 路由模式Routing

在java配置檔案DirectRabbitConfig中先宣告2個佇列和一個direct型別的交換機,然後將佇列1和與交換機用一個路由鍵1進行繫結,佇列2用路由鍵2與佇列進行繫結

public static final String DIRECT_QUEUE_ONE = "directQueue-1"; // Direct佇列名稱1
public static final String DIRECT_QUEUE_TWO = "directQueue-2"; // Direct佇列名稱2

public static final String MY_DIRECT_EXCHANGE = "myDirectExchange"; // Direct交換機名稱

public static final String ROUTING_KEY_ONE = "direct.routing-key-1"; // direct路由標識1
public static final String ROUTING_KEY_ONE = "direct.routing-key-2"; // direct路由標識2

/******************************* Direct ******************************/
@Bean
public Queue directQueueOne() {
    return new Queue(DIRECT_QUEUE_ONE,true);
}

@Bean
public Queue directQueueTwo() {
    return new Queue(DIRECT_QUEUE_TWO,true);
}

@Bean
public DirectExchange directExchange() {
    return new DirectExchange(MY_DIRECT_EXCHANGE,true,false);
}

@Bean
public Binding bindingDirectOne() {
    return BindingBuilder.bind(directQueueOne()).to(directExchange()).with(ROUTING_KEY_ONE);
}

@Bean
public Binding bindingDirectTwo() {
    return BindingBuilder.bind(directQueueTwo()).to(directExchange()).with(ROUTING_KEY_TWO);
}

建立一個消費者類DirectConsumers。在每個消費者上,我們用3個消費者註解@RabbitListener對宣告的佇列進行監聽。消費者1和3監聽佇列1,消費者2監聽佇列2

@Component
public class DirectConsumers extends Base {

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE_ONE)
    public void consumer1(Object testMessage) {
        logger.debug("Direct消費者[1]收到訊息  : " + testMessage.toString());
    }

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE_TWO)
    public void consumer2(Object testMessage) {
        logger.debug("Direct消費者[2]收到訊息  : " + testMessage.toString());
    }

    @RabbitListener(queues = DirectRabbitConfig.DIRECT_QUEUE_ONE)
    public void consumer3(Object testMessage) {
        logger.debug("Direct消費者[3]收到訊息  : " + testMessage.toString());
    }
}

創造一個訊息生產者。傳送訊息時,帶上路由鍵1資訊

/**
 * 【Direct路由模式】
 * @param message 訊息內容
 **/
@PostMapping("/direct/{message}")
public String sendDirectMessage(@PathVariable("message") String message) {

    Map<String, Object> map = new HashMap<>();
    map.put("messageId", String.valueOf(UUID.randomUUID()));
    map.put("messageData", message);

    /* 設定路由標識MY_ROUTING_KEY,傳送到交換機MY_DIRECT_EXCHANGE */
    rabbitTemplate.convertAndSend(DirectRabbitConfig.MY_DIRECT_EXCHANGE,DirectRabbitConfig.ROUTING_KEY_ONE, map);
    return "ok";
}

啟動專案,檢視該交換機的繫結情況

image

傳送多條資訊,可以看到,由於佇列2沒有通過路由鍵1跟交換機進行繫結,所以對於監控佇列2的消費者2,其無法結束到的帶有路由鍵1的訊息,而消費者1和3則競爭消費佇列1的訊息

- Direct消費者[3]收到訊息  : (Body:'{messageId=54682b16-0142-46af-be0c-1156df1f27a7, messageData=msg-1}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myDirectExchange, receivedRoutingKey=direct.routing-key-1, deliveryTag=15, consumerTag=amq.ctag-CsuZL9KKByH9IDtqTKe-fg, consumerQueue=directQueue-1])

- Direct消費者[1]收到訊息  : (Body:'{messageId=66cd296a-9a60-4458-8e87-72ed13f9964b, messageData=msg-2}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myDirectExchange, receivedRoutingKey=direct.routing-key-1, deliveryTag=2, consumerTag=amq.ctag-hWmdY04YuLL0O2rgeSlxsw, consumerQueue=directQueue-1])

- Direct消費者[3]收到訊息  : (Body:'{messageId=48c0830e-2207-47ec-bd3e-a958fec48118, messageData=msg-3}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myDirectExchange, receivedRoutingKey=direct.routing-key-1, deliveryTag=16, consumerTag=amq.ctag-CsuZL9KKByH9IDtqTKe-fg, consumerQueue=directQueue-1])

我們如果對新增一個佇列3,通過路由鍵1與交換機進行繫結,消費者獨立監聽佇列3,那麼我們不難猜到,佇列3將和佇列1同樣拿到一條訊息,相當於廣播的概念,但我們會發現如果要這麼做,似乎路由鍵無足輕重,因此rabbitmq提供了一種特殊的交換機來處理這種場景,不需要路由鍵的參與。我們接著往下看


4 釋出/訂閱模式Publish/Subscribe

在java配置檔案DirectRabbitConfig中先宣告Fanout交換機和兩佇列,並將兩個佇列與該交換機進行繫結

public static final String MY_FANOUT_EXCHANGE = "myFanoutExchange"; // Fanout交換機名稱

public static final String FANOUT_QUEUE_ONE = "fanout-queue-1"; // Fanout佇列名稱1
public static final String FANOUT_QUEUE_TWO = "fanout-queue-2"; // Fanout佇列名稱2

/******************************* Fanout ******************************/
@Bean
public Queue fanoutQueueOne() {
    return new Queue(FANOUT_QUEUE_ONE,true);
}

@Bean
public Queue fanoutQueueTwo() {
    return new Queue(FANOUT_QUEUE_TWO,true);
}

@Bean
public FanoutExchange fanoutExchange(){
    return new FanoutExchange(MY_FANOUT_EXCHANGE,true,false);
}

@Bean
public Binding bindingFanoutOne() {
    return BindingBuilder.bind(fanoutQueueOne()).to(fanoutExchange());
}

@Bean
public Binding bindingFanoutTwo() {
    return BindingBuilder.bind(fanoutQueueTwo()).to(fanoutExchange());
}

建立一個消費者類FanoutConsumers。建立兩個消費者,分表對兩個佇列進行監聽

@Component
public class FanoutConsumers extends Base {

    @RabbitListener(queues = DirectRabbitConfig.FANOUT_QUEUE_ONE)
    public void consumer1(Object testMessage) {
        logger.debug("FANOUT消費者[1]收到訊息  : " + testMessage.toString());
    }

    @RabbitListener(queues = DirectRabbitConfig.FANOUT_QUEUE_TWO)
    public void consumer2(Object testMessage) {
        logger.debug("FANOUT消費者[2]收到訊息  : " + testMessage.toString());
    }
}

創造一個訊息生產者。將訊息傳送給Fanout交換機

/**
 * 【工作佇列模式】
 * @param task 訊息內容
 **/
@PostMapping("/work/{task}")
public String sendWorkMessage(@PathVariable("task") String task){
	
	rabbitTemplate.convertAndSend(DirectRabbitConfig.WORK_QUEUE,task);
	return "ok";

}

啟動專案,我們可以看到交換機與兩個佇列進行了繫結,但是路由鍵那一欄是空的。

image

傳送兩條訊息。

/**
 * 【Fanout釋出訂閱模式】
 * @param message 訊息內容
 **/
@PostMapping("/fanout/{message}")
public String sendFanoutMessage(@PathVariable("message") String message) {

    Map<String, Object> map = new HashMap<>();
    map.put("messageId", String.valueOf(UUID.randomUUID()));
    map.put("messageData", message);

    /* 直接跟交換機MY_FANOUT_EXCHANGE互動 */
    rabbitTemplate.setExchange(DirectRabbitConfig.MY_FANOUT_EXCHANGE);
    rabbitTemplate.convertAndSend(map);
    return "ok";
}

可以看到,兩個消費者都拿到了同樣的資料,達到了廣播的效果。

- FANOUT消費者[2]收到訊息  : (Body:'{messageId=a4bf1931-1db8-4cb9-8b01-397f43a82660, messageData=Hi Fanout}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myFanoutExchange, receivedRoutingKey=, deliveryTag=1, consumerTag=amq.ctag-ncVmsRM7xHLZ0iAJT2tSTg, consumerQueue=fanout-queue-2])

- FANOUT消費者[1]收到訊息  : (Body:'{messageId=a4bf1931-1db8-4cb9-8b01-397f43a82660, messageData=Hi Fanout}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myFanoutExchange, receivedRoutingKey=, deliveryTag=1, consumerTag=amq.ctag-zR3Oi0MVESq8qushlAMa3Q, consumerQueue=fanout-queue-1])

- FANOUT消費者[1]收到訊息  : (Body:'{messageId=51f66720-35dd-4abf-9d33-24acf7786ed8, messageData=666}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myFanoutExchange, receivedRoutingKey=, deliveryTag=2, consumerTag=amq.ctag-zR3Oi0MVESq8qushlAMa3Q, consumerQueue=fanout-queue-1])

- FANOUT消費者[2]收到訊息  : (Body:'{messageId=51f66720-35dd-4abf-9d33-24acf7786ed8, messageData=666}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myFanoutExchange, receivedRoutingKey=, deliveryTag=2, consumerTag=amq.ctag-ncVmsRM7xHLZ0iAJT2tSTg, consumerQueue=fanout-queue-2])


5 萬用字元模式Topics

在java配置檔案DirectRabbitConfig中先宣告一個Topic交換機、兩個工作佇列和三個通配繫結鍵,其中一個佇列通過兩個不同通配繫結鍵與交換機繫結,另外一個佇列用第三個繫結鍵進行繫結。

public static final String WORK_QUEUE = "work-queue"; // 工作佇列


/******************************* Work Queue ******************************/
@Bean
public Queue workQueue() {
    return new Queue(WORK_QUEUE,true);
}

通過rabbitmq管理頁面我們可以看到交換機與佇列的繫結變化,可以看到佇列1車工繫結了兩個通配鍵

image

建立一個消費者類TopicConsumers。建立兩個消費者分別對兩個佇列做監聽。

@Component
public class WorkConsumers extends Base {

    @RabbitListener(queues = DirectRabbitConfig.WORK_QUEUE)
    public void consumer1(Object testMessage) {
        logger.debug("work消費者[1]收到訊息  : " + testMessage.toString());
    }

    @RabbitListener(queues = DirectRabbitConfig.WORK_QUEUE)
    public void consumer2(Object testMessage) {
        logger.debug("work消費者[2]收到訊息  : " + testMessage.toString());
    }
}

創造一個訊息生產者。傳送3條不同的訊息,分別帶上三個不同的路由鍵

/**
 * 【Topic萬用字元模式】
 * @param message 訊息內容
 **/
@PostMapping("/topic/{message}")
public String sendTopicMessage(@PathVariable("message") String message) {

    Map<String, Object> map = new HashMap<>();

    /* 直接跟交換機MY_FANOUT_EXCHANGE互動 */
    rabbitTemplate.setExchange(DirectRabbitConfig.MY_TOPIC_EXCHANGE);

    map.put("messageId", String.valueOf(UUID.randomUUID()));
    map.put("messageData", message + "TEST1");
    rabbitTemplate.convertAndSend(DirectRabbitConfig.TOPIC_ROUTING_KEY_ONE,map);

    map.put("messageId", String.valueOf(UUID.randomUUID()));
    map.put("messageData", message + "TEST2");
    rabbitTemplate.convertAndSend(DirectRabbitConfig.TOPIC_ROUTING_KEY_TWO,map);

    map.put("messageId", String.valueOf(UUID.randomUUID()));
    map.put("messageData", message + "TEST3");
    rabbitTemplate.convertAndSend(DirectRabbitConfig.TOPIC_ROUTING_KEY_THREE,map);

    return "ok";
}

路由鍵宣告如下:

public static final String TOPIC_ROUTING_KEY_ONE = "topic.a1.b1.c1"; // topic路由鍵1
public static final String TOPIC_ROUTING_KEY_TWO = "topic.a1.b1";    // topic路由鍵2
public static final String TOPIC_ROUTING_KEY_THREE = "topic.a2.b1";  // topic路由鍵3

啟動專案,呼叫生產者的介面,檢視兩個消費者的消費情況。

- TOPIC消費者[2]收到訊息  : (Body:'{messageId=82abd282-1110-4f1a-b09e-ae9a43c560c3, messageData=hi topic! TEST1}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myTopicExchange, receivedRoutingKey=topic.a1.b1.c1, deliveryTag=1, consumerTag=amq.ctag-wlRVC5xWiN8glrtA2_i6uA, consumerQueue=topic-queue-2])

- TOPIC消費者[1]收到訊息  : (Body:'{messageId=b2039557-75d8-47d5-93a0-2a03a38fabc7, messageData=hi topic! TEST2}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myTopicExchange, receivedRoutingKey=topic.a1.b1, deliveryTag=1, consumerTag=amq.ctag-F6ByjknEnCjh7XVolNfmcg, consumerQueue=topic-queue-1])

- TOPIC消費者[2]收到訊息  : (Body:'{messageId=b2039557-75d8-47d5-93a0-2a03a38fabc7, messageData=hi topic! TEST2}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myTopicExchange, receivedRoutingKey=topic.a1.b1, deliveryTag=2, consumerTag=amq.ctag-wlRVC5xWiN8glrtA2_i6uA, consumerQueue=topic-queue-2])

- TOPIC消費者[1]收到訊息  : (Body:'{messageId=3a8f3164-706f-4523-bd2a-4fee73595fbb, messageData=hi topic! TEST3}' MessageProperties [headers={}, contentType=application/x-java-serialized-object, contentLength=0, receivedDeliveryMode=PERSISTENT, priority=0, redelivered=false, receivedExchange=myTopicExchange, receivedRoutingKey=topic.a2.b1, deliveryTag=2, consumerTag=amq.ctag-F6ByjknEnCjh7XVolNfmcg, consumerQueue=topic-queue-1])

可以看到,路由鍵字首為topic.a1的資訊都可以被繫結了topic.a1.#的佇列接收到,而繫結了topic.a1.*的佇列只能接收到topic.a1後面帶一個單詞的資訊,由於佇列1還通過topic.*.b1繫結交換機,因此攜帶路由鍵"topic.a2.b1"的資訊同樣也被佇列1接收

topic交換機是direct交換機做的改造的。兩者的區別主要體現在路由鍵和繫結鍵格式上的限制不同。

路由鍵:必須是由點分隔的單詞列表。單詞形式不限。比如一個主題建:<主題1>.<主題2>.<主題3>

繫結鍵:格式上和路由鍵一致,但多了兩個萬用字元*##代表任意數量的單詞,包括0個。*標識一個單詞。

使用上,一個繫結鍵,我們可以看成是對一類具有多個特徵的物體的一個抽象,由點分割的每個單詞,我們可以看成一個主題或是一個特徵。因此只要做好訊息特徵的歸納抽象,加上萬用字元的使用,我們就有很高的自由度去處理任意型別的訊息


總結


以上就是關於RabbitMQ五種佇列模式的實戰演練,關於RabbitMQ其它實戰與知識理解後續會相繼分享,感興趣的同學歡迎留言討論