消息確認機制(Confirm模式) rabbitMQ

在上一篇文章中我們講解了RabbitMQ中的AMQP事務來保證消息發送到Broker端,同時我們可以在事務之間發送多條消息(即在channel.txSelect()和channel.txCommit()之間發送多條消息,通過使用事務來保證它們準確到達Broker),如果忘記了事務的使用,可以複習一下上一篇文章——RabbitMQ學習(五)——消息確認機制(AMQP事務)。

但是使用事務雖然可以保證消息的準確達到,但是它極大地犧牲了性能,因此我們爲了性能上的要求,可以採用另一種高效的解決方案——通過使用Confirm模式來保證消息的準確性。

這裏的Confirm模式可以分爲兩個方面來講解,一是消息的生產者(Producer)的Confirm模式,另一個是消息的消費者(Consumer)的Confirm模式。其實這兩種模式在前面幾節的代碼裏我們都有涉及到的,只是沒有詳細分析,這裏我們將詳細講解一下它們的具體用法和原理。

一、生產者(Producer)的Confirm模式
通過生產者的確認模式我們是要保證消息準確達到Broker端,而與AMQP事務不同的是Confirm是針對一條消息的,而事務是可以針對多條消息的。

發送原理圖大致如下:

爲了使用Confirm模式,client會發送confirm.select方法幀。通過是否設置了no-wait屬性,來決定Broker端是否會以confirm.select-ok來進行應答。一旦在channel上使用confirm.select方法,channel就將處於Confirm模式。處於 transactional模式的channel不能再被設置成Confirm模式,反之亦然。

這裏與前面的一些文章介紹的一致,發佈確認和事務兩者不可同時引入,channel一旦設置爲Confirm模式就不能爲事務模式,爲事務模式就不能爲Confirm模式。

在生產者將信道設置成Confirm模式,一旦信道進入Confirm模式,所有在該信道上面發佈的消息都會被指派一個唯一的ID(以confirm.select爲基礎從1開始計數),一旦消息被投遞到所有匹配的隊列之後,Broker就會發送一個確認給生產者(包含消息的唯一ID),這就使得生產者知道消息已經正確到達目的隊列了,如果消息和隊列是可持久化的,那麼確認消息會將消息寫入磁盤之後發出,Broker回傳給生產者的確認消息中deliver-tag域包含了確認消息的序列號,此外Broker也可以設置basic.ack的multiple域,表示到這個序列號之前的所有消息都已經得到了處理。

Confirm模式最大的好處在於它是異步的,一旦發佈一條消息,生產者應用程序就可以在等信道返回確認的同時繼續發送下一條消息,當消息最終得到確認之後,生產者應用便可以通過回調方法來處理該確認消息,如果RabbitMQ因爲自身內部錯誤導致消息丟失,就會發送一條basic.nack來代替basic.ack的消息,在這個情形下,basic.nack中各域值的含義與basic.ack中相應各域含義是相同的,同時requeue域的值應該被忽略。通過nack一條或多條消息, Broker表明自身無法對相應消息完成處理,並拒絕爲這些消息的處理負責。在這種情況下,client可以選擇將消息re-publish。

在channel 被設置成Confirm模式之後,所有被publish的後續消息都將被Confirm(即 ack)或者被nack一次。但是沒有對消息被Confirm的快慢做任何保證,並且同一條消息不會既被Confirm又被nack。

開啓confirm模式的方法
生產者通過調用channel的confirmSelect方法將channel設置爲Confirm模式,如果沒有設置no-wait標誌的話,Broker會返回confirm.select-ok表示同意發送者將當前channel信道設置爲Confirm模式(從目前RabbitMQ最新版本3.6來看,如果調用了channel.confirmSelect方法,默認情況下是直接將no-wait設置成false的,也就是默認情況下broker是必須回傳confirm.select-ok的)。

編程模式
對於固定消息體大小和線程數,如果消息持久化,生產者Confirm(或者採用事務機制),消費者ack那麼對性能有很大的影響.

消息持久化的優化沒有太好方法,用更好的物理存儲(SAS, SSD, RAID卡)總會帶來改善。生產者confirm這一環節的優化則主要在於客戶端程序的優化之上。歸納起來,客戶端實現生產者confirm有三種編程方式:

普通Confirm模式:每發送一條消息後,調用waitForConfirms()方法,等待服務器端Confirm。實際上是一種串行Confirm了,每publish一條消息之後就等待服務端Confirm,如果服務端返回false或者超時時間內未返回,客戶端進行消息重傳;
批量Confirm模式:批量Confirm模式,每發送一批消息之後,調用waitForConfirms()方法,等待服務端Confirm,這種批量確認的模式極大的提高了Confirm效率,但是如果一旦出現Confirm返回false或者超時的情況,客戶端需要將這一批次的消息全部重發,這會帶來明顯的重複消息,如果這種情況頻繁發生的話,效率也會不升反降;
異步Confirm模式:提供一個回調方法,服務端Confirm了一條或者多條消息後Client端會回調這個方法。
1、普通Confirm模式
主要代碼爲:

channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, (" Confirm模式, 第" + (i + 1) + "條消息").getBytes());
if (channel.waitForConfirms()) {
   System.out.println("發送成功");
}else{
  //進行消息重發
}
1
2
3
4
5
6
7
普通Confirm模式最簡單,publish一條消息後,等待服務器端Confirm,如果服務端返回false或者超時時間內未返回,客戶端就進行消息重傳。

我們還是結合代碼來講解,下載原來的代碼 rabbitmq-demo,然後在sender和receiver中分別新建代碼ConfirmSender1.java和ConfirmReceiver1.java。

ConfirmSender1.java:

package net.anumbrella.rabbitmq.sender;


import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.MessageProperties;
import org.apache.commons.lang.StringUtils;
import org.springframework.amqp.core.AmqpTemplate;
import org.springframework.beans.factory.annotation.Autowired;

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

/**
 * 這是java原生類支持RabbitMQ,直接運行該類
 */
public class ConfirmSender1 {

    private final static String QUEUE_NAME = "confirm";

    public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
        /**
         * 創建連接連接到RabbitMQ
         */
        ConnectionFactory factory = new ConnectionFactory();

        // 設置RabbitMQ所在主機ip或者主機名
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setHost("127.0.0.1");
        factory.setVirtualHost("/");
        factory.setPort(5672);

        // 創建一個連接
        Connection connection = factory.newConnection();

        // 創建一個頻道
        Channel channel = connection.createChannel();

        // 指定一個隊列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        // 發送的消息
        String message = "This is a confirm message!";

        channel.confirmSelect();
        final long start = System.currentTimeMillis();
        //發送持久化消息
        for (int i = 0; i < 5; i++) {
            //第一個參數是exchangeName(默認情況下代理服務器端是存在一個""名字的exchange的,
            //因此如果不創建exchange的話我們可以直接將該參數設置成"",如果創建了exchange的話
            //我們需要將該參數設置成創建的exchange的名字),第二個參數是路由鍵
            channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, (" Confirm模式, 第" + (i + 1) + "條消息").getBytes());
            if (channel.waitForConfirms()) {
                System.out.println("發送成功");
            }else{
                // 進行消息重發
            }
        }
        System.out.println("執行waitForConfirms耗費時間: " + (System.currentTimeMillis() - start) + "ms");
        // 關閉頻道和連接
        channel.close();
        connection.close();
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
我們在代碼中發送了5條消息到Broker端,每條消息發送後都會等待確認。

ConfirmReceiver1.java:

package net.anumbrella.rabbitmq.receiver;


import com.rabbitmq.client.*;

import java.io.IOException;
import java.text.SimpleDateFormat;
import java.util.Date;
import java.util.concurrent.TimeoutException;

/**
 * 這是java原生類支持RabbitMQ,直接運行該類
 */
public class ConfirmReceiver1 {

    private final static String QUEUE_NAME = "confirm";

    public static void main(String[] argv) throws IOException, InterruptedException, TimeoutException {

        ConnectionFactory factory = new ConnectionFactory();

        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setHost("127.0.0.1");
        factory.setVirtualHost("/");
        factory.setPort(5672);
        // 打開連接和創建頻道,與發送端一樣

        Connection connection = factory.newConnection();
        Channel channel = connection.createChannel();

        // 聲明隊列,主要爲了防止消息接收者先運行此程序,隊列還不存在時創建隊列。
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);
        System.out.println("ConfirmReceiver1 waiting for messages. To exit press CTRL+C");

        // 創建隊列消費者
        final Consumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties,
                                       byte[] body) throws IOException {
                SimpleDateFormat time = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss:SSSS");

                String message = new String(body, "UTF-8");

                System.out.println(" ConfirmReceiver1  : " + message);
                System.out.println(" ConfirmReceiver1 Done! at " + time.format(new Date()));
            }
        };
        channel.basicConsume(QUEUE_NAME, true, consumer);
    }
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
50
51
我們開啓WireShak,監聽RabbitMQ消息的發送。然後我們直接運行ConfirmSender1.java類,可以不用運行ConfirmReceiver.java,因爲我們主要是測試消息到達Broker端,這主要是涉及到Producer和RabbitMQ的服務端。

在控制檯打印出了信息:

發送成功
發送成功
發送成功
發送成功
發送成功
執行waitForConfirms耗費時間: 181ms
1
2
3
4
5
6
在RabbitMQ管理界面confirm隊列裏,我們可以查看到我們發送的5條消息數據。


在WireShark中也可以發現開啓了Confirm模式,以及我們發送的5條消息。

接着我們啓動ConfirmReceiver.java,可以收到我們發送的具體消息:

 ConfirmReceiver1 waiting for messages. To exit press CTRL+C
 ConfirmReceiver1  :  Confirm模式, 第1條消息
 ConfirmReceiver1 Done! at 2018-08-04 14:58:27:0014
 ConfirmReceiver1  :  Confirm模式, 第2條消息
 ConfirmReceiver1 Done! at 2018-08-04 14:58:27:0016
 ConfirmReceiver1  :  Confirm模式, 第3條消息
 ConfirmReceiver1 Done! at 2018-08-04 14:58:27:0016
 ConfirmReceiver1  :  Confirm模式, 第4條消息
 ConfirmReceiver1 Done! at 2018-08-04 14:58:27:0017
 ConfirmReceiver1  :  Confirm模式, 第5條消息
 ConfirmReceiver1 Done! at 2018-08-04 14:58:27:0017
1
2
3
4
5
6
7
8
9
10
11
2、批量Confirm模式
主要代碼爲:

channel.confirmSelect();
for(int i=0;i<5;i++){
     channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, (" Confirm模式, 第" + (i + 1) + "條消息").getBytes());
}
if(channel.waitForConfirms()){
    System.out.println("發送成功");
}else{
    // 進行消息重發
}
1
2
3
4
5
6
7
8
9
這裏主要更改代碼爲發送批量消息後再進行等待服務器確認,還可以調用channel.waitForConfirmsOrDie()方法,該方法會等到最後一條消息得到確認或者得到nack纔會結束,也就是說在waitForConfirmsOrDie處會造成當前程序的阻塞。更改代碼爲批量Confirm模式,運行我們查看控制檯:

發送成功
執行waitForConfirms耗費時間: 59ms
1
2
在WireShark查看信息如下:


可以發現這裏處理的就是在批量發送信息完畢後,再進行ACK確認。同時我們發現這裏只有三個Basic.Ack,這是因爲Broker對信息進行了批量處理。

我們可以發現multiple的值爲true,這與前面我們講解的一致,true確認所有將比第一個參數指定的 delivery-tag 小的消息都得到確認。

我們也可以發現執行時間比第一種模式縮短了很多,效率極大提高了。

如果我們要對每條消息進行監聽處理,可以通過在channel中添加監聽器來實現,

channel.addConfirmListener(new ConfirmListener() {

            @Override
            public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("nack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
            }

            @Override
            public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                System.out.println("ack: deliveryTag = " + deliveryTag + " multiple: " + multiple);
            }
        });
1
2
3
4
5
6
7
8
9
10
11
12
13
當收到Broker發送過來的ack消息時就會調用handleAck方法,收到nack時就會調用handleNack方法。

我們可以在控制檯看到信息,這次調用了兩次Basic.Ack方法。

ack: deliveryTag = 4 multiple: true
ack: deliveryTag = 5 multiple: false
發送成功
執行waitForConfirms耗費時間: 50ms
1
2
3
4
3、異步Confirm模式
這裏使用的異步Confirm模式,也要用到上面提到的監聽,但是這裏需要我們自己去維護實現一個waitForConfirms()方法或waitForConfirmsOrDie(),而waitForConfirms()是同步的,因此我們需要自己去實現維護delivery-tag。

我們可以在jar中查看到源碼,其實waitForConfirmsOrDie()最終調用的也是waitForConfirms()方法,在waitForConfirms()方法內部維護了一個同步塊代碼,而unconfirmedSet就是存儲delivery-tag標識的。

我們要實現自己異步調用,主要就是爲了維護delivery-tag,主要實現代碼如下:

SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
channel.confirmSelect();
channel.addConfirmListener(new ConfirmListener() {
       public void handleAck(long deliveryTag, boolean multiple) throws IOException {
            if (multiple) {
                  confirmSet.headSet(deliveryTag + 1L).clear();
              } else {
                    confirmSet.remove(deliveryTag);
              }
       }
       public void handleNack(long deliveryTag, boolean multiple) throws IOException {
             System.out.println("Nack, SeqNo: " + deliveryTag + ", multiple: " + multiple);
             if (multiple) {
                 confirmSet.headSet(deliveryTag + 1L).clear();
              } else {
                  confirmSet.remove(deliveryTag);
              }
        }
});
for(int i=0;i<5;i++){
            long nextSeqNo = channel.getNextPublishSeqNo();
     channel.basicPublish("", QUEUE_NAME, MessageProperties.PERSISTENT_BASIC, (" Confirm模式, 第" + (i + 1) + "條消息").getBytes());
            confirmSet.add(nextSeqNo);
}
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
維持異步調用要求我們不能斷掉連接,具體可以參考代碼ConfirmSender2.java。

4、關於Spring Boot使用Producer的Confirm模式
在前面RabbitMQ學習(三)——探索交換機(Exchange),結合SpringBoot實戰中就有提及到,主要是通過在Sender中實現RabbitTemplate.ConfirmCallback接口來實現該操作。可以參考rabbitmq-demo中的CallBackSender.java和CheckReceiver.java的實現。

二、消費者(Consumer)的Confirm模式
1、手動確認和自動確認
爲了保證消息從隊列可靠地到達消費者,RabbitMQ提供消息確認機制(message acknowledgment)。消費者在聲明隊列時,可以指定noAck參數,當noAck=false時,RabbitMQ會等待消費者顯式發回ack信號後才從內存(和磁盤,如果是持久化消息的話)中移去消息。否則,RabbitMQ會在隊列中消息被消費後立即刪除它。

採用消息確認機制後,只要令noAck=false,消費者就有足夠的時間處理消息(任務),不用擔心處理消息過程中消費者進程掛掉後消息丟失的問題,因爲RabbitMQ會一直持有消息直到消費者顯式調用basicAck爲止。

在Consumer中Confirm模式中分爲手動確認和自動確認。

手動確認主要並使用以下方法:

basic.ack: 用於肯定確認,multiple參數用於多個消息確認。
basic.recover:是路由不成功的消息可以使用recovery重新發送到隊列中。
basic.reject:是接收端告訴服務器這個消息我拒絕接收,不處理,可以設置是否放回到隊列中還是丟掉,而且只能一次拒絕一個消息,官網中有明確說明不能批量拒絕消息,爲解決批量拒絕消息纔有了basicNack。
basic.nack:可以一次拒絕N條消息,客戶端可以設置basicNack方法的multiple參數爲true,服務器會拒絕指定了delivery_tag的所有未確認的消息(tag是一個64位的long值,最大值是9223372036854775807)。

肯定的確認只是指導RabbitMQ將一個消息記錄爲已投遞。basic.reject的否定確認具有相同的效果。 兩者的差別在於:肯定的確認假設一個消息已經成功處理,而對立面則表示投遞沒有被處理,但仍然應該被刪除。

同樣的Consumer中的Confirm模式也具有同時確認多個投遞,通過將確認方法的 multiple “字段設置爲true完成的,實現的意義與Producer的一致。

在自動確認模式下,消息在發送後立即被認爲是發送成功。 這種模式可以提高吞吐量(只要消費者能夠跟上),不過會降低投遞和消費者處理的安全性。 這種模式通常被稱爲“發後即忘”。 與手動確認模式不同,如果消費者的TCP連接或信道在成功投遞之前關閉,該消息則會丟失。

使用自動確認模式時需要考慮的另一件事是消費者過載。 手動確認模式通常與有限的信道預取一起使用,限制信道上未完成(“進行中”)傳送的數量。 然而,對於自動確認,根據定義沒有這樣的限制。 因此,消費者可能會被交付速度所壓倒,可能積壓在內存中,堆積如山,或者被操作系統終止。 某些客戶端庫將應用TCP反壓(直到未處理的交付積壓下降超過一定的限制時才停止從套接字讀取)。 因此,只建議當消費者可以有效且穩定地處理投遞時才使用自動投遞方式。

主要實現代碼:

// 手動確認消息
channel.basicAck(envelope.getDeliveryTag(), false);

// 關閉自動確認
boolean autoAck = false;
channel.basicConsume(QUEUE_NAME, autoAck, consumer);

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