RabbitMQ的開發應用

KerryWu發表於2020-07-17

1.介紹

RabbitMQ 是一個由erlang語言編寫的、開源的、在AMQP基礎上完整的、可複用的企業訊息系統。支援多種語言,包括java、Python、ruby、PHP、C/C++等。

1.1.AMQP模型

AMQP:advanced message queuing protocol ,一個提供統一訊息服務的應用層標準高階訊息佇列協議,是應用層協議的一個開放標準,為面向訊息的中介軟體設計。基於此協議的客戶端與訊息中介軟體可傳遞訊息並不受客戶端/中介軟體不同產品、不同開發語言等條件的限制。

AMQP模型圖
amqp模型.jpg

1.1.1.工作過程

釋出者(Publisher)釋出訊息(Message),經由交換機(Exchange)。

交換機根據路由規則將收到的訊息分發給與該交換機繫結的佇列(Queue)。

最後 AMQP 代理會將訊息投遞給訂閱了此佇列的消費者,或者消費者按照需求自行獲取。

1、釋出者、交換機、佇列、消費者都可以有多個。同時因為 AMQP 是一個網路協議,所以這個過程中的釋出者,消費者,訊息代理 可以分別存在於不同的裝置上。

2、釋出者釋出訊息時可以給訊息指定各種訊息屬性(Message Meta-data)。有些屬性有可能會被訊息代理(Brokers)使用,然而其他的屬性則是完全不透明的,它們只能被接收訊息的應用所使用。

3、從安全形度考慮,網路是不可靠的,又或是消費者在處理訊息的過程中意外掛掉,這樣沒有處理成功的訊息就會丟失。基於此原因,AMQP 模組包含了一個訊息確認(Message Acknowledgements)機制:當一個訊息從佇列中投遞給消費者後,不會立即從佇列中刪除,直到它收到來自消費者的確認回執(Acknowledgement)後,才完全從佇列中刪除。

4、在某些情況下,例如當一個訊息無法被成功路由時(無法從交換機分發到佇列),訊息或許會被返回給釋出者並被丟棄。或者,如果訊息代理執行了延期操作,訊息會被放入一個所謂的死信佇列中。此時,訊息釋出者可以選擇某些引數來處理這些特殊情況。

1.1.2.Exchange交換機

交換機是用來傳送訊息的 AMQP 實體。交換機拿到一個訊息之後將它路由給一個或零個佇列。它使用哪種路由演算法是由交換機型別和繫結(Bindings)規則所決定的。常見的交換機有如下幾種:

  1. direct 直連交換機:Routing Key==Binding Key,嚴格匹配。
  2. fanout 扇形交換機:把傳送到該 Exchange 的訊息路由到所有與它繫結的 Queue 中。
  3. topic 主題交換機:Routing Key==Binding Key,模糊匹配。
  4. headers 頭交換機:根據傳送的訊息內容中的 headers 屬性進行匹配。
    具體有關這五種交換機的說明和用法,後續會有章節詳細介紹。

1.1.3.Queue佇列

AMQP 中的佇列(queue)跟其他訊息佇列或任務佇列中的佇列是很相似的:它們儲存著即將被應用消費掉的訊息。佇列跟交換機共享某些屬性,但是佇列也有一些另外的屬性。

  • Durable(訊息代理重啟後,佇列依舊存在)
  • Exclusive(只被一個連線(connection)使用,而且當連線關閉後佇列即被刪除)
  • Auto-delete(當最後一個消費者退訂後即被刪除)
  • Arguments(一些訊息代理用他來完成類似與 TTL 的某些額外功能)

1.2.rabbitmq和kafka對比

rabbitmq遵循AMQP協議,用在實時的對可靠性要求比較高的訊息傳遞上。kafka主要用於處理活躍的流式資料,大資料量的資料處理上。主要體現在:

1.2.1.架構

  1. rabbitmq:RabbitMQ遵循AMQP協議RabbitMQ的broker由Exchange,Binding,queue組成,其中exchange和binding組成了訊息的路由鍵;客戶端Producer通過連線channel和server進行通訊,Consumer從queue獲取訊息進行消費(長連線,queue有訊息會推送到consumer端,consumer迴圈從輸入流讀取資料)。rabbitMQ以broker為中心。
  2. kafka:kafka遵從一般的MQ結構,producer,broker,consumer,以consumer為中心,訊息的消費資訊儲存的客戶端consumer上,consumer根據消費的點,從broker上批量pull資料。

1.2.2.訊息確認

  1. rabbitmq:有訊息確認機制。
  2. kafka:無訊息確認機制。

1.2.3.吞吐量

  1. rabbitmq:rabbitMQ在吞吐量方面稍遜於kafka,他們的出發點不一樣,rabbitMQ支援對訊息的可靠的傳遞,支援事務,不支援批量的操作;基於儲存的可靠性的要求儲存可以採用記憶體或者硬碟。
  2. kafka:kafka具有高的吞吐量,內部採用訊息的批量處理,zero-copy機制,資料的儲存和獲取是本地磁碟順序批量操作,具有O(1)的複雜度,訊息處理的效率很高。
    (備註:kafka零拷貝,通過sendfile方式。(1)普通資料讀取:磁碟->核心緩衝區(頁快取 PageCache)->使用者緩衝區->核心緩衝區->網路卡輸出;(2)kafka的資料讀取:磁碟->核心緩衝區(頁快取 PageCache)->網路卡輸出。

1.2.4.可用性

  1. rabbitmq(1)普通叢集:在多臺機器上啟動多個rabbitmq例項,每個機器啟動一個。但是你建立的queue,只會放在一個rabbtimq例項上,但是每個例項都同步queue的後設資料。完了你消費的時候,實際上如果連線到了另外一個例項,那麼那個例項會從queue所在例項上拉取資料過來。(2)映象叢集:跟普通叢集模式不一樣的是,你建立的queue,無論後設資料還是queue裡的訊息都會存在於多個例項上,然後每次你寫訊息到queue的時候,都會自動把訊息到多個例項的queue裡進行訊息同步。這樣的話,好處在於,一個機器當機了,沒事兒,別的機器都可以用。壞處在於,第一,這個效能開銷太大了,訊息同步所有機器,導致網路頻寬壓力和消耗很重。第二,這麼玩兒,就沒有擴充套件性可言了,如果某個queue負載很重,你加機器,新增的機器也包含了這個queue的所有資料,並沒有辦法線性擴充套件你的queue
  2. kafka:kafka是由多個broker組成,每個broker是一個節點;每建立一個topic,這個topic可以劃分為多個partition,每個partition可以存在於不同的broker上,每個partition就放一部分資料。這就是天然的分散式訊息佇列,就是說一個topic的資料,是分散放在多個機器上的,每個機器就放一部分資料。每個partition的資料都會同步到其他機器上,形成自己的多個replica副本,然後所有replica會選舉一個leader出來,主從結構。

1.2.5.叢集負載均衡

  1. rabbitmq:rabbitMQ的負載均衡需要單獨的loadbalancer進行支援,如HAProxy和Keepalived等。
  2. kafka:kafka採用zookeeper對叢集中的broker、consumer進行管理,可以註冊topic到zookeeper上;通過zookeeper的協調機制,producer儲存對應topic的broker資訊,可以隨機或者輪詢傳送到broker上;並且producer可以基於語義指定分片,訊息傳送到broker的某分片上。

2.結構

2.1.交換機模式

RabbitMQ常用的Exchange Type有fanout、direct、topic、headers這四種。

2.1.1.Direct Exchange

direct型別的Exchange路由規則很簡單,它會把訊息路由到那些binding key與routing key完全匹配的Queue中。

2.1.2.Topic Exchange

前面講到direct型別的Exchange路由規則是完全匹配binding key與routing key,但這種嚴格的匹配方式在很多情況下不能滿足實際業務需求。topic型別的Exchange與direct型別的Exchage相似,也是將訊息路由到binding key與routing key相匹配的Queue中,但支援模糊匹配:

  • routing key為一個句點號“. ”分隔的字串(我們將被句點號“. ”分隔開的每一段獨立的字串稱為一個單詞),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”
  • binding key與routing key一樣也是句點號“. ”分隔的字串
  • binding key中可以存在兩種特殊字元"*"與“#”,用於做模糊匹配,其中" * "用於匹配一個單詞,“#”用於匹配多個單詞(可以是零個)

2.1.3.Fanout Exchange

fanout型別的Exchange路由規則非常簡單,它會把所有傳送到fanout Exchange的訊息都會被轉發到與該Exchange 繫結(Binding)的所有Queue上。
Fanout Exchange 不需要處理RouteKey 。只需要簡單的將佇列繫結到exchange 上。這樣傳送到exchange的訊息都會被轉發到與該交換機繫結的所有佇列上。類似子網廣播,每臺子網內的主機都獲得了一份複製的訊息。所以,Fanout Exchange 轉發訊息是最快的。

2.1.4.Headers Exchange

headers型別的Exchange也不依賴於routing key與binding key的匹配規則來路由訊息,而是根據傳送的訊息內容中的headers屬性進行匹配。
在繫結Queue與Exchange時指定一組鍵值對;當訊息傳送到Exchange時,RabbitMQ會取到該訊息的headers(也是一個鍵值對的形式),對比其中的鍵值對是否完全匹配Queue與Exchange繫結時指定的鍵值對;如果完全匹配則訊息會路由到該Queue,否則不會路由到該Queue。

2.1.5.Default Exchange 預設

嚴格來說,Default Exchange 並不應該和上面四個交換機在一起,因為它不屬於獨立的一種交換機型別,而是屬於Direct Exchange 直連交換機。

預設交換機(default exchange)實際上是一個由訊息代理預先宣告好的沒有名字(名字為空字串)的直連交換機(direct exchange)。

它有一個特殊的屬性使得它對於簡單應用特別有用處:那就是每個新建佇列(queue)都會自動繫結到預設交換機上,繫結的路由鍵(routing key)名稱與佇列名稱相同。

舉個例子:當你宣告瞭一個名為 “search-indexing-online” 的佇列,AMQP 代理會自動將其繫結到預設交換機上,繫結(binding)的路由鍵名稱也是為 “search-indexing-online”。所以當你希望將訊息投遞給“search-indexing-online”的佇列時,指定投遞資訊包括:交換機名稱為空字串,路由鍵為“search-indexing-online”即可。

因此 direct exchange 中的 default exchange 用法,體現出了訊息佇列的 point to point,感覺像是直接投遞訊息給指定名字的佇列。

2.2.持久化

雖然我們要避免系統當機,但是這種“不可抗力”總會有可能發生。rabbitmq如果當機了,再啟動便是了,大不了有短暫時間不可用。但如果你啟動起來後,發現這個rabbitmq伺服器像是被重置了,以前的exchange,queue和message資料都沒了,那就太令人崩潰了。不光業務系統因為無對應exchange和queue受影響,丟失的很多message資料更是致命的。所以如何保證rabbitmq的持久化,在服務使用前必須得考慮到位。

持久化可以提高RabbitMQ的可靠性,以防在異常情況(重啟、關閉、當機等)下的資料丟失。RabbitMQ的持久化分為三個部分:交換器的持久化、佇列的持久化和訊息的持久化。

2.2.1.exchange持久化

exchange交換器的持久化是在宣告交換器的時候,將durable設定為true

如果交換器不設定持久化,那麼在RabbitMQ交換器服務重啟之後,相關的交換器資訊會丟失,不過訊息不會丟失,但是不能將訊息傳送到這個交換器

spring中建立exchange時,構造方法預設設定為持久化。

2.2.2.queue持久化

佇列的持久化在宣告佇列的時候,將durable設定為true

如果佇列不設定持久化,那麼RabbitMQ交換器服務重啟之後,相關的佇列資訊會丟失,同時佇列中的訊息也會丟失

exchange和queue,如果一個是非持久化,另一個是持久化,中bind時會報錯。

spring中建立exchange時,構造方法預設設定為持久化。

2.2.3.message持久化

要確保訊息不會丟失,除了設定佇列的持久化,還需要將訊息設定為持久化。通過將訊息的投遞模式(BasicProperties中的deliveryMode屬性)設定為2即可實現訊息的持久化

  • 持久化的訊息在到達佇列時就被寫入到磁碟,並且如果可以,持久化的訊息也會在記憶體中儲存一份備份,這樣可以提高一定的效能,只有在記憶體吃緊的時候才會從記憶體中清除。
  • 非持久化的訊息一般只儲存在記憶體中,在記憶體吃緊的時候會被換入到磁碟中,以節省記憶體空間。

如果將所有的訊息都進行持久化操作,這樣會影響RabbitMQ的效能。寫入磁碟的速度比寫入記憶體的速度慢很,所以要在可靠性和吞吐量之間做權衡。

在spring中,BasicProperties中的deliveryMode屬性,對應的是MessageProperties中的deliveryMode。平時使用的RabbitTemplate.convertAndSend()方法預設設定為持久化,deliveryMode=2。如果需要設定非持久化傳送訊息,需要手動設定:

messageProperties.setDeliveryMode(MessageDeliveryMode.NON_PERSISTENT);

2.2.4.完整方案

這裡講解實現訊息持久化的完整方案。

一、exchange、queue、message

要保證訊息的持久化,在rabbitmq本身的結構上需要實現下面這些:

  • exchange交換機的durable設定為true。
  • queue佇列的durable設定為true。
  • message訊息的投遞模式deliveryMode設定為2。

二、釋出確認
前面是保證了訊息在投遞到rabbitmq中,如何保證rabbit中訊息的持久化。
那麼還需要保證生產者能成功釋出訊息,如交換機名字寫錯了等等。可以在釋出訊息時設定投遞成功的回撥,確定訊息能成功投遞到目標佇列中。

三、接收確認
對於消費者來說,如果在訂閱訊息的時候,將autoAck設定為true,那麼消費者接收到訊息後,還沒有處理,就出現了異常掛掉了,此時,佇列中已經將訊息刪除,消費者不能夠在收到訊息。

這種情況可以將autoAck設定為false,進行手動確認。

四、映象佇列叢集
在持久化後的訊息存入RabbitMQ之後,還需要一段時間才能存入磁碟。RabbitMQ並不會為每條訊息都進行同步存檔,可能僅僅是儲存到作業系統快取之中而不是物理磁碟。如果在這段時間,伺服器當機或者重啟,訊息還沒來得及儲存到磁碟當中,從而丟失。對於這種情況,可以引入RabiitMQ映象佇列機制。

這裡強調是映象佇列叢集,而非普通叢集。因為出於同步效率考慮,普通叢集只會同步佇列的後設資料,而不會同步佇列中的訊息。只有升級成映象佇列叢集后,才能也同步訊息。

每個映象佇列由一個master和一個或多個mirrors組成。主節點位於一個通常稱為master的節點上。每個佇列都有自己的主節點。給定佇列的所有操作首先應用於佇列的主節點,然後傳播到映象。這包括佇列釋出(enqueueing publishes)、向消費者傳遞訊息、跟蹤消費者的確認等等。

釋出到佇列的訊息將複製到所有映象。不管消費者連線到哪個節點,都會連線到master,映象會刪除在master上已確認的訊息。因此,佇列映象提高了可用性,但不會在節點之間分配負載。 如果承載佇列master的節點出現故障,則最舊的映象將升級為新的master,只要它已同步。根據佇列映象引數,也可以升級未同步的映象。

3.開發

java開發上,這裡以spring-boot-starter-amqp為例,記錄在springboot中使用rabbitmq的一些關注點。pom.xml中引用為:

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

3.1.簡單示例

一個簡單的示例,僅限於文字訊息的釋出和接收。

3.1.1.生產者

ProducerController.java
@RestController
public class ProducerController {
    private static final String HEADER_KEY_UID="uid";
    @Autowired
    private ProducerService producerService;

    @PostMapping("/sendText")
    public void sendText(@RequestParam("uid")String uid,@RequestParam("msg")String msg){
        MessageProperties messageProperties=new MessageProperties();
        messageProperties.setHeader(HEADER_KEY_UID,uid);
        producerService.sendText(msg,messageProperties);
    }
}
ProducerService.java
@Service
public class ProducerService {
    private static final String EXCHANGE_NAME="direct.exchange.a";
    private static final String ROUTING_KEY_NAME="direct.routingKey.a";
    @Resource
    private RabbitTemplate rabbitTemplate;


    /**
     * 傳送 訊息文字
     * @param data 文字訊息
     * @param messageProperties 訊息屬性
     */
    public void sendText(String data, MessageProperties messageProperties) {
        Message message = rabbitTemplate.getMessageConverter().toMessage(data, messageProperties);
        rabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY_NAME, message);
    }
}

訊息傳送的常用方法:

  • rabbitTemplate.send(message); //發訊息,引數型別為org.springframework.amqp.core.Message
  • rabbitTemplate.convertAndSend(object); //轉換併傳送訊息。 將引數物件轉換為org.springframework.amqp.core.Message後傳送
  • rabbitTemplate.convertSendAndReceive(message) //轉換併傳送訊息,且等待訊息者返回響應訊息。

3.1.2.消費者

MessageListener.java
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.ExchangeTypes;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.MessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;

@Component
@Slf4j
public class MessageListener {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "direct.queue.d",
                    durable = "true"),
            exchange = @Exchange(value = "direct.exchange.a",
                    durable = "true",
                    type = ExchangeTypes.DIRECT,
                    ignoreDeclarationExceptions = "true"),
            key = "direct.routingKey.a"
    )
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        MessageConverter messageConverter = rabbitTemplate.getMessageConverter();
        String msg = (String) messageConverter.fromMessage(message);
        log.info("消費端 Body: " + msg);
    }
}
  • @RabbitListener 可以標註在類上面,需配合 @RabbitHandler 註解一起使用
  • @RabbitListener 標註在類上面表示當有收到訊息的時候,就交給 @RabbitHandler 的方法處理,具體使用哪個方法處理,根據 MessageConverter 轉換後的引數型別

3.2.訊息序列化

rabbitmq中訊息的序列化依賴於MessageConvert,這是一個介面,用於訊息內容的序列化。

  • Message分為body和MessageProperties兩部分。RabbitMQ的序列化是指Message的 body 屬性,即我們真正需要傳輸的內容,RabbitMQ 抽象出一個MessageConvert 介面處理訊息的序列化,其實現有SimpleMessageConverter(預設)、Jackson2JsonMessageConverter等。
  • 當呼叫了 convertAndSend方法時,方法內部會使用MessageConvert進行訊息的序列化。
  • MessageConvert是在RabbitTemplate中定義的屬性,如果專案中需要使用多種MessageConvert。因為Spring中RabbitTemplate是單例模式注入,建議每種MessageConvert單獨定義一種RabbitTemplate。

3.2.1.生產者

RabbitConfig.java
public class RabbitConfig {
    
    @Bean("jsonRabbitTemplate")
    public RabbitTemplate jsonRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
        rabbitTemplate.setMessageConverter(new Jackson2JsonMessageConverter());
        return rabbitTemplate;
    }

    @Bean("defaultRabbitTemplate")
    public RabbitTemplate defaultRabbitTemplate(ConnectionFactory connectionFactory){
        RabbitTemplate rabbitTemplate=new RabbitTemplate(connectionFactory);
        return rabbitTemplate;
    }
}
ProducerService.java
@Service
public class ProducerService {
    private static final String EXCHANGE_NAME="direct.exchange.a";
    private static final String ROUTING_KEY_NAME="direct.routingKey.a";

    @Resource(name = "defaultRabbitTemplate")
    private RabbitTemplate defaultRabbitTemplate;
    @Resource(name = "jsonRabbitTemplate")
    private RabbitTemplate jsonRabbitTemplate;

    /**
     * 傳送 訊息物件 json
     *
     * @param data
     * @param messageProperties
     */
    public void sendObject(Object data, MessageProperties messageProperties) {
        Message message = jsonRabbitTemplate.getMessageConverter().toMessage(data, messageProperties);
        jsonRabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY_NAME, message);
    }

    /**
     * 傳送 訊息文字
     *
     * @param data
     * @param messageProperties
     */
    public void sendText(String data, MessageProperties messageProperties) {
        Message message = defaultRabbitTemplate.getMessageConverter().toMessage(data, messageProperties);
        defaultRabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY_NAME, message);
    }
}

3.2.2.消費者

MessageListener.java
@Component
@Slf4j
public class MessageListener {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private ObjectMapper objectMapper;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "direct.queue.d",
                    durable = "false"),
            exchange = @Exchange(value = "direct.exchange.a",
                    durable = "true",
                    type = ExchangeTypes.DIRECT,
                    ignoreDeclarationExceptions = "true"),
            key = "direct.routingKey.a"
    )
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        String contentType = message.getMessageProperties().getContentType();
        String bodyText = null;
        System.out.println(contentType);
        switch (contentType) {
            //字串
            case MessageProperties.CONTENT_TYPE_TEXT_PLAIN:
                bodyText = (String) rabbitTemplate.getMessageConverter().fromMessage(message);
                break;
            //json物件
            case MessageProperties.CONTENT_TYPE_JSON:
                User user = objectMapper.readValue(message.getBody(), User.class);
                bodyText = user.toString();
                break;
        }
        log.info("消費端Payload: " + bodyText);
    }  
}

生產者傳送物件訊息時,我們使用Jackson2JsonMessageConverter,並用其toMessage方法封裝。但是在消費者接收物件訊息時,我們卻沒有用Jackson2JsonMessageConverter的fromMessage方法,而是使用ObjectMapper來反序列化Json物件。是因為rabbitmq在傳送Jackson2JsonMessageConverter的序列化物件時,會在包含類的包名資訊,消費者在使用fromMessage反序列化時,必須建立一個和生產者中包名等一模一樣的類。明顯不太現實。

3.3.釋出確認(生產者)

3.3.1.ConfirmCallback

ConfirmCallback介面用於實現訊息傳送到RabbitMQ交換器後接收ack回撥。

  • 投遞物件:exchange
  • 回撥觸發:無論成功或失敗,都會觸發回撥。
  • 投遞成功:ack=true
  • 投遞失敗:ack=false

使用方式在於:

  • 設定 publisher-confirm-type 為 correlated。
  • 實現RabbitTemplate.ReturnCallback 的函式式介面,並使用。
ProducerService.java
@Slf4j
@Service
public class ProducerService {
    private static final String EXCHANGE_NAME = "direct.exchange.a";
    private static final String ROUTING_KEY_NAME = "direct.routingKey.ab";

    @Resource(name = "defaultRabbitTemplate")
    private RabbitTemplate defaultRabbitTemplate;

    /**
     * ConfirmCallback
     *
     * 投遞物件:exchange
     * 回撥觸發:無論成功或失敗,都會觸發回撥。
     * 投遞成功:ack=true
     * 投遞失敗:ack=false
     */
    RabbitTemplate.ConfirmCallback confirmCallback = (CorrelationData correlationData, boolean ack, String cause) -> {
        log.info("ack: " + ack);
        if (!ack) {
            log.info("投遞exchange失敗!....可以進行日誌記錄、異常處理、補償處理等");
        } else {
            log.info("投遞exchange成功!");
        }
    };

 
    /**
     * 傳送 訊息文字
     *
     * @param data
     * @param messageProperties
     */
    public void sendText(String data, MessageProperties messageProperties) {
        Message message = defaultRabbitTemplate.getMessageConverter().toMessage(data, messageProperties);
        
        //confirmCallback
        defaultRabbitTemplate.setConfirmCallback(confirmCallback);

        defaultRabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY_NAME, message);
    }
}

配置檔案需要設定:

spring.rabbitmq.publisher-confirm-type = correlated

3.3.2.ReturnCallback

ReturnCallback介面用於實現訊息傳送到RabbitMQ交換器,但無相應佇列與交換器繫結時的回撥。

  • 投遞物件:queue
  • 回撥觸發:只有投遞失敗,才會觸發回撥。

使用方式在於:

  • 設定 publisher-returns 為 true。
  • 設定 mandatory 為 true。
  • 實現RabbitTemplate.ReturnCallback的函式式介面,並使用。
ProducerService.java
@Slf4j
@Service
public class ProducerService {
    private static final String EXCHANGE_NAME = "direct.exchange.a";
    private static final String ROUTING_KEY_NAME = "direct.routingKey.ab";

    @Resource(name = "defaultRabbitTemplate")
    private RabbitTemplate defaultRabbitTemplate;

    /**
     * ReturnCallback
     *
     * 投遞物件:queue
     * 回撥觸發:只有投遞失敗,才會觸發回撥。
     */
    RabbitTemplate.ReturnCallback returnCallback = (Message message, int replyCode, String replyText,
                                                    String exchange, String routingKey) -> {
        log.info("投遞到queue失敗! exchange: " + exchange + ", routingKey: "
                + routingKey + ", replyCode: " + replyCode + ", replyText: " + replyText);
    };

    /**
     * 傳送 訊息文字
     *
     * @param data
     * @param messageProperties
     */
    public void sendText(String data, MessageProperties messageProperties) {
        Message message = defaultRabbitTemplate.getMessageConverter().toMessage(data, messageProperties);
        //returnCallback
        defaultRabbitTemplate.setMandatory(true);
        defaultRabbitTemplate.setReturnCallback(returnCallback);

        defaultRabbitTemplate.convertAndSend(EXCHANGE_NAME, ROUTING_KEY_NAME, message);
    }
}

需要在配置檔案中配置:

spring.rabbitmq.publisher-returns = true

3.4.接收確認(消費者)

上一節講解的是,如何在生產者釋出訊息時,確認訊息釋出到rabbitmq的交換機和佇列中。那麼這一節講解的是,如何保障消費者能完全“消費”了訊息。

通常情況下,rabbitmq作為訊息中介軟體,它把message推送給消費者就完成了它的使命,該message就自動被“簽收”了。而消費者在接收到message後,再去實現關於該message的業務邏輯。可如果在實現該業務邏輯過程中發生了錯誤,需要重新執行,那就難辦了。因為message一旦被“簽收”後,就從rabbitmq中被刪除,不可能重新再傳送。

如果消費者能手動控制message的“簽收”操作,只有當關於message的業務邏輯執行完成後再“簽收”,message再從rabbitmq中刪除,否則可以讓message重發就好了。這一節就講這個。

3.4.1.AcknowledgeMode

Acknowledge意思是“確認”,訊息通過 ACK 確認是否被正確接收,每個 Message 都要被確認(acknowledged),可以手動去 ACK 或自動 ACK。

使用手動應答訊息,有一點需要特別注意,那就是不能忘記應答訊息,因為對於RabbitMQ來說處理訊息沒有超時,只要不應答訊息,他就會認為仍在正常處理訊息,導致訊息佇列出現阻塞,影響業務執行。如果不想處理,可以reject丟棄該訊息。

訊息確認模式有:

  • AcknowledgeMode.NONE:自動確認
  • AcknowledgeMode.AUTO:根據情況確認
  • AcknowledgeMode.MANUAL:手動確認

預設是自動確認,可以通過RabbitListenerContainerFactory 中進行開啟手動ack,或者中配置檔案中開啟:

spring.rabbitmq.listener.simple.acknowledge-mode = manual
MessageListener.java
@Component
@Slf4j
public class MessageListener {
    @Autowired
    private RabbitTemplate rabbitTemplate;
    @Autowired
    private ObjectMapper objectMapper;

    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "direct.queue.d",
                    durable = "false"),
            exchange = @Exchange(value = "direct.exchange.a",
                    durable = "true",
                    type = ExchangeTypes.DIRECT,
                    ignoreDeclarationExceptions = "true"),
            key = "direct.routingKey.a"
    )
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        String contentType = message.getMessageProperties().getContentType();
        String bodyText = null;
        System.out.println(contentType);
        switch (contentType) {
            //字串
            case MessageProperties.CONTENT_TYPE_TEXT_PLAIN:
                bodyText = (String) rabbitTemplate.getMessageConverter().fromMessage(message);
                break;
            //json物件
            case MessageProperties.CONTENT_TYPE_JSON:
                User user = objectMapper.readValue(message.getBody(), User.class);
                bodyText = user.toString();
                break;
        }
        log.info("消費端Payload: " + bodyText);
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }

}

3.4.2.Ack/Nack/Reject

設定為手動確認後,有3種確認操作:

  • Ack:確認收到訊息,然後訊息從佇列中刪除。
  • Nack:確認沒有收到訊息,訊息重新回到佇列中傳送。
  • Reject:拒絕該訊息,直接丟棄該訊息,不會回到佇列中。

如示例程式碼中的 basicAck 方法,需要注意的是,要傳遞兩個引數:

  • deliveryTag(唯一標識 ID):當一個消費者向 RabbitMQ 註冊後,會建立起一個 Channel ,RabbitMQ 會用 basic.deliver 方法向消費者推送訊息,這個方法攜帶了一個 delivery tag, 它代表了 RabbitMQ 向該 Channel 投遞的這條訊息的唯一標識 ID,是一個單調遞增的正整數,delivery tag 的範圍僅限於 Channel
  • multiple:為了減少網路流量,手動確認可以被批處理,當該引數為 true 時,則可以一次性確認 delivery_tag 小於等於傳入值的所有訊息

3.4.3.異常重試

除了上述手動確認的方式,還有一種不太常用的方式,可以實現重複傳送訊息。在開啟異常重試的前提下,在消費者程式碼中丟擲異常,會自動重發訊息。

application.properties

spring.rabbitmq.listener.simple.retry.enabled=true 是否開啟消費者重試
spring.rabbitmq.listener.simple.retry.max-attempts=5  最大重試次數
spring.rabbitmq.listener.simple.retry.initial-interval=5000 重試間隔時間(單位毫秒)
spring.rabbitmq.listener.simple.default-requeue-rejected=false 重試次數超過上面的設定之後是否丟棄
MessageListener.java
    @RabbitListener(bindings = @QueueBinding(
            value = @Queue(value = "direct.queue.d",
                    durable = "false"),
            exchange = @Exchange(value = "direct.exchange.a",
                    durable = "true",
                    type = ExchangeTypes.DIRECT,
                    ignoreDeclarationExceptions = "true"),
            key = "direct.routingKey.a"
    )
    )
    @RabbitHandler
    public void onMessage(Message message, Channel channel) throws Exception {
        String contentType = message.getMessageProperties().getContentType();
        String bodyText = null;
        System.out.println(contentType);
        switch (contentType) {
            //字串
            case MessageProperties.CONTENT_TYPE_TEXT_PLAIN:
                bodyText = (String) rabbitTemplate.getMessageConverter().fromMessage(message);
                break;
            //json物件
            case MessageProperties.CONTENT_TYPE_JSON:
                User user = objectMapper.readValue(message.getBody(), User.class);
                bodyText = user.toString();
                break;
        }
        log.info("消費端Payload: " + bodyText);
        throw new RuntimeException("重試啦");
    }

3.5.消費模式

在RabbitMQ中消費者有2種方式獲取佇列中的訊息:

  • push:basic.consume命令訂閱某一個佇列中的訊息,channel會自動在處理完上一條訊息之後,接收下一條訊息。(同一個channel訊息處理是序列的)。除非關閉channel或者取消訂閱,否則客戶端將會一直接收佇列的訊息。
  • pull:basic.get命令主動獲取佇列中的訊息,但是絕對不可以通過迴圈呼叫basic.get來代替basic.consume,這是因為basic.get RabbitMQ在實際執行的時候,是首先consume某一個佇列,然後檢索第一條訊息,然後再取消訂閱。如果是高吞吐率的消費者,最好還是建議使用basic.consume。

對比來說,如果有持續消費的需求,建議用push的方式,通過監聽器來訂閱。如果只是特定時刻需要從佇列中,一次性取些資料,可以用pull方式。

4.名詞概念

4.1.channel

我們知道無論是生產者還是消費者,都需要和 RabbitMQ Broker 建立連線,這個連線就是一條 TCP 連線,也就是 Connection。一旦 TCP 連線建立起來,客戶端緊接著可以建立一個 AMQP 通道(Channel),每個通道都會被指派一個唯一的 ID。

通道是建立在 Connection 之上的虛擬連線,RabbitMQ 處理的每條 AMQP 指令都是通過通道完成的。

我們完全可以使用 Connection 就能完成通道的工作,為什麼還要引入通道呢?試想這樣一個場景,一個應用程式中有很多個執行緒需要從 RabbitMQ 中消費訊息,或者生產訊息,那麼必然需要建立很多個 Connection,也就是多個 TCP 連線。然而對於作業系統而言,建立和銷燬 TCP 連線是非常昂貴的開銷,如果遇到使用高峰,效能瓶頸也隨之顯現。

RabbitMQ 採用類似 NIO(Non-blocking I/O)的做法,選擇 TCP 連線複用,不僅可以減少效能開銷,同時也便於管理。

每個執行緒把持一個通道,所以通道複用了 Connection 的 TCP 連線。同時 RabbitMQ 可以確保每個執行緒的私密性,就像擁有獨立的連線一樣。當每個通道的流量不是很大時,複用單一的 Connection 可以在產生效能瓶頸的情況下有效地節省 TCP 連線資源。但是通道本身的流量很大時,這時候多個通道複用一個 Connection 就會產生效能瓶頸,進而使整體的流量被限制了。此時就需要開闢多個 Connection,將這些通道均攤到這些 Connection 中,至於這些相關的調優策略需要根據業務自身的實際情況進行調節。

通道在 AMQP 中是一個很重要的概念,大多數操作都是在通道這個層面展開的。比如 channel.exchangeDeclare、channel.queueDeclare、channel.basicPublish、channel.basicConsume 等方法。RabbitMQ 相關的 API 與 AMQP 緊密相連,比如 channel.basicPublish 對應 AMQP 的 Basic.Publish 命令。

4.2.QoS

針對push方式,RabbitMQ可以設定basicQoS(Consumer Prefetch)來對consumer進行流控,從而限制未Ack的訊息數量。

前提包括,訊息確認模式必須是手動確認。

basicQos(int var1, boolean var2)
  • 第一個引數是限制未Ack訊息的最大數量。
  • 第二個引數是布林值,(1)為true時,說明是針對channel做的流控限制;(2)為false時,說明是針對整個消費者做的流控限制。

相關文章