RabbitMQ 消息可靠性投遞

  • 想要保證消息不丟失,首先要保證生產者將消息成功發送到RabbiMQ服務器上
  • 當生產者將消息發送出去之後,消息到底有沒有正確地到達服務器呢?
  • 如果不進行特殊處理,默認情況下發送消息的操作是不會有任何返回信息給生產者的,也就是默認情況下生產者是無法確定消息是否正確送達
  • import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import java.io.IOException;
    import java.util.concurrent.TimeoutException;
    
    /**
     * 生產者
     *
     * @author SGJAN
     */
    public class TxProducerTest {
        // 隊列名稱
        private final static String QUEUE_NAME = "test_queue";
    
        public static void main(String[] args) throws IOException, TimeoutException {
            // 創建連接
            ConnectionFactory factory = new ConnectionFactory();
            // 設置 RabbitMQ 的主機名
            factory.setHost("39.106.128.197");
            factory.setPort(5672);
            factory.setVirtualHost("/");
            factory.setUsername("sgjan");
            factory.setPassword("sgjan");
            // 創建一個連接
            Connection connection = factory.newConnection();
            // 創建一個通道
            Channel channel = connection.createChannel();
            // 發送消息
            String message = "Hello World!";
            channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
            // 提交事務
            channel.txCommit();
            System.out.println(" [x] Sent '" + message + "'");
            // 關閉頻道和連接
            channel.close();
            connection.close();
        }
    }
  • 此時運行代碼,因爲Broker中不存在test_queue隊列,消息無法存儲,程序未出現錯誤,此時消息丟失,並無反饋信息
  • 針對於這個問題,RabbitMQ提供了兩種解決方案
    • 事務機制
    • 發送方確認機制( publisher confirm )
  • 事務機制
    • RabbitMQ 客戶端與事務機制相關的方法有以下三個:
      • channel.txSelect():將當前的信道設置成事務模式
      • channel.txCommit():提交事務
      • channel.txRollback():回滾事務
    • 代碼如下
    • import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      /**
       * 事務模式 channel.txSelect();
       *
       * @author SGJAN
       */
      public class TxProducerTest {
          // 隊列
          private final static String QUEUE_NAME = "test_queue";
      
          public static void main(String[] args) throws IOException, TimeoutException {
              // 創建連接
              ConnectionFactory factory = new ConnectionFactory();
              // 設置 RabbitMQ 的主機名
              factory.setHost("39.106.128.197");
              factory.setPort(5672);
              factory.setVirtualHost("/");
              factory.setUsername("sgjan");
              factory.setPassword("sgjan");
              // 創建一個連接
              Connection connection = factory.newConnection();
              // 創建一個通道
              Channel channel = connection.createChannel();
              // todo 指定一個隊列,不存在的話自動創建
              channel.queueDeclare(QUEUE_NAME, false, false, false, null);
              // todo 將當前的信道設置成事務模式
              channel.txSelect();
              // 發送消息
              String message = "Hello World!";
              channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
              // 提交事務
              channel.txCommit();
              System.out.println(" [x] Sent '" + message + "'");
              // 關閉頻道和連接
              channel.close();
              connection.close();
          }
      }
    • 運行代碼,隊列新增成功,消息發送成功
    • 修改代碼,驗證異常回滾機制
    • import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      /**
       * 事務模式 channel.txSelect();
       * @author SGJAN
       */
      public class TxProducerTest {
          // 隊列
          private final static String QUEUE_NAME = "test_queue";
      
          public static void main(String[] args) throws IOException, TimeoutException {
              // 創建連接
              ConnectionFactory factory = new ConnectionFactory();
              // 設置 RabbitMQ 的主機名
              factory.setHost("39.106.128.197");
              factory.setPort(5672);
              factory.setVirtualHost("/");
              factory.setUsername("sgjan");
              factory.setPassword("sgjan");
              // 創建一個連接
              Connection connection = factory.newConnection();
              // 創建一個通道
              Channel channel = connection.createChannel();
              // todo 指定一個隊列,不存在的話自動創建
              channel.queueDeclare(QUEUE_NAME, false, false, false, null);
              try {
                  // todo 將當前的信道設置成事務模式
                  channel.txSelect();
                  // 發送消息
                  String message = "Hello World!";
                  channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
                  // todo 模擬報錯
                  int result = 1 / 0;
                  // 提交事務
                  channel.txCommit();
                  System.out.println(" [x] Sent '" + message + "'");
                  // 關閉頻道和連接
                  channel.close();
                  connection.close();
              } catch (Exception e) {
                  e.printStackTrace();
                  // todo 事務回滾
                  channel.txRollback();
              }
          }
      }

       

    • 觸發 java.lang.ArithmeticException: / by zero 異常,事務回滾,消息發送失敗。
  • 雖然事務能解決消息發送和RabbitMQ之間消息消息確認的問題,只有消息成功被RabbitMQ接收,事務才能提交成功,否則可在異常之後進行事務回滾,這種事務機制會壓縮RabbitMQ的性能,因此建議使用發送方確認機制( publisher confirm )》機制。
  • 發送方確認機制( publisher confirm )
    • 發送方確認機制是生產者將信道設置稱confirm(確認)模式,一旦信道進入confirm模式,所有在該信道上面發佈的消息都會被指派一個唯一的ID(從1開始),一旦消息被投遞到RabbitMQ服務器之後,RabbitMQ就會發送一個確認(Basic Ack)給生產者(包含消息的唯一ID),這就使得生產者知道消息已經正確到達了目的地了。
    • 如果RabbitMQ因爲自身錯誤導致信息丟失,就會發送一條nack( Basic.Nack ),生產者應用程序同樣可以在回調方法中處理該nack指令。
    • 如果消息和隊列是可持久化的,那麼確認消息會在消息寫入磁盤之後發出。
    • 相比之下,發送方確認機制最大的好處在於它是異步的,一旦發佈一條消息,生產者應用程序就可以在等信道返回確認的同時繼續發送下一條消息,當消息最終得到確認後,生產者應用程序便可以通過回調方法拉處理該確認消息。
    • 普通 confirm
    • import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      /**
       * 生產者 confirm模式
       * @author SGJAN
       */
      public class ConfirmProducerTest {
          // 交換機
          private final static String EXCHANGE_NAME = "confirm-exchange";
      
          public static void main(String[] args) throws IOException, TimeoutException {
              // 創建連接
              ConnectionFactory factory = new ConnectionFactory();
              // 設置 RabbitMQ 的主機名
              factory.setHost("39.106.128.197");
              factory.setPort(5672);
              factory.setVirtualHost("/");
              factory.setUsername("sgjan");
              factory.setPassword("sgjan");
              // 創建一個連接
              Connection connection = factory.newConnection();
              // 創建一個通道
              Channel channel = connection.createChannel();
              // 創建一個Exchange
              channel.exchangeDeclare(EXCHANGE_NAME, "direct");
              try {
                  // 將信道設置成confirm模式
                  channel.confirmSelect();
                  // 發送消息
                  String message = "confirm test";
                  channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
                  // 等待發送消息的確認消息,如果發送成功,則返回ture,如果發送失敗,則返回false            if (channel.waitForConfirms()) {
                      System.out.println("send message success");
                  } else {
                      System.out.println("send message failed");
                      // do something else...
                  }
              } catch (InterruptedException e) {
                  e.printStackTrace();
              }
              // 關閉頻道和連接
              channel.close();
              connection.close();
          }
      }
    • 運行結果,雖然消息推送成功,但達到exchange後沒有匹配隊列消息會丟失。
    • 發送多條消息
    • import com.rabbitmq.client.Channel;
      import com.rabbitmq.client.Connection;
      import com.rabbitmq.client.ConnectionFactory;
      import java.io.IOException;
      import java.util.concurrent.TimeoutException;
      
      /**
       * 批量發送消息
       * @author SGJAN
       */
      public class MoreConfirmProducerTest {
          // 交換機
          private final static String EXCHANGE_NAME = "confirm-exchange";
      
          public static void main(String[] args) throws IOException, TimeoutException {
              // 創建連接
              ConnectionFactory factory = new ConnectionFactory();
              // 設置 RabbitMQ 的主機名
              factory.setHost("39.106.128.197");
              factory.setPort(5672);
              factory.setVirtualHost("/");
              factory.setUsername("sgjan");
              factory.setPassword("sgjan");
              // 創建一個連接
              Connection connection = factory.newConnection();
              // 創建一個通道
              Channel channel = connection.createChannel();
              // 創建一個Exchange
              channel.exchangeDeclare(EXCHANGE_NAME, "direct");
              channel.confirmSelect();
              int loopTimes = 10;
              for (int i = 0; i < loopTimes; i++) {
                  try {
                      // 發送消息
                      String message = "normal confirm test" + i;
                      channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
                      if (channel.waitForConfirms()) {
                          System.out.println("send message success");
                      } else {
                          System.out.println("send message failed");
                          // do something else...
                      }
                  } catch (InterruptedException e) {
                      e.printStackTrace();
                  }
              }
              // 關閉頻道和連接
              channel.close();
              connection.close();
          }
      }
    • 運行結果,雖然消息推送成功,但達到exchange後沒有匹配隊列消息會丟失。
  • 注意事項:
    • 事務機制和publisher confirm機制互斥。
    • // 將信道設置成confirm模式
      channel.confirmSelect();
      // 將信道設置成事務模式
      channel.txSelect();
    • 拋出異常:
    • 顛倒順序後,拋出異常:
  • 事務機制publisher confirm機制確保的是消息能夠正確地發送至RabbitMQ的交換機,如果交換機沒有匹配隊列,那麼消息還是會丟失,所以在使用這兩種機制時,需要確保交換機能夠匹配到隊列。
  • confirm模式:每發送一批消息後,調用 channel.waitForConfirms() 方法,等待服務器的確認返回,實際上是一種串行同步等待的方式相比事務機制性能提升的並不多
  • 批量confirm模式:每發送一批消息後,調用 channel.waitForConfirms() 方法,等待服務器的確認返回,相confirm模式性能更好。如果出現返回 Basic.Nack或者超時情況,生產者客戶端需要將這一批次的消息全部重發,這樣會帶來明顯的重複消費數量,如果消息經常丟失,性能應該是不升反降。
  • import com.rabbitmq.client.Channel;
    import com.rabbitmq.client.Connection;
    import com.rabbitmq.client.ConnectionFactory;
    import java.io.IOException;
    import java.util.concurrent.ArrayBlockingQueue;
    import java.util.concurrent.BlockingQueue;
    import java.util.concurrent.TimeoutException;
    
    /**
     * 批量confirm模式
     * @author SGJAN
     */
    public class BatchConfirmProducerTest {
        // 交換機
        private final static String EXCHANGE_NAME = "confirm-exchange";
    
        public static void main(String[] args) throws IOException, TimeoutException {
            // 創建連接
            ConnectionFactory factory = new ConnectionFactory();
            // 設置 RabbitMQ 的主機名
            factory.setHost("39.106.128.197");
            factory.setPort(5672);
            factory.setVirtualHost("/");
            factory.setUsername("sgjan");
            factory.setPassword("sgjan");
            // 創建一個連接
            Connection connection = factory.newConnection();
            // 創建一個通道
            Channel channel = connection.createChannel();
            // 創建一個Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");
            int batchCount = 10;
            int msgCount = 0;
            BlockingQueue blockingQueue = new ArrayBlockingQueue(100);
            try {
                channel.confirmSelect();
                while (msgCount <= batchCount) {
                    String message = "batch confirm test";
                    channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes());
                    // 將發送出去的消息存入緩存中,緩存可以是一個ArrayList或者BlockingQueue之類的
                    blockingQueue.add(message);
                    if (++msgCount >= batchCount) {
                        try {
                            if (channel.waitForConfirms()) {
                                // 將緩存中的消息清空
                                blockingQueue.clear();
                                System.out.println("send message success");
                            } else {
                                // 將緩存中的消息重新發送
                                System.out.println("send message failed");
                            }
                        } catch (InterruptedException e) {
                            e.printStackTrace();
                            // 將緩存中的消息重新發送
                        }
                    }
                }
            } catch (IOException e) {
                e.printStackTrace();
            }
            // 關閉頻道和連接
            channel.close();
            connection.close();
        }
    }
  • 運行結果:
  • 異步confirm機制
  • import com.rabbitmq.client.*;
    import java.io.IOException;
    import java.util.SortedSet;
    import java.util.TreeSet;
    
    /**
     * 異步confirm模式
     * @author SGJAN
     */
    public class AsyncConfirmProducerTest {
        // 交換機
        private final static String EXCHANGE_NAME = "confirm-exchange";
    
        public static void main(String[] args) throws Exception {
            // 創建連接
            ConnectionFactory factory = new ConnectionFactory();
            // 設置 RabbitMQ 的主機名
            factory.setHost("39.106.128.197");
            factory.setPort(5672);
            factory.setVirtualHost("/");
            factory.setUsername("sgjan");
            factory.setPassword("sgjan");
            // 創建一個連接
            Connection connection = factory.newConnection();
            // 創建一個通道
            Channel channel = connection.createChannel();
            // 創建一個Exchange
            channel.exchangeDeclare(EXCHANGE_NAME, "direct");
            int batchCount = 100;
            long msgCount = 1;
            SortedSet<Long> confirmSet = new TreeSet<Long>();
            channel.confirmSelect();
            channel.addConfirmListener(new ConfirmListener() {
                @Override
                public void handleAck(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("Ack,SeqNo" + deliveryTag + ",multiple" + multiple);
                    if (multiple) {
                        confirmSet.headSet(deliveryTag - 1).clear();
                    } else {
                        confirmSet.remove(deliveryTag);
                    }
                }
    
                @Override
                public void handleNack(long deliveryTag, boolean multiple) throws IOException {
                    System.out.println("Nack,SeqNo" + deliveryTag + ",multiple" + multiple);
                    if (multiple) {
                        confirmSet.headSet(deliveryTag - 1).clear();
                    } else {
                        confirmSet.remove(deliveryTag);
                    }
                    // 注意這裏需要添加處理消息重發的場景
                }
            });
            // 演示發送100個消息
            while (msgCount <= batchCount) {
                long nextSeqNo = channel.getNextPublishSeqNo();
                channel.basicPublish(EXCHANGE_NAME, "", null, "async confirm test".getBytes());
                confirmSet.add(nextSeqNo);
                msgCount = nextSeqNo;
            }
            // 關閉頻道和連接
            channel.close();
            connection.close();
        }
    }
  • 性能比較
  • 事務機制,confirm機制,批量confirm機制,異步confirm機制 這四種可以實現生產者確認,以下數據參考晚上文章:
  • 發送10000條消息,事務機制耗時:2103
     
    發送10000條消息,普通confirm機制耗時:1483
     
    發送10000條消息,批量confirm機制耗時:281
     
    發送10000條消息,異步confirm機制耗時:214

     

  • 可以看出,事務機制最慢,confirm機制雖然有所提升但是並不多,批量confirm和異步confirm機制性能最好,建議使用異步confirm機制。

 

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