在上篇文章中,我们解决了从发送端(Producer)向接收端(Consumer)发送“Hello World”的问题。在实际的应用场景中,这是远远不够的。从本篇文章开始,我们将结合更加实际的应用场景来讲解更多的高级用法。

当有Consumer需要大量的运算时,RabbitMQ Server需要一定的分发机制来balance每个Consumer的load。接下来我们分布讲解。

应用场景就是RabbitMQ Server会将queue的Message分发给不同的Consumer以处理计算密集型的任务:

1. Message acknowledgment 消息确认

每个Consumer可能需要一段时间才能处理完收到的数据。如果在这个过程中,Consumer出错了,异常退出了,而数据还没有处理完成,那么 非常不幸,这段数据就丢失了。因为我们采用no-ack的方式进行确认,也就是说,每次Consumer接到数据后,而不管是否处理完 成,RabbitMQ Server会立即把这个Message标记为完成,然后从queue中删除了。

如果一个Consumer异常退出了,它处理的数据能够被另外的Consumer处理,这样数据在这种情况下就不会丢失了(注意是这种情况下)。

为了保证数据不被丢失,RabbitMQ支持消息确认机制,即acknowledgments。为了保证数据能被正确处理而不仅仅是被Consumer收到,那么我们不能采用no-ack。而应该是在处理完数据后发送ack。

在处理数据后发送的ack,就是告诉RabbitMQ数据已经被接收,处理完成,RabbitMQ可以去安全的删除它了。

如果Consumer退出了但是没有发送ack,那么RabbitMQ就会把这个Message发送到下一个Consumer。这样就保证了在Consumer异常退出的情况下数据也不会丢失。

这里并没有用到超时机制。RabbitMQ仅仅通过Consumer的连接中断来确认该Message并没有被正确处理。也就是说,RabbitMQ给了Consumer足够长的时间来做数据处理。

这样即使你通过Ctr-C中断了Recieve.cs,那么Message也不会丢失了,它会被分发到下一个Consumer。

如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于 RabbitMQ会长时间运行,因此这个“内存泄漏”是致命的。去调试这种错误,可以通过一下命令打印un-acked Messages.

2. Round-robin dispatching 循环分发

RabbitMQ的分发机制非常适合扩展,而且它是专门为并发程序设计的。如果现在load加重,那么只需要创建更多的Consumer来进行任务处理即 可。当然了,对于负载还要加大怎么办?我没有遇到过这种情况,那就可以创建多个virtual Host,细化不同的通信类别了。

1、首先开启两个Consumer,即运行两个Recieve.cs。

2、在开启两个Producer,即运行两个Producer.cs。

默认情况下,RabbitMQ 会顺序的分发每个Message。当每个收到ack后,会将该Message删除,然后将下一个Message分发到下一个Consumer。这种分发方式叫做round-robin(优雅分发)。

Producer.cs

 class Program
{
static void Main(string[] args)
{
ConnectionFactory factory = new ConnectionFactory() { HostName = "localhost" };
using (IConnection connection = factory.CreateConnection())
{
using (IModel channel = connection.CreateModel())
{
channel.QueueDeclare("hello", false, false, false, null);
var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message); var properties = channel.CreateBasicProperties();
properties.DeliveryMode = ;//non-persistent (1) or persistent (2)
//channel.TxSelect();
channel.BasicPublish("", "hello", properties, body);
//channel.TxCommit();
}
}
} private static string GetMessage(string[] args)
{
return ((args.Length > ) ? string.Join(" ", args) : "Hello World!");
}
}

Consumer.cs

 //#define demo1
#define demo2
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading;
using System.Threading.Tasks; namespace ReceiveDemo2
{
/// <summary>
/// 一个Send和多个Receive的例子,
/// 还加上了ack的例子.
/// 优雅分发
/// </summary>
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("hello", false, false, false, null);
var consumer = new QueueingBasicConsumer(channel);
#if demo1
channel.BasicConsume("hello", true, consumer);//自动删除消息
#else
channel.BasicConsume("hello", false, consumer);//需要接受方发送ack回执,删除消息
#endif
Console.WriteLine(" [*] Waiting for messages." + "To exit press CTRL+C");
while (true)
{
var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();//挂起的操作
#if demo2
channel.BasicAck(ea.DeliveryTag, false);//与channel.BasicConsume("hello", false, null, consumer);对应
#endif
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] Received {0}", message);
int dots = message.Split('.').Length - ;
Thread.Sleep(dots * );
Console.WriteLine(" [x] Done");
#if demo2
//channel.BasicAck(ea.DeliveryTag, false);//与channel.BasicConsume("hello", false, null, consumer);对应,这句话写道40行和49行运行结果就会不一样.写到这里会发生如果输出[x] Received {0}之后,没有输出 [x] Done之前,CTRL+C结束程序,那么message会自动发给另外一个客户端,当另外一个客户端收到message后,执行完49行的命令之后,服务器会删掉这个message
#endif
}
}
}
}
}
}

3. Message durability消息持久化

在上一节中我们知道了即使Consumer异常退出,Message也不会丢失。但是如果RabbitMQ Server退出呢?软件都有bug,即使RabbitMQ Server是完美毫无bug的(当然这是不可能的,是软件就有bug,没有bug的那不叫软件),它还是有可能退出的:被其它软件影响,或者系统重启 了,系统panic了。。。

为了保证在RabbitMQ退出或者crash了数据仍没有丢失,需要将queue和Message都要持久化。queue的持久化需要在声明时指定durable=True,修改Producer和Consumer的channel.QueueDeclare代码,再次强调,Producer和Consumer都应该去创建这个queue,尽管只有一个地方的创建是真正起作用的:

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

上述语句执行不会有什么错误,但是确得不到我们想要的结果,原因就是RabbitMQ Server已经维护了一个叫hello的queue,那么上述执行不会有任何的作用,也就是hello的任何属性都不会被影响。这一点在上篇文章也讨论过。

那么workaround也很简单,声明一个另外的名字的queue,比如名字定位task_hello,或者通过监控http://localhost:15672/,删除名为“hello”的Queue。

接下来,还需要持久化Message,即在Producer.cs里面Publish的时候指定一个properties,方式如下:

 static void Main(string[] args)
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
bool durable = true;
channel.QueueDeclare("task_queue", durable, false, false, null);//queue的持久化需要在声明时指定durable=True
var message = GetMessage(args);
var body = Encoding.UTF8.GetBytes(message);
var properties = channel.CreateBasicProperties();
properties.SetPersistent(true);//需要持久化Message,即在Publish的时候指定一个properties,
channel.BasicPublish("", "task_hello", properties, body);
}
}
}

关于持久化的进一步讨论:

为了数据不丢失,我们采用了:

  1. 在数据处理结束后发送ack,这样RabbitMQ Server会认为Message Deliver 成功。
  2. 持久化queue,可以防止RabbitMQ Server 重启或者crash引起的数据丢失。
  3. 持久化Message,理由同上。

但是这样能保证数据100%不丢失吗?

答案是否定的。问题就在与RabbitMQ需要时间去把这些信息存到磁盘上,这个time
window虽然短,但是它的确还是有。在这个时间窗口内如果数据没有保存,数据还会丢失。还有另一个原因就是RabbitMQ并不是为每个Message都做fsync:它可能仅仅是把它保存到Cache里,还没来得及保存到物理磁盘上。

因此这个持久化还是有问题。但是对于大多数应用来说,这已经足够了。当然为了保持一致性,你可以把每次的publish放到一个transaction中。这个transaction的实现需要user defined codes。

那么商业系统会做什么呢?一种可能的方案是在系统panic时或者异常重启时或者断电时,应该给各个应用留出时间去flash cache,保证每个应用都能exit gracefully。

4. Fair dispatch 公平分发

你可能也注意到了,分发机制不是那么优雅。默认状态下,RabbitMQ将第n个Message分发给第n个Consumer。当然n是取余后的。它不管Consumer是否还有unacked Message,只是按照这个默认机制进行分发。

那么如果有个Consumer工作比较重,那么就会导致有的Consumer基本没事可做,有的Consumer却是毫无休息的机会。那么,RabbitMQ是如何处理这种问题呢?

通过 BasicQos 方法设置prefetchCount = 1。这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。 设置方法如下:

channel.BasicQos(, , false);

注意,这种方法可能会导致queue满。当然,这种情况下你可能需要添加更多的Consumer,或者创建更多的virtualHost来细化你的设计。

Consumer.cs

 static void Main(string[] args)
{
var factory = new ConnectionFactory() { HostName = "localhost" };
using (var connection = factory.CreateConnection())
{
using (var channel = connection.CreateModel())
{
bool durable = true;
channel.QueueDeclare("task_queue", durable, false, false, null);
channel.BasicQos(, , false);//这样RabbitMQ就会使得每个Consumer在同一个时间点最多处理一个Message。换句话说,在接收到该Consumer的ack前,他它不会将新的Message分发给它。
var consumer = new QueueingBasicConsumer(channel);
channel.BasicConsume("task_hello", false, null, consumer);//需要接受方发送ack回执,删除消息
Console.WriteLine(" [*] Waiting for messages." + "To exit press CTRL+C");
while (true)
{
var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();//挂起的操作
channel.BasicAck(ea.DeliveryTag, false);//与channel.BasicConsume("task_queue", false, null, consumer);对应
var body = ea.Body;
var message = Encoding.UTF8.GetString(body);
Console.WriteLine(" [x] Received {0}", message);
int dots = message.Split('.').Length - ;
Thread.Sleep(dots * );
Console.WriteLine(" [x] Done");
}
}
}
}

转:

http://www.rabbitmq.com/tutorials/tutorial-two-dotnet.html(官网)

http://blog.csdn.net/anzhsoft/article/details/19607841(翻译)

RabbitMQ消息队列(三):任务分发机制[转]的更多相关文章

  1. OpenStack 安装数据库和rabbitmq消息队列 (三)

    一)安装配置数据库 1.1.安装包 # yum install mariadb mariadb-server python2-PyMySQL -y 1.2.配置数据库 # vim /etc/my.cn ...

  2. (转)RabbitMQ消息队列(三):任务分发机制

    在上篇文章中,我们解决了从发送端(Producer)向接收端(Consumer)发送“Hello World”的问题.在实际的应用场景中,这是远远不够的.从本篇文章开始,我们将结合更加实际的应用场景来 ...

  3. RabbitMQ消息队列(三):任务分发机制

    在上篇文章中,我们解决了从发送端(Producer)向接收端(Consumer)发送“Hello World”的问题.在实际的应用场景中,这是远远不够的.从本篇文章开始,我们将结合更加实际的应用场景来 ...

  4. (六)RabbitMQ消息队列-消息任务分发与消息ACK确认机制(PHP版)

    原文:(六)RabbitMQ消息队列-消息任务分发与消息ACK确认机制(PHP版) 在前面一章介绍了在PHP中如何使用RabbitMQ,至此入门的的部分就完成了,我们内心中一定还有很多疑问:如果多个消 ...

  5. (转)RabbitMQ消息队列(九):Publisher的消息确认机制

    在前面的文章中提到了queue和consumer之间的消息确认机制:通过设置ack.那么Publisher能不到知道他post的Message有没有到达queue,甚至更近一步,是否被某个Consum ...

  6. RabbitMQ消息队列(九):Publisher的消息确认机制

    在前面的文章中提到了queue和consumer之间的消息确认机制:通过设置ack.那么Publisher能不到知道他post的Message有没有到达queue,甚至更近一步,是否被某个Consum ...

  7. RabbitMQ消息队列(六):使用主题进行消息分发[转]

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity(严重级别)的log.但是,这也是它之所以叫做简单日志 ...

  8. (八)RabbitMQ消息队列-通过Topic主题模式分发消息

    原文:(八)RabbitMQ消息队列-通过Topic主题模式分发消息 前两章我们讲了RabbitMQ的direct模式和fanout模式,本章介绍topic主题模式的应用.如果对direct模式下通过 ...

  9. (转)RabbitMQ消息队列(六):使用主题进行消息分发

    在上篇文章RabbitMQ消息队列(五):Routing 消息路由 中,我们实现了一个简单的日志系统.Consumer可以监听不同severity的log.但是,这也是它之所以叫做简单日志系统的原因, ...

随机推荐

  1. 用Java操纵HBase数据库(新建表,插入,删除,查找)

    java代码如下: package db.insert; /* * 创建一个students表,并进行相关操作 */ import java.io.IOException; import java.i ...

  2. Apache PHP Mysql 开发环境快速配置

    学习PHP开发要配置各种环境,一般会用到apache作为服务器.Mysql数据库.如何快速的配置环境成为困扰大家的烦恼,之前自己也配过,比较繁琐. 最新发现一款集成安装软件“phpStudy”.真可谓 ...

  3. Python爬虫之selenium爬虫,模拟浏览器爬取天猫信息

    由于工作需要,需要提取到天猫400个指定商品页面中指定的信息,于是有了这个爬虫.这是一个使用 selenium 爬取天猫商品信息的爬虫,虽然功能单一,但是也算是 selenium 爬虫的基本用法了. ...

  4. [leetcode]Valid Number @ Python

    原题地址:http://oj.leetcode.com/problems/valid-number/ 题意:判断输入的字符串是否是合法的数. 解题思路:这题只能用确定有穷状态自动机(DFA)来写会比较 ...

  5. 比较全的OA系统功能模块列表

    如何判断一款协同OA软件,是否智能,是否注重细节,是否足够成熟呢?产品的设计优势.功能特性,需要我们总结,也需要让更多的用户了解.功能到底强在哪里?下文中将给出一个详尽的答案. 软件安装 傻瓜化向导式 ...

  6. 未知高度的图片在div设置垂直居中

    方法一: 该方法是将外部容器的显示模式设置成display:table,img标签外部再嵌套一个span标签,并设置span的显示模式为display:table-cell,这样就可以很方便的使用ve ...

  7. 利用WebClient实现对Http协议的Post和Get对网站进行模拟登陆和浏览

    我们在一些场合经常需要模拟浏览器进行一些操作,比如模拟投票,或者模拟点击,或者Web游戏外挂.而C#中封装好的WebClient可以在某些要求不算搞的场景实现Http的Post和Get.具体请见代码: ...

  8. java获取指定文件夹下的所有文件名

    package com.henu.util; import java.io.File; public class TakeFilePathAndName { public static void ma ...

  9. Angular报错

    报错: Module 'App' is not available! You either misspelled the module name or forgot to load it. If re ...

  10. jquery制作一个简单的轮播

    效果图: 演示地址: http://ae6623.cn/demo/slider/index.html 思路: 利用css的定位属性 left 进行调整图片的显示,每次点击上一页下一页按钮的时候,-图片 ...