简单了解RabbitMQ

来源:互联网 发布:微信网络出错1001 编辑:程序博客网 时间:2024/06/11 18:44

      在项目中会经常用到消息队列,现在我们来简单了解一下消息队列以加深对MQ的理解。

      到目前接触了三种MQ,一个是最常见的RabbitMQ,剩下的两个是公司目前在用的KafkaSwallow。这里详讲RabbitMQ,后边再带一下后者。

     MQ的基本概念这里不赘述了,自行搜索。

     RabbitMQ使用场景:

        1.可用来解藕繁复的业务。

        2.服务之间或服务内部的通信。(最常见)

        3.可用来实现异步调用。

    阅读RabbitMQ官方tutorial,有以下几个点需要关注:

    1)最基本的消息队列模型

           p是生产者,c是消费者

           红色的队列可简单理解为存储消息的buffer


    2)多消费者模型



        这里的问题是:多个消费者监听同一个队列的消息,那么消息是以广播的形式分发到各消费者吗?当然不是。

        这就是所谓的消费组的概念:一个消费组可能包含多个消费者,一条消息只能被消费组内的某个消费者消费。可以把消费组抽象出来,理解为处理消息的逻辑单元。消费者可以横向扩展,以加快消息的处理速率。

       Round-robin dispatching

       那么如何保证消费组内的每个消费者并行工作呢?如果这一点做不好,那么多消费者将不能显著提高消息的处理速率,这样的话,多个消费者的模式没有任何意义。

       Round-robindispatching(轮询分发)是最简单的负载均衡策略,即消息队列将消息依次轮流发送给消费者,以两消费者为例:第奇数个消息总是发给C1,第偶数个消息总是发给C2。例如10个消息:C1消费:13579C2:246810;粗略的看,这样至少让每个消费者都有了事做,多消费者模型也有了意义。(tips:知识都是相通的,操作系统从最初的单道系统进化到多道系统,最初提出的时间片轮转调度也是这个思想:就是让每个进程轮流使用固定的cpu间,时间到,换下一个进程使用,这样从宏观上来看,就是多个任务在同时跑。)

       当然,RR这种简单的负载均衡策略显然不能做到公平分发,比如第奇数个消息处理起来总是比较麻烦,第偶数个消息总是比较简单,这样就会造成C1很忙而C2很闲,这显然不行。

       RabbitMQ实际中采用方法channel.basicQos(int prefetchCount)做到公平分发,perfetchCount表示队列一次分发给消费者的消息数,比如我们将perfetchCount的值设置为1,就是说,消费者收到一条消息进行处理,处理成功后会通知队列,此时队列知道消息消费完了才会再次给此消费者指派消息。通俗来讲,这样做就是把消息总是发给闲着的消费者。这样,多消费者模型就做到了在实际的生产环境中加快消费速率。


     3)Message acknowledgment

      Message acknowledement(确认重传机制)是RabbitMQ的一个重要的特性,刚才其实我们已经提到了,就是消费者在消费成功后会发一条ack消息来通知队列这条消息已经被处理了,如果没有收到ack(比如消费者宕机,网络传输丢包),队列认为这条消息没有被消费成功,将重新发送,直到收到ack。这样做的好处是显然易见的,就是让消息的消费是有保障的,不好的地方也很明显,这样消息队列要一一确认每个消息是否消费成功,这显然会降低消息的处理速率。(tips:ack在许多地方都有应用,比如TCP协议)


    4)Message durability

    ACK机制确实显著提高了消息消费的保障性,是不是只要这样就万无一失了?当然不是,设想如果队列服务器宕机,那么存储在队列中的消息都会丢失。RabbitMQ通过持久化消息到磁盘来保障消息不会丢失,队列服务器宕机后,服务重启,会从磁盘上读取消息来恢复宕机之前的队列状态。为了达到持久化的目的,我们需要将队列和消息都设置为持久化。

     队列持久化:

       boolean durable= true;

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

     消息持久化:

       import com.rabbitmq.client.MessageProperties;

       channel.basicPublish("","task_queue",

                MessageProperties.PERSISTENT_TEXT_PLAIN,

                message.getBytes());

     RabbitMQ通过上述方法做到了消息消费的保障,RabbitMQ的保障性是其一大特点,当然也牺牲了吞吐量为代价,RabbitMQ为消息的保障做了许多,后面我们还会讲到其他的灾备措施。


  5) Publish/Subscribe

    在实际使用中,消息队列的模型一般如下:

     

           X为Exchange(类型为topic),可以理解为交换机,起到路由的作用,生产者不会直接将消息发送到消息队列,是将消息发送给Exchange,生产者甚至感知不到消息队列的存在,1个Exchange对应n个队列,生产者将消息发送到Exchange,消息中带着路由信息这里是Rounting key,消息队列会与Exchange绑定从而接收转发的消息,绑定时会确定一个bindingkey即为上图中的*.orange.*,当Exchange收到的生产者发来的消息时,会解析其中的routingkey,当routingkey与某个binding key匹配时,便会发向绑定的那个队列中,当匹配不到时,则会将消息丢弃。(消息队列与exchange绑定会指定一个binding key,binding key就是表明该消息队列想接收什么类型的消息。)
          exchange的类型有direct,topic,headers 和 fanout,实际使用中,一般都是使用topic类型的Exchange,因其能满足的使用场景最多;其他几个类型相对比较简单,有兴趣花几分钟查一下就明白了,这里不再赘述。
          从设计模式的角度讲,上述模型是典型的“发布/订阅”模式,自己有兴趣也可以写个demo简单实现一下过程。
         理解这个模型,也可以类比别的场景,个人觉得这个与客户端向服务端发送http请求的场景有些相似:1.客户端http请求头中的IP地址,想当于生产者发消息带的rounting key;2.客户端将请求发送到路由器,路由器解析请求中的IP地址,将其路由到服务端。这里Exchange相当于扮演了路由器的角色,exchange解析生产者发送的rounting key,根据rountingkey判断应该发到哪些队列中去。


         6)基于Rabbit MQ实现RPC


            简单提一下,根据官方tutorial,RabbitMQ还可以用来实现rpc框架,因为rabbit mq消息传递的可靠性,所以用他来实现RPC是可行的。
            大致过程为:客户端发送调用请求,exchange接收到发送的调用请求路由到服务端,服务端收到消息解析调用后将结果再通过消息的方式回复给客户端,这样就完成了一次基本的调用。调用过程中,客户端扮演了生产者的角色而服务端扮演了消费者的角色。当服务端返回调用结果给客户端时,他们的角色同时也颠倒了过来。
                            
           基于mq实现的rpc与普通的rpc有什么区别呢?
           显而易见,基于mq实现的rpc是异步的,因为整个调用过程都是通过消息来传递。


              7)RabbitMQ的应用
          RabbitMQ因其消息传递的可靠性,在许多地方都受到青睐,特别是在跟金融有关的项目中,我第一次听说RabbitMQ就是朋友在银行实习的时候其项目中有用到这个组件,将其介绍给我。Rabbit MQ在项目Openstack中也扮演了很重要的角色,其内部各个组件的通信、协作很多都是通过RabbitMQ完成的,至于Openstack具体是什么,我只是曾经在实习中很浅的接触过,当时我所在的部门是虚拟化,openstack是一个虚拟化的云平台(大概是一个协调管理虚拟机的系统),我们的产品有用到它,有兴趣的可以下去了解一下,了解其对Rabbit MQ的应用。


          8)RabbitMQ集群
              横向扩展能力是任何消息中间件都应该具备的基本素质。Rabbit MQ有两种集群模式:
            1.默认的集群模式:
             
              横向扩展队列:以上图为例,消费者可在任何一个队列读取消息进行消费,但exchange发给队列的消息实体,只有其中某个固定的队列可以接收,也就是说,所有的消息只存在于某个固定的队列节点中(我们将其记为主队列),当消费者在非主队列进行消费时,主队列会将消息发给这个非主队列,非主队列再将消息发给其消费者。这样做的缺点很明显:如果主队列发生宕机之类的故障,那么整个集群将瘫痪。为什么不能让集群中的所有队列都接收消息呢?这样做基本上意义不大,横向扩展的初衷是通过扩展节点线性的提升性能,这样做保障性是上来了,但对性能上没有什么帮助。


              2.镜像模式
            我的理解,镜像模式相当于是在吞吐能力和数据保障性之间做了个折中方案,对可靠性要求比较高的队列,可将其做为镜像队列,即数据同时存在于多个队列中,多个队列之间的消息保持同步。但提高了可靠性难免会降低性能,如果镜像队列比较多,消息也多,那么队列中消息的变化就比较频繁,而各个镜像队列又必须保持消息同步,那么消息的同步必然会消耗性能,从而使吞吐量下降。


           可见,Rabbit MQ在许多方面都为消息的保障性做了措施,选择怎样的模式要根据实际的使用场景来决定,比如日志系统,对消息的保障性要求不高,希望有一个高的吞吐量,那么从单个队列考虑,我们可以关掉ack和持久化,从集群的角度,我们可以使用默认的集群模式。

 

---------------------------------------------------------

       Kafka:

           Kafka集群的结构我简单画个图来说明


          broker:可以理解为队列,负责存储消息及将消息发送给消费者,可以通过横向扩展来增加消息的接收能力。

          zookeeper:在许多分布式系统中都有应用,在Kafka中,zookeeper负责管理、协调broker节点和Consumer节点,Kafka支持动态加入节点,这都是依赖于zookeeper完成的。

          这里说几个我了解的Kafka的点:

         1.kafka有push和pull两种模式,push模式是由broker将消息推给消费者,broker决定消息的发送速率,这样做的缺点也显而易见,如果发送速率远远大于消费者的消费能力,那么将造成消息的拥塞。pull模式是由消费者向broker索取消息,根据其消费能力决定消息的发送速率,这样做的缺点是,如果broker没有消息,那么消费者要不断的轮询,kafka在这里做的措施是,如果没有消息,将阻塞消费者。不管是push模式还是pull模式,mq在这一步都是致力于最大化利用消费者的消费能力。

         2.kafka的吞吐能力很强,其原因之一是broker对消息的读写不仅在逻辑上是有序的,物理上也是有序的,每个topic对应的消息根据计算(hash等)被分配到不同的分区(partition)上,单个消费者从特定的partition中拉取消息,kafka将消息持久化到对应的partition上,充分利用了磁盘顺序读写的性能,新来的消息直接根据偏移量追加到后面即可,顺序读写大大减少了磁头的寻道时间,提高了读写性能。消费者根据偏移量(offset)来计算该消费哪条消息,一般都是通过递增偏移量来顺序的消费分区中的消息。

        3.传统的mq一般会在消费者消费完消息后将持久化的消息删除,而确认消费者是否删除了消息一般都是通过消费者回发ack。Kafka删除消息不是根据消费者的情况来决策,它是简单的通过设置消息的持久化时间,时间到则删除,这样在一定程度上保障了消息的安全,同时也提升了吞吐能力。

         Kafka的吞吐能力远远大于传统mq,在qps比较高的场景中,可以考虑使用Kafka。

 

--------------------------------------------------------

         Swallow:swallow是公司的mq组件

       消息传递的大概流程为:producer通过公司的rpc组件pigeon将消息发给producer server ,producer server负责接收所有的producer发来的消息,然后存储在数据库中(MongoDB),consumer server通过轮询数据库来给消费者发送消息,消费者需要注册并实现onMessage()接口,将自身对象传给consumer server,consumer server一有消息便会回调onMessage()方法。与其他mq不同的是,swallow删除消息的逻辑是等到数据库被写满后,删除保留时间最久的那个,通俗点说就是删除最老的消息。具体使用一起来看看:

1.继承接口:实现onMessage()回调方法

2.注册消费者


              

         首先根据消费者工厂创建一个消费者:需要提供topic

         上图中setListener就是将实现了回调方法的对象传进去,一旦consumer server轮询到消息便回调这个对象的onMessage()方法,这里传this是因为这里把注册及实现回调的方法放到了一个类中。       

---------------------------------------------------------

        Redis实现消息队列

        做为一个NoSQL数据库,Redis也可以用来实现消息队列,Redis里面有一个list数据类型,其支持push和pop操作,且命令brpop是阻塞模式

的pop操作,避免了消费者在list没有数据的情况下无休止轮询,这对CPU资源是一种浪费。当然,Redis实现的消息队列只是满足了基本的队列操作,

并不具备高吞吐量,高保障的特性,所以如果真的需要一个稳定,扩展性强的消息系统,还是需要使用专门的消息队列中间件,redis并不是用来专门干这个的,这里提出来,只是开阔一下思路,可能在一些业务中需要一些轻量的消息机制,引入消息队列中间件显得小题大做,如果项目中恰巧使用了redis,便可以用其简单的实现一个消息队列以满足业务需求。

---------------------------------------------------------

      总结:这里我们简单了解了MQ的基本知识,可见,优秀的MQ基本上都是在围绕着数据的保障性和吞吐量做工作,消息传输是MQ的最基本功能,而保障性、吞吐量才是决定一个MQ是否能将其应用到实际生产环境中的关键要素。

0 0
原创粉丝点击