一、RabbitMQ介绍
1、概念
RabbitMQ是一个开源的消息中间件,用于在分布式系统中传递和存储消息。它实现了高级消息队列协议(AMQP),提供了可靠的消息传递机制。
RabbitMQ基于生产者-消费者模型,其中生产者将消息发送到队列中,而消费者从队列中接收并处理消息。它支持多种消息传递模式,包括点对点、发布/订阅和请求/响应模式。
2、特点
RabbitMQ具有以下特点:
-
可靠性:RabbitMQ使用持久化机制来确保消息不会丢失,并提供了消息确认机制,确保消息被正确地传递和处理。
-
灵活性:它支持多种消息传递模式和交换机类型,可以根据需求灵活配置和定制。
-
可扩展性:RabbitMQ可以通过添加更多的节点来实现水平扩展,以处理更大的消息负载。
-
高性能:它采用异步消息传递机制,能够处理大量的消息并保持低延迟。
-
多语言支持:RabbitMQ提供了多种编程语言的客户端库,使得开发者可以使用自己熟悉的语言进行消息传递。
-
可管理性:RabbitMQ提供了一个易于使用的管理界面,可以监控和管理消息队列、交换机和绑定等。
RabbitMQ广泛应用于各种场景,包括微服务架构、异步任务处理、日志收集和分析、实时数据处理等。它是一个可靠、灵活和高性能的消息中间件解决方案。
3、优势
RabbitMQ与其他消息中间件相比,各有优缺点。下面是一些常见的消息中间件与RabbitMQ的对比:
-
Apache Kafka:
- 优点:Kafka适用于高吞吐量和低延迟的场景,特别擅长处理大规模的实时数据流。它具有高可扩展性和持久化能力,并支持分布式订阅和发布。
- 缺点:相对于RabbitMQ而言,Kafka的配置和部署较为复杂。此外,Kafka不提供消息确认机制,因此在某些场景下可能会出现消息丢失的风险。
-
ActiveMQ:
- 优点:ActiveMQ是一个功能丰富的消息中间件,支持多种消息传递模式和协议。它易于使用和部署,并提供了可靠的消息传递机制。
- 缺点:相对于RabbitMQ而言,ActiveMQ的性能稍逊一筹。在高负载情况下,ActiveMQ可能会出现性能瓶颈。
-
Redis Pub/Sub:
- 优点:Redis Pub/Sub是一个简单而高效的消息中间件,适用于快速发布和订阅消息。它具有低延迟和高并发性能。
- 缺点:Redis Pub/Sub不支持持久化消息,当消费者离线时,无法保证消息的可靠传递。此外,它也不支持复杂的消息路由和过滤机制。
总体而言,RabbitMQ在可靠性、灵活性和可管理性方面表现出色。它适用于大多数场景,并且具有广泛的语言支持和成熟的社区生态系统。然而,在需要高吞吐量和低延迟的大规模实时数据处理场景下,Kafka可能更适合。选择合适的消息中间件取决于具体的需求和系统架构。
二、RabbitMQ模式与应用场景介绍
1、订阅模式(Publish/Subscribe)
一次向许多消费者发送消息,一个生产者发送的消息会被多个消费者获取,也就是将消息将广播到所有的消费者中。
应用场景: 更新商品库存后需要通知多个缓存和多个数据库,这里的结构应该是:
一个fanout类型交换机扇出两个个消息队列,分别为缓存消息队列、数据库消息队列
一个缓存消息队列对应着多个缓存消费者
一个数据库消息队列对应着多个数据库消费者
发布者:
/// <summary>
/// 发布订阅模式--生产者
/// </summary>
public void fabudingyueSender()
{
string EXCHANGE_NAME = "logs";
_model.ExchangeDeclare("logs", "fanout");
String message = "publish subscribe message";
var body = Encoding.UTF8.GetBytes(message);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "",
basicProperties: null,
body: body
);
Console.WriteLine(" [x] Sent '" + message + "'");
}
订阅者:
/// <summary>
/// 发布订阅模式--消费者1
/// </summary>
public void fabudingyueReceiver1()
{
string EXCHANGE_NAME = "logs";
_model.ExchangeDeclare(exchange: EXCHANGE_NAME, "fanout");
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
// 将队列绑定到交换机
_model.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: "");
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("Receiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
2、路由模式(Routing)
有选择地(Routing key)接收消息,发送消息到交换机并且要指定路由key ,消费者将队列绑定到交换机时需要指定路由key,仅消费指定路由key的消息
应用场景: 如在超市打折促销活动中增加了香蕉,香蕉促销活动消费者指定routing key为香蕉,只有香蕉促销活动会接收到消息,其它促销活动不关心也不会消费此routing key的消息
发布者:
/// <summary>
/// 路由模式 -- 生产者
/// </summary>
public void routingSender()
{
string EXCHANGE_NAME = "exchange_direct";
string EXCHANGE_TYPE = "direct";
_model.ExchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
string key1 = "this is key1 message";
string key2 = "this is key2 message";
var body1 = Encoding.UTF8.GetBytes(key1);
var body2 = Encoding.UTF8.GetBytes(key2);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "key1",
basicProperties: null,
body: body1);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "key2",
basicProperties: null,
body: body2);
Console.WriteLine($"push suessce:{key1}&{key2}");
}
消费者1:只接收指定路由key1的
/// <summary>
/// 路由模式 -- 消费者1
/// </summary>
public void routingReceiver1() {
string EXCHANGE_NAME = "exchange_direct";
string EXCHANGE_TYPE = "direct";
//说明交换机
_model.ExchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(仅接收路由key1的)
_model.QueueBind(queueName,EXCHANGE_NAME,"key1");
//设置消息QPS
_model.BasicQos(0,1,false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
消费者2:同时接收路由key1和key2的
/// <summary>
/// 路由模式 -- 消费者2
/// </summary>
public void routingReceiver2() {
string EXCHANGE_NAME = "exchange_direct";
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(接收路由key1和key2)
_model.QueueBind(queueName, EXCHANGE_NAME, "key1");
_model.QueueBind(queueName, EXCHANGE_NAME, "key2");
//设置消息QPS
//_model.BasicQos(0, 1, false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver2收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
3、主题模式(Topics)
根据主题(Topics)来接收消息,将路由key和某模式进行匹配,此时队列需要绑定在一个模式上,#匹配一个词或多个词,*只匹配一个词。
应用场景: 同上,水果促销活动可以接收主题为火龙果的消息,如白肉火龙果、红肉火龙果等
3、远程过程调用(RPC)
如果我们需要在远程计算机上运行功能并等待结果就可以使用RPC,具体流程可以看图。应用场景:需要等待接口返回数据,如订单支付
发布者确认(Publisher Confirms)
与发布者进行可靠的发布确认,发布者确认是RabbitMQ扩展,可以实现可靠的发布。在通道上启用发布者确认后,RabbitMQ将异步确认发送者发布的消息,这意味着它们已在服务器端处理。
应用场景: 对于消息可靠性要求较高,比如钱包扣款
(后面会针对使用RabbitMQ实现RPCclient端与server远程端通信的专题博客文章,敬请期待)
三、RabbitMQ的交换机介绍
RabbitMQ中的交换机(Exchange)是消息的分发中心,它接收生产者发送的消息,并根据一定的规则将消息路由到一个或多个队列中。下面是对RabbitMQ交换机的几种常见类型的说明:
-
直连交换机(Direct Exchange):
- 直连交换机是最简单的交换机类型,它根据消息的路由键(Routing Key)将消息发送到与之匹配的队列。
- 生产者在发送消息时指定了一个路由键,交换机会将消息发送到与该路由键完全匹配的队列中。
-
主题交换机(Topic Exchange):
- 主题交换机根据消息的路由键和绑定键(Binding Key)的模式匹配将消息发送到一个或多个队列中。
- 绑定键可以使用通配符进行模糊匹配,其中“*”表示匹配一个单词,“#”表示匹配零个或多个单词。
- 生产者在发送消息时指定了一个路由键,交换机会将消息发送到与该路由键匹配的队列中。
-
扇形交换机(Fanout Exchange):
- 扇形交换机将消息广播到所有与之绑定的队列中,忽略消息的路由键。
- 当生产者发送消息到扇形交换机时,交换机会将消息复制并发送到所有与之绑定的队列中。
-
头交换机(Headers Exchange):
- 头交换机根据消息的头部属性(Headers)进行匹配,并将消息发送到与之匹配的队列中。
- 生产者在发送消息时可以设置自定义的头部属性,交换机会根据这些属性进行匹配。
交换机通过绑定(Binding)与队列进行关联,一个交换机可以绑定多个队列。绑定时需要指定一个绑定键,交换机根据绑定键和消息的路由键来确定消息应该被发送到哪个队列。
不同类型的交换机适用于不同的消息路由需求,开发者可以根据具体的场景选择合适的交换机类型来实现灵活的消息路由和分发
四、实战项目演练
新增.NET 6 WEB API 项目,项目结构如下图:
1、配置本地RabbitMQ服务信息
修改appsettings.json文件,新增以下配置
"RabbitMQ": {
"HostName": "localhost",
"Port": "5672",
"UserName": "", //用户名
"Password": "" //用户密码
}
2、新增RabbitMQConfigs.cs
RabbitMQConfigs.cs是一个抽象基类,主要实现通过IConfiguration获取本地RabbitMQ服务信息,创建RabbitMQ连接工厂。
public abstract class RabbitMQConfigs
{
protected ConnectionFactory GreateConnectionFactory(IConfiguration configuration)
{
// 在这里设置ConnectionFactory的属性
var factory = new ConnectionFactory
{
// 设置连接属性
HostName = configuration["RabbitMQ:HostName"],
Port = int.Parse(configuration["RabbitMQ:Port"]),
UserName = configuration["RabbitMQ:UserName"],
Password = configuration["RabbitMQ:Password"]
};
return factory;
}
}
3、新增IRabbitMQServices接口
定义需要实现的RabbitMQ的各种模式的接口,本文的该项目演示只针对发布订阅和路由模式去实践演示
public interface IRabbitMQServices
{
/// <summary>
/// 发布订阅模式--生产者
/// </summary>
void fabudingyueSender();
/// <summary>
/// 发布订阅模式--消费者1
/// </summary>
void fabudingyueReceiver1();
/// <summary>
/// 发布订阅模式--消费者2
/// </summary>
void fabudingyueReceiver2();
/// <summary>
/// 路由模式--生产者
/// </summary>
void routingSender();
/// <summary>
/// 路由模式--消费者1
/// </summary>
void routingReceiver1();
/// <summary>
/// 路由模式--消费者2
/// </summary>
void routingReceiver2();
}
4、新建RabbitMQServices
RabbitMQServices是基于RabbitMQConfigs抽象基类并继承IRabbitMQServices的实现
public class RabbitMQServices : RabbitMQConfigs, IRabbitMQServices
{
private readonly IConnection _connection;
private readonly IModel _model;
static ConcurrentDictionary<string, TaskCompletionSource<string>> pendingRequests = new();
public RabbitMQServices(IConfiguration configuration)
{
var factory = GreateConnectionFactory(configuration);
_connection = factory.CreateConnection();
_model = _connection.CreateModel();
}
/// <summary>
/// 发布订阅模式--生产者
/// </summary>
public void fabudingyueSender()
{
string EXCHANGE_NAME = "logs";
_model.ExchangeDeclare("logs", "fanout");
String message = "publish subscribe message";
var body = Encoding.UTF8.GetBytes(message);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "",
basicProperties: null,
body: body
);
Console.WriteLine(" [x] Sent '" + message + "'");
}
/// <summary>
/// 发布订阅模式--消费者1
/// </summary>
public void fabudingyueReceiver1()
{
string EXCHANGE_NAME = "logs";
_model.ExchangeDeclare(exchange: EXCHANGE_NAME, "fanout");
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
// 将队列绑定到交换机
_model.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: "");
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("Receiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
/// <summary>
/// 发布订阅模式--消费者2
/// </summary>
public void fabudingyueReceiver2()
{
string EXCHANGE_NAME = "logs";
//声明交换机
_model.ExchangeDeclare(exchange: EXCHANGE_NAME, "fanout");
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
// 将队列绑定到交换机
_model.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: "");
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("Receiver2收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
/// <summary>
/// 路由模式 -- 生产者
/// </summary>
public void routingSender()
{
string EXCHANGE_NAME = "exchange_direct";
string EXCHANGE_TYPE = "direct";
_model.ExchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
string key1 = "this is key1 message";
string key2 = "this is key2 message";
var body1 = Encoding.UTF8.GetBytes(key1);
var body2 = Encoding.UTF8.GetBytes(key2);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "key1",
basicProperties: null,
body: body1);
_model.BasicPublish(exchange: EXCHANGE_NAME,
routingKey: "key2",
basicProperties: null,
body: body2);
Console.WriteLine($"push suessce:{key1}&{key2}");
}
/// <summary>
/// 路由模式 -- 消费者1
/// </summary>
public void routingReceiver1() {
string EXCHANGE_NAME = "exchange_direct";
string EXCHANGE_TYPE = "direct";
//说明交换机
_model.ExchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(仅接收路由key1的)
_model.QueueBind(queueName,EXCHANGE_NAME,"key1");
//设置消息QPS
_model.BasicQos(0,1,false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
/// <summary>
/// 路由模式 -- 消费者2
/// </summary>
public void routingReceiver2() {
string EXCHANGE_NAME = "exchange_direct";
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(接收路由key1和key2)
_model.QueueBind(queueName, EXCHANGE_NAME, "key1");
_model.QueueBind(queueName, EXCHANGE_NAME, "key2");
//设置消息QPS
//_model.BasicQos(0, 1, false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver2收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
}
5、ApiController.cs 实现api请求,模拟发布消息
// POST api/<ApiController>
[HttpPost]
public IActionResult Post(string value)
{
// 发布消息到 RabbitMQ
_rabbitMQServices.fabudingyueSender();
_rabbitMQServices.routingSender();
return Ok();
}
五、测试
新建一个控制台应用程序,修改Program.cs文件,实现对MQ消息的监听消费的逻辑。如下
using rabbirmqtestReciver;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
using System.Text;
IConnection _connection = null;
IModel _model = null;
var factory = new ConnectionFactory
{
// 设置连接属性
HostName = "localhost",
Port = 5672,
UserName = "",//用户名
Password = "" //用户密码
};
_connection = factory.CreateConnection();
_model = _connection.CreateModel();
//发布订阅模式的消费者
fabudingyueReceiver1();
fabudingyueReceiver2();
//路由模式的消费者(指定接收路由key1)
routingReceiver1();
//路由模式的消费者(指定接收路由key1和key2)
routingReceiver2();
Console.WriteLine("按任意键退出...");
Console.ReadKey();
void fabudingyueReceiver1()
{
string EXCHANGE_NAME = "logs";
_model.ExchangeDeclare(exchange: EXCHANGE_NAME, "fanout");
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
// 将队列绑定到交换机
_model.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: "");
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("fabudingyueReceiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
/// <summary>
/// 发布订阅模式--消费者2
/// </summary>
void fabudingyueReceiver2()
{
string EXCHANGE_NAME = "logs";
//声明交换机
_model.ExchangeDeclare(exchange: EXCHANGE_NAME, "fanout");
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
// 将队列绑定到交换机
_model.QueueBind(queue: queueName, exchange: EXCHANGE_NAME, routingKey: "");
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("fabudingyueReceiver2收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
/// <summary>
/// 路由模式 -- 消费者1
/// </summary>
void routingReceiver1()
{
string EXCHANGE_NAME = "exchange_direct";
string EXCHANGE_TYPE = "direct";
//说明交换机
_model.ExchangeDeclare(EXCHANGE_NAME, EXCHANGE_TYPE);
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(仅接收路由key1的)
_model.QueueBind(queueName, EXCHANGE_NAME, "key1");
//设置消息QPS
_model.BasicQos(0, 1, false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver1收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
void routingReceiver2()
{
string EXCHANGE_NAME = "exchange_direct";
// 声明临时队列
var queueName = _model.QueueDeclare().QueueName;
//绑定队列、交换机与路由(接收路由key1和key2)
_model.QueueBind(queueName, EXCHANGE_NAME, "key1");
_model.QueueBind(queueName, EXCHANGE_NAME, "key2");
//设置消息QPS
//_model.BasicQos(0, 1, false);
// 创建消费者,绑定回调函数,只执行一次
var consumer = new EventingBasicConsumer(_model);
consumer.Received += (model, ea) =>
{
var body = ea.Body.ToArray();
var message = Encoding.UTF8.GetString(body);
Console.WriteLine("routingReceiver2收到消息:{0}", message);
};
// 启动消费者
_model.BasicConsume(queue: queueName, autoAck: true, consumer: consumer);
}
启动两个项目,调用发布项目的api接口模拟发布消息,查看控制台输出结果。
结果: