消息队列基础知识以及在 C# 中使用 RabbitMQ

消息队列基础知识以及在 C# 中使用 RabbitMQ

我在这里只记录点基本内容,不会写太深入的内容

同步与异步对比

这是使用消息队列之前的服务器架构

这是使用消息队列之后的服务器架构

异步化架构流程

异步,不是同步,也就是不立即处理,而是延迟处理。时间换性能

举例:
一个请求,需要一秒才能处理完,请求多了,服务器就处理不过来
如果能优化到一毫秒处理一个请求,那就提升了 1000 倍处理能力,但是这是不可能的,因为数据库有瓶颈

解决方案:

  1. 应用服务器(Web API)数据库之间加一层消息队列
  2. 应用服务器消息队列只是保存一下操作的信息(在内存),直接返回结果给用户,但是并未完成业务。这是可以达到毫秒级响应,因为没有处理业务逻辑
  3. 消息队列数据库之间再加一层处理器,处理器就是从消息队列里面拿数据的,然后处理器再去与数据库交互,处理业务逻辑

消息应该使用 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()写在同一个函数中

消息队列基础知识以及在 C# 中使用 RabbitMQ 结束

posted @ 2021-08-17 16:56  .NET好耶  阅读(1441)  评论(0编辑  收藏  举报