• 聊聊RabbitMQ那一些事儿之一基础应用


    聊聊RabbitMQ那一些事儿之一基础应用

      Hi,各位热爱技术的小伙伴您们好,今年的疫情害人啊,真心祝愿您和您的家人大家都平平安安,健健康康。年前到现在一直没有总结点东西,写点东西,不然久了自己感觉自己都要被废啦。这个周末花了一些时间来梳理了一下RabbitMQ的相关知识点。先来一个基础篇,先用起来。我也是一个边学习边梳理的过程,如果有什么梳理的不妥之处,多多指点,相互学习,谢谢!

      在使用前,我们首先第一件事情就是环境搭建。至于RabbitMQ的环境搭建,我就不在此啰嗦了,网上一搜一大堆,还没有搭建环境的小伙伴,可以网上找度娘哈,嘿嘿。

    一、什么是MQ

      MQ简单的说就是队列,队列的特性就是先进先出。我们其实可以把队列理解为一个消息管道,通过消息管道实现消息传递。最终达到不同的进程间、不同服务间的通讯需要。

      在一个程序中,我们 可以通过MQ实现不同进程间的通讯。在不同程序/服务间,我们同样可以通过MQ来实现相互通讯,这也是本文的重点,这个时候就该今天的主角登场了。

    二、RabbitMQ介绍

      RabbitMQ是一个开源的,在AMQP基础完整的,可复用的企业消息系统。我个人的简单的理解就是,实现消息的接收、存储、管理、分发。在操作系统支持上,支持主流的操作系统(Linux、Windows);在开发语言接口支持上,支持所有的主流开发语言;在性能上,支持消息持久化、集群化、高并发等等。

    三、RabbitMQ关键词介绍

      Broker(Server):接受客户端连接,实现AMQP消息队列和路由功能的进程,我们可以把Broker叫做RabbitMQ服务器。

      Virtual Host:一个虚拟概念,其实简单的理解你可以认为是在逻辑上对MQ进行分区隔离,这样避免不同业务的MQ直接交叉感染。一个Virtual Host里面可以有若干个Exchange和Queue,主要用于权限控制,隔离应用。如应用程序A使用VhostA,应用程序B使用VhostB,那么我们在VhostA中只存放应用程序A的exchange,queue和消息,应用程序A的用户只能访问VhostA,不能访问VhostB中的数据。

      Exchange:接受生产者发送的消息,并根据Binding规则将消息路由给服务器中的队列。ExchangeType决定了Exchange路由消息的行为,例如,在RabbitMQ中,ExchangeType有Direct、Fanout、Topic和Header四种,不同类型的Exchange路由规则是不一样的(这些以后会详细介绍)。

      Queue:消息队列,用于存储还未被消费者消费的消息,队列是先进先出的,默认情况下先存储的消息先被处理。

      Message:就是消息,由Header和Body组成,Header是由生产者添加的各种属性的集合,包括Message是否被持久化、由哪个Message Queue接受、优先级是多少等,Body是真正传输的数据,内容格式为byte[]。

      Connection:连接,对于RabbitMQ而言,其实就是一个位于客户端和Broker之间的TCP连接。

      Channel:道,仅仅创建了客户端到Broker之间的连接Connection后,客户端还是不能发送消息的。需要在Connection的基础上创建Channel,AMQP协议规定只有通过Channel才能执行AMQP的命令,一个Connection可以包含多个Channel。之所以需要Channel,是因为TCP连接的建立和释放都是十分昂贵的。

    四、RabbitMQ三大角色介绍

      通过上面的一些简单介绍,我相信你对MQ有了一个初步的印象。也许你会云里雾里的,到底是怎么运行起来的啊,来一个实际点的。哈哈,不急,下面马上进入RabbitMQ跑起来阶段。其实要跑起来,我们还要简单介绍一下RabbitMQ重要的三个角色:生产者、服务器、消费者。

      生产者:也就是消息生产方,通过RabbitMQ提高的API,将消息推送到RabbitMQ服务器。

      服务器:RabbitMQ的服务中心,接收生产者生产的消息,并根据分发规则,将消息推送到对应的消费者。

      消费者:顾名思义,就是消息的最终接收处理者。

      这样一来,我相信大家脑海里面已经有一个画面了,生产者--生成消息-->服务器--转发-->消费者(最终处理消息)。这就是一个消息的整体流程和生命周期。

    五、RabbitMQ跑起来

      通过上面的介绍,我们应该知道MQ的简单的消息交互的流程。有了这个基础,下面我们就分类来介绍一下三大角色的数据交付方式。整体上来说,数据交互方式上有以下5种方式(5种工作模式),在网上找了一张图,很方便的供大家参考。

      其实通过上面的图,我们会发现,前两种情况,消费者和生成者之间都是直接通过连接,后面三种情况,消费者和生产者直接有一层交换机(Exchange)。这样一来,我们可以从整体上分为两个大类:其一、消息直推队列;其二、消息推送给交换机,交换机根据路由规则转发至队列。

      其实在实际的工作中,第一大类,我们是不会使用到的,都是采用的第二大类来实现实际的项目开发需求。但是第一大类,能够很好的将我们先领我们入门,先简单的把程序跑起来。由于时间原因,今天我们也就先实现第一大类的两种情况,第二大类的,明后天在专门的文章来详细介绍。

    简单模式:

    简单模式就是只有一个生产者,一个消费者。这个很简单,下面用一个实际例子来说明。直接贴代码:

    生产者代码: 

    /// <summary>
     /// 消息生成者
     /// </summary>
    public class Program
    {
        static void Main(string[] args)
        {
            // rabbitMQ链接对象
            var factory = new ConnectionFactory();
            // RabbitMQ服务在本地运行
            factory.HostName = "192.168.1.1";
            // RabbitMQ服务端口
            factory.Port = 5672;
            // 用户名
            factory.UserName = "guest";
            // 密码
            factory.Password = "guest";
            // 虚拟主机名称
            factory.VirtualHost = "/";
    
            // 队列名称
            string queueName = "hello";
    
            // 创建链接
            using (var connection = factory.CreateConnection())
            {
                // 创建通道
                using (var channel = connection.CreateModel())
                {
                    // 创建一个名称为hello的消息队列--当然一步也可以通过RabbitMQ管理后台添加
                    // 当已经存在该队列时,不会重复添加,但是如果已存在的队列和新建的队列存在属性差异时,会创建失败,会抛异常,所以在实际使用时,如果要通过程序创建队列,最好要捕捉异常,避免因为这样的问题而导致程序崩溃。
                    channel.QueueDeclare(queueName, false, false, false, null);
                    Console.WriteLine("我是生成者");
    
                    while (true)
                    {
                        Console.WriteLine("请输入你要发送的消息,并按Enter键结束");
    
                        // 接收用户输入的消息
                        string message = Console.ReadLine();
                        // 消息编码
                        var body = Encoding.UTF8.GetBytes(message);
                        // 向消息服务器推送消息
                        channel.BasicPublish("", queueName, null, body);
    
                        Console.WriteLine($"已发送 {System.DateTime.Now.ToString("HH:mm:ss")}: {message}");
                    }
                }
            }
        }
    }
    

      消费者代码:

     /// <summary>
     /// 消息消费者
     /// </summary>
     public class Program
     {
         static void Main(string[] args)
         {
             // rabbitMQ链接对象
             var factory = new ConnectionFactory();
             // RabbitMQ服务在本地运行
             factory.HostName = "192.168.1.1";
             // RabbitMQ服务端口
             factory.Port = 5672;
             // 用户名
             factory.UserName = "guest";
             // 密码
             factory.Password = "guest";
             // 虚拟主机名称
             factory.VirtualHost = "/";
    
             // 队列名称
             string queueName = "hello";
    
             // 创建链接
             using (var connection = factory.CreateConnection())
             {
                 // 创建通道
                 using (var channel = connection.CreateModel())
                 {
    
                     // 创建一个名称为hello的消息队列--当然一步也可以通过RabbitMQ管理后台添加
                     // 当已经存在该队列时,不会重复添加,但是如果已存在的队列和新建的队列存在属性差异时,会创建失败,会抛异常,所以在实际使用时,如果要通过程序创建队列,最好要捕捉异常,避免因为这样的问题而导致程序崩溃。
                     channel.QueueDeclare(queueName, false, false, false, null);
                     Console.WriteLine("我是消费者");
    
                     // 创建一个消费者
                     var consumer = new EventingBasicConsumer(channel);
                     // 订阅对应的消息 autoAck:是否自动确认
                     channel.BasicConsume(queueName, autoAck:false, consumer);
    
                     consumer.Received += (model, ea) =>
                     {
                         var body = ea.Body;
                         var message = Encoding.UTF8.GetString(body);
                         Console.WriteLine($"已接收 {System.DateTime.Now.ToString("HH:mm:ss")}: {message}");
    
                         // 为了模拟推送过程,在此程序休息1分钟
                         Thread.Sleep(6000);
                         // 确认消费
                         channel.BasicAck(ea.DeliveryTag, false);
                     };
                     Console.ReadLine();
                 }
             }
         }
     }
    

      

    运行结果:

      通过实际的运行结果图,我们很清楚的知道,生产者的消息发生顺序,和消费者消费的顺序是一直的,这也就MQ的基本原理所在。

    上面介绍了简单模式,下面我在来介绍一下比简单模式复杂一点的工作模式。

    工作模式:

      我理解的简单模式,只是带我们入门,让我们明白MQ的运行效果是咋样的。但是在实际工作中,不可能只会有一个消费者,在实际的生产环境中生产者、消费者都可能会有多个存在,这也就是我们说的工作模式。那么,有多个生成的者的时候,不同的生产者之间又是怎么来消费消息的呢?下面我们先通过实践的例子来说明:

      具体的代码和上面的代码是一样的,我们可以直接开两个消费者就可以实现数据模拟,直接看运行结果:

      同上面的实际运行结果我们可以简单的得出以下结论:

      当一个队列有多个消费者时,在生成的实时消息时,消息队列服务器会轮询的均匀的分发给每一个消费者。

      哈哈哈,注意了,上面的结论我说的是实时消息哦,这里面就包含了一个坑,在实际的使用过程中要特别注意。那就是历史消息处理上,在实际项目使用过程中,我们经常会遇到,当消费者打开时,队列中已经有很多消息待消费,这个时候又该如何保证多个消费均匀分配消息呢?避免忙绿的消费者累死现象。其实很简单,只需在消费端加上如下一个配置即可:

     
     // 通过Qos设置每次接收消息的条数
     // 三个参数说明
     // prefetchSize:为预取的长度,一般设置为0即可,表示长度不限
     // prefetchCount:表示预取的条数,即发送的最大消息条数
     // global表示是否在Connection中全局设置,true表示Connetion下的所有channel都设置为这个配置。
     channel.BasicQos(prefetchSize: 0,
                      prefetchCount: 1,
                      global: false);
    

      

      上面的配置中,最关键的一个参数就是prefetchCount,当我们设置为1时,就是能够实现均匀的分发。下面分别对prefetchCount设置不同的值,来看看不同的效果:
      实例一:将prefetchCount设置为10,并生成3条历史消息,然后同时打开两个消费者,看看3条消息的分发消费情况:

      通过图,我们得出,3条历史消息全部推送给了一个消费者,这样就导致了一个消费者累死,一个消费者闲的慌。
      实例二:将prefetchCount设置为1,并生成4条历史消息,然后同时打开两个消费者,看看3条消息的分发消费情况:


      通过图,我们得出,4条历史消息平均的分发给了两个消费者,这也是我们想要的效果。
      所以在实际工作中,一定要注意这一个细节,不然有可能导致在服务器重启时,有的服务器直接卡死现象。
      好了,时间不早了,今天就先写到这,明天我们继续分享后面的几种模式。在分析完每一种模式后,我还好结合实际,封装一个dll出来,供大家参考,到时候也会直接把源码提出来。欢迎大家关注,持续交流。疫情无情,我们学习不能停。加油吧,每一个小伙伴!​

    END
    为了更高的交流,欢迎大家关注我的公众号,扫描下面二维码即可关注,谢谢:

  • 相关阅读:
    HDU 5438 Ponds
    [HNOI2013]比赛
    [HNOI2009]最小圈
    【模板】高斯消元法
    控制公司 Controlling Companies
    sdut 2878 圆圈
    滑雪
    [ZJOI2010]排列计数
    [HNOI2003]激光炸弹
    [BZOJ 3732]Network
  • 原文地址:https://www.cnblogs.com/xiaoXuZhi/p/RabbitMQ_01.html
Copyright © 2020-2023  润新知