今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题。   (使用.NET 客户端 进行事例演示)
      
    在第一个教程中,我们编写了一个从命名队列中发送和接收消息的程序。在本教程中,我们将创建一个工作队列,这个队列将用于在多个工人之间分配耗时的任务。

工作队列【又名:任务队列】背后主要的思想是避免立刻执行耗时的工作任务,并且一直要等到它结束为止。相反,我们规划任务并晚些执行。我们封装一个任务作为消息发送到一个命名的消息队列中,后台运行的工作线程将获取任务并且最终执行该任务。当你运行很多的任务的时候他们会  共享工作线程和队列。

这个概念在Web应用程序中是尤其有用的,异步执行可以在短时间内处理一个复杂Http请求。

1、准备工作

在本系列教程的前一个教程中,我们发送了一个包含“Hello World!”的消息,现在我们发送一个代表复杂任务的字符串。我们不会创建一个真实的任务,比如对图像文件进行处理或PDF文件的渲染,因此让我们假装我们很忙-通过采用Thread.Sleep()功能来实现复杂和繁忙。我们将根据字符串中的点的数量作为它的复杂性,每一个点将占一秒钟的“工作”。例如,一个假的任务描述Hello…,有三个点,我们就需要三秒。

我们将稍微修改一下我们以前的例子中Send 程序的代码,允许从命令行发送任意消息。这个程序将把任务发送到我们的消息队列中,所以我们叫它NewTask:

像教程一,我们需要生成两个项目。

dotnet new console --name NewTask
mv NewTask/Program.cs NewTask/NewTask.cs
dotnet new console --name Worker
mv Worker/Program.cs Worker/Worker.cs
cd NewTask
dotnet add package RabbitMQ.Client
dotnet restore
cd ../Worker
dotnet add package RabbitMQ.Client
dotnet restore var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message); var properties = channel.CreateBasicProperties();
properties.Persistent = true; channel.BasicPublish(exchange: "",
routingKey: "task_queue",
basicProperties: properties,
body: body);

信息数据我们可以从命令行的参数获得:

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

我们的旧Receive.cs代码也需要一些修改:需要为消息体中每个点都需要消耗一秒钟的工作,先要计算出消息体内有几个点号,然后在乘以1000,就是这个复杂消息所消耗的时间,同时表示这是一个复杂任务。RabbitMQ将处理和发送理消息,并且执行这个任务,让我们拷贝以下代码黏贴到Worker的项目中,并进行相应的修改:

   var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] Received {0}", message); int dots = message.Split('.').Length - 1;
Thread.Sleep(dots * 1000); Console.WriteLine(" [x] Done");
};
channel.BasicConsume(queue: "task_queue", noAck: true, consumer: consumer);

我们自己假设的任务的模拟执行时间就是:

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

2、轮询调度

我们使用任务队列的好处之一就是使任务可以并行化,增加系统的并行处理能力。如果我们正在建立一个积压的工作,我们可以紧紧增加更多的Worker实例就可以完成大量工作的处理,修改和维护就很容易。

首先,让我们同时运行两个Worker实例。他们都会从队列中得到消息,但具体如何?让我想想。

你需要打开三个控制台的应用程序。两个控制台程序将运行Wroker程序。这些控制台程序将是我们的两个消费者C1和C2。

    # shell 1
cd Worker
dotnet run
# => [*] Waiting for messages. To exit press CTRL+C # shell 2
cd Worker
dotnet run
# => [*] Waiting for messages. To exit press CTRL+C

在第三个控制台应用程序中我们将发布新的任务。只要你已经启动了消费者程序,你可以看到一些发布的信息:

   # shell 3
cd NewTask
dotnet run "First message."
dotnet run "Second message.."
dotnet run "Third message..."
dotnet run "Fourth message...."
dotnet run "Fifth message....."

让我们看看交付了什么东西在Workers:

   # shell 1
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'First message.'
# => [x] Received 'Third message...'
# => [x] Received 'Fifth message.....' # shell 2
# => [*] Waiting for messages. To exit press CTRL+C
# => [x] Received 'Second message..'
# => [x] Received 'Fourth message....'

默认情况下,RabbitMQ将会发送每一条消息给序列中每一个消费者。每个消费者都会得到相同数量的信息。这种分发消息的方式叫做轮询。我们尝试这三个或更多的Workers。

3、消息确认

处理一个任务可能需要几秒钟。如果有一个消费者开始了一个长期的任务,并且只做了一部分就发生了异常,你可能想知道到底发生了什么。我们目前的代码,一旦RabbitMQ发送一个消息给客户立即从内存中移除。在这种情况下,如果你关掉了一个Worker,我们将失去它正在处理的信息。我们也将丢失发送给该特定员工但尚未处理的所有信息。

但我们不想失去任何任务。如果一个Worker出现了问题,我们希望把这个任务交给另一个Woker。

为了确保消息不会丢失,RabbitMQ支持消息确认机制。ACK(nowledgement)确认消息是从【消息使用者】发送回来告诉RabbitMQ结果的一种特殊消息,确认消息告诉RabbitMQ指定的接受者已经收到、处理,并且RabbitMQ你可以自由删除它。

如果一个【消费者Consumer】死亡(其通道关闭,连接被关闭,或TCP连接丢失)不会发送ACK,RabbitMQ将会知道这个消息并没有完全处理,将它重新排队。如果有其他用户同时在线,它就会快速地传递到另一个【消费者】。这样你就可以肯定,没有消息丢失,即使【Worker】偶尔死了或者出现问题。

在没有任何消息超时;当【消费者】死亡的时候RabbitMQ会重新发送消息。只要是正常的,即使处理消息需要很长很长的时间也会重发消息给【消费者】。

消息确认的机制默认是打开的。在以前的例子中,我们明确地把它们关闭设置noAck(“没有手动确认”)参数为true。是时候删除这个标志了,并且从Worker发送一个适当确认消息,一旦我们完成了工作任务。

   var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) =>
{
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] Received {0}", message); int dots = message.Split('.').Length - 1;
Thread.Sleep(dots * 1000); Console.WriteLine(" [x] Done"); channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
};
channel.BasicConsume(queue: "task_queue", noAck: false, consumer: consumer);

使用这个代码,我们可以肯定的是,即使你使用Ctrl + C关掉一个正在处理消息的Worker,也不会丢失任何东西。【Worker】被杀死后,未被确认的消息很快就会被退回。

4、忘记确认

忘记调用BasicAck这是一个常见的错误。虽然这是一个简单的错误,但后果是严重的。消息会被退回时,你的客户退出(这可能看起来像是随机的)但是RabbitMQ将会使用更多的内存保存这些任何延迟确认消息。

为了调试这种错误,你可以使用rabbitmqctl打印messages_unacknowledged字段值:sudo rabbitmqctl list_queues name messages_ready messages_unacknowledged

如果是在Window环境下,删除掉sudo字符就可以:rabbitmqctl.bat list_queues name messages_ready messages_unacknowledged

5、持久性的消息

我们已经学会了如何确保即使【消费者】死亡,任务也不会丢失。但是如果RabbitMQ服务器停止了,我们的任务仍然会丢失的。

当RabbitMQ退出或死机会清空队列和消息,除非你告诉它即使宕机也不能丢失任何东西。要确保消息不会丢失,有两件事情我们是必需要做的:我们需要将队列和消息都标记为持久的。

首先,我们需要确保我们RabbitMQ从来都不会损失我们的的队列。为了做到这一点,我们需要声明我们的队列为持久化的:

channel.QueueDeclare(queue: "hello",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

虽然这个命令本身是正确的,它不会起作用在我们目前的设置中。这是因为我们已经定义了一个叫hello的队列,它不是持久化的。RabbitMQ不允许你使用不同的参数重新定义一个已经存在的队列,在任何程序代码中,都试图返回一个错误。但有一个快速的解决方法-让我们声明一个名称不同的队列,例如task_queue:

channel.QueueDeclare(queue: "task_queue",
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);

这行代码QueueDeclare表示队列的声明,创建并打开队列,这个段代码需要应用到【生产者】和【消费者】中。

在这一点上,我们相信,task_queue队列不会丢失任何东西即使RabbitMQ重启了。现在我们要通过设置IbasicProperties.SetPersistent属性值为true来标记我们的消息持久化的。

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

关于消息持久性的注意

将消息标记为持久性并不能完全保证消息不会丢失。虽然该设置告诉RabbitMQ时时刻刻把保存消息到磁盘上,但是这个时间间隔还是有的,当RabbitMQ已经接受信息但并没有保存它,此时还有可能丢失。另外,RabbitMQ不会为每个消息调用fsync(2)--它可能只是保存到缓存并没有真正写入到磁盘。虽然他的持久性保证不强,但它我们简单的任务队列已经足够用了。如果您需要更强的保证,那么您可以使用Publisher Comfirms。

6、公平调度

你可能已经注意到,调度仍然没有像我们期望的那样的工作。例如,在两个Workers的情况下,当所有的奇数消息是沉重的,甚至消息是轻的,一个Worker忙个不停,而另一个Worker几乎没事可做。哎,RabbitMQ对上述情况一无所知,仍将消息均匀发送。

发生这种情况是因为当有消息进入队列的时候RabbitMQ才仅仅调度了消息。它根本不看【消费者】未确认消息的数量,它只是盲目的把第N个消息发送给第N个【消费者】。

为了避免上述情况的发生,我们可以使用prefetchcount = 1的设置来调用BasicQos方法。这个方法告诉RabbitMQ在同一时间不要发送多余一个消息的数据给某个【Worker】。或者,换句话说,当某个消息处理完毕,并且已经收到了消息确认之后,才可以继续发送消息给那个【Worker】。相反,它将把消息分配给给下一个不忙的【Worker】。

channel.BasicQos(0, 1, false);

注意队列大小

如果所有的工人都很忙,你的队列可以填满。你要留意这一点,也许会增加更多的【Worker】,或者有其他的策略。

7、把所有的代码放在一起

NewTask.cs类最终的代码是:

 1 using System;
2 using RabbitMQ.Client;
3 using System.Text;
4
5 class NewTask
6 {
7 public static void Main(string[] args)
8 {
9 var factory = new ConnectionFactory() { HostName = "localhost" };
10 using(var connection = factory.CreateConnection())
11 using(var channel = connection.CreateModel())
12 {
13 channel.QueueDeclare(queue: "task_queue",
14 durable: true,
15 exclusive: false,
16 autoDelete: false,
17 arguments: null);
18
19 var message = GetMessage(args);
20 var body = Encoding.UTF8.GetBytes(message);
21
22 var properties = channel.CreateBasicProperties();
23 properties.Persistent = true;
24
25 channel.BasicPublish(exchange: "",
26 routingKey: "task_queue",
27 basicProperties: properties,
28 body: body);
29 Console.WriteLine(" [x] Sent {0}", message);
30 }
31
32 Console.WriteLine(" Press [enter] to exit.");
33 Console.ReadLine();
34 }
35
36 private static string GetMessage(string[] args)
37 {
38 return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
39 }
40 }

Worker.cs完整源码如下

 1 using System;
2 using RabbitMQ.Client;
3 using RabbitMQ.Client.Events;
4 using System.Text;
5 using System.Threading;
6
7 class Worker
8 {
9 public static void Main()
10 {
11 var factory = new ConnectionFactory() { HostName = "localhost" };
12 using(var connection = factory.CreateConnection())
13 using(var channel = connection.CreateModel())
14 {
15 channel.QueueDeclare(queue: "task_queue",
16 durable: true,
17 exclusive: false,
18 autoDelete: false,
19 arguments: null);
20
21 channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
22
23 Console.WriteLine(" [*] Waiting for messages.");
24
25 var consumer = new EventingBasicConsumer(channel);
26 consumer.Received += (model, ea) =>
27 {
28 var body = ea.Body;
29 var message = Encoding.UTF8.GetString(body);
30 Console.WriteLine(" [x] Received {0}", message);
31
32 int dots = message.Split('.').Length - 1;
33 Thread.Sleep(dots * 1000);
34
35 Console.WriteLine(" [x] Done");
36
37 channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
38 };
39 channel.BasicConsume(queue: "task_queue",
40 noAck: false,
41 consumer: consumer);
42
43 Console.WriteLine(" Press [enter] to exit.");
44 Console.ReadLine();
45 }
46 }
47 }

使用消息确认和BasicQos方法可以建立一个工作队列。持久化的选项可以让我们的任务队列保持存活即使RabbitMQ重启。

好了,写完了,翻译的不好,大家见谅。

原文地址如下:http://www.rabbitmq.com/tutorials/tutorial-two-dotnet.html

欢迎大家来探讨。

天下国家,可均也;爵禄,可辞也;白刃,可蹈也;中庸不可能也

RabbitMQ系列教程之二:工作队列(Work Queues)(转载)的更多相关文章

  1. RabbitMQ系列教程之二:工作队列(Work Queues)

        今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题.   (使用.NET 客户端 进行事例演示)          在第一个教程中,我们编写了一个从命名队列中发送和接收消息的程序. ...

  2. Apache Shiro系列教程之二:十分钟上手Shiro

    在本教程中,我们会写一个简单的.仅仅输出一些内容命令行程序,从而对Shiro有一个大体的感觉. 一.准备工作 本教程需要Java1.5+,并且我们用Maven生成项目,当然Maven不是必须的,你也可 ...

  3. 【前端】CentOS 7 系列教程之二: 安装 git 最新版

    转载请注明出处:http://www.cnblogs.com/shamoyuu/p/linux_2.html 这一篇我们来安装git高版本. 卸载yum安装的旧版本 yum remove git 安装 ...

  4. Kali Linux系列教程之OpenVas安装

    Kali Linux系列教程之OpenVas安装 文 /玄魂 目录 Kali Linux系列教程之OpenVas安装 前言 1.  服务器层组件 2.客户层组件 安装过程 Initial setup ...

  5. WCF系列教程之WCF服务协定

    本文参考自:http://www.cnblogs.com/wangweimutou/p/4422883.html,纯属读书笔记,加深记忆 一.服务协定简介: 1.WCF所有的服务协定层里面的服务接口, ...

  6. WCF系列教程之WCF服务宿主与WCF服务部署

    本文参考自http://www.cnblogs.com/wangweimutou/p/4377062.html,纯属读书笔记,加深记忆. 一.简介 任何一个程序的运行都需要依赖一个确定的进程中,WCF ...

  7. SpringBoot系列教程之Bean加载顺序之错误使用姿势辟谣

    在网上查询 Bean 的加载顺序时,看到了大量的文章中使用@Order注解的方式来控制 bean 的加载顺序,不知道写这些的博文的同学自己有没有实际的验证过,本文希望通过指出这些错误的使用姿势,让观文 ...

  8. RabbitMQ入门教程(四):工作队列(Work Queues)

    原文:RabbitMQ入门教程(四):工作队列(Work Queues) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https:/ ...

  9. kali linux 系列教程之metasploit 连接postgresql可能遇见的问题

    kali linux 系列教程之metasploit 连接postgresql可能遇见的问题 文/玄魂   目录 kali linux 下metasploit 连接postgresql可能遇见的问题. ...

随机推荐

  1. mysql 8.0 错误The server requested authentication method unknown to the client

    mysql 安装了最新版本8.0.11后创建用户并授权后,授权的用户连接数据库提示 The server requested authentication method unknown to the ...

  2. ip route 命令详解

    linux的ip命令和ifconfig类似,但前者功能更强大,并旨在取代后者.使用ip命令,只需一个命令,你就能很轻松地执行一些网络管理任务.ifconfig是net-tools中已被废弃使用的一个命 ...

  3. Java - 34 Java 文档注释

    Java 文档注释 Java只是三种注释方式.前两种分别是// 和/* */,第三种被称作说明注释,它以/** 开始,以 */结束. 说明注释允许你在程序中嵌入关于程序的信息.你可以使用javadoc ...

  4. MySQL ERROR 1300 (HY000): Invalid utf8 character string

    load data报错 原因:原始数据含有 中文\中文 这样的带斜线的字符串. 解决方案:导出的时候替换 replace(d.role_name,'\\','.' ),这样导入时候就不用处理了

  5. python 之 XML的基本应用总结

    1.XML 的特征:xml即可扩展标记语言,它可以用来标记数据.定义数据类型,是一种允许用户对自己的标记语言进行定义的源语言.从结构上,很像HTML超文本标记语言.但他们被设计的目的是不同的,超文本标 ...

  6. Ubuntu下RabbitMQ安装

    由于RabbitMQ需要erlang语言的支持,在安装RabbitMQ之前需要安装erlang,执行命令: sudo apt-get install erlang-nox 安装RabbitMQ命令: ...

  7. WPF圆角按钮与触发颜色变化

    <Button x:Name="button1" Content="按钮1" Margin="10,10,0,0" Cursor=&q ...

  8. c# 值类型与引用类型的传参(形参与实参)

    形参是指被调用方法中的参数 实参是指传递给方法的参数 (1)值类型传参是按值传递 值类型传参,形参接受到的是实参的一个副本,即形参发生变化,实参不会发生任何变化 (2)引用类型参数按值传递 当参数传递 ...

  9. 29.Mongodb可视化工具 Studio 3t

    Studio 3T 链接地址:https://pan.baidu.com/s/1X-Sqk50Xm76NJIZOz0ehFw 密码:td2a 安装配置成功链接本地如图所示:

  10. mysql 外键引发的删除失败

    mysql> TRUNCATE TABLE role ; ERROR 1701 (42000): Cannot truncate a table referenced in a foreign ...