VB.net 2010 视频教程 VB.net 2010 视频教程 python基础视频教程
SQL Server 2008 视频教程 c#入门经典教程 Visual Basic从门到精通视频教程
当前位置:
首页 > Python基础教程 >
  • C#队列学习笔记:RabbitMQ安装及使用

 一、环境搭建

    1.1、由于RabbitMQ是使用Erlang语言开发的,因此要安装Erlang运行时环境,下载地址:Erlang官网下载  CSDN分享下载

    1.2、去RabbitMQ官网下载RabbitMQ Server服务端程序,选择合适的平台版本下载并安装。

    RabbitMQ安装时,会自动在Windows服务中创建RabbitMQ服务,并自动启动。

    1.3、开始->所有程序->RabbitMQ Server->RabbitMQ Command Prompt (sbin dir):

    运行RabbitMQ Command Prompt与cmd下cd C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.3\sbin的效果是一样的。

    1.3.1、sbin目录下的rabbitmqctl.bat,是用来查看和控制服务端状态的。运行rabbitmqctl status检查RabbitMQ状态:

    1.3.3、RabbitMQ Server上面也有用户概念,使用rabbitmqctl list_users命令,可以看到目前的用户:

    可以看到,现在只有一个名为gues角色为administratort的用户,这个是RabbitMQ默认为我们创建的,它有RabbitMQ的所有权限。一般情况下,我们需要新建一个自己的用户,并设置密码及授予权限,同时设置为管理员。操作方法如下:

rabbitmqctl add_user hello world
rabbitmqctl set_permissions hello ".*" ".*" ".*"
rabbitmqctl set_user_tags hello administrator

    上面的第一命令添加了一个名为hello的用户并设置了密码world;第二条命令为用户hello分别授予对所有消息队列的配置、读和写的权限;第三条命令将用户hello设置为管理员。

    现在我们可以将默认的guest用户删掉,使用下面的命令即可:

rabbitmqctl delete_user guest

    如果要修改密码,可以使用下面的命令:

rabbitmqctl change_password {username} {newpassowrd}

   二、管理界面

    RabbitMQ还有一个管理界面,是以插件形式提供的,通过该界面可以查看RabbitMQ Server当前的状态。启用命令如下: 

rabbitmq-plugins enable rabbitmq_management

    现在,在浏览器中输入 http://server-name:15672/ 即可。

    注:server-name为计算机名或IP地址,如果是本地的,直接用localhost即可。登录界面,使用我们之前创建的hello用户登录。

    三、开始使用

    在.NET中使用RabbitMQ需要下载RabbitMQ客户端程序集,下载解压后在bin下找到RabbitMQ.Client.dll,并添加引用到项目中。

    3.1、Hello World

    为了展示RabbitMQ的基本使用,我们发送一个HelloWorld消息,然后接收并处理。

rabbitmq hello world

    3.1.1、创建一个名为Send的客户端控制台程序,用来将消息发送到RabbitMQ消息队列中,代码如下:

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.建立信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5.构建byte消息数据包
                    string message = args.Length > 0 ? args[0] : "Hello World";
                    var body = Encoding.UTF8.GetBytes(message); //消息是以二进制数组的形式传输的
                    //6.发送数据包
                    channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
                    Console.WriteLine($"Send {message}");
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码

    3.1.2、创建一个名为Receive的服务端控制台程序,用来接收RabbitMQ消息队列中的消息,代码如下:

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.创建信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5.构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //6.绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body);
                        Console.WriteLine($"Received {message}");
                    };
                    //7.启动消费者
                    channel.BasicConsume(queue: "hello", noAck: true, consumer: consumer);
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码

    3.1.3、先运行消息接收端,再运行消息发送端,结果如下:

    从上面的代码中可以看出,发送端和接收端的代码前4步都是一样的。主要的区别在于发送端调用channel.BasicPublish方法发送消息,而接收端需要实例化一个EventingBasicConsumer实例来进行消息处理。另外一点需要注意的是:消息接收端和发送端的队列名称(queue)必须保持一致,这里指定的队列名称为hello。

    3.2、工作队列

    工作队列(work queues,又称Task Queues)的主要思想是:为了避免立即执行一些实时性要求不高但是比较耗资源或时间的操作(如写日志),把任务当作消息发送到队列中,由一个运行在后台的工作者(worker)进程取出并处理。当有多个工作者(workers)运行时,任务会在它们之间共享。

    现在发送一些字符串来模拟耗时的任务,在字符串中加上点号(.)来表示任务的复杂程度。一个点号将会耗时1秒钟,比如"Hello World..."就会耗时3秒钟。

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.建立信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5.构建byte消息数据包
                    string message = args.Length > 0 ? string.Join(" ", args) : "Hello World...";
                    var properties = channel.CreateBasicProperties();
                    properties.DeliveryMode = 2;//设置消息是否持久化 1:非持久化 2:持久化
                    var body = Encoding.UTF8.GetBytes(message);//消息是以二进制数组的形式传输的
                    //6.发送数据包
                    channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: null, body: body);
                    Console.WriteLine($"Send {message}");
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码
复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.创建信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5.构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //6.绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body);
                        int dots = message.Split('.').Length - 1;
                        Thread.Sleep(dots * 1000);
                        Console.WriteLine($"Received {message}");
                    };
                    //7.启动消费者
                    channel.BasicConsume(queue: "hello", noAck: true, consumer: consumer);
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码

    3.3轮询分发

    使用工作队列的一个好处就是它能够并行的处理队列。如果堆积了很多任务,我们只需要添加更多的工作者(workers)就可以了,扩展很简单。

    现在,我们先启动两个接收端,等待接受消息,然后启动一个发送端开始发送消息(cmd->send.exe所在的目录)。

    上面发了10条信息,两个接收端各收到5条信息。

    默认情况下,RabbitMQ会将每个消息按照顺序依次分发给下一个消费者,所以每个消费者接收到的消息个数大致是平均的。 这种消息分发的方式称之为轮询(round-robin)。

    3.4、消息响应

    当处理一个比较耗时得任务的时候,也许想知道消费者(consumers)是否运行到一半就挂掉。在当前的代码中,当RabbitMQ将消息发送给消费者之后,马上就会将该消息从队列中移除。此时,如果把处理这个消息的工作者(worker)停掉,正在处理的这条消息就会丢失。同时,所有发送到这个工作者的还没有处理的消息都会丢失。

    我们不想丢失任何任务消息,如果一个工作者挂掉了,我们希望该消息能够重新发送给其它的工作者。

    为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)机制。消费者会通过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,然后RabbitMQ才会释放并删除这条消息。如果消费者挂掉了,没有发送响应,RabbitMQ就会认为消息没有被完全处理,然后重新发送给其它消费者。这样,即使工作者偶尔的挂掉,也不会丢失消息。

    消息是没有超时这个概念的。当工作者与它断开连的时候,RabbitMQ会重新发送消息。这样在处理一个耗时非常长的消息任务的时候就不会出问题了。

    消息响应默认是开启的。在之前的例子中使用了no_Ack=true标识把它关闭。是时候移除这个标识了,当工作者完成了任务,就会发送一个响应。

    下面修改Receive.cs,主要改动的是:将 autoAck:true修改为autoAck:fasle,以及在消息处理完毕后手动调用BasicAck方法进行手动消息确认。

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.创建信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列
                    channel.QueueDeclare(queue: "hello", durable: false, exclusive: false, autoDelete: false, arguments: null);
                    //5.构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //6.绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body);
                        int dots = message.Split('.').Length - 1;
                        Thread.Sleep(dots * 1000);
                        Console.WriteLine($"Received {message}");
                        //7.发送消息确认信号(手动消息确认)
                        channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
                    };
                    //8.启动消费者(noAck: false 启用消息响应)
                    channel.BasicConsume(queue: "hello", noAck: false, consumer: consumer);
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码

    一个很常见的错误就是忘掉了BasicAck这个方法,这个错误很常见,但是后果很严重。当客户端退出时,待处理的消息就会被重新分发,但是RabitMQ会消耗越来越多的内存,因为这些没有被应答的消息不能够被释放。调试这种case,可以使用rabbitmqctl打印messages_unacknowledged字段。

C:\Program Files\RabbitMQ Server\rabbitmq_server-3.8.3\sbin>rabbitmqctl list_queues name messages_ready messages_unacknowledged
Timeout: 60.0 seconds ...
Listing queues for vhost / ...
name    messages_ready  messages_unacknowledged
hello   1       0

    3.5、消息持久化

    消息确认确保了即使消费端异常,消息也不会丢失能够被重新分发处理。但是如果RabbitMQ服务端异常,消息依然会丢失。除非我们指定durable:true,否则当RabbitMQ退出或崩溃时,消息将依然会丢失。通过指定durable:true(队列),并指定Persistent=true(消息),来告知RabbitMQ将消息持久化。一句话概括:需要保证队列和消息都是持久化的。

复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.建立信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列(指定durable:true,告知rabbitmq对消息进行持久化。)
                    channel.QueueDeclare(queue: "hello", durable: true, exclusive: false, autoDelete: false, arguments: null);
                    //将消息标记为持久性 - 将IBasicProperties.SetPersistent设置为true
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    //5.构建byte消息数据包
                    string message = args.Length > 0 ? args[0] : "Hello World";
                    var body = Encoding.UTF8.GetBytes(message);//消息是以二进制数组的形式传输的
                    //6.发送数据包(指定basicProperties)
                    channel.BasicPublish(exchange: "", routingKey: "hello", basicProperties: properties, body: body);
                    Console.WriteLine($"Send {message}");
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码
复制代码
    class Program
    {
        static void Main(string[] args)
        {
            #region Hello World
            //1.实例化连接工厂
            var factory = new ConnectionFactory
            {
                HostName = "localhost",
                UserName = "hello",
                Password = "world"
            };
            //2.建立连接
            using (var connection = factory.CreateConnection())
            {
                //3.创建信道
                using (var channel = connection.CreateModel())
                {
                    //4.声明队列(指定durable:true,告知rabbitmq对消息进行持久化。)
                    channel.QueueDeclare(queue: "hello", durable: true, exclusive: false, autoDelete: false, arguments: null);
                    //将消息标记为持久性 - 将IBasicProperties.SetPersistent设置为true
                    var properties = channel.CreateBasicProperties();
                    properties.Persistent = true;
                    //5.构造消费者实例
                    var consumer = new EventingBasicConsumer(channel);
                    //6.绑定消息接收后的事件委托
                    consumer.Received += (model, ea) =>
                    {
                        var message = Encoding.UTF8.GetString(ea.Body);
                        int dots = message.Split('.').Length - 1;
                        Thread.Sleep(dots * 1000);
                        Console.WriteLine($"Received {message}");
                        //7.发送消息确认信号(手动消息确认)
                        channel.BasicAck(deliveryTag: ea.DeliveryTag, multiple: false);
                    };
                    //8.启动消费者(noAck: false 启用消息响应)
                    channel.BasicConsume(queue: "hello", noAck: false, consumer: consumer);
                    Console.Read();
                }
            }
            #endregion
        }
    }
复制代码

    将消息标记为持久性不能完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但是当RabbitMQ接受消息并且还没有保存时​​,仍然有一个很短的时间窗口。RabbitMQ可能只是将消息保存到了缓存中,并没有将其写入到磁盘上。持久化不是一定能够保证的,但是对于一个简单任务队列来说已经足够。

    如果需要确保消息队列的持久化,可以使用publisher confirms。

    3.6、公平分发

    你可能会注意到,消息的分发可能并没有如我们想要的那样公平分配。比如,对于两个工作者。当奇数个消息的任务比较重但是偶数个消息任务比较轻时,奇数个工作者始终处于忙碌状态,而偶数个工作者始终处于空闲状态,但是RabbitMQ并不知道这些,它仍然会平均依次地分发消息。

    为了改变这一状态,我们可以使用basicQos方法,设置perfetchCount=1 。这样就告诉RabbitMQ 不要在同一时间给一个工作者发送多于1个的消息。换句话说,在一个工作者还在处理消息并且没有响应消息之前,不要给它分发新的消息,而是将这条新的消息发送给下一个不那么忙碌的工作者。

复制代码
//Receive.cs
//4.声明队列(指定durable:true,告知rabbitmq对消息进行持久化。)
//channel.QueueDeclare(queue: "hello", durable: true, exclusive: false, autoDelete: false, arguments: null);
channel.QueueDeclare("hello", true, false, false, null);
//将消息标记为持久性 - 将IBasicProperties.SetPersistent设置为true
var properties = channel.CreateBasicProperties();
properties.Persistent = true;
//设置prefetchCount : 1来告知RabbitMQ,在未收到消费端的消息确认时,不再分发消息。
//channel.BasicQos(prefetchSize: 0, prefetchCount: 1, global: false);
channel.BasicQos(0, 1, false);
复制代码

    3.7完整实例

复制代码
    class Program
    {
        static