RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

申城異鄉人發表於2019-05-30

1. 前情回顧

RabbitMQ使用教程(一)RabbitMQ環境安裝配置及Hello World示例

RabbitMQ使用教程(二)RabbitMQ使用者管理,角色管理及許可權設定

RabbitMQ使用教程(三)如何保證訊息99.99%被髮送成功?

在上一篇部落格中,我們講解了如何通過RabbitMQ的生產者確認機制,保證訊息儘可能的成功的傳送到RabbitMQ伺服器,這只是從源頭降低了訊息丟失的機率,並沒有真正解決之前提到的問題:如何保證RabbitMQ異常情況(人為重啟、異常當機等)下,佇列和訊息不丟失?

2. 本篇概要

要解決該問題,就要用到RabbitMQ中持久化的概念,所謂持久化,就是RabbitMQ會將記憶體中的資料(Exchange 交換器,Queue 佇列,Message 訊息)固化到磁碟,以防異常情況發生時,資料丟失。

其中,RabblitMQ的持久化分為三個部分:

  1. 交換器(Exchange)的持久化
  2. 佇列(Queue)的持久化
  3. 訊息(Message)的持久化

3. 交換器(Exchange)的持久化

在上篇部落格中,我們宣告Exchange的程式碼是這樣的:

private final static String EXCHANGE_NAME = "normal-confirm-exchange";

// 建立一個Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct");

這種情況下宣告的Exchange是非持久化的,在RabbitMQ出現異常情況(重啟,當機)時,該Exchange會丟失,會影響後續的訊息寫入該Exchange,那麼如何設定Exchange為持久化的呢?答案是設定durable引數。

durable:設定是否持久化。durable設定為true表示持久化,反之是非持久化。

持久化可以將交換器存檔,在伺服器重啟的時候不會丟失相關資訊。

設定Exchange持久化:

channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);

此時呼叫的過載方法為:

public DeclareOk exchangeDeclare(String exchange, String type, boolean durable) throws IOException {
    return this.exchangeDeclare(exchange, (String)type, durable, false, (Map)null);
}

為了能更好的理解,我們新建個生產類如下:

package com.zwwhnly.springbootaction.rabbitmq.durable;

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

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

public class DurableProducer {
    private final static String EXCHANGE_NAME = "durable-exchange";
    private final static String QUEUE_NAME = "durable-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 建立連線
        ConnectionFactory factory = new ConnectionFactory();
        // 設定 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 建立一個連線
        Connection connection = factory.newConnection();
        // 建立一個通道
        Channel channel = connection.createChannel();
        // 建立一個Exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct");
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 傳送訊息
        String message = "durable exchange test";
        channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());

        // 關閉頻道和連線
        channel.close();
        connection.close();
    }
}

示例程式碼中,我們新建了1個非持久化的Exchange,1個非持久化的Queue,並將它們做了繫結,此時執行程式碼,Exchange和Queue新建成功,訊息‘durable exchange test’也被正確地投遞到了佇列中:

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

此時重啟下RabbitMQ服務,會發現Exchange丟失了:

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

修改下程式碼,將durable引數設定為ture:

// 建立一個Exchange
channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);

此時執行完程式碼,然後重啟下RabbitMQ服務,會發現Exchange不再丟失:

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

4. 佇列(Queue)的持久化

細心的網友可能會發現,雖然現在重啟RabbitMQ服務後,Exchange不丟失了,但是佇列和訊息丟失了,那麼如何解決佇列不丟失呢?答案也是設定durable引數。

durable:設定是否持久化。為true則設定佇列為持久化。

持久化的佇列會存檔,在伺服器重啟的時候可以保證不丟失相關資訊。

簡單修改下上面宣告Queue的程式碼,將durable引數設定為true:

channel.queueDeclare(QUEUE_NAME, true, false, false, null);

此時呼叫的過載方法如下:

public com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments) throws IOException {
    validateQueueNameLength(queue);
    return (com.rabbitmq.client.impl.AMQImpl.Queue.DeclareOk)this.exnWrappingRpc((new com.rabbitmq.client.AMQP.Queue.Declare.Builder()).queue(queue).durable(durable).exclusive(exclusive).autoDelete(autoDelete).arguments(arguments).build()).getMethod();
}

執行程式碼,然後重啟RabbitMQ服務,會發現佇列現在不丟失了:

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

5. 訊息(Message)的持久化

雖然現在RabbitMQ重啟後,Exchange和Queue都不丟失了,但是儲存在Queue裡的訊息卻仍然會丟失,那麼如何保證訊息不丟失呢?答案是設定訊息的投遞模式為2,即代表持久化。

修改傳送訊息的程式碼為:

// 傳送訊息
String message = "durable exchange test";
AMQP.BasicProperties props = new AMQP.BasicProperties().builder().deliveryMode(2).build();
channel.basicPublish(EXCHANGE_NAME, "", props, message.getBytes());

呼叫的過載方法為:

public void basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body) throws IOException {
    this.basicPublish(exchange, routingKey, false, props, body);
}

執行程式碼,然後重啟RabbitMQ服務,發現此時Exchange,Queue,訊息都不丟失了:

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

RabbitMQ使用教程(四)如何通過持久化保證訊息99.99%不丟失?

至此,我們完美的解決了RabbitMQ重啟後,訊息丟失的問題。

最終的程式碼如下,你也可以通過文末的原始碼連結下載本文用到的所有原始碼:

package com.zwwhnly.springbootaction.rabbitmq.durable;

import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

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

public class DurableProducer {
    private final static String EXCHANGE_NAME = "durable-exchange";
    private final static String QUEUE_NAME = "durable-queue";

    public static void main(String[] args) throws IOException, TimeoutException {
        // 建立連線
        ConnectionFactory factory = new ConnectionFactory();
        // 設定 RabbitMQ 的主機名
        factory.setHost("localhost");
        // 建立一個連線
        Connection connection = factory.newConnection();
        // 建立一個通道
        Channel channel = connection.createChannel();
        // 建立一個Exchange
        channel.exchangeDeclare(EXCHANGE_NAME, "direct", true);
        channel.queueDeclare(QUEUE_NAME, true, false, false, null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "");

        // 傳送訊息
        String message = "durable exchange test";
        AMQP.BasicProperties props = new AMQP.BasicProperties().builder().deliveryMode(2).build();
        channel.basicPublish(EXCHANGE_NAME, "", props, message.getBytes());

        // 關閉頻道和連線
        channel.close();
        connection.close();
    }
}

6. 注意事項

1)理論上可以將所有的訊息都設定為持久化,但是這樣會嚴重影響RabbitMQ的效能。因為寫入磁碟的速度比寫入記憶體的速度慢得不止一點點。對於可靠性不是那麼高的訊息可以不採用持久化處理以提高整體的吞吐量。在選擇是否要將訊息持久化時,需要在可靠性和吞吐量之間做一個權衡。

2)將交換器、佇列、訊息都設定了持久化之後仍然不能百分之百保證資料不丟失,因為當持久化的訊息正確存入RabbitMQ之後,還需要一段時間(雖然很短,但是不可忽視)才能存入磁碟之中。如果在這段時間內RabbitMQ服務節點發生了當機、重啟等異常情況,訊息還沒來得及落盤,那麼這些訊息將會丟失。

3)單單隻設定佇列持久化,重啟之後訊息會丟失;單單隻設定訊息的持久化,重啟之後佇列消失,繼而訊息也丟失。單單設定訊息持久化而不設定佇列的持久化顯得毫無意義。

7. 原始碼

原始碼地址:https://github.com/zwwhnly/springboot-action.git,歡迎下載。

8. 參考

《RabbitMQ實戰指南》

相關文章