上手了RabbitMQ?再來看看它的交換機(Exchange)吧

和耳朵發表於2020-08-19
人生終將是場單人旅途,孤獨之前是迷茫,孤獨過後是成長。

楔子

本篇是訊息佇列RabbitMQ的第三彈。

RabbitMQ的入門RabbitMQ+SpringBoot的整合可以點此連結進去回顧,今天要講的是RabbitMQ的交換機。

本篇是理解RabbitMQ很重要的一篇,交換機是訊息的第一站,只有理解了交換機的分發模式,我們才能知道不同交換機根據什麼規則分發訊息,才能明白在面對不同業務需求的時候應採用哪種交換機。


祝有好收穫,先贊後看,快樂無限。

本文程式碼: 碼雲地址GitHub地址

1. ?Exchange

rabbit架構圖

先來放上幾乎每篇都要出現一遍的我畫了好久的RabbitMQ架構圖。

前兩篇文中我們一直沒有顯式的去使用Exchange,都是使用的預設Exchange,其實Exchange是一個非常關鍵的元件,有了它才有了各種訊息分發模式。

我先簡單說說Exchange有哪幾種型別:

  1. fanoutFanout-Exchange會將它接收到的訊息發往所有與他繫結的Queue中。
  2. directDirect-Exchange會把它接收到的訊息發往與它有繫結關係且Routingkey完全匹配的Queue中(預設)。
  3. topicTopic-Exchange與Direct-Exchange相似,不過Topic-Exchange不需要全匹配,可以部分匹配,它約定:Routingkey為一個句點號“. ”分隔的字串(我們將被句點號“. ”分隔開的每一段獨立的字串稱為一個單詞)。
  4. headerHeader-Exchange不依賴於RoutingKey或繫結關係來分發訊息,而是根據傳送的訊息內容中的headers屬性進行匹配。此模式已經不再使用,本文中也不會去講,大家知道即可。

本文中我們主要講前三種Exchange方式,相信憑藉著我簡練的文字和靈魂的畫技給大家好好講講,爭取老嫗能解。

Tip:本文的程式碼演示直接使用SpringBoot+RabbitMQ的模式。

2. ?Fanout-Exchange

先來看看Fanout-ExchangeFanout-Exchange又稱扇形交換機,這個交換機應該是最容易理解的。

扇形交換機

ExchangeQueue建立一個繫結關係,Exchange會分發給所有和它有繫結關係的Queue中,繫結了十個Queue就把訊息複製十份進行分發。

這種繫結關係為了效率肯定都會維護一張表,從演算法效率上來說一般是O(1),所以Fanout-Exchange是這幾個交換機中查詢需要被分發佇列最快的交換機。


下面是一段程式碼演示:

    @Bean
    public Queue fanout1() {
        return new Queue("fanout1");
    }

    @Bean
    public Queue fanout2() {
        return new Queue("fanout2");
    }

    @Bean
    public FanoutExchange fanoutExchange() {
        // 三個構造引數:name durable autoDelete
        return new FanoutExchange("fanoutExchange", false, false);
    }

    @Bean
    public Binding binding1() {
        return BindingBuilder.bind(fanout1()).to(fanoutExchange());
    }

    @Bean
    public Binding binding2() {
        return BindingBuilder.bind(fanout2()).to(fanoutExchange());
    }

為了清晰明瞭,我新建了兩個演示用的佇列,然後建了一個FanoutExchange,最後給他們都設定上繫結關係,這樣一組佇列和交換機的繫結設定就算完成了。

緊接著編寫一下生產者和消費者:

    public void sendFanout() {
        Client client = new Client();

        // 應讀者要求,以後程式碼列印的地方都會改成log方式,這是一種良好的程式設計習慣,用System.out.println一般是不推薦的。
        log.info("Message content : " + client);

        rabbitTemplate.convertAndSend("fanoutExchange",null,client);
        System.out.println("訊息傳送完畢。");
    }

    @Test
    public void sendFanoutMessage() {
        rabbitProduce.sendFanout();
    }
@Slf4j
@Component("rabbitFanoutConsumer")
public class RabbitFanoutConsumer {
    @RabbitListener(queues = "fanout1")
    public void onMessage1(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

    @RabbitListener(queues = "fanout2")
    public void onMessage2(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

}

這兩段程式碼都很好理解,不再贅述,有遺忘的可以去看RabbitMQ第一彈的內容。

其中傳送訊息的程式碼有三個引數,第一個引數是Exchange的名稱,第二個引數是routingKey的名稱,這個引數在扇形交換機裡面用不到,在其他兩個交換機型別裡面會用到。

程式碼的準備到此結束,我們可以執行傳送方法之後run一下了~

專案啟動後,我們可以先來觀察一下佇列與交換機的繫結關係有沒有生效,我們在RabbitMQ控制檯使用rabbitmqctl list_bindings命令檢視繫結關係。

扇形交換機繫結關係

關鍵部分我用紅框標記了起來,這就代表著名叫fanoutExchange的交換機繫結著兩個佇列,一個叫fanout1,另一個叫fanout2

緊接著,我們來看控制檯的列印情況:

扇形交換機確認訊息

可以看到,一條資訊傳送出去之後,兩個佇列都接收到了這條訊息,緊接著由我們的兩個消費者消費。

Tip: 如果你的演示應用啟動之後沒有消費資訊,可以嘗試重新執行一次生產者的方法傳送訊息。

3. ?Direct-Exchange

Direct-Exchange是一種精準匹配的交換機,我們之前一直使用預設的交換機,其實預設的交換機就是Direct型別。

如果將Direct交換機都比作一所公寓的管理員,那麼佇列就是裡面的住戶。(繫結關係)

管理員每天都會收到各種各樣的信件(訊息),這些信件的地址不光要標明地址(ExchangeKey)還需要標明要送往哪一戶(routingKey),不然訊息無法投遞。

扇形交換機

以上圖為例,準備一條訊息發往名為SendService的直接交換機中去,這個交換機主要是用來做傳送服務,所以其繫結了兩個佇列,SMS佇列和MAIL佇列,用於傳送簡訊和郵件。

我們的訊息除了指定ExchangeKey還需要指定routingKeyroutingKey對應著最終要傳送的是哪個佇列,我們的示例中的routingKey是sms,這裡這條訊息就會交給SMS佇列。


聽了上面這段,可能大家對routingKey還不是很理解,我們上段程式碼實踐一下,大家應該就明白了。

準備工作:

    @Bean
    public Queue directQueue1() {
        return new Queue("directQueue1");
    }

    @Bean
    public Queue directQueue2() {
        return new Queue("directQueue2");
    }

    @Bean
    public DirectExchange directExchange() {
        // 三個構造引數:name durable autoDelete
        return new DirectExchange("directExchange", false, false);
    }

    @Bean
    public Binding directBinding1() {
        return BindingBuilder.bind(directQueue1()).to(directExchange()).with("sms");
    }

    @Bean
    public Binding directBinding2() {
        return BindingBuilder.bind(directQueue2()).to(directExchange()).with("mail");
    }

新建兩個佇列,新建了一個直接交換機,並設定了繫結關係。

這裡的示例程式碼和上面扇形交換機的程式碼很像,唯一可以說不同的就是繫結的時候多呼叫了一個withroutingKey設定了上去。

所以是交換機和佇列建立繫結關係的時候設定的routingKey,一個訊息到達交換機之後,交換機通過訊息上帶來的routingKey找到自己與佇列建立繫結關係時設定的routingKey,然後將訊息分發到這個佇列去。

生產者:

    public void sendDirect() {
        Client client = new Client();

        log.info("Message content : " + client);

        rabbitTemplate.convertAndSend("directExchange","sms",client);
        System.out.println("訊息傳送完畢。");
    }

消費者:

@Slf4j
@Component("rabbitDirectConsumer")
public class RabbitDirectConsumer {
    @RabbitListener(queues = "directQueue1")
    public void onMessage1(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

    @RabbitListener(queues = "directQueue2")
    public void onMessage2(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

}

效果圖如下:

扇形交換機

只有一個消費者進行了訊息,符合我們的預期。

4. ?Topic-Exchange

Topic-Exchange是直接交換機的模糊匹配版本,Topic型別的交換器,支援使用"*"和"#"萬用字元定義模糊bindingKey,然後按照routingKey進行模糊匹配佇列進行分發。

  • *:能夠模糊匹配一個單詞。
  • #:能夠模糊匹配零個或多個單詞。

因為加入了兩個通配定義符,所以Topic交換機的routingKey也有些變化,routingKey可以使用.將單詞分開。


這裡我們直接來用一個例子說明會更加的清晰:

準備工作:

    // 主題交換機示例
    @Bean
    public Queue topicQueue1() {
        return new Queue("topicQueue1");
    }

    @Bean
    public Queue topicQueue2() {
        return new Queue("topicQueue2");
    }

    @Bean
    public TopicExchange topicExchange() {
        // 三個構造引數:name durable autoDelete
        return new TopicExchange("topicExchange", false, false);
    }

    @Bean
    public Binding topicBinding1() {
        return BindingBuilder.bind(topicQueue1()).to(topicExchange()).with("sms.*");
    }

    @Bean
    public Binding topicBinding2() {
        return BindingBuilder.bind(topicQueue2()).to(topicExchange()).with("mail.#");
    }

新建兩個佇列,新建了一個Topic交換機,並設定了繫結關係。

這裡的示例程式碼我們主要看設定routingKey,這裡的routingKey用上了萬用字元,且中間用.隔開,這就代表topicQueue1消費sms開頭的訊息,topicQueue2消費mail開頭的訊息,具體不同往下看。

生產者:

    public void sendTopic() {
        Client client = new Client();

        log.info("Message content : " + client);

        rabbitTemplate.convertAndSend("topicExchange","sms.liantong",client);
        System.out.println("訊息傳送完畢。");
    }

消費者:

@Slf4j
@Component("rabbitTopicConsumer")
public class RabbitTopicConsumer {
    @RabbitListener(queues = "topicQueue1")
    public void onMessage1(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

    @RabbitListener(queues = "topicQueue2")
    public void onMessage2(Message message, Channel channel) throws Exception {
        log.info("Message content : " + message);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
        log.info("訊息已確認");
    }

}

這裡我們的生產者傳送的訊息routingKeysms.liantong,它就會被髮到topicQueue1佇列中去,這裡訊息的routingKey也需要用.隔離開,用其他符號無法正確識別。

如果我們的routingKeysms.123.liantong,那麼它將無法找到對應的佇列,因為topicQueue1的模糊匹配用的萬用字元是*而不是#,只有#是可以匹配多個單詞的。

Topic-ExchangeDirect-Exchange很相似,我就不再贅述了,萬用字元*#的區別也很簡單,大家可以自己試一下。

後記

週一沒更文實在慚愧,去醫院抽血了,抽了三管~,吃多少才能補回來~

RabbitMQ已經更新了三篇了,這三篇的內容有些偏基礎,下一篇將會更新高階部分內容:包括防止訊息丟失,防止訊息重複消費等等內容,希望大家持續關注。


最近這段時間壓力挺大,優狐令我八月底之前升級到三級,所以各位讀者的贊對我很重要,希望大家能夠高抬貴手,幫我一哈~

好了,以上就是本期的全部內容,感謝你能看到這裡,歡迎對本文點贊收藏與評論,?你們的每個點贊都是我創作的最大動力。

我是耳朵,一個一直想做知識輸出的偽文藝程式設計師,我們下期見。

本文程式碼:碼雲地址GitHub地址

相關文章