RabbitMQ系列教程之二:工作队列(Work Queues)(转载)
今天开始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)(转载)的更多相关文章
- RabbitMQ系列教程之二:工作队列(Work Queues)
今天开始RabbitMQ教程的第二讲,废话不多说,直接进入话题. (使用.NET 客户端 进行事例演示) 在第一个教程中,我们编写了一个从命名队列中发送和接收消息的程序. ...
- Apache Shiro系列教程之二:十分钟上手Shiro
在本教程中,我们会写一个简单的.仅仅输出一些内容命令行程序,从而对Shiro有一个大体的感觉. 一.准备工作 本教程需要Java1.5+,并且我们用Maven生成项目,当然Maven不是必须的,你也可 ...
- 【前端】CentOS 7 系列教程之二: 安装 git 最新版
转载请注明出处:http://www.cnblogs.com/shamoyuu/p/linux_2.html 这一篇我们来安装git高版本. 卸载yum安装的旧版本 yum remove git 安装 ...
- Kali Linux系列教程之OpenVas安装
Kali Linux系列教程之OpenVas安装 文 /玄魂 目录 Kali Linux系列教程之OpenVas安装 前言 1. 服务器层组件 2.客户层组件 安装过程 Initial setup ...
- WCF系列教程之WCF服务协定
本文参考自:http://www.cnblogs.com/wangweimutou/p/4422883.html,纯属读书笔记,加深记忆 一.服务协定简介: 1.WCF所有的服务协定层里面的服务接口, ...
- WCF系列教程之WCF服务宿主与WCF服务部署
本文参考自http://www.cnblogs.com/wangweimutou/p/4377062.html,纯属读书笔记,加深记忆. 一.简介 任何一个程序的运行都需要依赖一个确定的进程中,WCF ...
- SpringBoot系列教程之Bean加载顺序之错误使用姿势辟谣
在网上查询 Bean 的加载顺序时,看到了大量的文章中使用@Order注解的方式来控制 bean 的加载顺序,不知道写这些的博文的同学自己有没有实际的验证过,本文希望通过指出这些错误的使用姿势,让观文 ...
- RabbitMQ入门教程(四):工作队列(Work Queues)
原文:RabbitMQ入门教程(四):工作队列(Work Queues) 版权声明:本文为博主原创文章,遵循CC 4.0 BY-SA版权协议,转载请附上原文出处链接和本声明. 本文链接:https:/ ...
- kali linux 系列教程之metasploit 连接postgresql可能遇见的问题
kali linux 系列教程之metasploit 连接postgresql可能遇见的问题 文/玄魂 目录 kali linux 下metasploit 连接postgresql可能遇见的问题. ...
随机推荐
- 倒数第N个字符串
给定一个完全由小写英文字母组成的字符串等差递增序列,该序列中的每个字符串的长度固定为 L,从 L 个 a 开始,以 1 为步长递增.例如当 L 为 3 时,序列为 { aaa, aab, aac, . ...
- Junit4简单使用
一.Junit4是JAVA语言的单元测试框架,用于编写和运行可重复的测试,可以大大提高效率 1.使用Junit4必须在项目中导入Junit4.har文件 2.新建Junit Test case 勾选s ...
- 第11章 拾遗5:IPv6和IPv4共存技术(3)_NAT-PT技术【全书完】
6.4 NAT-PT (1)NAT-PT和NAT的差别 ①NAT-PT(附带协议转换的网络地址转换)技术秉承NAT技术的思想,但在原理方面大有不同. ②NAT-PT和NAT本质的区别在于应用场合的不同 ...
- WordPress整站轻松开启HTTPS
近两年来HTTPS取代HTTP已经成为大势所趋.早在2014年google Chromium安全团队提议将所有的HTTP协议网站标注为不安全.现在,Chrome浏览器已经开始执行这一标准了.从 Chr ...
- 绘制字母和数字组合的验证码(原生php)
<?php $font = array('font/FZZQJW.TTF','font/STHUPO.TTF');//字体 $str = '689acdefhjkmnpqrtuvwxyACDEF ...
- 软硬链接ln
1:软链接 -- 快捷方式 I:ln -s 文件名 快捷方式的名字 文件名:使用绝对路径 II':目录也可以创建链接 2:硬链接 I: ln 文件名 硬链接的名字
- flask-日料网站搭建-ajax传值+返回json字符串
引言:想使用python的flask框架搭建一个日料网站,主要包含web架构,静态页面,后台系统,交互,今天教大家实现ajax操作,返回json. 本节知识:jquery,json,ajax pyth ...
- JavaScript简单的实例应用
1.显示当前时间: <!doctype> <html> <head> <meta charset='utf-8'> <script type='t ...
- npm降低版本(降级)
博主现在的npm版本是5.8.0 想要还原到原来的3.8.6,执行以下命令: npm install npm@3.8.6 -g
- [python] 初学python,级联菜单输出
#Author:shijt china_map = { "河北": { '石家庄': ['辛集', '正定', '晋州'], '邯郸': ['涉县', '魏县', '磁县'], ' ...