RabbitMQ入门篇:初识AMQP协议与七种消息模式

前言

在之前的文章,我们已经简单了解过RabbitMQ是什么,然后怎么安装,本文主要接着细说如何在代码上实现RabbitMQ的七种消息模式,这里我使用的是.net core为例来具体实现每种模式的玩法。

AMQP协议

我们都知道RabbitMQ是基于AMQP协议的一种消息中间件,那么AMQP协议到底是什么呢?

AMQP全称:Advanced Message Queuing Protocol(高级消息队列协议)

AMQP概述:是具有现代特征的二进制协议。是一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。

一些AMQP涉及的专业术语:

连接(Connection):一个网络连接,比如TCP/IP套接字连接。
会话(Session):端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。
信道(Channel):多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。
客户端(Client):AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。
服务器(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程。也称为“消息代理”。
端点(Peer):AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。
搭档(Partner):当描述两个端点之间的交互过程时,使用术语“搭档”来表示“另一个”端点的简记法。比如我们定义端点A和端点B,当它们进行通信时,端点B是端点A的搭档,端点A是端点B的搭档。
片段集(Assembly):段的有序集合,形成一个逻辑工作单元。
段(Segment):帧的有序集合,形成片段集中一个完整子单元。
帧(Frame):AMQP传输的一个原子单元。一个帧是一个段中的任意分片。
控制(Control):单向指令,AMQP规范假设这些指令的传输是不可靠的。
命令(Command):需要确认的指令,AMQP规范规定这些指令的传输是可靠的。
异常(Exception):在执行一个或者多个命令时可能发生的错误状态。
类(Class):一批用来描述某种特定功能的AMQP命令或者控制。
消息头(Header):描述消息数据属性的一种特殊段。
消息体(Body):包含应用程序数据的一种特殊段。消息体段对于服务器来说完全透明——服务器不能查看或者修改消息体。
消息内容(Content):包含在消息体段中的的消息数据。
交换器(Exchange):服务器中的实体,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
交换器类型(Exchange Type):基于不同路由语义的交换器类。
消息队列(Message Queue):一个命名实体,用来保存消息直到发送给消费者。
绑定器(Binding):消息队列和交换器之间的关联。
绑定器关键字(Binding Key):绑定的名称。一些交换器类型可能使用这个名称作为定义绑定器路由行为的模式。
路由关键字(Routing Key):一个消息头,交换器可以用这个消息头决定如何路由某条消息。
持久存储(Durable):一种服务器资源,当服务器重启时,保存的消息数据不会丢失。
临时存储(Transient):一种服务器资源,当服务器重启时,保存的消息数据会丢失。
持久化(Persistent):服务器将消息保存在可靠磁盘存储中,当服务器重启时,消息不会丢失。
非持久化(Non-Persistent):服务器将消息保存在内存中,当服务器重启时,消息可能丢失。
消费者(Consumer):一个从消息队列中请求消息的客户端应用程序。
生产者(Producer):一个向交换器发布消息的客户端应用程序。
虚拟主机(Virtual Host):一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。客户端应用程序在登录到服务器之后,可以选择一个虚拟主机。

下面在看看AMQP协议模型图:

请添加图片描述

工作原理:

  • 生产者是投递消息的一方,首先连接到Server,建立一个连接,开启一个信道;然后生产者声明交换器和队列,设置相关属性,并通过路由键将交换器和队列进行绑定。同理,消费者也需要进行建立连接,开启信道等操作,便于接收消息。

  • 接着生产者就可以发送消息,发送到服务端中的虚拟主机,虚拟主机中的交换器根据路由键选择路由规则,然后发送到不同的消息队列中,这样订阅了消息队列的消费者就可以获取到消息,进行消费。

RabbitMQ的整体架构

在这里插入图片描述
RabbitMQ中的消息流转过程:

1、生产者(producer)向交换机(exchange)发布消息。创建交换时,必须指定类型。
2、交换机收到消息,现在负责路由消息。交换会根据交换类型考虑不同的消息属性,例如路由密钥。
3、必须从交换创建绑定到队列(queues)。在这种情况下,有两个绑定到来自交换的两个不同队列。交换根据消息属性将消息路由到队列中。
4、消息保留在队列中,直到被消费者处理
5、消费者(consumer)处理消息。

发布与订阅

RabbitMQ 默认使用称为 AMQP 的协议。为了能够与 RabbitMQ 通信,您需要一个理解与 RabbitMQ 相同协议的库。下载您打算用于应用程序的编程语言的客户端库。客户端库是用于编写客户端应用程序的应用程序编程接口 (API)。客户端库有几种方法;在这种情况下,与 RabbitMQ 进行通信。例如,当您连接到 RabbitMQ 代理(使用给定的参数、主机名、端口号等)或声明队列或交换时,应使用这些方法。几乎每种编程语言都有库可供选择。

建立连接和发布消息/使用消息时要遵循的步骤:

1、设置/创建连接对象。需要指定用户名、密码、连接 URL、端口等。调用start 方法 时,将在应用程序和 RabbitMQ 之间建立 TCP 连接 。
2、在 TCP 连接中创建一个通道,然后该连接接口可用于打开一个通道,通过该通道发送和接收消息。
3、声明/创建队列。如果队列不存在,则声明队列将导致它被创建。所有队列都需要先声明后才能使用。
4、设置交换并将队列绑定到订阅者/消费者中的交换。所有交换必须在使用前声明。交换器接受来自生产者应用程序的消息并将它们路由到消息队列。对于要路由到队列的消息,队列必须绑定到交换器。
5、在发布者中:将消息发布到交换中 在订阅者/消费者中:从队列中消费消息。
6、关闭通道和连接。

七种消息模式

简单列队(Hello World)

在这里插入图片描述

  • P(producer/ publisher):是我们的发布者
  • C(consumer):是我们的消费者
  • 中间的框是一个队列(Queue)

工作原理
做最简单的事情,一个生产者对应一个消费者,RabbitMQ相当于一个消息代理,负责将A的消息转发给B

应用场景:短信验证码或邮箱验证码等等、用户通过接收手机验证码进行注册,页面上点击获取验证码后,将验证码放到消息队列,然后短信服务从队列中获取到验证码,并发送给用户。

这里我们简单创建两个控制台项目,一个用于发布者,一个用于消费者

发送端

我们将调用我们的消息发布者(发送者)Send.cs和我们的消息消费者(接收者) Receive.cs。发布者将连接到 RabbitMQ,持续发送消息给消费者。

新建个Send控制台程序项目,然后添加RabbitMQ客户端依赖包(nuget包)

 <ItemGroup>
   <PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
 </ItemGroup>

Program.cs

using System;
using RabbitMQ.Client;
using System.Text;

namespace Send
{
    class Program
    {
        public static void Main()
        {
            //实例化连接工厂
            var factory = new ConnectionFactory() { HostName = "localhost" };
            //建立连接
            using (var connection = factory.CreateConnection())
            //创建信道 
            using (var channel = connection.CreateModel())
            {
                //声明队列
                channel.QueueDeclare(queue: "hello",
                                     durable: false,
                                     exclusive: false,
                                     autoDelete: false,
                                     arguments: null);
                //保持持续输入消息
                while (true)
                {
                    var input = Console.ReadLine();
                    string message = input;
                    var body = Encoding.UTF8.GetBytes(message);
                    //发送数据包
                    channel.BasicPublish(exchange: "",
                     routingKey: "hello",
                     basicProperties: null,
                     body: body);
                    Console.WriteLine(" [x] Sent {0}", message);
                }
            }

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }
}

接收端

至于消费者,它正在监听来自 RabbitMQ 的消息。因此,与发布单个消息的发布者不同,我们将让消费者持续运行以侦听消息并将其打印出来。

新建个Receive控制台程序项目,然后一样添加RabbitMQ客户端依赖包(nuget包)

 <ItemGroup>
   <PackageReference Include="RabbitMQ.Client" Version="6.2.2" />
 </ItemGroup>

Program.cs

using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System;
using System.Text;

namespace Receive
{
    class Program
    {
        public static void Main()
        {
            //实例化连接工厂
            var factory = new ConnectionFactory() { HostName = "localhost" };
            //建立连接
            using (var connection = factory.CreateConnection())
            //创建信道
            using (var channel = connection.CreateModel())
            {
                //申明队列
                channel.QueueDeclare(queue: "hello",
                                     durable: false,
                                     exclusive: false,
                                     autoDelete: false,
                                     arguments: null);
                //构造消费者实例
                var consumer = new EventingBasicConsumer(channel);
                //绑定消息接收后的事件委托
                consumer.Received += (model, ea) =>
                {
                    var body = ea.Body.ToArray();
                    var message = Encoding.UTF8.GetString(body);
                    Console.WriteLine(" [x] Received {0}", message);
                };
                //启动消费者
                channel.BasicConsume(queue: "hello",
                                     autoAck: true,
                                     consumer: consumer);
                Console.WriteLine(" Press [enter] to exit.");
                Console.ReadLine();
            }
        }
    }
}

分别运行发送端与接收端项目,运行效果图如下:
请添加图片描述
可以看出,我们发送出去的消息,已经直接被接收端读取到了,那这里肯定有小伙伴就有疑问了,假如这时候又多了一个接收端怎么办,会不会出现两个消费端都读取到了?
抱着这个疑问,我们接着看下一种消息模型。

工作队列(Work queues)

在这里插入图片描述
工作队列(又名:任务队列)背后的主要思想是避免立即执行资源密集型任务而不得不等待它完成。相反,我们将任务安排在以后完成。我们将任务封装 为消息并将其发送到队列。在后台运行的工作进程将弹出任务并最终执行作业。当您运行许多工作人员时,任务将在他们之间共享。

工作原理
在多个消费者之间分配任务(竞争的消费者模式),一个生产者对应多个消费者,一般适用于执行资源密集型任务,单个消费者处理不过来,需要多个消费者进行处理

应用场景:一个订单的处理需要10s,有多个订单可以同时放到消息队列,然后让多个消费者同时处理,这样就是并行了,而不是单个消费者的串行情况

我们尝试多运行一个接收端的项目,看看效果如何
请添加图片描述
从图中可知,我发送了4条信息,两个消息接收端按顺序被循环分配。
默认情况下,RabbitMQ将按顺序将每条消息发送给下一个消费者。平均每个消费者将获得相同数量的消息。这种分发消息的方式叫做循环(round-robin)。

消息确认
按照我们上面的例子,一旦RabbitMQ将消息发给消费端,它就会立即移除队列。假如这中间发生了什么意外情况,我们都不知道消费端到底有没有完成,就会存在消息丢失的情况。
因此为了确保消息永远不会丢失,RabbitMQ 支持 消息确认。一个 ack(nowledgement) 由消费者发回,告诉 RabbitMQ 一个特定的消息已经被接收、处理并且 RabbitMQ 可以自由地删除它。

如果消费者在没有发送 ack 的情况下挂掉(其通道关闭、连接关闭或 TCP 连接丢失),RabbitMQ 将理解消息未完全处理并将重新排队。如果同时有其他消费者在线,它会迅速将其重新发送给另一个消费者。这样,即使消费端偶尔挂掉,你也可以确保不会丢失任何消息。

然后我们调整下接收端的代码,主要改动的是将 autoAck:true修改为autoAck:fasle,以及在消息处理完毕后手动调用BasicAck方法进行手动消息确认。

Program.cs

public static void Main()
{
    //实例化连接工厂
    var factory = new ConnectionFactory() { HostName = "localhost" };
    //建立连接
    using (var connection = factory.CreateConnection())
    //创建信道
    using (var channel = connection.CreateModel())
    {
        //声明队列
        channel.QueueDeclare(queue: "hello",
                             durable: false,
                             exclusive: false,
                             autoDelete: false,
                             arguments: null);
        //构造消费者实例
        var consumer = new EventingBasicConsumer(channel);
        //绑定消息接收后的事件委托
        consumer.Received += (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var message = Encoding.UTF8.GetString(body);
            Console.WriteLine(" [x] Received {0}", message);
            Thread.Sleep(6000);//模拟耗时
            Console.WriteLine(" [x] Done");
            //发送消息确认信号(手动消息确认)
            channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
        };
        //启动消费者
        /*
         * autoAck:true;自动进行消息确认,当消费端接收到消息后,就自动发送ack信号,不管消息是否正确处理完毕
         * autoAck:false;关闭自动消息确认,通过调用BasicAck方法手动进行消息确认
         */
        channel.BasicConsume(queue: "hello",
                             autoAck: false,
                             consumer: consumer);
        Console.WriteLine(" Press [enter] to exit.");

        Console.ReadLine();
    }
}

发布订阅(Publish/Subscribe)

交换机(Exchange)

在讲发布/订阅模式之前,我们要聊一聊什么是交换机,为什么要有交换机?

在上面的例子,生产者和消费者直接是通过相同队列名称进行匹配。消费者订阅某个队列,生产者创建消息发布到队列中,队列再将消息转发到订阅的消费者。这样就会有一个局限性,即消费者一次只能发送消息到某一个队列。

那消费者如何才能发送消息到多个消息队列呢?
RabbitMQ提供了Exchange,它类似于路由器的功能,它用于对消息进行路由,将消息发送到多个队列上。Exchange一方面从生产者接收消息,另一方面将消息推送到队列。但exchange必须知道如何处理接收到的消息,是将其附加到特定队列还是附加到多个队列,还是直接忽略。而这些规则由exchange type定义,exchange的原理如下图所示。

在这里插入图片描述
常见的exchange type 有以下几种:

  • direct(明确的路由规则:消费端绑定的队列名称必须和消息发布时指定的路由名称一致)
  • topic (模式匹配的路由规则:支持通配符)
  • fanout (消息广播,将消息分发到exchange上绑定的所有队列上)

工作原理
Pulish/Subscribe,无选择接收消息,一个消息生产者,一个交换机(交换机类型为fanout),多个消息队列,多个消费者。称为发布/订阅模式

应用场景:用户添加了一件商品信息,这时候可能需要同时去更新多个缓存跟数据库

发送端

public static void Main()
{
    //实例化连接工厂
    var factory = new ConnectionFactory() { HostName = "localhost" };
    //建立连接
    using (var connection = factory.CreateConnection())
    //创建信道 
    using (var channel = connection.CreateModel())
    {
        string exchangeName = "fanout_exchange";
        //创建交换机,fanout类型
        channel.ExchangeDeclare(exchangeName, ExchangeType.Fanout);
        string queueName1 = "fanout_queue1";
        string queueName2 = "fanout_queue2";
        string queueName3 = "fanout_queue3";
        //创建队列
        channel.QueueDeclare(queueName1, false, false, false);
        channel.QueueDeclare(queueName2, false, false, false);
        channel.QueueDeclare(queueName3, false, false, false);

        //把创建的队列绑定交换机,routingKey不用给值,给了也没意义的
        channel.QueueBind(queue: queueName1, exchange: exchangeName, routingKey: "");
        channel.QueueBind(queue: queueName2, exchange: exchangeName, routingKey: "");
        channel.QueueBind(queue: queueName3, exchange: exchangeName, routingKey: "");
        var properties = channel.CreateBasicProperties();
        properties.Persistent = true; //消息持久化
        //向交换机写10条消息
        for (int i = 0; i < 10; i++)
        {
            string message = $"RabbitMQ Fanout {i + 1} Message";
            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish(exchangeName, routingKey: "", null, body);
            Console.WriteLine($"发送Fanout消息:{message}");
        }
    
    }
}

运行效果
在这里插入图片描述
由此可见,向交换机发送10条消息,则绑定这个交换机的3个队列都会生成10条消息。

消费端的代码和工作队列的一样,只需知道队列名即可消费,声明时要和生产者的声明一样。

路由模式(Routing)

在这里插入图片描述
工作原理
在发布/订阅模式的基础上,有选择的接收消息,此模式也就是 Exchange 模式中的direct模式。也就是发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key,仅消费指定路由key的消息

应用场景:用户新增一个促销商品A,商品A促销活动消费者指定routing key为A,这时候只有此促销活动消费者会接收到消息,其它商品促销活动消费者不关心也不会消费此routing key的消息

发送端

public static void Main()
{
     //实例化连接工厂
     var factory = new ConnectionFactory() { HostName = "localhost" };
     //建立连接
     using (var connection = factory.CreateConnection())
     //创建信道 
     using (var channel = connection.CreateModel())
     {
         //声明交换机对象,fanout类型
         string exchangeName = "direct_exchange";
         channel.ExchangeDeclare(exchangeName, ExchangeType.Direct);
         //创建队列
         string queueName1 = "direct_errorlog";
         string queueName2 = "direct_alllog";
         channel.QueueDeclare(queueName1, true, false, false);
         channel.QueueDeclare(queueName2, true, false, false);

         //把创建的队列绑定交换机,direct_errorlog队列只绑定routingKey:error
         channel.QueueBind(queue: queueName1, exchange: exchangeName, routingKey: "error");
         //direct_alllog队列绑定routingKey:error,info
         channel.QueueBind(queue: queueName2, exchange: exchangeName, routingKey: "info");
         channel.QueueBind(queue: queueName2, exchange: exchangeName, routingKey: "error");
         var properties = channel.CreateBasicProperties();
         properties.Persistent = true; //消息持久化
         //向交换机写10条错误日志和10条Info日志
         for (int i = 0; i < 10; i++)
         {
             string message = $"RabbitMQ Direct {i + 1} error Message";
             var body = Encoding.UTF8.GetBytes(message);
             channel.BasicPublish(exchangeName, routingKey: "error", properties, body);
             Console.WriteLine($"发送Direct消息error:{message}");

             string message2 = $"RabbitMQ Direct {i + 1} info Message";
             var body2 = Encoding.UTF8.GetBytes(message);
             channel.BasicPublish(exchangeName, routingKey: "info", properties, body2);
             Console.WriteLine($"info:{message2}");

         }
     }
}

运行效果
在这里插入图片描述
查看RabbitMQ管理界面,direct_errorlog队列10条,而direct_alllog有20条,因为direct_alllog队列两个routingKey的消息都进去了。点击direct_alllog队列里面去看,可以看到它有两个routingKey,分别为error和info
在这里插入图片描述
接收端
消费者和工作队列一样,只需根据队列名消费即可,这里只消费direct_errorlog队列作示例

string queueName = "direct_errorlog";

//实例化连接工厂
var factory = new ConnectionFactory() { HostName = "localhost" };
//建立连接
using (var connection = factory.CreateConnection())
//创建信道
using (var channel = connection.CreateModel())
{
    //创建队列
    channel.QueueDeclare(queueName, durable: true, exclusive: false, autoDelete: false, arguments: null);
    Console.WriteLine(" [*] Waiting for messages.");
    //构造消费者实例
    var consumer = new EventingBasicConsumer(channel);
    consumer.Received += (model, ea) =>
    {
        var body = ea.Body.ToArray();
        var message = Encoding.UTF8.GetString(body);
        var routingKey = ea.RoutingKey;
        Console.WriteLine(" [x] Received '{0}':'{1}'",
                          routingKey, message);
    };
    //启动消费者
    /*
     * autoAck:true;自动进行消息确认,当消费端接收到消息后,就自动发送ack信号,不管消息是否正确处理完毕
     * autoAck:false;关闭自动消息确认,通过调用BasicAck方法手动进行消息确认
     */
    channel.BasicConsume(queue: queueName,
                         autoAck: true,
                         consumer: consumer);

    Console.WriteLine(" Press [enter] to exit.");
    Console.ReadLine();
}

运行效果
在这里插入图片描述

主题模式(Topics)

在这里插入图片描述
工作原理
路由模式的一种,算是路由模式的升级版,指定exchange类型为topic,路由功能添加了模糊匹配。星号(*)代表1个单词,#号(#)代表一个或多个单词。单词之间用.分割。

以上图为例:
如果发送消息的routingKey设置为:
aaa.orange.rabbit,那么消息会路由到Q1与Q2,
routingKey=aaa.orange.bb的消息会路由到Q1,
routingKey=lazy.aa.bb.cc的消息会路由到Q2;
routingKey=lazy.aa.rabbit的消息会路由到 Q2(只会投递给Q2一次,虽然这个routingKey 与 Q2 的两个 bindingKey 都匹配);
没匹配routingKey的消息将会被丢弃。

应用场景:跟上面的场景有点像,比如这个A商品是华为P10,那华为P系列的促销活动就可以接收到P10、P11等等的消息

发送端

//实例化连接工厂
var factory = new ConnectionFactory() { HostName = "localhost" };
//建立连接
using (var connection = factory.CreateConnection())
//创建信道 
using (var channel = connection.CreateModel())
{
    //声明交换机对象,topic类型
    string exchangeName = "topic_exchange";
    channel.ExchangeDeclare(exchangeName, ExchangeType.Topic);
    //队列名
    string queueName1 = "topic_queue1";
    string queueName2 = "topic_queue2";
    //路由名
    string routingKey1 = "*.orange.*";
    string routingKey2 = "*.*.rabbit";
    string routingKey3 = "lazy.#";
    channel.QueueDeclare(queueName1, true, false, false);
    channel.QueueDeclare(queueName2, true, false, false);

    //把创建的队列绑定交换机,routingKey指定routingKey
    channel.QueueBind(queue: queueName1, exchange: exchangeName, routingKey: routingKey1);
    channel.QueueBind(queue: queueName2, exchange: exchangeName, routingKey: routingKey2);
    channel.QueueBind(queue: queueName2, exchange: exchangeName, routingKey: routingKey3);
    //向交换机写10条消息
    for (int i = 0; i < 10; i++)
    {
        string message = $"RabbitMQ Direct {i + 1} Message";
        var body = Encoding.UTF8.GetBytes(message);
        channel.BasicPublish(exchangeName, routingKey: "aaa.orange.rabbit", null, body);
        channel.BasicPublish(exchangeName, routingKey: "lazy.aa.rabbit", null, body);
        Console.WriteLine($"发送Topic消息:{message}");
    }
}

这里演示了 routingKey为aaa.orange.rabbit,和lazy.aa.rabbit的情况,第一个匹配到Q1和Q2,第二个匹配到Q2,所以应该Q1是10条,Q2有20条,
运行效果
在这里插入图片描述

远程过程调用(RPC)

在这里插入图片描述
如果我们需要在远程计算机上运行功能并等待结果就可以使用RPC,具体流程可以看图。
应用场景:需要等待接口返回数据,如订单支付

客户端C声明一个排他队列自己订阅,然后发送消息到RPC队列同时也把这个排他队列名也在消息里传进去,服务端监听RPC队列,处理完业务后把处理结果发送到这个排他队列,然后客户端收到结果,继续处理自己的逻辑。

RPC的处理流程:

1、当客户端启动时,创建一个匿名的回调队列。
2、客户端为RPC请求设置2个属性:replyTo:设置回调队列名字;correlationId:标记request。
3、请求被发送到rpc_queue队列中。
4、RPC服务器端监听rpc_queue队列中的请求,当请求到来时,服务器端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。
5、客户端监听回调队列,当有消息时,检查correlationId属性,如果与request中匹配,那就是结果了。

RPC服务端

using System;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;

class RPCServer
{
    public static void Main()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };
        using (var connection = factory.CreateConnection())
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare(queue: "rpc_queue", durable: false,
              exclusive: false, autoDelete: false, arguments: null);
            channel.BasicQos(0, 1, false);
            var consumer = new EventingBasicConsumer(channel);
            channel.BasicConsume(queue: "rpc_queue",
              autoAck: false, consumer: consumer);
            Console.WriteLine(" [x] Awaiting RPC requests");

            consumer.Received += (model, ea) =>
            {
                string response = null;

                var body = ea.Body.ToArray();
                var props = ea.BasicProperties;
                var replyProps = channel.CreateBasicProperties();
                replyProps.CorrelationId = props.CorrelationId;

                try
                {
                    var message = Encoding.UTF8.GetString(body);
                    int n = int.Parse(message);
                    Console.WriteLine(" [.] fib({0})", message);
                    response = fib(n).ToString();
                }
                catch (Exception e)
                {
                    Console.WriteLine(" [.] " + e.Message);
                    response = "";
                }
                finally
                {
                    var responseBytes = Encoding.UTF8.GetBytes(response);
                    channel.BasicPublish(exchange: "", routingKey: props.ReplyTo,
                      basicProperties: replyProps, body: responseBytes);
                    channel.BasicAck(deliveryTag: ea.DeliveryTag,
                      multiple: false);
                }
            };

            Console.WriteLine(" Press [enter] to exit.");
            Console.ReadLine();
        }
    }

    /// 

    /// Assumes only valid positive integer input.
    /// Don't expect this one to work for big numbers, and it's
    /// probably the slowest recursive implementation possible.
    /// 

    private static int fib(int n)
    {
        if (n == 0 || n == 1)
        {
            return n;
        }

        return fib(n - 1) + fib(n - 2);
    }
}

RPC客户端

using System;
using System.Collections.Concurrent;
using System.Text;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;

public class RpcClient
{
    private readonly IConnection connection;
    private readonly IModel channel;
    private readonly string replyQueueName;
    private readonly EventingBasicConsumer consumer;
    private readonly BlockingCollection<string> respQueue = new BlockingCollection<string>();
    private readonly IBasicProperties props;

    public RpcClient()
    {
        var factory = new ConnectionFactory() { HostName = "localhost" };

        connection = factory.CreateConnection();
        channel = connection.CreateModel();
        replyQueueName = channel.QueueDeclare().QueueName;
        consumer = new EventingBasicConsumer(channel);

        props = channel.CreateBasicProperties();
        var correlationId = Guid.NewGuid().ToString();
        props.CorrelationId = correlationId;//给消息id
        props.ReplyTo = replyQueueName;//回调的队列名,Client关闭后会自动删除
        //监听的消息Id和定义的消息Id相同代表这条消息服务端处理完成
        consumer.Received += (model, ea) =>
        {
            var body = ea.Body.ToArray();
            var response = Encoding.UTF8.GetString(body);
            if (ea.BasicProperties.CorrelationId == correlationId)
            {
                respQueue.Add(response);
            }
        };

        channel.BasicConsume(
            consumer: consumer,
            queue: replyQueueName,
            autoAck: true);
    }

    public string Call(string message)
    {
        var messageBytes = Encoding.UTF8.GetBytes(message);
        //发送消息
        channel.BasicPublish(
            exchange: "",
            routingKey: "rpc_queue",
            basicProperties: props,
            body: messageBytes);
        //等待回复
        return respQueue.Take();
    }

    public void Close()
    {
        connection.Close();
    }
}

public class Rpc
{
    public static void Main()
    {
        var rpcClient = new RpcClient();

        Console.WriteLine(" [x] Requesting fib(30)");
        //向服务端发送消息,等待回复
        var response = rpcClient.Call("30");

        Console.WriteLine(" [.] Got '{0}'", response);
        rpcClient.Close();
    }
}

运行效果
在这里插入图片描述

发布者确认(Publisher Confirms)

工作原理
与发布者进行可靠的发布确认,发布者确认是RabbitMQ扩展,可以实现可靠的发布。在通道上启用发布者确认后,RabbitMQ将异步确认发送者发布的消息,这意味着它们已在服务器端处理
应用场景:对于消息可靠性要求较高,比如钱包扣款

在Channel上启动发布者确认
发布者确认是 AMQP 0.9.1 协议的 RabbitMQ 扩展,因此默认情况下不启用它们。使用ConfirmSelect方法在通道级别启用发布者确认

var channel = connection.CreateModel();
channel.ConfirmSelect();

开启发布者确认机制的方法只使用一次,不需要每条发送的消息都使用

策略一:单独发布消息确认
让我们从使用confirms发布消息的最简单方法开始,即发布消息并同步等待消息的确认

while (ThereAreMessagesToPublish())
{
    byte[] body = ...;
    IBasicProperties properties = ...;
    channel.BasicPublish(exchange, queue, properties, body);
    // uses a 5 second timeout
    channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));
}

在前面的示例中,我们像往常一样发布一条消息,并使用Channel#WaitForConfirmsOrDie(TimeSpan)方法等待其确认。该方法在消息被确认后立即返回。如果消息在超时时间内没有得到确认,或者它被 nack-ed(意味着代理由于某种原因无法处理它),该方法将抛出异常。异常的处理通常包括记录错误消息和/或重试发送消息。

不同的客户端库有不同的方式来同步处理发布者确认,因此请务必仔细阅读您正在使用的客户端的文档。

这种技术非常简单,但也有一个主要缺点:它显着减慢了发布速度,因为消息的确认会阻止所有后续消息的发布。这种方法不会提供超过每秒数百条已发布消息的吞吐量。尽管如此,这对于某些应用程序来说已经足够了。

策略二:批量发布消息
为了改进我们之前的示例,我们可以发布一批消息并等待整个批次得到确认。以下示例使用 100 个批次:

var batchSize = 100;
var outstandingMessageCount = 0;
while (ThereAreMessagesToPublish())
{
    byte[] body = ...;
    IBasicProperties properties = ...;
    channel.BasicPublish(exchange, queue, properties, body);
    outstandingMessageCount++;
    if (outstandingMessageCount == batchSize)
    {
        channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));
        outstandingMessageCount = 0;
    }
}
if (outstandingMessageCount > 0)
{
    channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));
}

与等待单个消息的确认相比,等待一批消息被确认大大提高了吞吐量(使用远程 RabbitMQ 节点最多 20-30 次)。一个缺点是我们不知道在失败的情况下到底出了什么问题,所以我们可能必须在内存中保留一整批来记录有意义的东西或重新发布消息。而且这个方案还是同步的,所以会阻塞消息的发布。

策略三:异步处理发布者确认
代理以异步方式确认发布的消息,只需在客户端注册一个回调即可收到这些确认的通知:

var channel = connection.CreateModel();
channel.ConfirmSelect();
channel.BasicAcks += (sender, ea) =>
{
  // code when message is confirmed
};
channel.BasicNacks += (sender, ea) =>
{
  //code when message is nack-ed
};

有 2 种回调:一种用于确认消息,另一种用于 nack-ed 消息(可被代理视为丢失的消息)。两个回调都有一个对应的EventArgs参数 ( ea ),其中包含:

delivery tag:标识已确认或未确认消息的序列号。我们将很快看到如何将它与发布的消息关联起来。
multiple:这是一个布尔值。如果为假,则仅确认/确认一条消息,如果为真,则确认/确认所有具有较低或相等序列号的消息。

在发布消息前,可以通过Channel中的NextPublishSeqNo去获取消息的序列号:

var sequenceNumber = channel.NextPublishSeqNo;
channel.BasicPublish(exchange, queue, properties, body);

将消息与序列号相关联的一种简单方法是使用字典。假设我们想要发布字符串,因为它们很容易变成一个字节数组来发布。下面是一个代码示例,它使用字典将发布序列号与消息的字符串正文相关联:

var outstandingConfirms = new ConcurrentDictionary<ulong, string>();
// ... code for confirm callbacks will come later
var body = "...";
outstandingConfirms.TryAdd(channel.NextPublishSeqNo, body);
channel.BasicPublish(exchange, queue, properties, Encoding.UTF8.GetBytes(body));

发布代码现在使用字典跟踪出站消息。我们需要在确认到达时清理这个字典,并在消息被 nack 时记录警告:

var outstandingConfirms = new ConcurrentDictionary<ulong, string>();

void cleanOutstandingConfirms(ulong sequenceNumber, bool multiple)
{
    if (multiple)
    {
        var confirmed = outstandingConfirms.Where(k => k.Key <= sequenceNumber);
        foreach (var entry in confirmed)
        {
            outstandingConfirms.TryRemove(entry.Key, out _);
        }
    }
    else
    {
        outstandingConfirms.TryRemove(sequenceNumber, out _);
    }
}

channel.BasicAcks += (sender, ea) => cleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
channel.BasicNacks += (sender, ea) =>
{
    outstandingConfirms.TryGetValue(ea.DeliveryTag, out string body);
    Console.WriteLine($"Message with body {body} has been nack-ed. Sequence number: {ea.DeliveryTag}, multiple: {ea.Multiple}");
    cleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
};

// ... publishing code

前面的示例包含一个回调,当确认到达时清理字典。请注意,此回调处理单个和多个确认。当确认到达时使用此回调(Channel#BasicAcks)。nack-ed 消息的回调检索消息正文并发出警告。然后它重新使用之前的回调来清理未完成确认的字典(无论消息是确认还是 nack-ed,都必须删除它们在字典中的相应条目。)

综上所述,异步处理发布者确认通常需要以下步骤:

  • 提供一种将发布序列号与消息相关联的方法。
  • 在通道上注册确认侦听器,以便在发布者确认 / 确认到达以执行适当的操作时收到通知,例如记录或重新发布已确认的消息。在此步骤中,序列号与消息的关联机制也可能需要进行一些清理。
  • 在发布消息之前跟踪发布序列号。

概括
在某些应用程序中,确保已发布的消息发送到代理可能是必不可少的。发布者确认是有助于满足此要求的 RabbitMQ 功能。发布者确认本质上是异步的,但也可以同步处理它们。没有明确的方法来实现发布者确认,这通常归结为应用程序和整个系统中的约束。典型的技术是:

  • 单独发布消息,同步等待确认:简单,但吞吐量非常有限。
  • 批量发布消息,批量同步等待确认:简单,合理的吞吐量,但当出现问题时很难推理。
  • 异步处理:最好的性能和资源使用,在错误的情况下很好的控制,但可以参与正确实现。

代码整合

using RabbitMQ.Client;
using System;
using System.Collections.Concurrent;
using System.Diagnostics;
using System.Text;
using System.Linq;
using System.Threading;

class PublisherConfirms
{
    private const int MESSAGE_COUNT = 50_000;

        public static void Main()
        {
            PublishMessagesIndividually();
            PublishMessagesInBatch();
            HandlePublishConfirmsAsynchronously();
        }

        private static IConnection CreateConnection()
        {
            var factory = new ConnectionFactory { HostName = "localhost" };
            return factory.CreateConnection();
        }

        private static void PublishMessagesIndividually()
        {
            using (var connection = CreateConnection())
            using (var channel = connection.CreateModel())
            {
                // declare a server-named queue
                var queueName = channel.QueueDeclare(queue: "").QueueName;
                channel.ConfirmSelect();

                var timer = new Stopwatch();
                timer.Start();
                for (int i = 0; i < MESSAGE_COUNT; i++)
                {
                    var body = Encoding.UTF8.GetBytes(i.ToString());
                    channel.BasicPublish(exchange: "", routingKey: queueName, basicProperties: null, body: body);
                    channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));
                }
                timer.Stop();
                Console.WriteLine($"Published {MESSAGE_COUNT:N0} messages individually in {timer.ElapsedMilliseconds:N0} ms");
            }
        }

        private static void PublishMessagesInBatch()
        {
            using (var connection = CreateConnection())
            using (var channel = connection.CreateModel())
            {
                // declare a server-named queue
                var queueName = channel.QueueDeclare(queue: "").QueueName;
                channel.ConfirmSelect();

                var batchSize = 100;
                var outstandingMessageCount = 0;
                var timer = new Stopwatch();
                timer.Start();
                for (int i = 0; i < MESSAGE_COUNT; i++)
                {
                    var body = Encoding.UTF8.GetBytes(i.ToString());
                    channel.BasicPublish(exchange: "", routingKey: queueName, basicProperties: null, body: body);
                    outstandingMessageCount++;

                    if (outstandingMessageCount == batchSize)
                    {
                        channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));
                        outstandingMessageCount = 0;
                    }
                }

                if (outstandingMessageCount > 0)
                    channel.WaitForConfirmsOrDie(new TimeSpan(0, 0, 5));

                timer.Stop();
                Console.WriteLine($"Published {MESSAGE_COUNT:N0} messages in batch in {timer.ElapsedMilliseconds:N0} ms");
            }
        }

        private static void HandlePublishConfirmsAsynchronously()
        {
            using (var connection = CreateConnection())
            using (var channel = connection.CreateModel())
            {
                // declare a server-named queue
                var queueName = channel.QueueDeclare(queue: "").QueueName;
                channel.ConfirmSelect();

                var outstandingConfirms = new ConcurrentDictionary<ulong, string>();

                void cleanOutstandingConfirms(ulong sequenceNumber, bool multiple)
                {
                    if (multiple)
                    {
                        var confirmed = outstandingConfirms.Where(k => k.Key <= sequenceNumber);
                        foreach (var entry in confirmed)
                            outstandingConfirms.TryRemove(entry.Key, out _);
                    }
                    else
                        outstandingConfirms.TryRemove(sequenceNumber, out _);
                }

                channel.BasicAcks += (sender, ea) => cleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
                channel.BasicNacks += (sender, ea) =>
                {
                    outstandingConfirms.TryGetValue(ea.DeliveryTag, out string body);
                    Console.WriteLine($"Message with body {body} has been nack-ed. Sequence number: {ea.DeliveryTag}, multiple: {ea.Multiple}");
                    cleanOutstandingConfirms(ea.DeliveryTag, ea.Multiple);
                };

                var timer = new Stopwatch();
                timer.Start();
                for (int i = 0; i < MESSAGE_COUNT; i++)
                {
                    var body = i.ToString();
                    outstandingConfirms.TryAdd(channel.NextPublishSeqNo, i.ToString());
                    channel.BasicPublish(exchange: "", routingKey: queueName, basicProperties: null, body: Encoding.UTF8.GetBytes(body));
                }

                if (!WaitUntil(60, () => outstandingConfirms.IsEmpty))
                    throw new Exception("All messages could not be confirmed in 60 seconds");

                timer.Stop();
                Console.WriteLine($"Published {MESSAGE_COUNT:N0} messages and handled confirm asynchronously {timer.ElapsedMilliseconds:N0} ms");
            }
        }

        private static bool WaitUntil(int numberOfSeconds, Func<bool> condition)
        {
            int waited = 0;
            while(!condition() && waited < numberOfSeconds * 1000)
            {
                Thread.Sleep(100);
                waited += 100;
            }

            return condition();
        }
}

参考资料:
rabbitmq官网
RabbitMQ从零到集群高可用.NetCore(.NET5) - RabbitMQ简介和六种工作模式详解

  • 1
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 打赏
    打赏
  • 0
    评论
AMQP(高级消息队列协议)是一种开放的网络协议,用于在分布式系统中传递和存储消息。它广泛应用于各种实时通讯、物联网、金融交易等领域。 AMQP定义了消息的格式和交换方式,并提供了强大的消息路由和可靠性机制,确保消息的可靠传输和处理。它可实现多对多的通信模式,支持消息的分发、过滤、排序等高级特性。 RabbitMQ是一个基于AMQP协议的开源消息中间件。它提供了一个可靠的、灵活的、可扩展的消息分发系统。RabbitMQ基于可插拔的插件机制,可以在不同的场景中灵活配置和扩展其功能。它具有高可用性、可靠性和可伸缩性,在分布式系统中被广泛应用。 RabbitMQ的核心概念包括生产者、消息队列和消费者。生产者负责产生消息并将其发送到RabbitMQ消息队列中。消息队列RabbitMQ的核心组件,用于存储消息,并根据一定的规则将消息分发给消费者。消费者则从消息队列中订阅并接收消息进行处理。 RabbitMQ还支持多种消息模型,如发布-订阅模型、工作队列模型和路由模型等。它可以根据不同的业务需求和场景,选择合适的消息模型来实现消息的可靠传输和处理。 总之,AMQP是一种用于分布式系统的高级消息队列协议,而RabbitMQ是基于AMQP的开源消息中间件。它们通过提供消息的格式、交换方式和路由规则等机制,实现了可靠的、灵活的消息传输和处理,极大地方便了分布式系统的开发和部署。

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包

打赏作者

Hello,Mr.S

你的鼓励将是我创作的最大动力

¥1 ¥2 ¥4 ¥6 ¥10 ¥20
扫码支付:¥1
获取中
扫码支付

您的余额不足,请更换扫码支付或充值

打赏作者

实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值