rabbitMQ的使用

 

1.1 Hello World

爲了展示RabbitMQ的基本使用,我們發送一個HelloWorld消息,然後接收並處理。

首先創建一個控制檯程序,用來將消息發送到RabbitMQ的消息隊列中,代碼如下:

static void Main(string[] args)
    {
        var factory = new ConnectionFactory();
        factory.HostName = "localhost";
        factory.UserName = "yy";
        factory.Password = "hello!";

        using (var connection = factory.CreateConnection())
        {
            using (var channel = connection.CreateModel())
            {
                channel.QueueDeclare("hello", false, false, false, null);
                string message = "Hello World";
                var body = Encoding.UTF8.GetBytes(message);
                channel.BasicPublish("", "hello", null, body);
                Console.WriteLine(" set {0}", message);
            }
        }
    }

 

首先,需要創建一個ConnectionFactory,設置目標,由於是在本機,所以設置爲localhost,如果RabbitMQ不在本機,只需要設置目標機器的IP地址或者機器名稱即可,然後設置前面創建的用戶名yy和密碼hello!。

緊接着要創建一個Channel,如果要發送消息,需要創建一個隊列,然後將消息發佈到這個隊列中。在創建隊列的時候,只有RabbitMQ上該隊列不存在,纔會去創建。消息是以二進制數組的形式傳輸的,所以如果消息是實體對象的話,需要序列化和然後轉化爲二進制數組。

現在客戶端發送代碼已經寫好了,運行之後,消息會發布到RabbitMQ的消息隊列中,現在需要編寫服務端的代碼連接到RabbitMQ上去獲取這些消息。

同樣,創建一個名爲Receive的服務端控制檯應用程序,服務端代碼如下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("hello", true, consumer);

            Console.WriteLine(" waiting for message.");
            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine("Received {0}", message);

            }
        }
    }
}

 

和發送一樣,首先需要定義連接,然後聲明消息隊列。要接收消息,需要定義一個Consume,然後從消息隊列中不斷Dequeue消息,然後處理。

現在發送端和接收端的代碼都寫好了,運行發送端,發送消息:

現在,名爲hello的消息隊列中,發送了一條消息。這條消息存儲到了RabbitMQ的服務器上了。使用rabbitmqctl 的list_queues可以查看所有的消息隊列,以及裏面的消息個數,可以看到,目前Rabbitmq上只有一個消息隊列,裏面只有一條消息:

D:\Program Files\RabbitMQ Server\rabbitmq_server-3.4.2\sbin>rabbitmqctl list_queues
Listing queues ...
hello   1

 

現在運行接收端程序,如下:

可以看到,已經接受到了客戶端發送的Hello World,現在再來看RabitMQ上的消息隊列信息:

D:\Program Files\RabbitMQ Server\rabbitmq_server-3.4.2\sbin>rabbitmqctl list_queues
Listing queues ...
hello   0

 

可以看到,hello這個隊列中的消息隊列個數爲0,這表示,當接收端,接收到消息之後,RabbitMQ上就把這個消息刪掉了。

1.2 工作隊列

前面的例子展示瞭如何往一個指定的消息隊列中發送和收取消息。現在我們創建一個工作隊列(work queue)來將一些耗時的任務分發給多個工作者(workers):

工作隊列(work queues, 又稱任務隊列Task Queues)的主要思想是爲了避免立即執行並等待一些佔用大量資源、時間的操作完成。而是把任務(Task)當作消息發送到隊列中,稍後處理。一個運行在後臺的工作者(worker)進程就會取出任務然後處理。當運行多個工作者(workers)時,任務會在它們之間共享。

這個在網絡應用中非常有用,它可以在短暫的HTTP請求中處理一些複雜的任務。在一些實時性要求不太高的地方,我們可以處理完主要操作之後,以消息的方式來處理其他的不緊要的操作,比如寫日誌等等。

準備

在第一部分,發送了一個包含“Hello World!”的字符串消息。現在發送一些字符串,把這些字符串當作複雜的任務。這裏使用time.sleep()函數來模擬耗時的任務。在字符串中加上點號(.)來表示任務的複雜程度,一個點(.)將會耗時1秒鐘。比如"Hello..."就會耗時3秒鐘。

對之前示例的send.cs做些簡單的調整,以便可以發送隨意的消息。這個程序會按照計劃發送任務到我們的工作隊列中。

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);
            string message = GetMessage(args);var properties = channel.CreateBasicProperties();
            properties.DeliveryMode = 2;var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish("", "hello", properties, body);
            Console.WriteLine(" set {0}", message);
        }
    }

    Console.ReadKey();
}

private static string GetMessage(string[] args)
{
    return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
}

 

加粗部分是經過修改過了的。

接着我們修改接收端,讓他根據消息中的逗點的個數來Sleep對應的秒數:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("hello", true, consumer);

            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);

              int dots = message.Split('.').Length - 1;
                Thread.Sleep(dots * 1000);Console.WriteLine("Received {0}", message);
                Console.WriteLine("Done");
            }
        }
    }
}

 

輪詢分發

使用工作隊列的一個好處就是它能夠並行的處理隊列。如果堆積了很多任務,我們只需要添加更多的工作者(workers)就可以了,擴展很簡單。

現在,我們先啓動兩個接收端,等待接受消息,然後啓動一個發送端開始發送消息。

 

在cmd條件下,發送了5條消息,每條消息後面的逗點表示該消息需要執行的時長,來模擬耗時的操作。

然後可以看到,兩個接收端依次接收到了發出的消息:

默認,RabbitMQ會將每個消息按照順序依次分發給下一個消費者。所以每個消費者接收到的消息個數大致是平均的。 這種消息分發的方式稱之爲輪詢(round-robin)。

1.3 消息響應

當處理一個比較耗時得任務的時候,也許想知道消費者(consumers)是否運行到一半就掛掉。在當前的代碼中,當RabbitMQ將消息發送給消費者(consumers)之後,馬上就會將該消息從隊列中移除。此時,如果把處理這個消息的工作者(worker)停掉,正在處理的這條消息就會丟失。同時,所有發送到這個工作者的還沒有處理的消息都會丟失。

我們不想丟失任何任務消息。如果一個工作者(worker)掛掉了,我們希望該消息會重新發送給其他的工作者(worker)。

爲了防止消息丟失,RabbitMQ提供了消息響應(acknowledgments)機制。消費者會通過一個ack(響應),告訴RabbitMQ已經收到並處理了某條消息,然後RabbitMQ纔會釋放並刪除這條消息。

如果消費者(consumer)掛掉了,沒有發送響應,RabbitMQ就會認爲消息沒有被完全處理,然後重新發送給其他消費者(consumer)。這樣,即使工作者(workers)偶爾的掛掉,也不會丟失消息。

消息是沒有超時這個概念的;當工作者與它斷開連的時候,RabbitMQ會重新發送消息。這樣在處理一個耗時非常長的消息任務的時候就不會出問題了。

消息響應默認是開啓的。在之前的例子中使用了no_ack=True標識把它關閉。是時候移除這個標識了,當工作者(worker)完成了任務,就發送一個響應。

channel.BasicConsume("hello", false, consumer);

while (true)
{
    var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

    var body = ea.Body;
    var message = Encoding.UTF8.GetString(body);

    int dots = message.Split('.').Length - 1;
    Thread.Sleep(dots * 1000);

    Console.WriteLine("Received {0}", message);
    Console.WriteLine("Done");

    channel.BasicAck(ea.DeliveryTag, false);
}

 

現在,可以保證,即使正在處理消息的工作者被停掉,這些消息也不會丟失,所有沒有被應答的消息會被重新發送給其他工作者.

一個很常見的錯誤就是忘掉了BasicAck這個方法,這個錯誤很常見,但是後果很嚴重. 當客戶端退出時,待處理的消息就會被重新分發,但是RabitMQ會消耗越來越多的內存,因爲這些沒有被應答的消息不能夠被釋放。調試這種case,可以使用rabbitmqct打印messages_unacknoledged字段。

rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done.

 

1.4 消息持久化

前面已經搞定了即使消費者down掉,任務也不會丟失,但是,如果RabbitMQ Server停掉了,那麼這些消息還是會丟失。

當RabbitMQ Server 關閉或者崩潰,那麼裏面存儲的隊列和消息默認是不會保存下來的。如果要讓RabbitMQ保存住消息,需要在兩個地方同時設置:需要保證隊列和消息都是持久化的。

首先,要保證RabbitMQ不會丟失隊列,所以要做如下設置:

bool durable = true;
channel.QueueDeclare("hello", durable, false, false, null);

 

雖然在語法上是正確的,但是在目前階段是不正確的,因爲我們之前已經定義了一個非持久化的hello隊列。RabbitMQ不允許我們使用不同的參數重新定義一個已經存在的同名隊列,如果這樣做就會報錯。現在,定義另外一個不同名稱的隊列:

bool durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

 

queueDeclare 這個改動需要在發送端和接收端同時設置。

現在保證了task_queue這個消息隊列即使在RabbitMQ Server重啓之後,隊列也不會丟失。 然後需要保證消息也是持久化的, 這可以通過設置IBasicProperties.SetPersistent 爲true來實現:

var properties = channel.CreateBasicProperties();
properties.SetPersistent(true);

 

需要注意的是,將消息設置爲持久化並不能完全保證消息不丟失。雖然他告訴RabbitMQ將消息保存到磁盤上,但是在RabbitMQ接收到消息和將其保存到磁盤上這之間仍然有一個小的時間窗口。 RabbitMQ 可能只是將消息保存到了緩存中,並沒有將其寫入到磁盤上。持久化是不能夠一定保證的,但是對於一個簡單任務隊列來說已經足夠。如果需要消息隊列持久化的強保證,可以使用publisher confirms

1.5 公平分發

你可能會注意到,消息的分發可能並沒有如我們想要的那樣公平分配。比如,對於兩個工作者。當奇數個消息的任務比較重,但是偶數個消息任務比較輕時,奇數個工作者始終處理忙碌狀態,而偶數個工作者始終處理空閒狀態。但是RabbitMQ並不知道這些,他仍然會平均依次的分發消息。

爲了改變這一狀態,我們可以使用basicQos方法,設置perfetchCount=1 。這樣就告訴RabbitMQ 不要在同一時間給一個工作者發送多於1個的消息,或者換句話說。在一個工作者還在處理消息,並且沒有響應消息之前,不要給他分發新的消息。相反,將這條新的消息發送給下一個不那麼忙碌的工作者。

channel.BasicQos(0, 1, false); 

 

1.6 完整實例

現在將所有這些放在一起:

發送端代碼如下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
                   
            bool durable = true;
            channel.QueueDeclare("task_queue", durable, false, false, null);
                    
            string message = GetMessage(args);
            var properties = channel.CreateBasicProperties();
            properties.SetPersistent(true);
                  

            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish("", "task_queue", properties, body);
            Console.WriteLine(" set {0}", message);
        }
    }

    Console.ReadKey();
}

private static string GetMessage(string[] args)
{
    return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
}

 

接收端代碼如下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            bool durable = true;
            channel.QueueDeclare("task_queue", durable, false, false, null);
            channel.BasicQos(0, 1, false);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("task_queue", false, consumer);

            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);

                int dots = message.Split('.').Length - 1;
                Thread.Sleep(dots * 1000);

                Console.WriteLine("Received {0}", message);
                Console.WriteLine("Done");

                channel.BasicAck(ea.DeliveryTag, false);
            }
        }
    }
}

 

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