RabbitMQ(三):Hello RabbitMQ

一、概念

RabbitMQ官网介绍:

RabbitMQ是消息代理,用来接收并转发消息。可以把RabbitMQ看作是邮局。将要邮寄的邮件放在邮箱中,确保邮递员会把邮件传递给接收者。

 

二、模型

"P"—producer:生产者。创建消息后发送到队列(queue)中。

queue:队列。消息存储在队列中,队列容量仅受主机的内存和磁盘约束,基本上是一个无限的缓冲区。多个生产者(producers)能够把消息发送给同一个队列,同样,多个消费者(consumers)也能从同一个队列(queue)中获取数据。

“C”—consumers:消费者,一个等待获取消息的程序。

生产者、队列和消费者不必位于同一台机器上。一个应用程序可以是生产者,也可以是消费者。

 

三、RabbitMQ简单使用

编写两个控制台程序,发送单个消息的生产者和接收消息的消费者。引用RabbitMQ.Client.dll

  • 生产者:
 1 //Send.cs
 2         static void Main(string[] args)
 3         {
 4             //实例化连接工厂
 5             var factory = new ConnectionFactory()
 6             {
 7                 HostName = "localhost", //RabbitMQ服务在本地运行
 8                 UserName = "guest", //用户名
 9                 Password = "guest"  //密码
10             };
11             //建立连接
12             using (var connection = factory.CreateConnection())
13             {
14                 //创建信道
15                 using (var channel = connection.CreateModel())
16                 {
17                     //申明一个名为hello的队列
18                     channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
19                     //创建传输的消息内容
20                     string message = "hello rabbitmq";
21                     //构建byte消息数据包
22                     var body = Encoding.UTF8.GetBytes(message);
23                     //发送数据包
24                     channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
25                     Console.WriteLine("Sent: {0}", message);
26                     Console.ReadLine();
27                 }
28             }
29         }
  1. 创建一个连接工厂,设置目标(如果是在本机,所以设置为localhost;如果RabbitMQ不在本机,只需要设置目标机器的IP地址或者机器名称即可),然后设置用户名和密码。
  2. 创建一个channel。
  3. 创建队列,将消息发送到队列中。消息是以二进制数组形式传输的,所以如果消息是实体对象,需要序列化然后转化为二进制数组。
  4. 客户端(消费者)发送代码后,消息会发送到消息队列中。

结果显示:

可在控制台使用rabbitmqctl list_queues来查看所有的消息队列

或者通过web管理界面查看

  • 消费者:
 1 //Receive.cs
 2         static void Main(string[] args)
 3         {
 4             //实例化连接工厂
 5             var factory = new ConnectionFactory()
 6             {
 7                 HostName = "localhost", //RabbitMQ服务在本地运行
 8                 UserName = "guest", //用户名
 9                 Password = "guest"  //密码
10             };
11 
12             //建立连接
13             using (var connection = factory.CreateConnection())
14             {
15                 //创建信道
16                 using (var channel = connection.CreateModel())
17                 {
18                     //申明一个名为hello的队列
19                     channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
20                     //构造消费者实例
21                     var consumer = new EventingBasicConsumer(channel);
22                     //绑定消息接收后的事件委托
23                     consumer.Received += (model, ea) =>
24                     {
25                         var body = ea.Body;
26                         var message = Encoding.UTF8.GetString(body.ToArray());
27                         Console.WriteLine("Received: {0}", message);
28                     };
29                     //启动消费者
30                     channel.BasicConsume(queue: "hello", autoAck: true, consumer: consumer);
31                     Console.ReadLine();
32                 }
33             }
34         }

结果显示:

再查queue中的消息

发现消息的内容再被接收后就被删除了。

可以看出来,生产者和消费者的代码前部分是一样的。主要区别在于生产者(发送端)调用channel.BasicPublish()来发送消息;而消费者(接收端)需要实例化一个EventBasicConsumer实例来进行消息处理。需要注意的是,两者的队列名称需保持一致

 

四、工作队列

下面看一下RabbitMQ的工作队列(Work Queues/Task Queues)。

工作队列的好处就是能并行处理队列,在多个工作人员之间分配任务。如果队列中堆积了很多消息/任务,只需要添加多个工作者就可以了。

例子:启动两个接收端来等待接收消息,再开启一个发送端发送消息。

我们稍微修改上面例子中的Send程序,允许从命令行发送任意命令:

 1  //Send.cs
 2         static void Main(string[] args)
 3         {
 4             var factory = new ConnectionFactory()
 5             {
 6                 HostName = "localhost", 
 7                 UserName = "guest", 
 8                 Password = "guest" 
 9             };
10             using (var connection = factory.CreateConnection())
11             {
12                 using (var channel = connection.CreateModel())
13                 {
14                     channel.QueueDeclare(queue: "task_queue", durable: false, exclusive: false, autoDelete: false, arguments: null);
15                     string message = GetMessage(args);
16                     var body = Encoding.UTF8.GetBytes(message);
17                     var properties = channel.CreateBasicProperties();
18                     properties.Persistent = true;
19 
20                     channel.BasicPublish(exchange: "", routingKey: "task_queue", basicProperties: properties, body: body);
21                     Console.WriteLine("Send {0}", message);
22                 }
23             }
24         }
25 
26         private static string GetMessage(string[] args)
27         {
28             return ((args.Length > 0) ? string.Join(" ", args) : "hello rabbitmq");
29         }

修改接收端,伪造任务的复杂度,根据消息中“.”的个数增加对应的Sleep时间:

 1  //Receive.cs
 2         static void Main(string[] args)
 3         {
 4             var factory = new ConnectionFactory()
 5             {
 6                 HostName = "localhost",
 7                 UserName = "guest",
 8                 Password = "guest"
 9             };
10             using (var connection = factory.CreateConnection())
11             {
12                 using (var channel = connection.CreateModel())
13                 {
14                     channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
15 
16                     var consumer = new EventingBasicConsumer(channel);
17                     consumer.Received += (model, ea) =>
18                     {
19                         var body = ea.Body.ToArray();
20                         var message = Encoding.UTF8.GetString(body);
21                         Console.WriteLine("Received {0}", message);
22 
23                         //模拟执行时间的假任务
24                         int dot = message.Split('.').Length - 1;
25                         Thread.Sleep(dot * 1000);
26 
27                         Console.WriteLine("Done");
28                     };
29                     channel.BasicConsume(queue: "task_queue", autoAck: true, consumer: consumer);
30                     Console.ReadLine();
31                 }
32             }
33         }

先启动两个接收端等待接收消息,然后再启动一个发送端开始发送消息。

通过cmd使用Send发送端发送5条消息,每条消息后“.”的数量代表消耗的时长:

通过两个接收端查看消息的接收:

从图中可以看出,两个接收端按顺序依次接收到消息。第一条消息给接收端A,第二条消息给接收端B,第三条消息给接收端A......以此类推。

所以在默认情况下,RabbitMQ是按顺序依次将每条消息发送给消费者,平均每个消费者将会接收到相同数量的消息。这种分发的方式称为循环(round-robin)。

 

五、消息确认

在上面的例子中,当RabbitMQ将消息发送给消费者,消息就会从队列中移除。如果这时消费者突然挂掉(比如通道关闭、连接关闭、TCP连接丢失等),就会造成消息丢失。

为了防止消息丢失,RabbitMQ提供了消息确认(message acknowledgements)。当消费者成功接收消息并处理完成,会发送一个ack(确认)到RabbitMQ,Rabbit接收到这个信号后就会把这条已处理的消息从队列中删除。如果消费者挂掉了,没有发送ack,RabbitMQ就会明白某个消息没有被正常处理,就会将该消息重新入队。如果同一时间还有其他消费者在线,RabbitMQ会将这条消息重新发送给另一个消费者。

RabbitMQ中没有消息超时的概念,只有当消费者关闭或失去连接时,RabbitMQ才会重新分发消息。

消息确认是默认开启的。

下面通过修改Receive.cs的代码来看消息确认这一机制

 1     var consumer = new EventingBasicConsumer(channel);
 2 
 3     consumer.Received += (model, ea) =>
 4     {
 5         var body = ea.Body;
 6         var message = Encoding.UTF8.GetString(body.ToArray());
 7         Console.WriteLine("Received {0}", message);
 8 
 9         //模拟执行时间的假任务
10         int dot = message.Split('.').Length - 1;
11         Thread.Sleep(dot * 1000);
12 
13         Console.WriteLine("Done");
14         //发送消息确认信号(手动确认)
15         channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
16     };
17     //启动消费者
18     //autoAck: true  自动进行消息确认,当消费者接收到消息后自动发送ack信号,不管消息是否处理完毕
19     //autoAck: false 关闭自动确认,通过调用BasicAck方法手动确认
20     channel.BasicConsume(queue: "task_queue", autoAck: false, consumer: consumer);

主要改动在于将autoAck设置为false,并在消息处理完后调用BasicAck进行手动确认。

由上图可知,消息发送端连续发送五条消息,接收端A接收并处理了第一条消息,接收端B被循环分配到第二条消息,但在处理完成之前接收端B中断了。于是RabbitMQ将被中断的消息2(Second Message)分发给接收端A。

 

 六、消息持久化

消息确认确保在消费端异常时,消息不会丢失并能够被重新分发。但如果是RabbitMQ服务端出现异常,消息依然会丢失。

当RabbitMQ服务端奔溃或者关闭时,它会忘记队列和消息。我们可以通过指定 durable:true 和 Persisent=true,来将队列和消息标记为持久化。

 1     //Send.cs
 2     //申明队列时,指定durable: true对消息进行持久化
 3     channel.QueueDeclare(queue: "task_queue", durable: true, exclusive: false, autoDelete: false, arguments: null);
 4     string message = GetMessage(args);
 5     var body = Encoding.UTF8.GetBytes(message);
 6     //将消息标志为持久性:将IBasicProperties.Persistent设置为true
 7     var properties = channel.CreateBasicProperties();
 8     properties.Persistent = true;
 9 
10     channel.BasicPublish(exchange: "", routingKey: "task_queue", basicProperties: properties, body: body);

 

注释:将消息标记为持久性并不能完全保证消息不会丢失。当它告诉RabbitMQ将消息保存至磁盘,RabbitMQ接收消息并且还没保存时,然后有一个很短的时间间隔,RabbitMQ可能只是将消息保存在缓存中,并没有写入磁盘。所以持久化并不是一定保证的。如果需要消息队列持久化的强保证,可以使用Publisher Confirms

 

 、公平分发

上面提到RabbitMQ时按照循环分发的方式进行消息发送,保证每个消费者收到的消息数量。但是忽略了消费者的闲忙情况。比如有两个消费者,奇数类的任务较繁重,偶数类消息较为轻松,就会出现一个消费者一直处理耗时任务处于阻塞状态,另一个消费者一直处理简单任务处于空闲状态,但是它们接收的任务量却是一致的。

可以通过在消费者端使用BasicQos方法,设置prefetchCount:1,告知RabbitMQ,在未接收到消费者的消息确认时不再分发消息,避免消费者一直处于忙碌状态。

1     //Receive.cs
2     //申明队列
3     channel.QueueDeclare(queue: "task_queue", durable: false, exclusive: false, autoDelete: false, arguments: null);
4     //设置prefetchCount:1告知RabbitMQ,在未接收到消费者的消息确认时不再分发消息
5     channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);

注释:如果是所有的消费者都处于忙碌状态,队列可能会被塞满。需要采取其他方法,比如增加更多的消费者。

 

 

参考:

https://www.rabbitmq.com/getstarted.html

https://stackoverflow.com/questions/61374796/c-sharp-convert-readonlymemorybyte-to-byte

posted @ 2020-10-27 14:32  凌晨4time  阅读(241)  评论(0)    收藏  举报