消息队列基础知识以及在 C# 中使用 RabbitMQ
消息队列基础知识以及在 C# 中使用 RabbitMQ
我在这里只记录点基本内容,不会写太深入的内容
同步与异步对比
这是使用消息队列之前的服务器架构
这是使用消息队列之后的服务器架构
异步化架构流程
异步,不是同步,也就是不立即处理,而是延迟处理。时间换性能
举例:
一个请求,需要一秒才能处理完,请求多了,服务器就处理不过来
如果能优化到一毫秒处理一个请求,那就提升了 1000 倍处理能力,但是这是不可能的,因为数据库有瓶颈
解决方案:
- 在应用服务器(Web API)和数据库之间加一层消息队列
- 从应用服务器到消息队列只是保存一下操作的信息(在内存),直接返回结果给用户,但是并未完成业务。这是可以达到毫秒级响应,因为没有处理业务逻辑
- 在消息队列和数据库之间再加一层处理器,处理器就是从消息队列里面拿数据的,然后处理器再去与数据库交互,处理业务逻辑
消息应该使用 JSON
应用服务器是不能主动去查询数据库再返回结果给用户的,因为成本比较高,一般都是客户端轮询或WebSocket之类的,去问应用服务器业务逻辑是否完成,比如扫码之类的功能
异步化优劣势解读
优势
1. 流量削峰
流量削峰,消息队列的最重要特性,把流量高峰的业务延迟到后面再处理
流量高峰期,请求特别多,但是应用服务器不处理业务逻辑,所以无所谓,吞吐量高
业务堆积在队列里面,晚点再处理
2. 高可用
可用性,就是对外不间断的提供服务
简化应用服务,处理器异常也不影响我们对外提供服务
使用消息队列,应用服务器可能也就做一下数据验证什么的工作,具体的业务逻辑在处理器中处理,即使所有的处理器都宕机,但是我们的消息队列依然可以接收消息,这样,我们的系统依然能对外提供服务
3. 扩展性
消息队列的消息来自应用服务器,如果我们需要做扩展功能和手机业务逻辑,只需要给处理器这一层升级就可以了。用户提交的信息不需要改变,也完全不影响应用服务器,耦合度也降低了
直接从物理层面隔离,扩展互不影响
4. 重试机制
以前我们的应用服务器是直接连接数据库,对数据进行操作,操作失败就返回失败。
消息队列如果失败了,并不会把消息扔掉,之后再重新尝试操作
缺陷
1. 降低用户体验
因为不能快速拿到结果,会降低用户体验,需要业务妥协一下
2. 代码的复杂度
原来我们只有客户端、应用服务器、数据库这三层
现在我们有客户端、应用服务器、消息队列、处理器、数据库
应用服务器层,除了做验证和消息序列化操作,还要支持用户查询业务处理结果
处理器层,需要写业务逻辑
3. 重放攻击
页面连点几次,数据库有多条相同数据
但是在消息队列里面就比较难处理了,会有多条重复消息
可能因为网络抖动,操作信息已经保存到消息队列中,但是应用服务器没有返回结果。用户就以为请求没有成功,很可能会再次发起请求,消息队列会出现多条相同的数据
4. 幂等性设计
处理器处理完业务逻辑会返回结果,并删除消息队列中的信息,但是在消息队列里,也有可能处理器对数据库处理完成,但是没有移除消息队列的信息
幂等性设计就是说,处理器处理重复的消息,不会在数据库产生新的数据,也不会影响结果
消息队列
消息队列,是一个独立进程,(一般)使用内存保存(速度快,有丢失),支持网络读写
生产者消费者模式:
- 生产者负责往消息队列里写入数据,生产者可以有多个
- 消费者负责使用消息队列中的数据
- 一条消息消费一次
生产者消费者模式长这样
发布订阅模式:
- 发布者负责往消息队列里写入数据,发布者可以有多个
- 订阅者负责使用消息队列中的数据,订阅者可以有多个
- 一条消息订阅多次
发布订阅模式长这样
安装 RabbitMQ
安装过程就没必要写了,教程到处都是,也有 Windows 版一键安装
但是注意:guest 不支持远程访问,所以要新增一个用户
RabbitMQ 集群
因为整几台 Linux 有点麻烦,而且我也不会用 Docker,所以我就不写了
RabbitMQ 集群搭建参考:https://blog.csdn.net/qq_28533563/article/details/107932737
RabbitMQ
RabbitMQ 相对于一般的消息队列来说,有一个比较独特的设计,就是Exchangeds(交换机)和Queues(队列)
所以系统结构就变成了,生产者先连接交换机,交换机再去连接消息队列。
多了交换机这一层,就可以有很多功能:
- 路由:由交换机去转发到消息队列
- 实现了一条消息多个队列使用:因为消息由交换机转发,所以可以转发到多个队列中
在 C# 中使用 RabbitMQ
首先,使用 NuGet 安装一个包:RabbitMQ.Client
会涉及到以下几个函数
这些参数我也不太懂,所以我把源码的函数声明给拿出来了,自行翻译
定义队列
/// <summary>
/// Declares a queue. See the <a href="https://www.rabbitmq.com/queues.html">Queues guide</a> to learn more.
/// </summary>
/// <param name="queue">The name of the queue. Pass an empty string to make the server generate a name.</param>
/// <param name="durable">Should this queue will survive a broker restart?</param>
/// <param name="exclusive">Should this queue use be limited to its declaring connection? Such a queue will be deleted when its declaring connection closes.</param>
/// <param name="autoDelete">Should this queue be auto-deleted when its last consumer (if any) unsubscribes?</param>
/// <param name="arguments">Optional; additional queue arguments, e.g. "x-queue-type"</param>
QueueDeclareOk QueueDeclare(string queue, bool durable, bool exclusive, bool autoDelete, IDictionary<string, object> arguments);
定义交换机
/// <summary>Declare an exchange.</summary>
/// <remarks>
/// The exchange is declared non-passive and non-internal.
/// The "nowait" option is not exercised.
/// </remarks>
void ExchangeDeclare(string exchange, string type, bool durable, bool autoDelete, IDictionary<string, object> arguments);
将队列绑定到交换机上
/// <summary>
/// Bind a queue to an exchange.
/// </summary>
/// <remarks>
/// <para>
/// Routing key must be shorter than 255 bytes.
/// </para>
/// </remarks>
void QueueBind(string queue, string exchange, string routingKey, IDictionary<string, object> arguments);
发布消息
/// <summary>
/// (Extension method) Convenience overload of BasicPublish.
/// </summary>
/// <remarks>
/// The publication occurs with mandatory=false
/// </remarks>
public static void BasicPublish(this IModel model, string exchange, string routingKey, IBasicProperties basicProperties, ReadOnlyMemory<byte> body)
{
model.BasicPublish(exchange, routingKey, false, basicProperties, body);
}
/// <summary>
/// Publishes a message.
/// </summary>
/// <remarks>
/// <para>
/// Routing key must be shorter than 255 bytes.
/// </para>
/// </remarks>
void BasicPublish(string exchange, string routingKey, bool mandatory, IBasicProperties basicProperties, ReadOnlyMemory<byte> body);
启动消费者,接收消息
/// <summary>Start a Basic content-class consumer.</summary>
public static string BasicConsume(this IModel model, string queue, bool autoAck, IBasicConsumer consumer)
{
return model.BasicConsume(queue, autoAck, "", false, false, null, consumer);
}
/// <summary>Start a Basic content-class consumer.</summary>
string BasicConsume(
string queue,
bool autoAck,
string consumerTag,
bool noLocal,
bool exclusive,
IDictionary<string, object> arguments,
IBasicConsumer consumer);
Received 事件,消费者接收到消息事件,即,消费者处理消息
///<summary>
/// Event fired when a delivery arrives for the consumer.
/// </summary>
/// <remarks>
/// Handlers must copy or fully use delivery body before returning.
/// Accessing the body at a later point is unsafe as its memory can
/// be already released.
/// </remarks>
public event EventHandler<BasicDeliverEventArgs> Received;
生产者消费者模式
生产者部分
生产者项目就用 RESTful
[HttpGet("one/{count}")]
public async Task<ActionResult> One(int count)
{
string queueName = "queue_demo_one";
string exchangeName = "exchange_demo_one";
//先创建连接
var factory = new ConnectionFactory()
{
HostName = "192.168.0.102",//ip
Port = 5672,//端口,15672 是 web 端管理用的,5672 是用于客户端与消息中间件之间可以传递消息
UserName = "admin",//用户名
Password = "123456"//密码
};
//打开连接
using var connection = factory.CreateConnection();
using IModel channel = connection.CreateModel();
//定义队列
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
//定义交换机
channel.ExchangeDeclare(exchange: exchangeName,
type: ExchangeType.Direct,
durable: true,
autoDelete: false,
arguments: null);
//将队列绑定到交换机上
channel.QueueBind(queue: queueName,
exchange: exchangeName,
routingKey: string.Empty,
arguments: null);
//发送队列
for (int i = 0; i < count; i++)
{
string message = $"Task {i}";
byte[] body = Encoding.UTF8.GetBytes(message);
//发送消息
channel.BasicPublish(exchange: exchangeName,
routingKey: string.Empty,
basicProperties: null,
body: body);
Console.WriteLine($"消息:{message} 已发送");
}
return Ok();
}
消费者部分
处理器这里用控制台
static void Foo_One()
{
string queueName = "queue_demo_one";
string exchangeName = "exchange_demo_one";
var factory = new ConnectionFactory()
{
HostName = "192.168.0.102",
Port = 5672,
UserName = "admin",
Password = "123456"
};
using var connection = factory.CreateConnection();
using IModel channel = connection.CreateModel();
//定义消费者
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, args) =>
{
var body = args.Body;
var message = Encoding.UTF8.GetString(body.ToArray());
Console.WriteLine($"消费者接收消息 {message}");
};
//启动消费者
channel.BasicConsume(queue: queueName,
autoAck: true,//自动确认
consumer: consumer);
//处理完消息后,保持程序继续运行,可以继续接收消息
Console.ReadLine();
}
效果
先启动 RESTful 项目,访问一次,会给队列增加消息,我在这里添加 10 条消息
可以看到 RabbitMQ 服务器这里多了一个 exchange
消息队列里也多了 10 条消息
启动控制台项目,消费者会处理数据
再看 RabbitMQ 服务器,消息被处理
不关闭控制台,再次访问 webapi,这样会自动执行
发布订阅模式
发布者
发布者也是用 RESTful
[HttpGet("multi/{count}")]
public async Task<ActionResult> Multi(int count)
{
string queueName = "queue_demo_multi";
string smsQueueName = "queue_demo_multi_sms";
string emailQueueName = "queue_demo_multi_eamil";
string exchangeName = "exchange_demo_multi";
//先创建连接
var factory = new ConnectionFactory()
{
HostName = "192.168.0.102",
Port = 5672,
UserName = "admin",
Password = "123456"
};
using var connection = factory.CreateConnection();
using IModel channel = connection.CreateModel();
channel.ExchangeDeclare(exchange: exchangeName,
type: ExchangeType.Fanout,
durable: true,
autoDelete: false,
arguments: null);
//这里声明三个队列,并且绑定同一个交换机
channel.QueueDeclare(queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: queueName,
exchange: exchangeName,
routingKey: string.Empty,
arguments: null);
channel.QueueDeclare(queue: smsQueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: smsQueueName,
exchange: exchangeName,
routingKey: string.Empty,
arguments: null);
channel.QueueDeclare(queue: emailQueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: emailQueueName,
exchange: exchangeName,
routingKey: string.Empty,
arguments: null);
for (int i = 0; i < count; i++)
{
string message = $"Task {i}";
byte[] body = Encoding.UTF8.GetBytes(message);
channel.BasicPublish(exchange: exchangeName,
routingKey: string.Empty,
basicProperties: null,
body: body);
Console.WriteLine($"消息:{message} 已发送");
}
return Ok();
}
订阅者
订阅者这里也是控制台,输入索引,表示启动不同的消费者(处理器)
static void Foo_Multi()
{
string queueName = "queue_demo_multi";
string smsQueueName = "queue_demo_multi_sms";
string emailQueueName = "queue_demo_multi_eamil";
string exchangeName = "exchange_demo_multi";
string[] strs = new string[3];
strs[0] = queueName;
strs[1] = smsQueueName;
strs[2] = emailQueueName;
Console.Write("输入索引 0 ~ 2 :");
int index = Convert.ToInt32(Console.ReadLine());
var factory = new ConnectionFactory()
{
HostName = "192.168.0.102",
Port = 5672,
UserName = "admin",
Password = "123456"
};
using var connection = factory.CreateConnection();
using IModel channel = connection.CreateModel();
channel.ExchangeDeclare(exchange: exchangeName,
type: ExchangeType.Fanout,
durable: true,
autoDelete: false,
arguments: null);
channel.QueueDeclare(queue: strs[index],
durable: true,
exclusive: false,
autoDelete: false,
arguments: null);
channel.QueueBind(queue: strs[index],
exchange: exchangeName,
routingKey: string.Empty,
arguments: null);
//定义消费者
var consumer = new EventingBasicConsumer(channel);
consumer.Received += (model, args) =>
{
var body = args.Body;
var message = Encoding.UTF8.GetString(body.ToArray());
Console.WriteLine($"消费者 {strs[index]} 接收消息 {message}");
};
//启动消费者
channel.BasicConsume(queue: strs[index],
autoAck: true,//自动确认
consumer: consumer);
//处理完消息后,保持程序继续运行,可以继续接收消息
Console.ReadLine();
}
效果
先启动 RESTful 项目,访问一次,会给队列增加消息,这里添加 10 条消息
查看 exchange,没有问题
查看 Queue,三个队列都有消息
启动三个控制台项目,消费者会处理数据
都处理完了
不关闭控制台,再次访问 webapi,这样会自动执行,这里就不演示了
注意
Console.ReadLine()
一定要跟启动消费者
即channel.BasicConsume()
写在同一个函数中