rabbitmq消息队列

消息中间件是分布式系统中必不可少的一种组件,常用于异步通讯,解耦,并发缓冲。


消息中间件简介

常用的消息中间件有ActiveMQ, KafKa, RocketMQ, RabbitMQ。

ActiveMQ是Apache出品的历史比较久的一个开源消息中间件, 它是一个完全支持JMS规范的消息中间件。特点是API丰富,以前在中小企业中得到应用广泛。

Kafka是由Apache软件基金会开发的一个开源流处理平台,由Scala和Java编写。它是一种高吞吐量的分布式发布订阅消息系统。

RabbitMQ 是一个由 Erlang 语言开发的 AMQP (Advanced Message Queue,高级消息队列协议)的开源实现。最初起源于金融系统,用于在分布式系统中存储转发消息,在易用性、扩展性、高可用性等方面表现不俗。适用于对数据的一致性,稳定性和可靠性要求比较高的场景。

RocketMQ 是阿里巴巴在 2012 年开源的分布式消息中间件,目前已经捐赠给 Apache 软件基金会,并于 2017 年 9 月 25 日成为 Apache 的顶级项目。作为经历过多次阿里巴巴双十一这种“超级工程”的洗礼并有稳定出色表现的国产中间件,以其高性能、低延时和高可靠等特性近年来已经也被越来越多的国内企业使用。

本文主要介绍rabbitmq的基本原理和使用。

rabbitmq安装和使用

linux系统可通过命令行直接安装rabbitmq:

sudo apt-get install rabbitmq-server

安装完成后自动开启服务,也可通过命令行进行服务开启/关闭操作:

service rabbitmq-server start # 启动 
service rabbitmq-server stop # 停止 
service rabbitmq-server restart # 重启 

管理界面

可以使用web界面进行消息队列的后台管理。需要配置插件:

rabbitmq-plugins enable rabbitmq_management # 启用插件
 service rabbitmq-server restart # 重启

此时,应该可以通过 http://localhost:15672 查看,使用默认账户guest/guest 登录。

注意:RabbitMQ 3.3 及后续版本,guest 只能在服务本机登录。 要想连接远程服务器,建议创建其他新用户。

python中的使用

python通过pika负责与消息队列的连接。在项目的依赖清单requirements.txt中添加pika依赖,并将其安装到项目中。
简单demo如下:

# coding=utf-8
import pika

def pub():
    connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    # 不管是发送消息还是接受消息,都要通过信道channel来完成
    channel = connection.channel()
    # 队列声明,如果没有声明队列,发送到exchange中的消息就会丢失,所以先声明队列
    # 队列可以重复声明,消费者再次声明同样的队列时,不会有任何影响
    # queue创建时,会自动的以queue的名字作为Binding Key来将队列绑定到交换机上
    channel.queue_declare(queue='hello')  
    # 这里使用默认的exchange(name为“”),其类型为直连交换机(当消息的路由键与队列的绑定键相同时才路由消息)
    channel.basic_publish(exchange='', routing_key='hello', body='Hello World!')
    print "Send 'Hello World!'"
    connection.close()

def rev():
    connection = pika.BlockingConnection(pika.ConnectionParameters('localhost'))
    channel = connection.channel()
    channel.queue_declare(queue='hello')  # 队列声明,声明已存在的队列是不会影响队列属性的
    channel.basic_consume(callback, queue='hello', no_ack=True)
    channel.start_consuming()  # 进入阻塞状态,当队列里有消息时则通过callback进行处理

def callback(ch, method, properties, body):
    print "Received %r" % (body)

if __name__ == '__main__':
    pub()   # 发送消息
    rev()   # 接收消息

输出结果如下:

Send 'Hello World!'
Received 'Hello World!'

基本概念

RabbitMQ是AMQP协议的一个开源实现,其内部结构如下所示:
在这里插入图片描述
Broker
又称为Server,消息队列服务器实体。

Message
消息由消息头和消息体组成,消息头由一些可选属性组成,包括routing-key(路由键)、priority(优先级)、delivery-mode(是否需要持久化存储)等。

Exchange
交换器,用来接受队列生产者发送的消息并将这些消息路由到队列中。

Binding
绑定,消息队列和交换器之间的关联。Binding Key由Consumer在Binding Exchange与Message Queue时指定。
当message到达交换机时,交换机根据类型,消息的Routing Key以及交换机和队列之间的Binding Key来决定将消息路由到哪一个队列。

Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内的虚拟连接,AMQP命令都是通过信道发出去的,不管是发布消息还是接受消息。
因为对于操作系统来说,建立和销毁TCP都是非常昂贵的开销,所以引入了信道的概念,以复用一条TCP连接。

AMQP的消息路由

AMQP中的消息路由和JAVA中的消息规范JMS存在着不少差别,主要在于AMQP中增加了Exchange和Binding的角色。

生产者把消息发布到Exchange上,然后根据Binding决定将消息发送到哪个队列。如下图所示:
在这里插入图片描述

交换器类型

Exchange分发消息时根据类型的不同使用不同的分发策略。目前有四种类型,包括direct,fanout,topic,headers。除了headers外,另外三种均使用比较多。

直连交换器(direct):路由键(Routing Key)与绑定键(Binding Key)相同时投递。

扇形交换器(fanout):发送到交换机的消息会被转发到与该交换机绑定的所有队列上。Fanout交换机转发消息是最快的。很像子网广播,每台子网内的主机都获得了一份复制的消息。

主题交换器(topic):路由规则由绑定键决定,只有消息的路由键满足绑定键的规则,消息才可以路由到对应的队列上;* 用来表示一个单词,# 用来表示任意数量(零个或多个)单词。

头交换器(headers):类似主题交换机,但是头交换机使用多个消息属性来代替路由键建立路由规则。通过判断消息头的值能否与指定的绑定相匹配来确立路由规则。

心跳检测

服务端和客户端通过heartbeat参数协商心跳检测超时时间,当超过这个时间无数据通信时,则认为客户端失效,断开链接。
heartbeat参数将取服务端和客户端中的较小的那一个,如果客户端未设置,则服务端默认为60s(旧版本是580s)。当客户端设置为0时,表明禁用心跳检测,此时服务器将不会主动断开链接。通常不建议禁用心跳检测,因为当链接数量较大时,会给服务器带来较大的压力。

在新版中,服务器heartbeat默认时间为60s,如果消费回调函数耗时较长(超过60s),此时服务器将断开与客户端的链接。如果消费者需要回传ack确认,此时将无法通信,导致的一个问题就是:该消息无法被确认消费,下次将继续重复消费。

解决办法是调整服务器heartbeat参数(但无法调太大,一旦超过某个值如20h,其实际值会变小,原因未知。测试发现15h是没有问题的。难道是有上限?)

常用命令

rabbitmqctl list_queues:列出所有消息队列及其消息数
rabbitmqctl purge_queue name:清空指定队列
rabbitmqctl list_connections timeout:列出所有心跳检测超时时间
rabbitmqctl status:查看服务器状态
rabbitmqctl cluster_status:查看集群状态

消息的可靠性

在讨论rabbitmq消息的可靠性之前,先回顾下amqp规范中消息从生产者到消费者的完整链路:
在这里插入图片描述
整个链路可以分为4个阶段,分别是:

  • 消息从生产者发送到交换机
  • 消息从交换机路由到消息队列
  • 消息在消息队列上的存储
  • 消息从消息队列推送到消费者

一共有四个阶段,任何一个阶段出现问题都会导致消息丢失,因此需要从这四个阶段来考虑消息的可靠性。

生产者 - 交换机

消息从生产者发送到交换机的过程中,可能会发生各种意外情况,比如网络丢包,网络故障等。
一般情况下如果不采取措施,生产者是没办法判断消息是否确实到达了交换机的。如果消息在到达交换机的过程中出现问题而被生产者感知到的话,生产者就可以通过进一步的处理,如重新投递消息等方式来保证消息的可靠性。

为此AMQP协议在创建之初就考虑了这种情况,并为此提供了事务机制。RabbitMq客户端中与事务有关的方法有三个:channel.txSelect, chanel.txCommit, channel.rollBack,分别用于开启事务,提交事务,捕获异常回滚事务。虽然事务能够解决发送者和交换机之间的消息确认问题,但却会带来性能上的极大损耗,导致消息队列的吞吐量急剧降低。因此,这种方案并不是推荐的方案。

那么有没有更好的方法,既能保证生产者能够确认消息发送到了交换机,又不会带来性能上的损失呢?从AMQP层面上看似乎并没有更好的方法,但是rabbitmq提供了一种改进方案,即发送方确认机制(publisher confirm)。

生产者将信道(channel)设置成confirm模式,所有在该信道上发送的消息都会分配一个从1开始的唯一id,一旦消息达到交换机后,rabbitmq就会发送一个Basic.ack确认给生产者。在这个确认中,有一个deliveryTag字段,表示确认的消息id。如果设置了channel.basicAck中的multiple参数,则表示到这个id之前的所有消息都已经得到了处理,如下图所示:
在这里插入图片描述
相比于事务机制,发送方确认机制最大的好处是它是异步的。也就是说,生产者在发送消息之后,不用同步阻塞等待rabbitmq的确认,而是可以继续发送其他消息。同时通过回调函数来处理rabbitmq的确认。如果rabbitmq因为自身内部的原因导致消息丢失,就会发送一条nack(Basic.nack)命令。开启生产者确认机制之后,所有到达rabbitmq的消息都会有一个ack或nack响应。Rabbitmq没有对消息被confirm的快慢做任何保证。

只有在消息到达rabbitmq之后,rabbitmq才能confirm。但如果消息在发送过程中丢失了呢?此时会出现一种情况:生产者始终收不到丢失的消息的confirm。因此可以考虑将所有发送的消息存到本地数据库,收到ack的confirm则表示消息已经正常到达了交换机;收到nack则表示消息在rabbitmq内部发生丢失;始终没有收到confirm的消息,可以认为在达到rabbitmq的路上就丢失了。

交换机 - 消息队列

消息到达交换机之后,如果没有匹配的队列,消息仍然会丢失。rabbitmq提供了mandatory参数或备份交换机来保证这一步的消息可靠性。

路由失败回调

mandatory和immediate是channel.basicPublish方法中的两个参数,它们都有当消息传递过程中不可达目的地时将消息返回给生产者的功能。而RabbitMQ提供的备份交换器(Alternate Exchange)可以将未能被交换器路由的消息(没有绑定队列或者没有匹配的绑定)存储起来,而不用返回给客户端。

RabbitMQ 3.0版本开始去掉了对于immediate参数的支持,对此RabbitMQ官方解释是:immediate参数会影响镜像队列的性能,增加代码复杂性,建议采用TTL和DLX的方法替代。

当mandatory参数设为true时,交换器无法根据自身的类型和路由键找到一个符合条件的队列的话,那么RabbitMQ会调用Basic.Return命令将消息返回给生产者。当mandatory参数设置为false时,出现上述情形的话,消息直接被丢弃。 那么生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用channel.addReturnListener来添加ReturnListener监听器实现。使用mandatory参数的关键代码如下所示:

//开启失败回调
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
     @Override
     public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
         //message:发送的消息
         System.out.println(message);
         //返回码
         System.out.println(replyCode);
         //返回信息
         System.out.println(replyText);
         //交换机
         System.out.println(exchange);
         //路由键
         System.out.println(routingKey);
     }
 });

生产者可以通过ReturnListener中返回的消息来重新投递或者使用其它方案来提高消息的可靠性。

路由失败转发

备份交换器,英文名称Alternate Exchange,简称AE。生产者在发送消息的时候如果不设置mandatory参数,那么消息在未被路由的情况下将会丢失,如果设置了mandatory参数,那么需要添加ReturnListener的编程逻辑,生产者的代码将变得复杂化。如果你不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在RabbitMQ中,再在需要的时候去处理这些消息。 可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加alternate-exchange参数来实现,也可以通过策略的方式实现。如果两者同时使用的话,前者的优先级更高,会覆盖掉Policy的设置。

下图描述了备份交换机的使用:
在这里插入图片描述
注意,如果备份交换器和mandatory参数一起使用,那么mandatory参数无效。

消息队列持久化

前两个阶段可以通过不同的方式来保证消息的可靠性,那消息到达消息队列之后的可靠性呢?

首先是持久化。持久化可以提高队列的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。队列的持久化是通过在声明队列时将durable参数置为true实现的,如果队列不设置持久化,那么在RabbitMQ服务重启之后,相关队列的元数据将会丢失,此时数据也会丢失。队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties中的deliveryMode属性)设置为2即可实现消息的持久化。设置了队列和消息的持久化,当RabbitMQ服务重启之后,消息依旧存在。

在持久化的消息正确存入RabbitMQ之后,还需要有一段时间(虽然很短,但是不可忽视)才能存入磁盘之中。RabbitMQ并不会为每条消息都做同步存盘(调用内核的fsync方法)的处理,可能仅仅保存到操作系统缓存之中而不是物理磁盘之中。如果在这段时间内RabbitMQ服务节点发生了宕机、重启等异常情况,消息保存还没来得及落盘,那么这些消息将会丢失。

如果在第一阶段(生产者->交换机)中采用了事务机制或者publisher confirm机制的话,rabbitmq的confirm是在消息落盘之后执行的,这样可以进一步的提高消息的可靠性。但是即便如此也无法避免单机故障且无法修复(比如磁盘损毁)而引起的消息丢失,这里就需要引入镜像队列(下一章节集群方式中会讲到)。在镜像队列中,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave),这样有效的保证了高可用性,除非整个集群都挂掉。在实际生产环境中的关键业务队列一般都会设置镜像队列。

消息队列 - 消费者

最后一步,当消息从消息队列推送到消费者之后,如果消费者还没有来得及消费就宕机了或者消费异常了,那也算消息丢失了。为了保证消息从队列可靠地达到消费者,RabbitMQ提供了消息确认机制(message acknowledgement)。

消息确认机制有自动确认和手动确认两种方式,其中自动确认是不可靠的。 消费者在订阅队列时,可以指定autoAck参数来表示是自动确认还是手动确认。当autoAck为true时,即自动确认。RabbitMQ会自动把发送出去的消息置为确认,然后从内存(或者磁盘)中删除,而不管消费者是否真正的消费到了这些消息。当autoAck为false时,即手动确认。RabbitMQ会等待消费者显式地回复确认信号后才从内存(或者磁盘)中移除消息(实质上是先打上删除标记,之后再删除)。

采用消息确认机制后,只要设置autoAck参数为false,消费者就有足够的时间处理消息(任务),不用担心处理消息过程中消费者进程挂掉后消息丢失的问题,因为RabbitMQ会一直等待持有消息直到消费者显式调用Basic.Ack命令为止。

当autoAck参数置为false,对于RabbitMQ服务端而言,队列中的消息分成了两个部分:一部分是等待投递给消费者的消息;一部分是已经投递给消费者,但是还没有收到消费者确认信号的消息。如果RabbitMQ一直没有收到消费者的确认信号,并且消费此消息的消费者已经断开连接,则RabbitMQ会安排该消息重新进入队列,等待投递给下一个消费者,当然也有可能还是原来的那个消费者。

RabbitMQ不会为未确认的消息设置过期时间,它判断此消息是否需要重新投递给消费者的唯一依据是消费该消息的消费者连接是否已经断开,这么设计的原因是RabbitMQ允许消费者消费一条消息的时间可以很久很久。

如果消息消费失败,也可以调用Basic.Reject或者Basic.Nack来拒绝当前消息而不是确认,如果只是简单的拒绝那么消息会丢失,需要将相应的requeue参数设置为true,那么RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者。如果requeue参数设置为false的话,RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。

还有一种情况需要考虑:requeue的消息是存入队列头部的,即可以快速的又被发送给消费,如果此时消费者又不能正确的消费而又requeue的话就会进入一个无尽的循环之中。对于这种情况,建议在出现无法正确消费的消息时不要采用requeue的方式来确保消息可靠性,而是重新投递到新的队列中,比如设定的死信队列中,以此可以避免前面所说的死循环而又可以确保相应的消息不丢失。对于死信队列中的消息可以用另外的方式来消费分析,以便找出问题的根本。

rabbitmq集群方式

rabbitmq除了最简单的单节点模式之外,还有两种集群模式。分别是普通集群模式镜像集群模式

普通集群模式

普通集群模式是默认的集群模式。

在普通集群模式下,消息队列只存在于其中一个节点,而其他的非队列节点只知道队列的元数据和指向队列节点的指针。客户端可以连接到集群的任意一个节点,而该节点会从队列节点获取消息数据,也就是说,所有的消息出口实际上都是在消息队列存在的那个节点上,其他节点都需要通过消息队列节点来同步数据。如果没有设置消息持久化,一旦消息队列存在的节点宕机,则整个集群是不可用的。只有当设置消息持久化的情况下(交换机和队列也要持久化),等待宕机的消息队列节点恢复之后,其他节点才能继续从消息队列节点获取消息数据。

普通集群模式设置

假设现有rabbitmq@node1, rabbitmq@node2两个消息服务器节点。建立集群方式如下:
1.登录node1,先关闭rabbitmq:
./rabbitmqctl stop_app

2.重置rabbitmq:
./rabbitmqctl reset

3.加入集群:
./rabbitmqctl join_cluster rabbit@node2

4.修改节点类型为ram,在集群中,只要有一个节点是Disc Node则可将集群元数据写到磁盘,其他均为Ram Node:
./rabbitmqctl change_cluster_node_type ram

5.开启rabbitmq:
./rabbitmqctl start_app

6.修改集群名称:
./rabbitmqctl set_cluster_name rabbit_demo

镜像集群模式

镜像集群模式将消息队列复制到所有节点上,每个节点都有一份完整的消息队列。

与普通集群模式不同的是,镜像集群模式中,消息实体会主动在镜像节点中同步,而不是像前者那样只在消费者获取数据时临时拉取。当然,这种模式带来的副作用也很明显,大量消息的同步会占用网络带宽,同时,对于持久化消息来说,需要在每个节点上进行持久化,会带来频繁的磁盘活动。

参考资料

[1]. http://www.rabbitmq.com
[2]. https://blog.csdn.net/zhuiqiuk/article/details/78957349
[3]. https://www.jianshu.com/p/79ca08116d57
[4]. https://blog.csdn.net/weixin_41588751/article/details/105768017

  • 0
    点赞
  • 2
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值