《RabbitMQ實戰指南》整理(三)RabbitMQ進階

一、消息何去何從

mandatory和immediate是channe.BasicPublish方法中的兩個參數,他們都有當消息不可達時將消息返回給生產者的能力。而備份交換器Alternate Exchange可以將未能被交換器路由的消息存儲起來,而不用返回給客戶端。

1、mandatory參數

當mandatory參數設爲true時,交換器無法根據自身的類型和路由鍵找到符合條件的隊列時,會調用BasicReturn命令將消息返回給生產者,當mandatory參數設置爲false時,出現上述情況時,消息將被丟棄。生產者如何獲取沒有被正確路由到合適隊列的消息呢?這裏我們可以通過IModel.BasicReturn來實現

{
    ......
    channel.BasicReturn += Channel_BasicReturn;
	channel.BasicPublish("amq.direct", routingKey: "MyRoutKey", mandatory: true, basicProperties: null, body: body);
	......
}   

private static void Channel_BasicReturn(object sender, RabbitMQ.Client.Events.BasicReturnEventArgs e)
{
    ......
}

2、immediate參數

當immediate參數設爲true時,如果交換器在將消息路由到隊列時發現隊列上不存在任何消費者,那麼這條消息將不會存入隊列中。當與路由鍵匹配的所有隊列都沒有消費者時,該消息會通過BasicReturn返回給生產者。即至少將該消息路由到一個隊列中,否則將消息返回給生產者。RabbitMQ3.0版本已經去掉了對該參數的支持

3、備份交換器

生產者哎發送消息時如果不設置mandatory參數,那麼消息會在未被路由的情況下丟失;如果設置了mandatory參數,那麼需要添加BasicReturn的邏輯會變的複雜。使用備份交換器可以將未被路由的消息存儲在RabbitMQ中,在需要的時候再去處理這些消息。在聲明交換器時,可以通過添加alternate-exchange參數來實現,也可以通過策略Policy的方式實現,兩者同時使用前者的優先級更高,會覆蓋掉Policy的設置。

在使用時可以聲明兩個交換器,綁定不同的隊列,如消息不能被正確的路由到一個交換器綁定的隊列時,就會發送給另外一個交換器,進而發送到其綁定的隊列。注意這裏的“備份交換器”的交換器類型未fanout,當然使用其他類型也可以,但建議設置爲fanout

二、過期時間TTL

1、設置消息的TTL

目前有兩種辦法可以設置消息的TTL,第一種辦法是通過隊列的屬性進行設置,隊列中所有的消息都有相同的過期時間;第二種辦法是對消息本身進行單獨的設置,每條消息的TTL可以不同。如果兩者同時使用,消息的TTL以兩者中較小的爲準。消息在隊列中的生存時間一旦超過設置的TTL,就會變爲“死信”。

1、通過隊列屬性設置消息TTL是在channel.queueDeclare方法中加入x-message-ttl參數實現的,其單位是毫秒

var arg = new ConcurrentDictionary<string, object>();
arg.TryAdd("x-message-ttl", 6000);
channel.QueueDeclare(QueueName, false, false, false, arg);

2、針對每條消息設置TTL的方法是在channel.basicPublish方法中加入expiration的屬性參數,單位爲毫秒

var properties = channel.CreateBasicProperties();
properties.DeliveryMode = 2;//持久化消息
properties.Expiration = "5000";
channel.BasicPublish(ExchangeName, RoutingKey, properties, Encoding.UTF8.GetBytes(message));

對於第一種設置隊列TTL屬性的方法,一旦消息過期就會從隊列中抹去,而第二種即使消息過期也不會馬上從隊列中抹去,因爲每條消息是否過期是在即將投遞到消費者之前判斷的。前者所有的過期消息都會在隊列的頭部,所以只需要定期從頭部掃描即可,而後者因爲過期時間不同,如果刪除所有過期消息需要掃描整個隊列,因此在消息投遞前判斷能有效減少性能的損失

2、設置隊列的TTL

通過設置channel.queueDeclare方法中的x-expires參數可以控制隊列被自動刪除前處於未使用狀態的時間,即隊列上沒有任何的消費者,隊列也沒有被重新聲明,並且在過期時間內也未調用過Basic.Get方法。RabbitMQ會確保在過期時間到達後將隊列刪除,但不保障有多及時,RabbitMQ重啓後持久化隊列的過期時間會被重新計算

var arg = new ConcurrentDictionary<string, object>();
arg.TryAdd("x-expires", 6000);
channel.QueueDeclare(QueueName, false, false, false, arg);

三、死信隊列

1、DLX全稱爲Dead-Letter-Exchange,稱爲死信交換器。當消息在一個隊列中變成死信之後,它能被重新發送到另一個交換器中,這個交換器就是DLX,綁定DLX的隊列就稱爲死信隊列,消息變成死信一般是由於以下幾種情況:①消息被拒絕;②消息過期;③隊列到達最大長度

2、DLX和一般的交換器沒有區別,它可以在任何隊列上被指定,實際上就是設置某個隊列的屬性。通過在channel.queueDeclare方法中設置x-dead-letter-exchange參數來爲這個隊列添加DLX。

channel.ExchangeDeclare("dlx_exchange","direct");
var arg = new ConcurrentDictionary<string, object>();
arg.TryAdd("x-dead-letter-exchange", "dlx_exchange");
channel.QueueDeclare(QueueName, false, false, false, arg);

3、也可以爲這個DLX指定路由鍵arg.TryAdd("x-dead-letter-routing-key","dlx-routing-key"),如果沒有特殊指定,則使用原隊列的路由鍵

4、對於RabbitMQ來說,DLX是一個非常有用的特性,它可以處理異常情況下,不能被消費者正確消費而被置於死信隊列中情況,後續可以分析死信隊列中的內容來分析解決異常情況。

四、延遲隊列

1、延遲隊列存儲的對象是對應的延遲消息,即當消息被髮送後,並不像讓消費者立即拿到消息,而是等待特定時間後,消費者才能拿到這個消息進行消費,其應用場景可以對應下單後N分鐘內未支付,智能電器N分鐘後執行工作等情況

2、AMQP協議中並沒有直接支持延遲隊列的功能,但是通過前面介紹的DLX和TTL可以模擬出延遲隊列的功能。生產者可以將消息發送到與交換器綁定的不同隊列中,同時配置DLX和相應的死信隊列,當消息過期時會轉存到相應的死信隊列中,消費者則可以訂閱這些死信隊列(即所謂的延遲隊列)進行消費

五、優先級隊列

1、顧名思義,具有高優先級的隊列具有高的優先權,即具備優先被消費的特權。可以通過x-max-priority參數實現

var arg = new ConcurrentDictionary<string, object>();
arg.TryAdd("x-max-priority", 10);
channel.QueueDeclare(QueueName, false, false, false, arg);

2、同樣可以對消息進行優先級的配置,消息的優先級默認最低爲0,最大爲隊列的最大優先級,如下:

var properties = channel.CreateBasicProperties();
properties.Priority = 5;
channel.BasicPublish(ExchangeName, RoutingKey, properties, Encoding.UTF8.GetBytes(message));

六、RPC實現

1、RPC即Remote Procedure Call,即遠程過程調用,它是一種通過網絡從遠程計算機上請求服務,而不需要了解底層網絡的技術。RPC的主要功能是讓構建分佈式計算更容易,在提供強大的遠程調用能力時不損失本地調用的語義簡潔性。

2、一般在RabbitMQ中進行RPC是很簡單的,客戶端發送請求消息,服務端回覆響應的消息,爲了接受響應的消息,我們需要在請求消息中發送一個回調隊列

var queueName = channel.QueueDeclare().QueueName;
var props = channel.CreateBasicProperties();
props.ReplyTo = queueName;
channel.BasicPublish("", "rpc_queue", props, Encoding.UTF8.GetBytes(message));
//接受返回的消息並進行處理

3、其中replayTo用來設置一個回調隊列;correlationId用來關聯請求和調用RPC之後的回覆。爲每個RPC請求創建一個回調隊列是非常低效的,通常會爲每個客戶端創建一個單一的回調隊列,但是接收到回覆消息後無法對應是哪一個請求,這時候就用到correlationId這個屬性了。此外考慮極端情況,回調隊列可能會收到重複消息的情況,客戶端需要考慮到這種情況並進行相應的處理,並且RPC請求需要保證其本身是冪等的。

示例-客戶端:

using System;
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

namespace Producer
{
    public class Program
    {
        static void Main(string[] args)
        {
            var rpcClient = new RpcClient();

            Console.WriteLine(" [x] Requesting fib(30)");
            var response = rpcClient.Call("30");

            Console.WriteLine(" [.] Got '{0}'", response);
            rpcClient.Close();
        }
    }

    public class RpcClient
    {
        private readonly IConnection _connection;
        private readonly IModel _channel;
        private readonly string _replyQueueName;
        private readonly EventingBasicConsumer _consumer;
        private readonly BlockingCollection<string> _respQueue = new BlockingCollection<string>();
        private readonly IBasicProperties _props;

        public RpcClient()
        {
            var factory = new ConnectionFactory() { HostName = "localhost" };

            _connection = factory.CreateConnection();
            _channel = _connection.CreateModel();
            _replyQueueName = _channel.QueueDeclare().QueueName;
            _consumer = new EventingBasicConsumer(_channel);

            _props = _channel.CreateBasicProperties();
            var correlationId = Guid.NewGuid().ToString();
            _props.CorrelationId = correlationId;
            _props.ReplyTo = _replyQueueName;

            _consumer.Received += (model, ea) =>
            {
                var body = ea.Body.ToArray();
                var response = Encoding.UTF8.GetString(body);
                if (ea.BasicProperties.CorrelationId == correlationId)
                {
                    _respQueue.Add(response);
                }
            };
        }

        public string Call(string message)
        {
            var messageBytes = Encoding.UTF8.GetBytes(message);
            _channel.BasicPublish("", "rpc_queue", _props, messageBytes);
            _channel.BasicConsume(consumer: _consumer, queue: _replyQueueName, autoAck: true);
            return _respQueue.Take();
        }

        public void Close()
        {
            _connection.Close();
        }
    }
}

示例-服務端:

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace Consumer
{
  class Program
  {
      static void Main(string[] args)
      {
          var factory = new ConnectionFactory {HostName = "localhost"};
          using var connection = factory.CreateConnection();
          using var channel = connection.CreateModel();
          channel.QueueDeclare("rpc_queue", false, false, false, null);
          channel.BasicQos(0, 1, false);
          var consumer = new EventingBasicConsumer(channel);
          channel.BasicConsume("rpc_queue", false, consumer);
          Console.WriteLine(" [x] Awaiting RPC requests");

          consumer.Received += (model, ea) =>
          {
              string response = null;

              var body = ea.Body.ToArray();
              var props = ea.BasicProperties;
              var replyProps = channel.CreateBasicProperties();
              replyProps.CorrelationId = props.CorrelationId;

              try
              {
                  var message = Encoding.UTF8.GetString(body);
                  int n = int.Parse(message);
                  Console.WriteLine(" [.] fib({0})", message);
                  response = Fib(n).ToString();
              }
              catch (Exception e)
              {
                  Console.WriteLine(" [.] " + e.Message);
                  response = "";
              }
              finally
              {
                  var responseBytes = Encoding.UTF8.GetBytes(response);
                  channel.BasicPublish("", props.ReplyTo, replyProps, responseBytes);
                  channel.BasicAck(ea.DeliveryTag, false);
              }
          };

          Console.WriteLine(" Press [enter] to exit.");
          Console.ReadLine();
      }

      private static int Fib(int n)
      {
          if (n == 0 || n == 1)
          {
              return n;
          }

          return Fib(n - 1) + Fib(n - 2);
      }
  }
}

七、持久化

1、RabbitMQ的持久化分爲三個部分:

  • 交換器的持久化:通過在聲明交換器時將durable參數設置爲true實現,如未設置交換器元數據會丟失,消息不會丟失,但是不能將消息發送到這個交換器中了,一般建議設置爲持久化
  • 隊列的持久化:通過在聲明隊列時將durable參數設置爲true實現,如未設置隊列的元數據會丟失,消息也會丟失。隊列的持久化可以保證本身的元數據不會因爲異常丟失,但並不能保證內部的消息不丟失,除非將消息的投遞模式中的deliveryMode屬性設置爲2
  • 消息的持久化:可以通過將消息的投遞模式中的deliveryMode屬性設置爲2進行持久化

2、一般來說隊列的持久化和消息的持久化需要配合使用,否則是沒有意義的。但是將所有的消息全部設置爲持久化會嚴重影響RabbitMQ的性能,對於可靠性要求不高的消息可以在可靠性和吞吐量之間做權衡

3、將交換器、隊列和消息設置爲持久化之後並不能保證百分百數據不丟失,如消費者收到消息後未處理就宕機同樣會造成數據的丟失,所以一般情況下將autoAck參數設置爲false,並進行手動確認。另外RabbitMQ並不是爲每條數據進行同步存盤,而是先存在系統緩存中,再調用內核的fsync方法進行批量的存儲,如果在這期間發生宕機同樣會造成消息的丟失。RabbitMQ可以通過鏡像隊列機制爲其配置副本,主節點掛掉後從節點可以自動切換頂上,雖然仍不能保證數據百分比不丟失,但已經相對靠譜的多

八、生產者確認

當消息的生產者將消息發送出去後,再不進行特殊配置的情況下,是無法知道消息是否到達服務器的,如果發生丟失,即使設置了持久化也無法保證數據的到達,針對這種情況,RabbitMQ提供了兩種解決方式:①事務機制;②發送方確認機制

1、事務機制

RabbitMQ中與事務機制相關的方法有三個:TxSelect、TxCommit和TxRollback。TxSelect用於將當前信道設置成事務模式,TxCommit用於提交事務,TxRollback用於事務回滾

開啓事務與不開啓事務相比多了四個步驟:①客戶端發送Tx.Select將信道設置成事務模式;②Broker回覆Tx.Select爲OK確認已將信道設置成事務模式;③發送完消息後,客戶端發送Tx.Commit提交事務;④Broker回覆Tx.Commit爲OK確認提交事務

2、發送方確認機制

採用事務機制會嚴重降低RabbitMQ的消息吞吐量,使用發送方確認機制可以輕量級的實現生產者確認的問題。生產者將信道設置成confirm模式,一旦信道進入confirm模式,所有在該信道上面發佈的消息都會被指派一個唯一ID,一旦消息被投遞到匹配的隊列之後,RabbitMQ就會發送一個確認給生產者(包含唯一ID),使得生產者知曉消息已經正確到達目的地了。如果消息和隊列是持久化的,確認消息會在寫入磁盤後發出。確認消息中的deliveryTag包含了確認消息的序號,而basicAck方法中的multiple參數表示到這個序號之前的所有消息都已得到處理

事務機制會在一條消息發送之後使發送端阻塞,等待RabbitMQ迴應後才能繼續發送下一條消息;而發送方確認機制是異步的,生產者可以同各國回調方法來處理確認消息,即使消息丟失也可以在回調方法中處理nack命令。如果信道沒有開啓confirm模式,則調用任何WaitForConfirms方法都會報錯,對於沒有參數的WaitForConfirms,其返回條件是客戶端收到 了相應的ack/nack或者被中斷

channel.ConfirmSelect();
channel.BasicPublish("ExchangeName","RoutingKey",null, Encoding.UTF8.GetBytes("xxx"));
if (_channel.WaitForConfirms())
{
   Console.WriteLine("發送失敗...");
   //dosomething
}

注:事務機制和confirm機制兩者是互斥的,不能共存。Confirm的優勢在於不一定需要同步確認,可以使用批量Confirm方法;或者使用回調方法實現異步confirm,如可以使用 channel.BasicNacks和channel.BasicAcks來實現,其中參數DeliveryTag即爲confirm模式下用來標記消息的唯一序號

九、消費端要點介紹

消費端可以通過拉模式或者推模式的方法獲取並消費消息,消費者處理完消息後需要手動確認消息已被接受,RabbitMQ才能把消息從隊列中標記清除,如果因爲某些原因無法處理接受到的信息,可以使用channel.BasicNacks或者channel.BasicReject來拒絕掉。對於消費端來說有幾點需要注意:①消息分發;②消息順序性;③棄用QueueingConsumer

1、消息分發

RabbitMQ隊列收到消息後會以輪詢的方式分發給消費者,但如果不同消費者處理消息的能力差異較大,就會造成部分消費者一直處於忙碌狀態,另一部分消費者處於空閒狀態,這個時候我們可以使用channel.basicQos方法來限制信道上消費者所能保持的最大未確認消息的數量。需要注意的是Basic.Qos對拉模式的消費方式是無效的。此外該函數的global參數未false時表示信道上新的消費者需要遵循當前設定的prefetchCount,爲true表示信道上所有的消費者都要遵從當前設定的prefetchCount

如果在訂閱消息之前,即設置了global爲true,又設置了爲false那麼兩者都會生效,即每個消費者只能接收到設定爲false的Qos的prefetchCount,而兩個消費者收到未確認消息的總量不能超過設定爲true的Qos的prefetchCount。一般情況下是不建議這麼設定的,如無特殊需要,一般設定爲false即可

2、消息順序性

消息的順序性是指消費者消費到的消息和發送者發佈的消息順序是一致的。在有多個生產者同時發送消息的情況下是無法確定到達Broker的前後順序的,此外在補償發送和設定了延遲隊列或優先級的情況下,消息的順序性同樣是無法保證的。如果要確保消息的順序性,需要使用進一步的處理,比如添加全局有序標識等

十、消息傳輸保障

1、一般消息中間件的消息傳輸保障分爲三個層級:

  • 最多一次:消息可能會丟失,但絕不會重複傳輸;
  • 最少一次:消息絕不會丟失,但可能會重複傳輸;
  • 恰好一次:每條消息肯定會被傳輸一次且僅傳輸一次;

2、RabbitMQ支持最多一次和最少一次,並且最少一次的投遞實現需要考慮以下幾個方面:

  • 消息生產者需要開啓事務機制或confirm確認機制,確保消息的可靠性傳輸;
  • 消息生產者需要配合使用mandatory參數或者備份交換器來確保消息能夠從交換器路由到隊列中,不會被丟棄;
  • 消息和隊列都需要進行持久化處理,確保服務器在遇到異常情況時不會造成消息的丟失;
  • 消費者在消費消息時需要將autoAck設置爲false,並手動確認避免在消費端引起不必要的消息丟失

3、恰好一次的情況是無法保障的,且大多數消息中間件都沒有去重機制,一般情況下業務方可以根據業務的特性進行去重,或者是確保自身的冪等性,或者結束Redis等產品做去重處理

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