.NET 环境中使用RabbitMQ RabbitMQ与Redis队列对比 RabbitMQ入门与使用篇

.NET 环境中使用RabbitMQ

 

在企业应用系统领域,会面对不一样系统之间的通讯、集成与整合,尤为当面临异构系统时,这种分布式的调用与通讯变得愈加重要。其次,系统中通常会有不少对实时性要求不高的可是执行起来比较较耗时的地方,好比发送短信,邮件提醒,更新文章阅读计数,记录用户操做日志等等,若是实时处理的话,在用户访问量比较大的状况下,对系统压力比较大。html

面对这些问题,咱们通常会将这些请求,放在消息队列中处理;异构系统之间使用消息进行通信。消息传递相较文件传递与远程过程调用(RPC)而言,彷佛更胜一筹,由于它具备更好的平台无关性,并可以很好地支持并发与异步调用。因此若是系统中出现了以下状况:node

  • 对操做的实时性要求不高,而须要执行的任务极为耗时;
  • 存在异构系统间的整合;

通常的能够考虑引入消息队列。对于第一种状况,经常会选择消息队列来处理执行时间较长的任务。引入的消息队列就成了消息处理的缓冲区。消息队列引入的异步通讯机制,使得发送方和接收方都不用等待对方返回成功消息,就能够继续执行下面的代码,从而提升了数据处理的能力。尤为是当访问量和数据流量较大的状况下,就能够结合消息队列与后台任务,经过避开高峰期对大数据进行处理,就能够有效下降数据库处理数据的负荷。git

在前面的一篇讲解CQRS模式的文章中,全部的对系统的状态的更改都是经过事件来完成,通常的将事件存储到消息队列中,而后进行统一的处理。github

本文简单介绍在RabbitMQ这一消息代理工具,以及在.NET中如何使用RabbitMQ.数据库

一 环境搭建

首先,因为RabbitMQ使用Erlang编写的,须要运行在Erlang运行时环境上,因此在安装RabbitMQ Server以前须要安装Erlang 运行时环境,能够到Erlang官网下载对应平台的安装文件。若是没有安装运行时环境,安装RabbitMQ Server的时候,会提示须要先安装Erlang环境。 安装完成以后,确保已经将Erlang的安装路径注册到系统的环境变量中。安装完Erlang以后,这个环境会自动设置,若是没有,在administrator环境下在控制台下面输入,也能够设置:数组

Setx  ERLANG_HOME “D:\Program Files (x86)\erl6.3″

Erlang Enviroment Path

而后,去RabbitMQ官网下载RabbitMQ Server服务端程序,选择合适的平台版本下载。安装完成以后,就能够开始使用了。浏览器

如今就能够对RabbitMQ Server进行配置了。缓存

首先,切换到RabbitMQ Server的安装目录:bash

RabbitMQ Install folder

在sbin下面有不少batch文件,用来控制RabbitMQ Server,固然您也能够直接在安装开始菜单中来执行相应的操做:服务器

RabitMQ Menu

最简单的方式是使RabbitMQ以Windows Service的方式在后台运行,因此咱们须要以管理员权限打开cmd,而后切换到sbin目录下,执行这三条命令便可:

rabbitmq-service install
rabbitmq-service enable
rabbitmq-service start

Start RabbitMQ Service

如今RabbitMQ的服务端已经启动起来了。

下面可使用sbin目录下面的rabbitmqctl.bat这个脚原本查看和控制服务端状态的,在cmd中直接运行rabbitmqctl status。若是看到如下结果:

Unable to connect node

显示node没有链接上,须要到C:\Windows目录下,将.erlang.cookie文件,拷贝到用户目录下 C:\Users\{用户名},这是Erlang的Cookie文件,容许与Erlang进行交互,如今重复运行刚才的命令就会获得以下信息:

rabbit mq status

RabbitMQ Server上面也有用户概念,安装好以后,使用rabbitmqctl list_users命令,能够看到上面目前的用户:

RabbitMQ users

能够看到,如今只有一个角色为administrator的名为guest的用户,这个是RabbitMQ默认为咱们建立的,他有RabbitMQ的全部权限,通常的,咱们须要新建一个咱们本身的用户,设置密码,并授予权限,并将其设置为管理员,可使用下面的命令来执行这一操做:

rabbitmqctl  add_user  yy  hello!
rabbitmqctl  set_permissions  yy  ".*"  ".*"  ".*"
rabbitmqctl  set_user_tags yy administrator

Create RabbitMQ user

上面的一条命令添加了一个名为yy的用户,并设置了密码hello!,下面的命令为用户yy分别授予对全部消息队列的配置、读和写的权限。

如今咱们能够将默认的guest用户删掉,使用下面的命令便可:

rabbitmqctl delete_user guest

若是要修改密码,可使用下面的命令:

rabbitmqctl change_password {username}  {newpassowrd}

二 开始使用

在.NET中使用RabbitMQ须要下载RabbitMQ的客户端程序集,能够到官网下载,下载解压后就能够获得RabbitMQ.Client.dll,这就是RabbitMQ的客户端。

在使用RabitMQ以前,须要对下面的几个基本概念说明一下:

RabbitMQ是一个消息代理。他从消息生产者(producers)那里接收消息,而后把消息送给消息消费者(consumer)在发送和接受之间,他可以根据设置的规则进行路由,缓存和持久化。

通常提到RabbitMQ和消息,都用到一些专有名词。

  • 生产(Producing)意思就是发送。发送消息的程序就是一个生产者(producer)。咱们通常用"P"来表示:

producer

  • 队列(queue)就是邮箱的名称。消息经过你的应用程序和RabbitMQ进行传输,它们只能存储在队列(queue)中。 队列(queue)容量没有限制,你要存储多少消息均可以——基本上是一个无限的缓冲区。多个生产者(producers)可以把消息发送给同一个队列,一样,多个消费者(consumers)也能从同一个队列(queue)中获取数据。队列能够画成这样(图上是队列的名称):

queue

  • 消费(Consuming)和获取消息是同样的意思。一个消费者(consumer)就是一个等待获取消息的程序。咱们把它画做"C":

consumer

一般,消息生产者,消息消费者和消息代理不在同一台机器上。

2.1 Hello World

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

rabbitmq hello world

首先建立一个控制台程序,用来将消息发送到RabbitMQ的消息队列中,代码以下:

    static void Main(string[] args)
    {
        var factory = new ConnectionFactory();
        factory.HostName = "localhost";
        factory.UserName = "yy";
        factory.Password = "hello!";

        using (var connection = factory.CreateConnection())
        {
            using (var channel = connection.CreateModel())
            {
                channel.QueueDeclare("hello", false, false, false, null);
                string message = "Hello World";
                var body = Encoding.UTF8.GetBytes(message);
                channel.BasicPublish("", "hello", null, body);
                Console.WriteLine(" set {0}", message);
            }
        }
    }

首先,须要建立一个ConnectionFactory,设置目标,因为是在本机,因此设置为localhost,若是RabbitMQ不在本机,只须要设置目标机器的IP地址或者机器名称便可,而后设置前面建立的用户名yy和密码hello!。

紧接着要建立一个Channel,若是要发送消息,须要建立一个队列,而后将消息发布到这个队列中。在建立队列的时候,只有RabbitMQ上该队列不存在,才会去建立。消息是以二进制数组的形式传输的,因此若是消息是实体对象的话,须要序列化和而后转化为二进制数组。

如今客户端发送代码已经写好了,运行以后,消息会发布到RabbitMQ的消息队列中,如今须要编写服务端的代码链接到RabbitMQ上去获取这些消息。

一样,建立一个名为Receive的服务端控制台应用程序,服务端代码以下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("hello", true, consumer);

            Console.WriteLine(" waiting for message.");
            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);
                Console.WriteLine("Received {0}", message);

            }
        }
    }
}

和发送同样,首先须要定义链接,而后声明消息队列。要接收消息,须要定义一个Consume,而后从消息队列中不断Dequeue消息,而后处理。

如今发送端和接收端的代码都写好了,运行发送端,发送消息:

SendHelloword

如今,名为hello的消息队列中,发送了一条消息。这条消息存储到了RabbitMQ的服务器上了。使用rabbitmqctl 的list_queues能够查看全部的消息队列,以及里面的消息个数,能够看到,目前Rabbitmq上只有一个消息队列,里面只有一条消息:

D:\Program Files\RabbitMQ Server\rabbitmq_server-3.4.2\sbin>rabbitmqctl list_queues
Listing queues ...
hello   1

如今运行接收端程序,以下:

ReceiveHelloWorld

能够看到,已经接受到了客户端发送的Hello World,如今再来看RabitMQ上的消息队列信息:

D:\Program Files\RabbitMQ Server\rabbitmq_server-3.4.2\sbin>rabbitmqctl list_queues
Listing queues ...
hello   0

能够看到,hello这个队列中的消息队列个数为0,这表示,当接收端,接收到消息以后,RabbitMQ上就把这个消息删掉了。

2.2 工做队列

前面的例子展现了如何往一个指定的消息队列中发送和收取消息。如今咱们建立一个工做队列(work queue)来将一些耗时的任务分发给多个工做者(workers):

rabbitmq-work queue

工做队列(work queues, 又称任务队列Task Queues)的主要思想是为了不当即执行并等待一些占用大量资源、时间的操做完成。而是把任务(Task)看成消息发送到队列中,稍后处理。一个运行在后台的工做者(worker)进程就会取出任务而后处理。当运行多个工做者(workers)时,任务会在它们之间共享。

这个在网络应用中很是有用,它能够在短暂的HTTP请求中处理一些复杂的任务。在一些实时性要求不过高的地方,咱们能够处理完主要操做以后,以消息的方式来处理其余的不紧要的操做,好比写日志等等。

准备

在第一部分,发送了一个包含“Hello World!”的字符串消息。如今发送一些字符串,把这些字符串看成复杂的任务。这里使用time.sleep()函数来模拟耗时的任务。在字符串中加上点号(.)来表示任务的复杂程度,一个点(.)将会耗时1秒钟。好比"Hello..."就会耗时3秒钟。

对以前示例的send.cs作些简单的调整,以即可以发送随意的消息。这个程序会按照计划发送任务到咱们的工做队列中。

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);
            string message = GetMessage(args);
            var properties = channel.CreateBasicProperties();
            properties.DeliveryMode = 2;

            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish("", "hello", properties, body);
            Console.WriteLine(" set {0}", message);
        }
    }

    Console.ReadKey();
}

private static string GetMessage(string[] args)
{
    return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
}

加粗部分是通过修改过了的。

接着咱们修改接收端,让他根据消息中的逗点的个数来Sleep对应的秒数:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            channel.QueueDeclare("hello", false, false, false, null);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("hello", true, consumer);

            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);

                int dots = message.Split('.').Length - 1;
                Thread.Sleep(dots * 1000);
                        
                Console.WriteLine("Received {0}", message);
                Console.WriteLine("Done");
            }
        }
    }
}

轮询分发

使用工做队列的一个好处就是它可以并行的处理队列。若是堆积了不少任务,咱们只须要添加更多的工做者(workers)就能够了,扩展很简单。

如今,咱们先启动两个接收端,等待接受消息,而后启动一个发送端开始发送消息。

Send message queue 

在cmd条件下,发送了5条消息,每条消息后面的逗点表示该消息须要执行的时长,来模拟耗时的操做。

而后能够看到,两个接收端依次接收到了发出的消息:

receive message queue 

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

2.3 消息响应

当处理一个比较耗时得任务的时候,也许想知道消费者(consumers)是否运行到一半就挂掉。在当前的代码中,当RabbitMQ将消息发送给消费者(consumers)以后,立刻就会将该消息从队列中移除。此时,若是把处理这个消息的工做者(worker)停掉,正在处理的这条消息就会丢失。同时,全部发送到这个工做者的尚未处理的消息都会丢失。

咱们不想丢失任何任务消息。若是一个工做者(worker)挂掉了,咱们但愿该消息会从新发送给其余的工做者(worker)。

为了防止消息丢失,RabbitMQ提供了消息响应(acknowledgments)机制。消费者会经过一个ack(响应),告诉RabbitMQ已经收到并处理了某条消息,而后RabbitMQ才会释放并删除这条消息。

若是消费者(consumer)挂掉了,没有发送响应,RabbitMQ就会认为消息没有被彻底处理,而后从新发送给其余消费者(consumer)。这样,即便工做者(workers)偶尔的挂掉,也不会丢失消息。

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

消息响应默认是开启的。在以前的例子中使用了no_ack=True标识把它关闭。是时候移除这个标识了,当工做者(worker)完成了任务,就发送一个响应。

channel.BasicConsume("hello", false, consumer);

while (true)
{
    var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

    var body = ea.Body;
    var message = Encoding.UTF8.GetString(body);

    int dots = message.Split('.').Length - 1;
    Thread.Sleep(dots * 1000);

    Console.WriteLine("Received {0}", message);
    Console.WriteLine("Done");

    channel.BasicAck(ea.DeliveryTag, false);
}

如今,能够保证,即便正在处理消息的工做者被停掉,这些消息也不会丢失,全部没有被应答的消息会被从新发送给其余工做者.

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

rabbitmqctl list_queues name messages_ready messages_unacknowledged
Listing queues ...
hello    0       0
...done.

2.4 消息持久化

前面已经搞定了即便消费者down掉,任务也不会丢失,可是,若是RabbitMQ Server停掉了,那么这些消息仍是会丢失。

当RabbitMQ Server 关闭或者崩溃,那么里面存储的队列和消息默认是不会保存下来的。若是要让RabbitMQ保存住消息,须要在两个地方同时设置:须要保证队列和消息都是持久化的。

首先,要保证RabbitMQ不会丢失队列,因此要作以下设置:

bool durable = true;
channel.QueueDeclare("hello", durable, false, false, null);

虽然在语法上是正确的,可是在目前阶段是不正确的,由于咱们以前已经定义了一个非持久化的hello队列。RabbitMQ不容许咱们使用不一样的参数从新定义一个已经存在的同名队列,若是这样作就会报错。如今,定义另一个不一样名称的队列:

bool durable = true;
channel.queueDeclare("task_queue", durable, false, false, null);

queueDeclare 这个改动须要在发送端和接收端同时设置。

如今保证了task_queue这个消息队列即便在RabbitMQ Server重启以后,队列也不会丢失。 而后须要保证消息也是持久化的, 这能够经过设置IBasicProperties.SetPersistent 为true来实现:

var properties = channel.CreateBasicProperties();
properties.SetPersistent(true);

须要注意的是,将消息设置为持久化并不能彻底保证消息不丢失。虽然他告诉RabbitMQ将消息保存到磁盘上,可是在RabbitMQ接收到消息和将其保存到磁盘上这之间仍然有一个小的时间窗口。 RabbitMQ 可能只是将消息保存到了缓存中,并无将其写入到磁盘上。持久化是不可以必定保证的,可是对于一个简单任务队列来讲已经足够。若是须要消息队列持久化的强保证,可使用publisher confirms

2.5 公平分发

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

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

channel.BasicQos(0, 1, false); 

2.6 完整实例

如今将全部这些放在一块儿:

发送端代码以下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
                   
            bool durable = true;
            channel.QueueDeclare("task_queue", durable, false, false, null);
                    
            string message = GetMessage(args);
            var properties = channel.CreateBasicProperties();
            properties.SetPersistent(true);
                  

            var body = Encoding.UTF8.GetBytes(message);
            channel.BasicPublish("", "task_queue", properties, body);
            Console.WriteLine(" set {0}", message);
        }
    }

    Console.ReadKey();
}

private static string GetMessage(string[] args)
{
    return ((args.Length > 0) ? string.Join(" ", args) : "Hello World!");
}

接收端代码以下:

static void Main(string[] args)
{
    var factory = new ConnectionFactory();
    factory.HostName = "localhost";
    factory.UserName = "yy";
    factory.Password = "hello!";

    using (var connection = factory.CreateConnection())
    {
        using (var channel = connection.CreateModel())
        {
            bool durable = true;
            channel.QueueDeclare("task_queue", durable, false, false, null);
            channel.BasicQos(0, 1, false);

            var consumer = new QueueingBasicConsumer(channel);
            channel.BasicConsume("task_queue", false, consumer);

            while (true)
            {
                var ea = (BasicDeliverEventArgs)consumer.Queue.Dequeue();

                var body = ea.Body;
                var message = Encoding.UTF8.GetString(body);

                int dots = message.Split('.').Length - 1;
                Thread.Sleep(dots * 1000);

                Console.WriteLine("Received {0}", message);
                Console.WriteLine("Done");

                channel.BasicAck(ea.DeliveryTag, false);
            }
        }
    }
}

三 管理界面

RabbitMQ还有一个管理界面,经过该界面能够查看RabbitMQ Server 当前的状态,该界面是以插件形式提供的,而且在安装RabbitMQ的时候已经自带了该插件。须要作的是在RabbitMQ控制台界面中启用该插件,命令以下:

rabbitmq-plugins enable rabbitmq_management

rabbitmq management

如今,在浏览器中输入 http://server-name:15672/ server-name换成机器地址或者域名,若是是本地的,直接用localhost(RabbitMQ 3.0以前版本端口号为55672)在输入以后,弹出登陆界面,使用咱们以前建立的用户登陆。

RabbitMQ Web management.

在该界面上能够看到当前RabbitMQServer的全部状态。

四 总结

本文简单介绍了消息队列的相关概念,并介绍了RabbitMQ消息代理的基本原理以及在Windows 上如何安装RabbitMQ和在.NET中如何使用RabbitMQ。消息队列在构建分布式系统和提升系统的可扩展性和响应性方面有着很重要的做用,但愿本文对您了解消息队列以及如何使用RabbitMQ有所帮助。

五 参考文献

  1. http://www.infoq.com/cn/articles/message-based-distributed-architecture
  2. http://www.rabbitmq.com/getstarted.html
  3. http://www.codethinked.com/using-rabbitmq-with-c-and-net
  4. http://www.infoq.com/cn/articles/AMQP-RabbitMQ
  5. http://www.infoq.com/cn/articles/ebay-scalability-best-practices
做者:  yangecnuyangecnu's Blog on 博客园) 
出处: http://www.cnblogs.com/yangecnu/ 
做品由yangecnu 创做,采用知识共享署名-非商业性使用-禁止演绎 2.5 中国大陆许可协议进行许可。 欢迎转载,但任何转载必须保留完整文章,在显要地方显示署名以及原文连接。如您有任何疑问或者受权方面的协商,请 给我留言
 
 
 

RabbitMQ与Redis队列对比

2017-11-09 15:46 阅读 786 views 次 评论 0 条

本文仅针对RabbitMQ与Redis作队列应用时的状况进行对比,
具体采用什么方式实现,还须要取决于系统的实际需求

 

简要介绍

 

 

RabbitMQ

RabbitMQ是实现AMQP(高级消息队列协议)的消息中间件的一种,最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。消息中间件主要用于组件之间的解耦,消息的发送者无需知道消息使用者的存在,反之亦然。

 

Redis

是一个Key-Value的NoSQL数据库,开发维护很活跃,虽然它是一个Key-Value数据库存储系统,但它自己支持MQ功能,因此彻底能够当作一个轻量级的队列服务来使用。

 

具体对比

 

 

可靠消费

Redis:没有相应的机制保证消息的消费,当消费者消费失败的时候,消息体丢失,须要手动处理
RabbitMQ:具备消息消费确认,即便消费者消费失败,也会自动使消息体返回原队列,同时可全程持久化,保证消息体被正确消费

 

可靠发布

Reids:不提供,需自行实现
RabbitMQ:具备发布确认功能,保证消息被发布到服务器

 

高可用

Redis:采用主从模式,读写分离,可是故障转移尚未很是完善的官方解决方案
RabbitMQ:集群采用磁盘、内存节点,任意单点故障都不会影响整个队列的操做

 

持久化

Redis:将整个Redis实例持久化到磁盘
RabbitMQ:队列,消息,均可以选择是否持久化

 

消费者负载均衡

Redis:不提供,需自行实现
RabbitMQ:根据消费者状况,进行消息的均衡分发

 

队列监控

Redis:不提供,需自行实现
RabbitMQ:后台能够监控某个队列的全部信息,(内存,磁盘,消费者,生产者,速率等)

 

流量控制

Redis:不提供,需自行实现
RabbitMQ:服务器过载的状况,对生产者速率会进行限制,保证服务可靠性

 

出入队性能

对于RabbitMQ和Redis的入队和出队操做,各执行100万次,每10万次记录一次执行时间。
测试数据分为128Bytes、512Bytes、1K和10K四个不一样大小的数据。

注:此数据来源于互联网,部分数据有误,已修正


 

应用场景分析

Redis:轻量级,高并发,延迟敏感
即时数据分析、秒杀计数器、缓存等

RabbitMQ:重量级,高并发,异步
批量数据异步处理、并行任务串行化,高负载任务的负载均衡等

 

 

版权声明:本文版权由 木秀林网全部,转载请保留连接: RabbitMQ与Redis队列对比
 
 

RabbitMQ入门与使用篇

 

介绍

RabbitMQ是一个由erlang开发的基于AMQP(Advanced Message Queue)协议的开源实现。用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面都很是的优秀。是当前最主流的消息中间件之一。

RabbitMQ的官方

image

  • 概念:
    • Brocker:消息队列服务器实体。
    • Exchange:消息交换机,指定消息按什么规则,路由到哪一个队列。
    • Queue:消息队列,每一个消息都会被投入到一个或者多个队列里。
    • Binding:绑定,它的做用是把exchange和queue按照路由规则binding起来。
    • Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
    • Vhost:虚拟主机,一个broker里能够开设多个vhost,用做不用用户的权限分离。
    • Producer:消息生产者,就是投递消息的程序。
    • Consumer:消息消费者,就是接受消息的程序。
    • Channel:消息通道,在客户端的每一个链接里,可创建多个channel,每一个channel表明一个会话任务。
  • 消息队列的使用过程大概以下:
    • 消息接收
      • 客户端链接到消息队列服务器,打开一个channel。
      • 客户端声明一个exchange,并设置相关属性。
      • 客户端声明一个queue,并设置相关属性。
      • 客户端使用routing key,在exchange和queue之间创建好绑定关系。
    • 消息发布
      • 客户端投递消息到exchange。
      • exchange接收到消息后,就根据消息的key和已经设置的binding,进行消息路由,将消息投递到一个或多个队列里。
  • AMQP 里主要要说两个组件:
    • Exchange 和 Queue
    • 绿色的 X 就是 Exchange ,红色的是 Queue ,这二者都在 Server 端,又称做 Broker
    • 这部分是 RabbitMQ 实现的,而蓝色的则是客户端,一般有 Producer 和 Consumer 两种类型。
  • Exchange一般分为四种:
    • fanout:该类型路由规则很是简单,会把全部发送到该Exchange的消息路由到全部与它绑定的Queue中,至关于广播功能
    • direct:该类型路由规则会将消息路由到binding key与routing key彻底匹配的Queue中
    • topic:与direct类型类似,只是规则没有那么严格,能够模糊匹配和多条件匹配
    • headers:该类型不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配
  • 使用场景

下载与安装

  • 下载
  • 安装
    • 先安装erlang
    • 而后再安装rabbitmq

管理工具

操做起来很简单,只须要在DOS下面,进入安装目录(安装路径\RabbitMQ Server\rabbitmq_server-3.2.2\sbin)执行以下命令就能够成功安装。

rabbitmq-plugins enable rabbitmq_management 

能够经过访问:http://localhost:15672进行测试,默认的登录帐号为:guest,密码为:guest。

图片

其余配置

1. 安装完之后erlang须要手动设置ERLANG_HOME 的系统变量。

set ERLANG_HOME=F:\Program Files\erl9.0 #环境变量`path`里加入:%ERLANG_HOME%\bin #环境变量`path`里加入: 安装路径\RabbitMQ Server\rabbitmq_server-3.6.10\sbin 

2.激活Rabbit MQ’s Management Plugin

使用Rabbit MQ 管理插件,能够更好的可视化方式查看Rabbit MQ 服务器实例的状态,你能够在命令行中使用下面的命令激活。

rabbitmq-plugins.bat  enable rabbitmq_management 

3.建立管理用户

rabbitmqctl.bat add_user sa 123456

4. 设置管理员

rabbitmqctl.bat set_user_tags sa administrator

5.设置权限

rabbitmqctl.bat set_permissions -p / sa ".*" ".*" ".*" 

6. 其余命令

#查询用户: rabbitmqctl.bat list_users #查询vhosts: rabbitmqctl.bat list_vhosts #启动RabbitMQ服务: net stop RabbitMQ && net start RabbitMQ 

以上这些,帐号、vhost、权限、做用域等基本就设置完了。


基于.net使用

RabbitMQ.Client 是RabbiMQ 官方提供的的客户端 
EasyNetQ 是基于RabbitMQ.Client 基础上封装的开源客户端,使用很是方便

如下操做RabbitMQ的代码例子,都是基于EasyNetQ的使用和再封装,在文章底部有demo例子的源码下载地址


建立 IBus

/// <summary> /// 消息服务器链接器 /// </summary> public class BusBuilder { public static IBus CreateMessageBus() { // 消息服务器链接字符串 // var connectionString = ConfigurationManager.ConnectionStrings["RabbitMQ"]; string connString = "host=127.0.0.1:5672;virtualHost=TestQueue;username=sa;password=123456"; if (connString == null || connString == string.Empty) throw new Exception("messageserver connection string is missing or empty"); return RabbitHutch.CreateBus(connString); } } 

Fanout Exchange

image

全部发送到Fanout Exchange的消息都会被转发到与该Exchange 绑定(Binding)的全部Queue上。 
Fanout Exchange 不须要处理RouteKey 。只须要简单的将队列绑定到exchange 上。这样发送到exchange的消息都会被转发到与该交换机绑定的全部队列上。相似子网广播,每台子网内的主机都得到了一份复制的消息。 因此,Fanout Exchange 转发消息是最快的。


/// <summary> /// 消息消耗(fanout) /// </summary> /// <typeparam name="T">消息类型</typeparam> /// <param name="handler">回调</param> /// <param name="exChangeName">交换器名</param> /// <param name="queueName">队列名</param> /// <param name="routingKey">路由名</param> public static void FanoutConsume<T>(Action<T> handler, string exChangeName = "fanout_mq", string queueName = "fanout_queue_default", string routingKey = "") where T : class { var bus = BusBuilder.CreateMessageBus(); var adbus = bus.Advanced; var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Fanout); var queue = CreateQueue(adbus, queueName); adbus.Bind(exchange, queue, routingKey); adbus.Consume(queue, registration => { registration.Add<T>((message, info) => { handler(message.Body); }); }); } /// <summary> /// 消息上报(fanout) /// </summary> /// <typeparam name="T">消息类型</typeparam> /// <param name="topic">主题名</param> /// <param name="t">消息命名</param> /// <param name="msg">错误信息</param> /// <returns></returns> public static bool FanoutPush<T>(T t, out string msg, string exChangeName = "fanout_mq", string routingKey = "") where T : class { msg = string.Empty; try { using (var bus = BusBuilder.CreateMessageBus()) { var adbus = bus.Advanced; var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Fanout); adbus.Publish(exchange, routingKey, false, new Message<T>(t)); return true; } } catch (Exception ex) { msg = ex.ToString(); return false; } } 

image 
全部发送到Direct Exchange的消息被转发到RouteKey中指定的Queue。 
Direct模式,可使用RabbitMQ自带的Exchange:default Exchange 。因此不须要将Exchange进行任何绑定(binding)操做 。消息传递时,RouteKey必须彻底匹配,才会被队列接收,不然该消息会被抛弃。


/// <summary> /// 消息发送(direct) /// </summary> /// <typeparam name="T">消息类型</typeparam> /// <param name="queue">发送到的队列</param> /// <param name="message">发送内容</param> public static void DirectSend<T>(string queue, T message) where T : class { using (var bus = BusBuilder.CreateMessageBus()) { bus.Send(queue, message); } } /// <summary> /// 消息接收(direct) /// </summary> /// <typeparam name="T">消息类型</typeparam> /// <param name="queue">接收的队列</param> /// <param name="callback">回调操做</param> /// <param name="msg">错误信息</param> /// <returns></returns> public static bool DirectReceive<T>(string queue, Action<T> callback, out string msg) where T : class { msg = string.Empty; try { var bus = BusBuilder.CreateMessageBus(); bus.Receive<T>(queue, callback); } catch (Exception ex) { msg = ex.ToString(); return false; } return true; } /// <summary> /// 消息发送 /// <![CDATA[(direct EasyNetQ高级API)]]> /// </summary> /// <typeparam name="T"></typeparam> /// <param name="t"></param> /// <param name="msg"></param> /// <param name="exChangeName"></param> /// <param name="routingKey"></param> /// <returns></returns> public static bool DirectPush<T>(T t, out string msg, string exChangeName = "direct_mq", string routingKey = "direct_rout_default") where T : class { msg = string.Empty; try { using (var bus = BusBuilder.CreateMessageBus()) { var adbus = bus.Advanced; var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Direct); adbus.Publish(exchange, routingKey, false, new Message<T>(t)); return true; } } catch (Exception ex) { msg = ex.ToString(); return false; } } /// <summary> /// 消息接收 /// <![CDATA[(direct EasyNetQ高级API)]]> /// </summary> /// <typeparam name="T">消息类型</typeparam> /// <param name="handler">回调</param> /// <param name="exChangeName">交换器名</param> /// <param name="queueName">队列名</param> /// <param name="routingKey">路由名</param> public static bool DirectConsume<T>(Action<T> handler, out string msg, string exChangeName = "direct_mq", string queueName = "direct_queue_default", string routingKey = "direct_rout_default") where T : class { msg = string.Empty; try { var bus = BusBuilder.CreateMessageBus(); var adbus = bus.Advanced; var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Direct); var queue = CreateQueue(adbus, queueName); adbus.Bind(exchange, queue, routingKey); adbus.Consume(queue, registration => { registration.Add<T>((message, info) => { handler(message.Body); }); }); } catch (Exception ex) { msg = ex.ToString(); return false; } return true; } 

Topic Exchange

image

  • 消息发布(Publish)

要使用主题发布,只需使用带有主题的重载的Publish方法:

var bus = RabbitHutch.CreateBus(...); bus.Publish(message, "X.A"); 

订阅者能够经过指定要匹配的主题来过滤邮件。

  • 这些能够包括通配符:
    • *=>匹配一个字。
    • #=>匹配到零个或多个单词。

因此发布的主题为“X.A.2”的消息将匹配“#”,“X.#”,“* .A.*”,而不是“X.B. *”或“A”。

警告,Publish只顾发送消息到队列,可是无论有没有消费端订阅,因此,发布以后,若是没有消费者,该消息将不会被消费甚至丢失。

  • 消息订阅(Subscribe)

EasyNetQ提供了消息订阅,当调用Subscribe方法时候,EasyNetQ会建立一个用于接收消息的队列,不过与消息发布不一样的是,消息订阅增长了一个参数,subscribe_id.代码以下:

bus.Subscribe("my_id", handler, x => x.WithTopic("X.*")); 

警告: 具备相同订阅者但不一样主题字符串的两个单独订阅可能不会产生您指望的效果。 subscriberId有效地标识个体AMQP队列。 具备相同subscriptionId的两个订阅者将链接到相同的队列,而且二者都将添加本身的主题绑定。 因此,例如,若是你这样作:

bus.Subscribe("my_id", handlerOfXDotStar, x => x.WithTopic("X.*")); bus.Subscribe("my_id", handlerOfStarDotB, x => x.WithTopic("*.B")); 

匹配“x.”或“ .B”的全部消息将被传递到“XXX_my_id”队列。 而后,RabbitMQ将向两个消费者传递消息,其中handlerOfXDotStar和handlerOfStarDotB轮流获取每条消息。

如今,若是你想要匹配多个主题(“X. ”OR“ .B”),你可使用另外一个重载的订阅方法,它采用多个主题,以下所示:

bus.Subscribe("my_id", handler, x => x.WithTopic("X.*").WithTopic("*.B")); 

/// <summary> /// 获取主题 /// </summary> /// <typeparam name="T">主题内容类型</typeparam> /// <param name="subscriptionId">订阅者ID</param> /// <param name="callback">消息接收响应回调</param> /// <param name="topics">订阅主题集合</param> public static void TopicSubscribe<T>(string subscriptionId, Action<T> callback, params string[] topics) where T : class { var bus = BusBuilder.CreateMessageBus(); bus.Subscribe(subscriptionId, callback, (config) => { foreach (var item in topics) config.WithTopic(item); }); } /// <summary> /// 发布主题 /// </summary> /// <typeparam name="T">主题内容类型</typeparam> /// <param name="topic">主题名称</param> /// <param name="message">主题内容</param> /// <param name="msg">错误信息</param> /// <returns></returns> public static bool TopicPublish<T>(string topic, T message, out string msg) where T : class { msg = string.Empty; try { using (var bus = BusBuilder.CreateMessageBus()) { bus.Publish(message, topic); return true; } } catch (Exception ex) { msg = ex.ToString(); return false; } } /// <summary> /// 发布主题 /// </summary> /// <![CDATA[(topic EasyNetQ高级API)]]> /// <typeparam name="T">消息类型</typeparam> /// <param name="t">消息内容</param> /// <param name="topic">主题名</param> /// <param name="msg">错误信息</param> /// <param name="exChangeName">交换器名</param> /// <returns></returns> public static bool TopicSub<T>(T t, string topic, out string msg, string exChangeName = "topic_mq") where T : class { msg = string.Empty; try { if (string.IsNullOrWhiteSpace(topic)) throw new Exception("推送主题不能为空"); using (var bus = BusBuilder.CreateMessageBus()) { var adbus = bus.Advanced; //var queue = adbus.QueueDeclare("user.notice.zhangsan"); var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Topic); adbus.Publish(exchange, topic, false, new Message<T>(t)); return true; } } catch (Exception ex) { msg = ex.ToString(); return false; } } /// <summary> /// 获取主题 /// </summary> /// <![CDATA[(topic EasyNetQ高级API)]]> /// <typeparam name="T">消息类型</typeparam> /// <param name="subscriptionId">订阅者ID</param> /// <param name="callback">回调</param> /// <param name="exChangeName">交换器名</param> /// <param name="topics">主题名</param> public static void TopicConsume<T>(Action<T> callback, string exChangeName = "topic_mq",string subscriptionId = "topic_subid", params string[] topics) where T : class { var bus = BusBuilder.CreateMessageBus(); var adbus = bus.Advanced; var exchange = adbus.ExchangeDeclare(exChangeName, ExchangeType.Topic); var queue = adbus.QueueDeclare(subscriptionId); foreach (var item in topics) adbus.Bind(exchange, queue, item); adbus.Consume(queue, registration => { registration.Add<T>((message, info) => { callback(message.Body); }); }); } 

具体发布/订阅消息的Demo和相关测试看源码Demo

image

注意

当在建立订阅者去消费队列的时候

/// <summary> /// 获取主题 /// </summary> /// <param name="topic"></param> public static void GetSub<T>(T topic, Action<T> callback) where T : class { using (var bus = BusBuilder.CreateMessageBus()) { bus.Subscribe<T>(topic.ToString(), callback, x => x.WithTopic(topic.ToString())); } } 

using里的对象在执行完成后被回收了,致使刚链接上去就又断开了(刚开始写的时候,习惯性加using,排查了很久才发现,欲哭无泪)

源码项目运行前的准备与确认:

到RabbitMQ管理后台添加TestQueueVHost,而且分配用户权限,而后到RabbitMQHelper.BusBuilder类里配置RabbitMQ链接服务的相关信息 host=127.0.0.1:5672;virtualHost=TestQueue;username=sa;password=123456,(根据配置的内容和用户修改)

image


参考资料(鸣谢):


附:Demo源码GitHub地址 


 

欢迎到原文地址关注和交流

相关文章
相关标签/搜索