RabbitMQ中的事務與confirmSelect模式

好久沒寫技術文章了,由於公司馬上要做消息相關的業務,所以最近在Docker上搭了一臺RabbitMQ並研究研究。

從網易蜂巢上拉取的鏡像:

docker pull hub.c.163.com/library/rabbitmq:latest

啓動容器:

docker run -d --hostname my-rabbit --name some-rabbit -p 15672:15672 -p 5672:5672 rabbitmq:3-management

查看容器啓動情況:

docker ps
CONTAINER ID        IMAGE                   COMMAND                  CREATED             STATUS              PORTS                                                                                        NAMES
05fb983beef4        rabbitmq:3-management   "docker-entrypoint.s…"   3 days ago          Up About an hour    4369/tcp, 5671/tcp, 0.0.0.0:5672->5672/tcp, 15671/tcp, 25672/tcp, 0.0.0.0:15672->15672/tcp   my-rabbit1

我們可以看到, 主機的15672映射到docker的15672端口,主機的5672映射到docker的5672端口.

在瀏覽器中輸入網址:http://ip:5672/,輸入用戶名/密碼:guest/guest,即可進入RabbitMQ的主界面。

簡單的搭建過程就是這樣,廢話不多說,接下來介紹RabbitMQ事務方面的問題(本文的部分截圖來自於網絡)。

讓我們先看一個RabbitMQ的小例子:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ProducerDemo {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.232");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        try{
            String exchangeName = "exchangeName";
            String routingKey = "routingKey";
            String queueName = "queueName";
            channel.exchangeDeclare(exchangeName,"direct",true);
            channel.queueDeclare(queueName,true,false,false,null);
            channel.queueBind(queueName,exchangeName,routingKey);
            byte [] messageBodyBytes = "Hello World!" .getBytes();
            for(int i = 0;i<100;i++) {
                channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            channel.close();
            conn.close();
        }
    }
}

通過上面的代碼,消息生產者Producer向Broker發送100條消息(什麼是Broker本文暫不做解釋,請自行百度),然而生產環境異常複雜,我們怎麼確定Broker收到Producer的消息了呢??類似於JDBC中的事務:①開啓事務--> ②update/insert/delete-->3成功commit失敗rollback,我們來看RabbitMQ對事務的控制。

1、txSelect()、txCommit()與txRollback()

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ProducerDemo {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.232");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        try{
            String exchangeName = "exchangeName";
            String routingKey = "routingKey";
            String queueName = "queueName";
            channel.exchangeDeclare(exchangeName,"direct",true);
            channel.queueDeclare(queueName,true,false,false,null);
            channel.queueBind(queueName,exchangeName,routingKey);
            byte [] messageBodyBytes = "Hello World!" .getBytes();
            channel.txSelect(); //開啓事務
            channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
            channel.txCommit(); //提交事務
        }catch (Exception e){
            e.printStackTrace();
            channel.txRollback();   //回滾
        }finally {
            channel.close();
            conn.close();
        }
    }
}

在通過txSelect開啓事務之後,我們便可以發佈消息給broker服務器了,如果txCommit提交成功了,則消息一定到達了broker了,如果在txCommit執行之前broker異常崩潰或者由於其他原因拋出異常,這個時候我們便可以捕獲異常通過txRollback回滾事務了。

通過wireshark抓包,我們可以看到事務對RabbitMQ性能的影響。

è¿éåå¾çæè¿°

在事務中,整個過程如下:

Tx.Select-->Tx.Select-OK-->Basic.Publish-->Tx.Commit-->Tx.Commit-OK(注意這裏的Tx.Commit與Tx.Commit-Ok之間的時間間隔294ms,由此可見事務還是很耗時的。)

我們再來看看沒有事務時的通信是怎樣的:

只有Basic.Publish

最後我們看看事務回滾時的通信:

 try{
        channel.txSelect(); //開啓事務
        channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
         int i = 1/0;
             channel.txCommit(); //提交事務
 }catch (Exception e){
        e.printStackTrace();
        channel.txRollback();   //回滾
 }finally {
        channel.close();
        conn.close();
}

è¿éåå¾çæè¿°

Tx.Select-->Tx.Select-OK-->Basic.Publish-->Tx.Rollback-->Tx.Rollback-OK

事務確實可以判斷producer向Broker發送消息是否成功,只有Broker接受到消息,纔會commit,但是使用事務機制的話會降低RabbitMQ的性能,那麼有沒有更好的方法既能保障producer知道消息已經正確送到,又能基本上不帶來性能上的損失呢?從AMQP協議的層面看是沒有更好的方法,但是RabbitMQ提供了一個更好的方案,即將channel信道設置成confirm模式。

2、Comfirm模式

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

       confirm模式最大的好處在於他是異步的,一旦發佈一條消息,生產者應用程序就可以在等Channel返回確認的同時繼續發送下一條消息,當消息最終得到確認之後,生產者應用便可以通過回調方法來處理該確認消息,如果RabbitMQ因爲自身內部錯誤導致消息丟失,就會發送一條nack消息,生產者應用程序同樣可以在回調方法中處理該nack消息;

       開啓Comfire模式的方法:

channel.confirmSelect();

這裏注意一下:txSelect與Confirm模式不能共存。

Confirm模式的三種編程方式:

  1. 串行confirm模式:peoducer每發送一條消息後,調用waitForConfirms()方法,等待broker端confirm。
  2. 批量confirm模式:producer每發送一批消息後,調用waitForConfirms()方法,等待broker端confirm。
  3. 異步confirm模式:提供一個回調方法,broker confirm了一條或者多條消息後producer端會回調這個方法。

我們分別來看看這三種confirm模式

1、串行confirm模式

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ProducerDemo {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.232");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String exchangeName = "exchangeName";
        String routingKey = "routingKey";
        String queueName = "queueName";
        channel.exchangeDeclare(exchangeName,"direct",true);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);
        byte [] messageBodyBytes = "Hello World!" .getBytes();
        channel.confirmSelect();    //開啓confirm模式
        try{
            for(int i = 0;i<50;i++) {
                channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
                if (channel.waitForConfirms()) {  //broker confirm後producer調用
                    System.out.println("發送成功");
                } else {
                    System.out.println("發送失敗");
                }
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            channel.close();
            conn.close();
        }
    }
}

通過循環,發送了50條消息,在channel.waitForConfirms()等待broker發送ack或nack,這種模式每發送一條消息就會等待broker代理服務器返回消息,通過抓包我們可以看到:

2、批量confirm模式:

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
public class ProducerDemo {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.232");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String exchangeName = "exchangeName";
        String routingKey = "routingKey";
        String queueName = "queueName";
        channel.exchangeDeclare(exchangeName,"direct",true);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);
        byte [] messageBodyBytes = "Hello World!" .getBytes();
        channel.confirmSelect();    //開啓confirm模式
        try{
            for(int i = 0;i<50;i++) {
                channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
            }
            if (channel.waitForConfirms()) {  //broker confirm後producer調用
                System.out.println("發送成功");
            } else {
                System.out.println("發送失敗");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            channel.close();
            conn.close();
        }
    }
}

通過循環批量發送50條消息,但只在控制檯輸出了一行“發送成功”,該方法會等到最後一條消息得到ack或者得到nack纔會結束,也就是說在waitForConfirms處會造成當前程序的阻塞,這點我們看出broker端默認情況下是進行批量回復的,並不是針對每條消息都發送一條ack消息;

3、異步confirm模式:

通過添加監聽器,如果broker返回ack,producer回調handleAck,返回nack,producer回調handleNack

import com.rabbitmq.client.*;

import java.io.IOException;
public class Demo01_ConnectionMQ_Provider {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.232");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String exchangeName = "exchangeNamex";
        String routingKey = "routingKeyx";
        String queueName = "queueNamex";
        channel.exchangeDeclare(exchangeName,"direct",true);
        channel.queueDeclare(queueName,true,false,false,null);
        channel.queueBind(queueName,exchangeName,routingKey);
        byte [] messageBodyBytes = "你好,世界!" .getBytes();
        try{
            channel.confirmSelect();    // 開啓confirm模式
            long start  = System.currentTimeMillis();
            //設置監聽器
            channel.addConfirmListener(new ConfirmListener() {
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("ack:deliveryTag:"+deliveryTag+",multiple:"+multiple);
                }
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("nack:deliveryTag:"+deliveryTag+",multiple:"+multiple);
                }
            });
            System.out.println("花費時間:"+(System.currentTimeMillis()-start));
            for(int i = 0;i<100;i++) {   //循環發消息
                channel.basicPublish(exchangeName, routingKey, null, messageBodyBytes);
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            channel.close();
            conn.close();
        }
    }
}

在控制檯輸出結果:

花費時間:3
ack:deliveryTag:8,multiple:true
花費時間:1
ack:deliveryTag:1,multiple:false
ack:deliveryTag:3,multiple:true
ack:deliveryTag:6,multiple:true

Process finished with exit code 0

可以看到,發送100條消息,收到的ack個數不一樣。並且這兩個ack消息的multiple域都爲true,你多次運行程序會發現每次發送回來的ack消息中的deliveryTag域的值並不是一樣的,說明broker端批量回傳給發送者的ack消息並不是以固定的批量大小回傳的;

由於是異步的,producer不需要等待broker返回ack任可以繼續發送消息,比channel.waitForConfirms()速度快很多。

3、性能測試

Client端機器和RabbitMQ機器配置:CPU:24核,2600MHZ, 64G內存,1TB硬盤。 
Client端發送消息體大小10B,線程數爲1即單線程,消息都持久化處理(deliveryMode:2)。 
分別採用事務模式、普通confirm模式,批量confirm模式和異步confirm模式進行producer實驗,比對各個模式下的發送性能。 

è¿éåå¾çæè¿°

發送平均速率:

  • 事務模式(tx):1637.484
  • 普通confirm模式(common):1936.032
  • 批量confirm模式(batch):10432.45
  • 異步confirm模式(async):10542.06

可以看到事務模式性能是最差的,普通confirm模式性能比事務模式稍微好點,但是和批量confirm模式還有異步confirm模式相比,還是小巫見大巫。批量confirm模式的問題在於confirm之後返回false之後進行重發這樣會使性能降低,異步confirm模式(async)編程模型較爲複雜,至於採用哪種方式,看情況嘍。

4、Consumer端的消息確認

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

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

當noAck=false時,對於RabbitMQ服務器端而言,隊列中的消息分成了兩部分:一部分是等待投遞給消費者的消息;一部分是已經投遞給消費者,但是還沒有收到消費者ack信號的消息。如果服務器端一直沒有收到消費者的ack信號,並且消費此消息的消費者已經斷開連接,則服務器端會安排該消息重新進入隊列,等待投遞給下一個消費者(也可能還是原來的那個消費者)。

讓我們來看代碼:

1、consumer自動向broker發送ack

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

public class Consumer {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.190");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String queueName = "queueNamex";

        QueueingConsumer consumer = new QueueingConsumer(channel);
        //設置爲true,consumer自動向broker發送ack
        channel.basicConsume(queueName, true, consumer);

        for(int i=0;i<100;i++){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            System.out.println(msg);    //打印消息
        }
        channel.close();
        conn.close();
    }
}

        假設有100條消息,consumer 調用

channel.basicConsume(queueName, true, consumer);

設置爲true自動向broker發送ack,最後關閉鏈接。讀者可以在rabbitmq的管理界面看到消息從100條減少到0條。

2、consumer手動向broker發送ack

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

public class Consumer {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.190");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String queueName = "queueNamex";

        QueueingConsumer consumer = new QueueingConsumer(channel);
        //設置爲false,consumer手動向broker發送ack
        channel.basicConsume(queueName, false, consumer);

        for(int i=0;i<100;i++){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            //consumer手動向broker發送ack
            channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            System.out.println(msg);
        }
        channel.close();
        conn.close();
    }
}

在consumer端,調用

channel.basicConsume(queueName, false, consumer);
和 
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);

手動向broker發送ack確認消息被接受,隨後關閉鏈接。

3、consumer不發送ack並且consumer斷開鏈接:這一點要注意讓我們來看下面的代碼和rabbitmq的管理界面

我用producer發送了100條消息,可以看到,Ready=100,Unacked=0,Total=100;

  如果我在Consumer端,設置爲手動發送ack方式但最後一直沒有發送ack,並且在讀取消息後立刻關閉鏈接

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

public class Consumer {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.190");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String queueName = "queueNamex";

        QueueingConsumer consumer = new QueueingConsumer(channel);
        //設置爲false,consumer手動向broker發送ack
        channel.basicConsume(queueName, false, consumer);

        for(int i=0;i<100;i++){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            //不發送ack給broker
        //    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            System.out.println(msg);
        }
        //關閉連接
        channel.close();
        conn.close();
    }
}

我們再來運行Consumer,來看看輸出的結果和rabbitmq的管理界面:

打印了100條消息,但是從rabbitmq的管理界面來看,消息數任仍爲100條,並沒有被消費掉,這就驗證了我前面的話:

如果服務器端一直沒有收到消費者的ack信號,並且消費此消息的消費者已經斷開連接,則服務器端會安排該消息重新進入隊列,等待投遞給下一個消費者(也可能還是原來的那個消費者)。

4、consumer不發送ack,並且沒有關閉連接

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

public class Consumer {
    public static void main(String[] args)throws Exception {
        ConnectionFactory factory = new ConnectionFactory();
        factory.setUsername("guest");
        factory.setPassword("guest");
        factory.setVirtualHost("/");
        factory.setHost("172.16.41.190");
        factory.setPort(5672);

        Connection conn = factory.newConnection();
        Channel channel = conn.createChannel();
        String queueName = "queueNamex";

        QueueingConsumer consumer = new QueueingConsumer(channel);
        //設置爲false,consumer手動向broker發送ack
        channel.basicConsume(queueName, false, consumer);

        for(int i=0;i<100;i++){
            QueueingConsumer.Delivery delivery = consumer.nextDelivery();
            String msg = new String(delivery.getBody());
            //不發送ack給broker
        //    channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            System.out.println(msg);
        }
    }
}

我們可以看到,Ready變爲0,而Unacked變爲100,表示consumer沒有向broker發送ack,前面我們說過,只有consumer向broker發送了ack,broker纔會刪除消息,所以此時broker並沒有刪除消息,如果消費者再次正常消費,依然可以獲得消息。

 

這就是我這幾天來對RabbitMQ事務方面的理解,謝謝大家,歡迎轉載。

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