ASP.NET Core 6.0 使用RabbitMQ

1. 简介

RabbitMQ是一个开源的,基于AMQP(Advanced Message Queuing Protocol)协议的完整的可复用的企业级消息队,RabbitMQ可以实现点对点,发布订阅等消息处理模式。

RabbitMQ是一个开源的AMQP实现,服务器端用Erlang语言编写,支持Linux,windows,macOS,FreeBSD等操作系统,同时也支持很多语言,如:Python,Java,Ruby,PHP,C#,JavaScript,Go,Elixir,Objective-C,Swift等。

当今市面上有很多主流的消息中间件,如老牌的ActiveMQ、RabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。

不同MQ特点

  1. ActiveMQ
    是Apache出品,最流行的,能力强劲的开源消息总线。它是一个完全支持JMS规范的的消息中间件。丰富的API,多种集群架构模式让ActiveMQ在业界成为老牌的消息中间件,在中小型企业颇受欢迎!
  2. Kafka
    是LinkedIn开源的分布式发布-订阅消息系统,目前归属于Apache顶级项目。Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
  3. RocketMQ
    是阿里开源的消息中间件,它是纯Java开发,具有高吞吐量、高可用性、适合大规模分布式系统应用的特点。RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
  4. RabbitMQ
    RabbitMQ是使用Erlang语言开发的开源消息队列系统,基于AMQP协议来实现。AMQP的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。

RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。

RabbitMQ的工作机制:

首先要知道RabbitMQ的三种角色:生产者、消费者、消息服务器

  • 生产者:消息的创建者,负责创建和推送消息到消息服务器
  • 消费者:消息的接收方,接受消息并处理消息
  • 消息服务器:其实RabbitMQ本身,不会产生和消费消息,相当于一个中转站,将生产者的消息路由给消费者

RabbitMQ的一些角色

  • ConnectionFactory:连接管理,应用程序或消费方与RabbitMQ建立连接的管理器
  • Channel:信道,推送消息的通道
  • Exchange:交换机,用于接收分配消息到队列中
  • Queue:保存消息
  • Routingkey:消息会携带routingKey,决定消息最终的队列
  • BindingKey:Queue通过bindingKey与交换机绑定

2. 安装

网上有许多RabbitMQ的安装博客,所以在此不介绍。可以安装在 windows、linux、docker

web管理界面介绍

2.1. overview概览

2.2. Admin用户和虚拟主机管理

2.2.1. 添加用户


上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

  • 超级管理员(administrator)
    可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。
  • 监控者(monitoring)
    可登陆管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
  • 策略制定者(policymaker)
    可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。
  • 普通管理者(management)
    仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
  • 其他
    无法登陆管理控制台,通常就是普通的生产者和消费者。

2.2.2. 创建虚拟主机


为了让各个用户可以互不干扰的工作,RabbitMQ添加了虚拟主机(Virtual Hosts)的概念。其实就是一个独立的访问路径,不同用户使用不同路径,各自有自己的队列、交换机,互相不会影响。

2.2.3. 支持的消息模型


3. NET Core中使用RabbitMQ

RabbitMQ 从信息接收者角度可以看做三种模式,一对一,一对多(此一对多并不是发布订阅,而是每条信息只有一个接收者)和发布订阅。其中一对一是简单队列模式,一对多是Worker模式,而发布订阅包括发布订阅模式,路由模式和通配符模式,为什么说发布订阅模式包含三种模式呢,其实发布订阅,路由,通配符三种模式都是使用只是交换机(Exchange)类型不一致

3.1 简单队列

首先,我们需要创建两个控制台项目.Send(发送者)和Receive(接收者),然后为两个项目安装RabbitMQ.Client驱动

install-package rabbitmq.client

然后在Send和Receive项目中编写我们的消息队列代码

生产者代码

show code
using RabbitMQ.Client;
using System.Text;

Console.WriteLine("Hello, World! 生产者");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

string queueName = "queue1";

// 声明一个队列
channel.QueueDeclare(
    queue: queueName,   // 队列名称
    durable: false,     // 是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息
    exclusive: false,   // 是否排他,如果一个队列声明为排他队列,该队列仅对时候次声明它的连接可见,并在连接断开时自动删除
    autoDelete: false,  // 是否自动删除,自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除
    arguments: null     // 设置队列的其他参数
);

string str = string.Empty;

do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    // 消息内容
    byte[] body = Encoding.UTF8.GetBytes(str);

    // 发送消息
    channel.BasicPublish("", queueName, null, body);

    // Console.WriteLine("成功发送消息:" + str);
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

code describe

  • 可以看到 RabbitMQ 使用了 IConnectionFactory, IConnection和IModel 来创建链接和通信管道, IConnection 实例对象只负责与 Rabbit 的连接,而发送接收这些实际操作全部由会话通道进行。
  • 而后使用 QueneDeclare 方法进行创建消息队列,创建完成后可以在 RabbitMQ 的管理工具中看到此队列,QueneDelare 方法需要一个消息队列名称的必须参数.后面那些参数则代表缓存,参数等信息。
  • 最后使用 BasicPublish 来发送消息,在一对一中 routingKey 必须和 queueName 一致。

消费者代码

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

Console.WriteLine("Hello, World! 消费者1");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

string queueName = "queue1";
//声明一个队列
channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
};

// 消费者开启监听
channel.BasicConsume(queueName, true, consumer);

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 在接收者中是定义一个EventingBasicConsumer对象的消费者(接收者),这个消费者与会话对象关联,
  • 然后定义接收事件,输出从消息队列中接收的数据,
  • 最后使用会话对象的BasicConsume方法来启动消费者监听.消费者的定义也是如此简单.
  • 不过注意一点,可以看到在接收者代码中也有声明队列的方法,其实这句代码可以去掉,但是如果去掉的话接收者在程序启动时监听队列,而此时这个队列还未存在,所以会出异常,所以往往会在消费者中也添加一个声明队列方法

此时,简单消息队列传输就算写好了,我们可以运行代码就行测试

3.2 Worker模式

Worker模式其实是一对多的模式,但是这个一对多并不是像发布订阅那种,而是信息以顺序的传输给每个接收者,我们可以使用上个例子来运行worker模式甚至,只需要运行多个接收者即可

默认情况下,RabbitMQ会顺序的将message发给下一个消费者。每个消费者会得到平均数量的message。这种方式称之为round-robin(轮询).
但是很多情况下并不希望消息平均分配,而是要消费快的多消费,消费少的少消费。还有很多情况下一旦其中一个宕机,那么另外接收者的无法接收原本这个接收者所要接收的数据。

下面针对上面的两个问题进行处理
首先我们先来看一下所说的宕机丢失数据一说,我们在上个例子Receive接收事件中添加线程等待

consumer.Received += (model, ea) => {
    Thread.Sleep(3000);
    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
};

然后再次启动两个接收者进行测试

可以看到发送者发送了1-9的数字,第二个接收者在接收数据途中宕机,第一个接收者也并没有去接收第二个接收者宕机后的数据,有的时候我们会有当接收者宕机后,其余数据交给其它接收者进行消费,那么该怎么进行处理呢,解决这个问题得方法就是改变其消息确认模式

Rabbit中存在两种消息确认模式

  • 自动模式 - 只要消息从队列获取,无论消费者获取到消息后是否成功消费,都认为是消息成功消费.
  • 手动模式 - 消费从队列中获取消息后,服务器会将该消息处于不可用状态,等待消费者反馈。如果消费者在消费过程中出现异常,断开连接切没有发送应答,那么RabbitMQ会将这个消息重新投递。

修改两个消费者代码,并在其中一个中延迟确认。

consumer.Received += (model, ea) => {
    Thread.Sleep(3000);
    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
    
    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

如果在延迟中消费者断开连接,那么RabbitMQ会重新投递未确认的消息

‘能者多劳’模式
能者多劳是给消费速度快的消费更多的消息.少的责消费少的消息.能者多劳是建立在手动确认基础上实现。
在延迟确认的消费中添加BasicQos

3.3 Exchange模式(发布订阅模式,路由模式,通配符模式)

前面说过发布,路由,通配符这三种模式其实可以算为一种模式,区别仅仅是交互机类型不同.在这里出现了一个交换机的东西,发送者将消息发送发送到交换机,接收者创建各自的消息队列绑定到交换机,

通过上面三幅图可以看出这三种模式本质就是一种订阅模式,路由,通配符模式只是订阅模式的变种模式。使其可以选择发送订阅者中的接收者。
注意:交换机本身并不存储数据,数据存储在消息队列中,所以如果向没有绑定消息队列的交换机中发送信息,那么信息将会丢失

3.3.1 发布订阅模式(Fanout)

生产者代码

show code
using RabbitMQ.Client;
using System.Text;

Console.WriteLine("Hello, World! 生产者");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange1";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Fanout); // 把交换机设置为 fanout 发布订阅模式
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, "", null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

code describe

  • 代码与上面没有什么差异,只是由上面的消息队列声明变成了交换机声明(交换机类型为fanout),也就说发送者发送消息从原来的直接发送消息队列变成了发送到交换机

消费者代码

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

Console.WriteLine("Hello, World! 消费者1");

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange1";
channel.ExchangeDeclare(exchangeName, ExchangeType.Fanout);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion


channel.QueueBind(queueName, exchangeName, ""); // 将队列与交换机绑定

channel.BasicQos(0, 1, false);  // 告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));

    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 可以看到消费者代码与上面有些差异
  • 首先是声明交换机(同上面一样,为了防止异常)
  • 然后声明消息队列并对交换机进行绑定,在这里使用了随机数,目的是声明不重复的消息队列,如果是同一个消息队列,则就变成worker模式,也就是说对于发布订阅模式有多少接收者就有多少个消息队列,而这些消息队列共同从一个交换机中获取数据

然后同时开两个接收者,结果就如下

3.3.2 路由模式(Direct)

路由模式下,在发布消息时指定不同的routeKey,交换机会根据不同的routeKey分发消息到不同的队列中

生产者代码

show code
Console.WriteLine("Hello, World! 生产者");

Console.WriteLine($"输入 routingKey:");
string routingKey = Console.ReadLine()!;

// 创建连接工厂对象
var factory = new ConnectionFactory() {
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange2";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Direct);
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, routingKey, null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

申明一个routeKey值为key1,并在发布消息的时候告诉了RabbitMQ,消息传递时routeKey必须匹配,才会被队列接收否则消息会被抛弃。

消费者代码

show code
Console.WriteLine("Hello, World! 消费者1");

Console.WriteLine($"输入接受key名称:");
string routeKey = Console.ReadLine()!;

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange2";
channel.ExchangeDeclare(exchangeName, ExchangeType.Direct);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion

channel.QueueBind(queueName, exchangeName, routeKey); // 将队列与交换机绑定
channel.QueueBind(queueName, exchangeName, "key2"); 
channel.QueueBind(queueName, exchangeName, "key3"); // 可以通过绑定多个,来匹配多个路由 

// channel.BasicQos(0, 1, false);  // 告诉Rabbit每次只能向消费者发送一条信息,再消费者未确认之前,不再向他发送信息

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));

    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

code describe

  • 一个接收者消息队列可以声明多个路由与交换机进行绑定

运行结果如下

3.3.3 通配符模式(Topic)

通配符模式与路由模式一致,只不过通配符模式中的路由可以声明为模糊查询,RabbitMQ拥有两个通配符

  • #:匹配0-n个字符语句
  • *:匹配一个字符语句
  • 注意:RabbitMQ中通配符并不像正则中的单个字符,而是一个以“.”分割的字符串,如 ”topic1.*“匹配的规则以topic1开始并且"."后只有一段语句的路由 例:“topic1.aaa”,“topic1.bb”
  • 而“#”可以匹配到 “topic1.aaa.bb”,“topic1.bb.cc”.

生产者代码

show code
Console.WriteLine("Hello, World! 生产者");

Console.WriteLine($"输入 routingKey:");
string routingKey = Console.ReadLine()!;


// 创建连接工厂对象
var factory = new ConnectionFactory() {
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};
var connection = factory.CreateConnection();    // 创建连接对象
var channel = connection.CreateModel();         // 创建连接会话对象

#region 定义交换机
string exchangeName = "exchange3";

channel.ExchangeDeclare(exchange: exchangeName, type: ExchangeType.Topic);
#endregion

string str;
do {
    Console.WriteLine("发送内容:");
    str = Console.ReadLine()!;

    byte[] body = Encoding.UTF8.GetBytes(str); // 消息内容

    channel.BasicPublish(exchangeName, routingKey, null, body); // 发送消息
} while (str.Trim().ToLower() != "exit");

channel.Close();
connection.Close();

消费者代码

show code
Console.WriteLine("Hello, World! 消费者1");

Console.WriteLine($"输入接受key名称:");  // key.* 或者 key.#
string routeKey = Console.ReadLine()!;

var factory = new ConnectionFactory()       // 创建连接工厂对象
{
    HostName = "localhost",
    Port = 5672,
    UserName = "guest",
    Password = "guest"
};

IConnection connection = factory.CreateConnection();    // 创建连接对象
IModel channel = connection.CreateModel();         // 创建连接会话对象

#region 声明交换机
string exchangeName = "exchange3";
channel.ExchangeDeclare(exchangeName, ExchangeType.Topic);
#endregion

#region 声明队列
string queueName = exchangeName + "_" + new Random().Next(1, 1000);
Console.WriteLine("队列名称:" + queueName);

channel.QueueDeclare(
  queue: queueName,//消息队列名称
  durable: false,//是否持久化,true持久化,队列会保存磁盘,服务器重启时可以保证不丢失相关信息。
  exclusive: false,//是否排他,true排他的,如果一个队列声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除.
  autoDelete: false,//是否自动删除。true是自动删除。自动删除的前提是:致少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除.
  arguments: null ////设置队列的一些其它参数
);
#endregion

channel.QueueBind(queueName, exchangeName, routeKey); // 将队列与交换机绑定

// 创建消费者对象
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, ea) => {

    byte[] message = ea.Body.ToArray();
    Console.WriteLine("接收到的消息为:" + Encoding.UTF8.GetString(message));
    channel.BasicAck(ea.DeliveryTag, true); // 开启返回消息确认
};

channel.BasicConsume(queue: queueName, autoAck: false, consumer); // 将autoAck设置false 关闭自动确认.

Console.ReadKey();
channel.Dispose();
connection.Close();

只有在通配符匹配通过的情况下才会接收消息

这里引用两个链接是对RabbitMQ中参数和方法的说明:
https://blog.csdn.net/fly_leopard/article/details/102821776
https://www.cnblogs.com/cuijl/p/8075130.html

部分内容来自:
https://blog.csdn.net/qq_44845339/article/details/114848670
https://www.cnblogs.com/yan7/p/9498685.html

posted @ 2022-04-21 12:31  shenghuotaiai  阅读(6490)  评论(0编辑  收藏  举报