RabbitMQ消息可靠性投遞分析

1、消息可靠性投遞分析

由RabbitMQ消息發送的過程,我們可以知道如果要保證消息的可靠性投遞,必須要下保證如下圖的4個環節,消息的可靠性

  1. Producer發送消息到Broker。
  2. 消息從Exchange路由到Queue。
  3. 消息在Queue中的持久化存儲。
  4. Consumer訂閱並消費消息。

根據以上的4個環節,我們來逐一分析。

1.1、Producer發送消息到Broker

在Producer發送消息到Broker的過程中,如果Broker由於網絡或者磁盤問題,未能成功接收消息,這時候Producer如何知道消息是否成功發送給Broker呢?

所以這裏RabbitMQ提供了服務端的確認機制,只有Producer收到Broker的消息確認,才能說明消息被成功接收。這種確認機制有如下兩種:

  • **Transaction(事務)模式 **
  • Confirm(確認)模式

1.1.1、Transaction(事務)模式

這裏是事務和我正常理解的事務,其實就是一個道理,在消息發送過程中出現失敗的情況,會回滾消息。

通過WireShark抓包,我們可以知道。使用事務模式的時候,Producer和Broker的交互如下圖所示:

java API 如下:

代碼示例

try {

    channel.txSelect();// 開始事務模式
    channel.basicPublish("", QUEUE_NAME, null, (msg).getBytes());
    channel.txCommit();// 提交
    System.out.println("消息發送成功");
} catch (Exception e) {
    channel.txRollback();// 回滾
    System.out.println("消息已經回滾");
}

Springboot 如下

TemplateConfig中配置

代碼示例

rabbitTemplate.setChannelTransacted(true):

在事務模式裏面,只有收到了服務端的Commit-OK的指令,才能提交成功。所以可以解決生產者和服務端確認的問題。但是事務模式有一個缺點,它是阻塞的,一條消息沒有發送完畢,不能發送下一條消息,它會榨乾RabbitMQ服務器的性能。所以不建議大家在生產環境使用。

1.1.2、Confirm(確認)模式

1.1.2.1、普通確認模式

hannel.confirmSelect()開啓確認模式,Broker接收到消息會給一個Basic.Ack,Producer這邊通過channel.waitForConfirms()接收回執。

代碼示例

// 開啓發送方確認模式
channel.confirmSelect();
channel.basicPublish("", QUEUE_NAME, null, msg.getBytes());
// 普通Confirm,發送一條,確認一條
if (channel.waitForConfirms()) {
    System.out.println("消息發送成功" );
}else{
    System.out.println("消息發送失敗");
}
1.1.2.2、批量確認模式

hannel.confirmSelect()開啓確認模式,Broker接收到所有的消息纔會給一個Basic.Ack,Producer這邊通過channel.waitForConfirmsOrDie()接收回執。只要waitForConfirmsOrDie沒有拋出異常,就代表服務端接收成功。這種方式提高了效率。但是,這裏批量發送的消息如果有一條出現問題,所有的消息都發送不成功。

代碼示例

try {
    channel.confirmSelect();
    for (int i = 0; i < 5; i++) {
        // 發送消息
        // String exchange, String routingKey, BasicProperties props, byte[] body
        channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    }
    // 批量確認結果,ACK如果是Multiple=True,代表ACK裏面的Delivery-Tag之前的消息都被確認了
    // 比如5條消息可能只收到1個ACK,也可能收到2個(抓包纔看得到)
    // 直到所有信息都發布,只要有一個未被Broker確認就會IOException
    channel.waitForConfirmsOrDie();
    System.out.println("消息發送完畢,批量確認成功");
} catch (Exception e) {
    // 發生異常,可能需要對所有消息進行重發
    e.printStackTrace();
}
1.1.2.3、異步確認模式

異步確認,顧名思義就是發送和確認消息不是同步的。這樣就可以一邊發送消息,一邊確認消息。通過confirmSelect開啓確認模式,這裏通過添加ConfirmListener監聽來異步確認消息。其中handleAck方法是服務端已經確認的消息回調;handleNack方法是服務爲確認消息回調。

代碼示例

// 用來維護未確認消息的deliveryTag
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());

// 這裏不會打印所有響應的ACK;ACK可能有多個,有可能一次確認多條,也有可能一次確認一條
// 異步監聽確認和未確認的消息
// 如果要重複運行,先停掉之前的生產者,清空隊列
channel.addConfirmListener(new ConfirmListener() {
    public void handleNack(long deliveryTag, boolean multiple) throws IOException {
        System.out.println("Broker未確認消息,標識:" + deliveryTag);
        if (multiple) {
            // headSet表示後面參數之前的所有元素,全部刪除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            confirmSet.remove(deliveryTag);
        }
        // 這裏添加重發的方法
    }
    public void handleAck(long deliveryTag, boolean multiple) throws IOException {
        // 如果true表示批量執行了deliveryTag這個值以前(小於deliveryTag的)的所有消息,如果爲false的話表示單條確認
        System.out.println(String.format("Broker已確認消息,標識:%d,多個消息:%b", deliveryTag, multiple));
        if (multiple) {
            // headSet表示後面參數之前的所有元素,全部刪除
            confirmSet.headSet(deliveryTag + 1L).clear();
        } else {
            // 只移除一個元素
            confirmSet.remove(deliveryTag);
        }
        System.out.println("未確認的消息:"+confirmSet);
    }
});

// 開啓發送方確認模式
channel.confirmSelect();
for (int i = 0; i < 10; i++) {
    long nextSeqNo = channel.getNextPublishSeqNo();
    // 發送消息
    // String exchange, String routingKey, BasicProperties props, byte[] body
    channel.basicPublish("", QUEUE_NAME, null, (msg +"-"+ i).getBytes());
    confirmSet.add(nextSeqNo);
}
System.out.println("所有消息:"+confirmSet);

// 這裏註釋掉的原因是如果先關閉了,可能收不到後面的ACK
//channel.close();
//conn.close();

Springboot中示例

TemplateConfig中進行設置。代碼示例

rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
    public void confirm(CorrelationData correlationData, boolean ack, String cause) {
        if (!ack) {
            System.out.println("發送消息失敗:" + cause);
            throw new RuntimeException("發送異常:" + cause);
        }
    }
});

1.2、消息從Exchange路由到Queue

在消息從Exchange路由到Queue的過程中出現問題。可能的情況有如下兩種。一是,routing key錯誤;或隊列不存在。

RabbitMQ提供了兩種方式來處理:

  • Broker重新發送給Producer(通過Producer設置回調的方式)。
  • 讓交換機路由到備份的交換機。

1.2.1、Broker重新發送給Producer

java API,通過設置ReturnListener來實現回調。

代碼示例

channel.addReturnListener(new ReturnListener() {
    public void handleReturn(int replyCode,
                             String replyText,
                             String exchange,
                             String routingKey,
                             AMQP.BasicProperties properties,
                             byte[] body)
        throws IOException {
        System.out.println("=========監聽器收到了無法路由,被返回的消息============");
        System.out.println("replyText:"+replyText);
        System.out.println("exchange:"+exchange);
        System.out.println("routingKey:"+routingKey);
        System.out.println("message:"+new String(body));
    }
});

SpringBoot中使用,在TemplateConfig中設置,通過設置ReturnCallback來實現回調。

代碼示例

 rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback(){
     public void returnedMessage(Message message,
                                 int replyCode,
                                 String replyText,
                                 String exchange,
                                 String routingKey){
         System.out.println("回發的消息:");
         System.out.println("replyCode: "+replyCode);
         System.out.println("replyText: "+replyText);
         System.out.println("exchange: "+exchange);
         System.out.println("routingKey: "+routingKey);
     }
 });

1.2.2、交換機路由到備份的交換機

這裏是通過聲明交換機的時候,設置備用交換機的參數alternate-exchange,來指定備用交換機。

代碼示例

AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder().deliveryMode(2).
    contentEncoding("UTF-8").build();

// 備份交換機
channel.exchangeDeclare("ALTERNATE_EXCHANGE","topic", false, false, false, null);
channel.queueDeclare("ALTERNATE_QUEUE", false, false, false, null);
channel.queueBind("ALTERNATE_QUEUE","ALTERNATE_EXCHANGE","#");

// 在聲明交換機的時候指定備份交換機
Map<String,Object> arguments = new HashMap<String,Object>();
arguments.put("alternate-exchange","ALTERNATE_EXCHANGE");
channel.exchangeDeclare("TEST_EXCHANGE","topic", false, false, false, arguments);

1.3、消息在Queue中的持久化存儲

這裏我們來學習一些消息存儲持久化的設置。

1.3.1、隊列(Queue)的持久化

在聲明隊列的時候注意選擇參數durable=true,就代表持久化設置。

// String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
channel.queueDeclare(QUEUE_NAME, false, false, false, null);

durable:沒有持久化的隊列,保存在內存中,服務重啓後隊列和消息都會消失(錢和人一起沒了)。

autoDelete:沒有消費者連接的時候,自動刪除。

exclusive:排他性隊列的特點是:

  1. 只對首次聲明它的連接(Connection)可見。
  2. 會在其連接斷開的時候自動刪除。

1.3.2、交換機的持久化

同樣是設置如下的幾個參數,來實現持久化。durable

// 聲明交換機
// String exchange, String type, boolean durable, boolean autoDelete, Map<String, Object> arguments
channel.exchangeDeclare(EXCHANGE_NAME,"direct",false, false, null);

1.3.3、消息持久化

消息的持久化,是通過BasicProperties構造,設置deliveryMode=2,代表消息進行了持久化。

// 對每條消息設置過期時間
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
    .deliveryMode(2) // 持久化消息
    .contentEncoding("UTF-8")
    .expiration("10000") // TTL
    .build();

1.3.4、集羣

集羣就是通過備份的方式提高RabbitMQ的可用性。

1.4、Consumer消費消息

如果Consumer接收到消息以後,在處理的過程中出現了異常,導致了Comsumer消費消息的失敗。在這種情況下,該怎麼辦呢?

不用慌,RabbitMQ給我們提供了Consumer確認機制(熟悉不熟悉,服務端是不是也有個消息確認機制)。Comsumer收到並處理完消息以後,手動或者自動給服務端一個ACK。

沒有收到ACK的消息,消費者斷開連接後,RabbitMQ會把這條消息發送給其他消費者。如果沒有其他消費者,消費者重啓後會重新消費這條消息,重複執行業務邏輯(如果代碼修復好了還好)。

消費者確定接受消息的方式有如下兩種:

  • 自動確認(ACK)
  • 手動確認(ACK)

1.4.1、自動ACK

自動ACK,這個也是默認的情況。也就是我們沒有在消費者處編寫ACK的代碼,消費者會在收到消息的時候就自動發送ACK,而不是在方法執行完畢的時候發送ACK(並不關心你有沒有正常消息)。

1.4.2、手動ACK

可以實現在消費者業務邏輯處理完成後,在進行確認。

1、java api使用
// 第一步、autoAck設置成false,聲明自動確認
// String queue, boolean autoAck, Consumer callback
channel.basicConsume(QUEUE_NAME, false, consumer);

// 第二步、在Consumer的回調中,調用channel的如下方法,實現消息確認、拒絕、異常處理
 if (msg.contains("拒收")){
     // 拒絕消息
     // requeue:是否重新入隊列,true:是;false:直接丟棄,相當於告訴隊列可以直接刪除掉
     // TODO 如果只有這一個消費者,requeue 爲true 的時候會造成消息重複消費
     channel.basicReject(envelope.getDeliveryTag(), false);
 } else if (msg.contains("異常")){
     // 批量拒絕
     // requeue:是否重新入隊列
     // TODO 如果只有這一個消費者,requeue 爲true 的時候會造成消息重複消費
     channel.basicNack(envelope.getDeliveryTag(), true, false);
 } else {
     // 手工應答
     // 如果不應答,隊列中的消息會一直存在,重新連接的時候會重複消費
     channel.basicAck(envelope.getDeliveryTag(), true);
 }

代碼示例

2、springboot使用

首先在application.properties文件中,進行如下配置

spring.rabbitmq.listener.direct.acknowledge-mode=manual
spring.rabbitmq.listener.simple.acknowledge-mode=manual

注意這裏有三個選項可以選擇

  • none:自動ACK
  • manual:手動ACK
  • auto:如果方法未拋出異常,則發送ack。如果方法拋出異常,並且不是AmqpRejectAndDontRequeueException則發送nack,並且重新入隊列。如果拋出異常時AmqpRejectAndDontRequeueException則發送nack不會重新入隊列。

然後再如下消費方法中進行確認

@RabbitListener(queues = "${com.fanger.secondqueue}", containerFactory="rabbitListenerContainerFactory")
public class SecondConsumer {
    @RabbitHandler
    public void process(String msgContent,Channel channel, Message message) throws IOException {
        System.out.println("Second Queue received msg : " + msgContent );
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false); // 確認
//        channel.basicNack(message.getMessageProperties().getDeliveryTag(),true,false); // 批量拒絕
//        channel.basicReject(message.getMessageProperties().getDeliveryTag(),false); // 拒絕
    }
}

1.5、消費者回調

處理以上4個環節,保證消息可靠投遞,我們還可以通過消費者執行成功後,給生成這一回執的方式來確保消息可靠。這裏生產者可以提供響應的API來接收通知。

1.6、消息補償機制

如果,消費者由於種種原因,沒有回調生產者API。消息沒有被處理成功怎麼辦呢?

這時候,咱們就需要進行消息補償,也就重新發送消息。

這裏可以通過一張消息的狀態表,通過定時任務來,掃描表,然後重發消息。這裏要注意的是

  • 重發的次數?
  • 重發消息的時間間隔?

這兩個問題,可以根據系統設計而定,不過重試次數別超過3次。

1.7、消息的冪等性

如果有了1.6的消息補償機制,就一定要保證消息的冪等性。

可能出現重複可能的原因:

  1. 生產者的問題,環節①重複發送消息,比如在開啓了Confirm模式但未收到確認,消費者重複投遞。
  2. 環節④出了問題,由於消費者未發送ACK或者其他原因,消息重複消費。
  3. 生產者代碼或者網絡問題。

如何避免消息被重複消費呢?

重複消息,可以通過給每個消息生成唯一的業務ID,然後落庫的時候判重來控制。

1.8、消息的順序性

消息的順序性指的是消費者消費消息的順序跟生產者生產消息的順序是一致的。

在RabbitMQ中,一個隊列有多個消費者時,由於不同的消費者消費消息的速度是不一樣的,順序無法保證。只有一個隊列僅有一個消費者的情況才能保證順序消費(不同的業務消息發送到不同的專用的隊列)。

作者:fanger8848
鏈接:https://juejin.cn/post/6986572514826649636
來源:掘金

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