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 }
- 创建一个连接工厂,设置目标(如果是在本机,所以设置为localhost;如果RabbitMQ不在本机,只需要设置目标机器的IP地址或者机器名称即可),然后设置用户名和密码。
- 创建一个channel。
- 创建队列,将消息发送到队列中。消息是以二进制数组形式传输的,所以如果消息是实体对象,需要序列化然后转化为二进制数组。
- 客户端(消费者)发送代码后,消息会发送到消息队列中。
结果显示:

可在控制台使用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

浙公网安备 33010602011771号