RabbitMQ原理及集群的深入剖析

RabbitMQ原理及集群的深入剖析

  • 说在前面
    • RabbitMQ应用场景剖析
    • RabbitMQ的组件剖析
      • 说一说绑定过程:
      • Exchange类型
      • 一些事项
      • 一些思考
      • 关于持久化
    • RabbitMQ的集群详解
    • RabbitMQ的集群模式
      • 一、普通模式:
      • 二、镜像模式:
    • 消息中间件的比对分析
    • Win8下的RabbitMQ安装
    • RabbitMQ集群搭建
    • 生产-消费测试代码

说在前面

由于公司最近项目中使用到了RabbitMQ,以前只是停留在发消息、消费消息,通过学习一些资料,今日来记录和复习下RabbitMQ的基本原理及高级应用。本文主要阐述RabbitMQ工作原理及安装过程,以及集群的搭建过程,同时与常用的消息中间件进行了比对分析,简要阐述其不同的组件在不同场景下的应用。

文末是对RabbitMQ的安装及集群搭建实操过程,希望能对你带来帮助。

RabbitMQ应用场景剖析

在说到应用场景之前,我想这几个问题你不妨先思考一番;

  1. 什么时候会用到消息中间件?
    答:“当系统中出现“生产“和“消费“的速度或稳定性等因素不一致的时候,就需要消息队列”
  2. 为什么要用?
    本来蛮简单的一个系统,现在凭空接入一个中间件,是不是要考虑去维护它,而且使用的过程中还要要考虑各种问题,比如消息重复消费、消息丢失、消息的顺序消费等等。
  3. 消息中间件有什么具体作用?
    复杂系统的解耦,复杂链路的异步调用,瞬时高峰的削峰处理

这三个问题你清楚了,你对RabbitMQ的上手就快了!

  1. 订单-库存系统
    RabbitMQ原理及集群的深入剖析_第1张图片

订单系统:
用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
库存系统:
订阅下单的消息,获取下单消息,进行库操作。 就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。
(此图是不是有点熟悉,没事,下面的你也挺熟悉的。)

  1. 秒杀系统
    RabbitMQ原理及集群的深入剖析_第2张图片
    1.在服务器收到用户的请求之后,首先写入消息队列,假如消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。
    2.秒杀业务根据消息队列中的请求信息,再做后续处理。

  2. 微信、QQ红包

RabbitMQ原理及集群的深入剖析_第3张图片
1.用户的抢红包的请求首先写入消息队列.
2.红包业务根据消息队列中的请求信息,依次处理.

红包系统也好、库存系统也罢,相信通过枚举的这三种系统,你已经很清楚消息中间件的作用了。
到这,你也对RabbitMQ有了一个初步的认识,接下来我们来具体解刨一下RabbitMQ,它里面到底有什么!

RabbitMQ的组件剖析

RabbitMQ原理及集群的深入剖析_第4张图片
如图所示,此图为RabbitMQ整个系统内的结构。
Producer:消息生产者,就是投递消息的程序。
Broker:它提供一种传输服务,它的角色就是维护一条从生产者到消费者的路线,保证数据能按照指定的方式进行传输。
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
Queue:消息的载体,每个消息都会被投到一个或多个队列。
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来.
Routing Key:路由关键字,exchange根据这个关键字进行消息投递。
vhost:虚拟主机,一个broker里可以有多个vhost,用作不同用户的权限分离。
Consumer:消息消费者,就是接受消息的程序.
Channel:消息通道,在客户端的每个连接里,可建立多个channel.

通信过程如下
1:Producer生产消息,发送给服务器端的Exchange。
2:Exchange收到消息,根据ROUTINKEY,将消息转发给匹配的Queue。
3:Queue收到消息,将消息发送给订阅者Consumer。
4:Consumer收到消息,发送ACK给队列确认收到消息。
5:Queue收到ACK,删除队列中缓存的此条消息(ACK为确认机制,后续会做介绍)。

说一说绑定过程:

RabbitMQ原理及集群的深入剖析_第5张图片
交换机和队列之间的关系:
生产者将消息投递给交换机,然后由交换器路由到一个或多个队列中,在这里我们需要重点理解下,交换机是如何路由到对应的队列?

Bindings需要告诉exchange发送消息到队列中,exchange和queue之间的关系就叫binding。
RoutingKey和BindingKey生产者将消息发送给交换机,一般会指定一个RoutingKey,通过这个指定这个消息的路由规则,而这个RoutingKey需要与交换机的类型和Bindingkey联合使用才能生效。
RoutingKey是决定消息的流向的,生产者将消息发送给交换机时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。
Exchange和两个队列绑定在一起,假设队列1的bindingkey是orange,队列2的binding key是black和green. 当生产者的RoutingKey是orange时,exchange会把它放到队列1上,如果是black或green就会到队列2上,其余的Message被丢弃.

Exchange类型

Exchange交换机的类型有fanoutdirecttopicheaders四种模式。

RabbitMQ原理及集群的深入剖析_第6张图片

Fanout
会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中,也就是广播模式。
很容易理解,无定向,通通发出去。
RabbitMQ原理及集群的深入剖析_第7张图片

Direct
交换器会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中。
1:当生产者(P)发送消息时Rotuing key=booking时,这时候将消息传送给Exchange,Exchange获取到生产者发送过来消息后,会根据自身的规则进行与匹配相应的Queue,这时发现Queue1和Queue2都符合,就会将消息传送给这两个队列。
2:如果我们以Rotuing key=create和Rotuing key=confirm发送消息时,这时消息只会被推送到Queue2队列中,其他Routing Key的消息将会被丢弃。

应用场景
大规模多用户在线(MMO)游戏可以使用它来处理排行榜更新等全局事件
体育新闻网站可以用它来近乎实时地将比分更新分发给移动客户端,分发系统使用它来广播各种状态和配置更新
RabbitMQ原理及集群的深入剖析_第8张图片

topic
其与direct类型交换机规则些许不同,topic采用模糊匹配的方式,可以通过通配符满足一部分规则就可以传送:
1:routing key为一个句点号“.”用来分隔的字符串(句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit”。
2:binding key与routing key一样也是句点号“. ”分隔的字符串。
binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)

当生产者发送消息Routing Key=F.C.E的时候,这时候只满足Queue1,所以会被路由到Queue中,如果Routing Key=A.C.E这时候会被同是路由到Queue1和Queue2中,如果Routing Key=A.F.B时,这里只会发送一条消息到Queue2中。

应用场景:

1:例如销售点由多个工作者(workers)完成的后台任务,每个工作者负责处理某些特定的任务。
2:涉及到分类或者标签的新闻更新(例如,针对特定的运动项目或者队伍)。

一些事项

1:消息一般可以包含2个部分:消息体和标签(Label) 。消息体也可以称之为payload,在实际应用中,消息体一般是一个带有业务逻辑结构的数据,比如一个 JSON 字符串。
2:交换机匹配过程
★:Direct Routing Key=Binding Key,严格匹配
★:Fanout 把发送到该 Exchange 的消息路由到所有与它绑定的 Queue 中
★:Topic Routing Key=Binding Key,模糊匹配
★:Headers 根据发送的消息内容中的 headers 属性进行匹配
3:RabbitMQ中消息都只能存储在队列中,队列的特性是先进先出。
4:如果cosumer接受了消息, 但在发送ack之前断开连接,rabbitmq会认为这条消息没有被deliver,在consumer在次连接的时候,这条消息会被redeliver。

一些思考

1:如何确保消息正确地发送至 RabbitMQ? 如何确保消息接收方消费了消息?

发送方确认模式将信道设置成 confirm 模式(发送方确认模式),则所有在信道上发布的消息都会被指派一个唯一的 ID。一旦消息被投递到目的队列后,或者消息被写入磁盘后(可持久化的消息),信道会发送一个确认给生产者(包含消息唯一 ID)。

如果 RabbitMQ 发生内部错误从而导致消息丢失,会发送一条 nack(notacknowledged,未确认)消息。这里并没有用到超时机制,RabbitMQ 仅通过 Consumer 的连接中断来确认是否需要重新发送消息。

2:消息基于什么传输?

由于 TCP 连接的创建和销毁开销较大,且并发数受系统资源限制,会造成性能瓶颈。
RabbitMQ 使用信道的方式来传输数据。信道是建立在真实的 TCP 连接内的虚拟连接,且每条 TCP 连接上的信道数量没有限制。

3:消息产生者将消息放入队列消费者可以有多个消费!

消费者1,消费者2同时监听同一个队列,消息被消费。C1 C2共同争抢当前的消息队列内容,谁先拿到谁负责消费消息(隐患:高并发情况下,默认会产生某一个消息被多个消费者共同使用,可以设置一个开关(syncronize) 保证一条消息只能被一个消费者使用)。

关于持久化

1:为什么要持久化?

处理消息队列丢数据的情况,就需要开启持久化磁盘的配置,这个持久化配置可以和confirm机制配合使用,你可以在消息持久化磁盘后,再给生产者发送一个Ack信号。如果消息持久化磁盘之前,rabbitMQ阵亡了,那么生产者收不到Ack信号,生产者会自动重发。

2:如何实现持久化:

queue的持久化标识durable设置为true,deliveryMode=2, rabbitMQ挂了,重启后也能恢复数据消费者丢失消息。

3:当内存使用超过配置的阈值或者磁盘剩余控件低于配置的阈值时会发生什么?

rabbitmq会暂时阻塞客户端的连接,并停止接收从客户端发来的消息,以避免服务崩溃,客户端与服务端的心跳检测也会失败。

RabbitMQ的集群详解

集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。
一个rabbitmq集 群中可以共享 user,vhost,queue,exchange等,所有的数据和状态都是必须在所有节点上进行复制。
集群中有两种节点:
1 内存节点:只保存状态到内存(一个例外的情况是:持久的queue的持久内容将被保存到disk)。
2 磁盘节点:保存状态到内存和磁盘。内存节点虽然不写入磁盘,但是它执行速度要比磁盘块。集群中,只需要一个磁盘节点来保存状态就足够了,如果集群中只有内存节点,那么不能停止它们,否则所有的状态,消息等都会丢失。
良好的设计架构可以如下:在一个集群里,有3台以上机器,其中1台使用磁盘模式,其它使用内存模式。如果其它几台为内存模式的节点,速度将更快。而磁盘模式的节点,由于磁盘IO相对较慢,因此仅作数据备份使用。

RabbitMQ的集群模式

主备模式:主节点提供读写,从节点不提供读写服务,只负责提供备份服务,备份节点的主要功能是在主节点宕机时,自动切换
主从模式:主节点提供读写,从节点只读。
镜像模式
集群模式非常经典的就是Mirror镜像模式,保证100%数据不丢失,在实际工作中用的最多的。并且实现集群非常的简单,一般互联网大厂都会构建这种镜像集群模式
远程模式
远程模式:远距离通信和复制,所谓Shovel就是我们可以把消息进行不同数据中心的复制工作,我们可以跨地域的让两个mq集群互联
多活模式
这种模式也是实现异地数据复制的主流模式,因为Shovel模式配置比较复杂,所以一般来说实现异地集群都是使用双活或者多活模式来实现的。这种模式需要依赖rabbitmq的federation插件,可以实现继续的可靠AMQP数据通信,多活模式在实际配置与应用非常的简单。

接下来介绍下两种常用的也是常见的模式,其它模式的配置及安装我就不做具体介绍了,当然了,你熟悉了这两种模式后,并且自己手动搭建完毕后,其它模式的安装及配置,应该都不在话下了!

一、普通模式:

默认的集群模式,queue创建之后,如果没有其它policy,则queue就会按照普通模式集群。对于Queue来说,消息实体只存在于其中一个节点,A、B两个节点仅有相同的队列结构,但队列的元数据仅保存有一份,即创建该队列的rabbitmq节点(A节点),当A节点宕机,去B节点查看,./rabbitmqctl list_queues 发现该队列已经丢失,但声明的exchange还存在。
当消息进入A节点的Queue中后,consumer从B节点拉取时,RabbitMQ会临时在A、B间进行消息传输,把A中的消息实体取出并经过B发送给consumer,所以consumer应平均连接每一个节点,从中取消息。

该模式存在一个问题就是当A节点故障后,B节点无法取到A节点中还未消费的消息实体。如果做了队列持久化或消息持久化,那么得等A节点恢复,然后才可被消费,并且在A节点恢复之前其它节点不能再创建A节点已经创建过的持久队列;如果没有持久化的话,消息就会失丢。这种模式更适合非持久化队列,只有该队列是非持久的,客户端才能重新连接到集群里的其他节点,并重新创建队列。假如该队列是持久化的,那么唯一办法是将故障节点恢复起来。

为什么RabbitMQ不将队列复制到集群里每个节点呢?
答:
1。存储空间:如果每个集群节点每个队列的一个完整副本,增加节点需要更多的存储容量。例如,如果一个节点可以存储1 gb的消息,添加两个节点需要两份相同的1 gb的消息。
2。性能:发布消息需要将这些信息复制到每个集群节点。对持久消息,要求为每条消息触发磁盘活动在所有节点上。每次添加一个节点都会带来 网络和磁盘的负载。

二、镜像模式:

把需要的队列做成镜像队列,存在于多个节点(镜像模式是在普通模式的基础上,增加一些镜像策略)。

该模式解决了上述问题,其实质和普通模式不同之处在于,消息实体会主动在镜像节点间同步,而不是在consumer取数据时临时拉取。该模式带来的副作用也很明显,除了降低系统性能外,如果镜像队列数量过多,加之大量的消息进入,集群内部的网络带宽将会被这种同步通讯大大消耗掉。所以在对可靠性要求较高的场合中适用,一个队列想做成镜像队列,需要先设置policy,然后客户端创建队列的时候,rabbitmq集群根据“队列名称”自动设置是普通集群模式或镜像队列

下面拿主备模式和镜像模式为例子做个简单介绍:
RabbitMQ原理及集群的深入剖析_第9张图片
主备模式实现RabbitMQ的高可用集群,一般在并发和数据量不高的情况下,这种模式非常的好且简单。
RabbitMQ原理及集群的深入剖析_第10张图片
Mirror镜像队列,目的是为了保证rabbitmq数据的高可靠性解决方案,主要就是实现数据的同步,一般用2-3个镜像实现数据同步

消息中间件的比对分析

RabbitMQ的优缺点及比对分析
1:RabbitMQ比Kafka可靠,Kafka更适合IO高吞吐的处理,一般应用在大数据日志处理或对实时性(少量延迟),可靠性(少量丢数据)要求稍低的场景使用,比如ELK日志收集。
2:RabbitMQ,使用erlang 语言,阻止了大量的 Java 工程师去深入研究和掌控它,对公司而言,几乎处于不可控的状态,但是人家确实是开源的,单机支持1万以上持久化队列,比较稳定的支持,活跃度也高;
3:Kafka是LinkedIn开源的分布式发布-订阅消息系统,一开始的目的就是用于日志收集和传输,目前归属于Apache顶级项目。Kafka主要为高吞吐量的订阅发布系统而设计,在一台普通的服务器上既可以达到10W/s的吞吐速率,完全的分布式系统,自动实现负载均衡,无需停机即可扩展机器。
4:kafka具有高的吞吐量,内部采用消息的批量处理,zero-copy机制,数据的存储和获取是本地磁盘顺序批量操作,具有O(1)的复杂度,消息处理的效率很高。量大对数据不是百分之百保证的,会有数据丢失,不是百分百送达。吞吐量有提升,就得有牺牲,所以kafka适合大数据量流转,比如日志数据、统计的数据。
5:rabbitMQ在吞吐量方面稍逊于kafka,他们的出发点不一样,rabbitMQ支持对消息的可靠的传递,支持事务,不支持批量的操作;存储可以采用内存或者硬盘。
6:RocketMQ是阿里开源的消息中间件,它是纯Java开发,具有亿级消息堆积能力、单机支持1万以上持久化队列;高可用性、适合大规模分布式系统应用的特点。 RocketMQ思路起源于Kafka,但并不是Kafka的一个Copy,它对消息的可靠传输及事务性做了优化,目前在阿里集团被广泛应用于交易、充值、流计算、消息推送、日志流式处理、binglog分发等场景。
支持的客户端语言不多,目前是Java及C++,其中C++还不成熟,RocketMQ没有.NET下的客户端可用。
RocketMQ身出名门,但使用者不多,生态较小,毕竟消息量能达到这种体量的公司不多。

小结:
1:RabbitMQ中的AMQP协议的主要特征是面向消息、队列、路由(包括点对点和发布/订阅)、可靠性、安全。AMQP协议更多用在企业系统内,对数据一致性、稳定性和可靠性要求很高的场景,对性能和吞吐量的要求还在其次。
2:Kafka主要特点是基于Pull的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输。0.8版本开始支持复制,不支持事务,对消息的重复、丢失、错误没有严格要求,适合产生大量数据的互联网服务的数据收集业务。
3:RocketMQ:定位于非日志的可靠消息传输,目前RocketMQ在阿里集团被广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理等场景。
4:消息队列实际上是一种非常复杂的架构,引入它肯定有很多好处,但是事物总有相对的一面,给你带来便利的同时肯定会增加系统的复杂度,此外整个系统的稳定性及数据的一致性也会随之降低,我们就需要用额外的技术方案和架构来规避掉。

在众多消息中间件中没有说那一个更好,只有哪一个更合适,因此在选择消息中间件之前需先冷静分析自己实际的业务需求,从而选择更合适自己的业务的中间件。

在熟悉了RabbitMQ的原理之后,是不是迫不及待想上手安装测试一把了?肯定的,下面记录的是整个安装过程,在这部分中会讲到一些采坑的经历,以及一些经验分享。

Win8下的RabbitMQ安装

1:安装准备
在这里插入图片描述
这两个东东分别是ErLang环境与RabbitMQserver,(网上能搜到很多安装包)不要问我为什么装两个,因为人家rabbitmq就是这个东东开发的,你肯定要先把人家的环境装起来不是。

2:先安装Erlang 如图所示,比较简单,双击打开第一个安装包,一路装到底。
就是一直Next->Next->Next。

RabbitMQ原理及集群的深入剖析_第11张图片
3:Erlang安装好之后,打开rabbitmq-server-3.7.7.exe,同理一直装到底即可!

RabbitMQ原理及集群的深入剖析_第12张图片
安装完rabbitmq服务器,接着要把它打开呀,下面是启动过程:
RabbitMQ原理及集群的深入剖析_第13张图片
把第一个红框内的路径删除,在此输入CMD,这样就进入到了此目录下的DOS环境了,接着输入如下命令:

rabbitmq-plugins enable rabbitmq_management

RabbitMQ原理及集群的深入剖析_第14张图片
起来之后,重启一下服务器,怎么重启呢,双击下面红框内的批处理文件即可。
RabbitMQ原理及集群的深入剖析_第15张图片
至此单机版的rabbitmq就安装完成啦!赶紧打开浏览器访问一下:http://127.0.0.1:15672

备注:默认的登录账户和密码均为guest

RabbitMQ原理及集群的深入剖析_第16张图片
不知道你可发现我的端口号和别人的不太一样,别人的都是5678,我的居然是12345。

废话不多说,如果你后面有需要更改相应的端口号,请参考以下修改方法。

去找一下你的rabbitmq的日志文件,按照下图的提示,你找一下就能找到。

RabbitMQ原理及集群的深入剖析_第17张图片
然后双击打开这个日志文件,找到下面这个配置文件,然后双击打开,里面是个空的方括号。

把 []. 改为 [{rabbitmq_management,[{listener, [{port, 12345}]}]}].

这样端口号就改好了。然后重启一下Rabbitmq服务即可。

在这里插入图片描述

至此单机版的就搞定了!是不是不太够点意思,来,我们一起再装个集群!

RabbitMQ集群搭建

1:搭建集群,你首先肯定要有两台机子吧!并且两台机器要求能Ping的通。
赶紧去准备两台,ping的时候记得记录一下两台机器的IP和机器名。
2:通的吧?好,确认是通的之后你要按照我上面说的继续搭建一台单机版的RabbitMQ,等你一会儿,你先安装。
3:另外一台也装好了吧?那我默认你装好了哈!
4:接着打开一台机器的此目录:C:\Windows\System32\drivers\etc 找到host文件,在文件底部追加两台主机信息(修改后记得保存)。

RabbitMQ原理及集群的深入剖析_第18张图片
5:拷贝此host文件,覆盖另外一台主机此目录下此文件。

6:输入以下命令这样两台机器就互通啦!

rabbitmqctl stop_app
rabbitmqctl join_cluster rabbit@LENOV05-PC
rabbitmqctl start_app

两台机器互加之后,使用 rabbitmqctl cluster_status 命令查看集群状态。

再访问UI界面就会发现你的Nodes变成了如下两个。
RabbitMQ原理及集群的深入剖析_第19张图片
读到这里,相信你已经可以实现以上操作了,至于别的一些集群模式,笔者也没有做太详细的探索,不过有了这些铺垫,相信你再去接触别的集群模式也不会太头大。

说说代码吧,下面给出的代码不会太多,贴一部分消息收发的过程(C#)

生产-消费测试代码

发消息

// 生产者模拟代码:
public static void Cluster_Produce()
        {
            try
            {
                var factory = new ConnectionFactory();
                factory.UserName = userName;
                factory.Password = password;
                factory.AutomaticRecoveryEnabled = true;
                factory.TopologyRecoveryEnabled = true;

                string[] hostnames = custerIp.Split(';');
                using (var connection = factory.CreateConnection(hostnames))
                {
                    using (var channel = connection.CreateModel())
                    {
                        bool durable = true;
                        channel.QueueDeclare(queueName, durable, false, false, null);

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

                        for (int i = 0; i < 10000000; i++)
                        {
                            try
                            {
                                string msg = $"消息{i}";
                                Log.Info($"发送的消息:{msg}");
                                Console.WriteLine($"发送的消息:{msg}");
                                var body = Encoding.UTF8.GetBytes(msg);
                                channel.BasicPublish("", queueName, properties, body);
                            }
                            catch (Exception e)
                            {
                                Log.Error(e.ToString());
                                Console.WriteLine(e.ToString());
                            }
                            finally
                            {
                                Thread.Sleep(10);
                            }
                        }
                        Console.WriteLine("完成");
                    }
                }

            }
            catch (Exception e)
            {
                Console.WriteLine(e.ToString());
                Log.Error(e.ToString());
            }
        }

收消息

// 消费者模拟代码:
private static void Cluster_Consume2()
        {
            var factory = new ConnectionFactory();
            factory.Port = port;
            factory.HostName = "10.17.234.126";
            //factory.HostName = "10.17.234.218";
            factory.UserName = userName;
            factory.Password = password;
            factory.AutomaticRecoveryEnabled = true;
            factory.TopologyRecoveryEnabled = true;

            string[] hostnames = custerIp.Split(';');
            using (var connection = factory.CreateConnection())
            {
                using (var channel = connection.CreateModel())
                {
                    bool durable = true;
                    channel.QueueDeclare(mQueueName, durable, false, false, null);
                    channel.BasicQos(0, 1, false);
                    var consumer = new EventingBasicConsumer(channel);

                    consumer.Received += (model, ea) =>
                    {
                        try
                        {
                            var body = ea.Body;
                            var message = Encoding.UTF8.GetString(body);
                            Log.Info($"接收的消息:{message}");
                            Console.WriteLine($"接收的消息:{message}");
                        }
                        catch (Exception e)
                        {
                            Console.WriteLine(e.ToString());
                            Log.Error(e.ToString());
                        }
                        finally
                        {
                            channel.BasicAck(ea.DeliveryTag, false);
                        }
                    };

                    bool noASK = false;
                    channel.BasicConsume(mQueueName, noASK, consumer);

                    Console.ReadLine();
                }
            }
        }

以上就是我近期对RabbitMQ的学习及理解,如果有疑问欢迎在下方评论。后期有时间的话准备把Kafka的一些原理及实操发出来,一起学习,一起进步吧!

你可能感兴趣的:(RabbitMQ的深入剖析,rabbitmq,队列,交换机)