RabbitMQ详解

1. RabbitMQ 简介

1.1 什么是消息中间件

消息队列中间件(Message Queue Middleware,简称为MQ)是指利用高效可靠的消息传递机制进行与平台无关的数据交流,并基于数据通信来进行分布式系统的集成。通过提供消息传递和消息排队模型,它可以在分布式环境下扩展进程间的通信。 

消息队列中间件,也可以称为消息队列或者消息中间件。它一般有两种传递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为可能。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。 

目前开源的消息中间件有很多,比较主流的有 RabbitMQ、Kafka、ActiveMQ、RocketMQ 等。

1.2 消息中间件的作用

1)解耦在项目启动之初来预测将来会碰到什么需求是极其困难的。消息中间件在处理过程中间插入了一个隐含的、基于数据的接口层,两边的处理过程都要实现这一接口,这允许你独立地扩展或修改两边的处理过程,只要确保它们遵守同样的接口约束即可。 

场景:双11是购物狂节,用户下单后,订单系统需要通知库存系统,传统的做法就是订单系统调用库存系统的接口。

RabbitMQ详解_第1张图片

这种做法有一个缺点:

  • 当库存系统出现故障时,订单就会失败。
  • 订单系统和库存系统高耦合。

引入消息队列:

RabbitMQ详解_第2张图片

  • 订单系统:用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。

  • 库存系统:订阅下单的消息,获取下单消息,进行库操作。就算库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失。

2)冗余(存储)有些情况下,处理数据的过程会失败。消息中间件可以把数据进行持久化直到它们已经被完全处理,通过这一方式规避了数据丢失风险。在把一个消息从消息中间件中删除之前,需要你的处理系统明确地指出该消息已经被处理完成,从而确保你的数据被安全地保存直到你使用完毕。 

3)扩展性:因为消息中间件解耦了应用的处理过程,所以提高消息入队和处理的效率是很容易的,只要另外增加处理过程即可,不需要改变代码,也不需要调节参数。 

4)削峰:在访问量剧增的情况下,应用仍然需要继续发挥作用,但是这样的突发流量并不常见。如果以能处理这类峰值为标准而投入资源,无疑是巨大的浪费。使用消息中间件能够使关键组件支撑突发访问压力,不会因为突发的超负荷请求而完全崩溃。 

流量削峰一般在秒杀活动中应用广泛。

场景:秒杀活动,一般会因为流量过大,导致应用挂掉,为了解决这个问题,一般在应用前端加入消息队列。

作用:

  • 可以控制活动人数,超过此一定阀值的订单直接丢弃
  • 可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单)

RabbitMQ详解_第3张图片

用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面。

秒杀业务根据消息队列中的请求信息,再做后续处理。

5)可恢复性:当系统一部分组件失效时,不会影响到整个系统。消息中间件降低了进程间的耦合度,所以即使一个处理消息的进程挂掉,加入消息中间件中的消息仍然可以在系统恢复后进行处理。 

6)顺序保证:在大多数使用场景下,数据处理的顺序很重要,大部分消息中间件支持一定程度上的顺序性。 

7)缓冲:在任何重要的系统中,都会存在需要不同处理时间的元素。消息中间件通过一个缓冲层来帮助任务最高效率地执行,写入消息中间件的处理会尽可能快速。该缓冲层有助于控制和优化数据流经过系统的速度。 

8)异步通信:在很多时候应用不想也不需要立即处理消息。消息中间件提供了异步处理机制,允许应用把一些消息放入消息中间件中,但并不立即处理它,在之后需要的时候再慢慢处理。

场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种:a)串行的方式;b)并行的方式

①串行方式:将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。 这有一个问题是:邮件,短信并不是必须的,它只是一个通知,而这种做法让客户端等待没有必要等待的东西。

②并行方式:将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端,并行的方式能提高处理的时间。

RabbitMQ详解_第4张图片

假设三个业务节点分别使用 50ms,串行方式使用时间 150ms,并行使用时间 100ms。虽然并性已经提高的处理时间,但是,前面说过,邮件和短信对我正常的使用网站没有任何影响,客户端没有必要等着其发送完成才显示注册成功,应该是写入数据库后就返回。

③消息队列:引入消息队列后,把发送邮件,短信不是必须的业务逻辑异步处理:

RabbitMQ详解_第5张图片

由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),引入消息队列后处理后,响应时间是串行的 3 倍,是并行的 2 倍。

总的来说,MQ 的优势大体有三个:

  • 应用解耦:提高系统容错性和可维护性。
  • 异步提速:提升用户体验和系统吞吐量。
  • 削峰填谷:提高系统稳定性。

MQ 的劣势:

  • 系统可用性降低:系统引入的外部依赖越多,系统稳定性越差。一旦MQ宕机就会对业务造成影响。如何保证MQ的高可用?
  • 系统复杂度提高:MQ的加入大大增加了系统的复杂度,以前系统间是同步的远程调用,现在是通过MQ进行异步调用。如何保证消息不被丢失等情况?

1.3 MQ 的分类 

1.3.1 ActiveMQ 

优点:单机吞吐量万级,时效性 ms级,可用性高,基于主从架构实现高可用性,消息可靠性较低的概率丢失数据。

缺点:官方社区现在对ActiveMQ 5.x 维护越来越少,高吞吐量场景较少使用。 

1.3.2 Kafka

大数据的杀手锏,谈到大数据领域内的消息传输,则绕不开 Kafka,这款为大数据而生的消息中间件, 以其百万级 TPS 的吞吐量名声大噪,迅速成为大数据领域的宠儿,在数据采集、传输、存储的过程中发挥着举足轻重的作用。目前已经被 LinkedIn,Uber, Twitter, Netflix 等大公司所采纳。 

优点: 性能卓越,单机写入 TPS 约在百万条/秒,最大的优点,就是吞吐量高。时效性 ms 级可用性非常高,kafka是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用,消费者采用 Pull 方式获取消息, 消息有序,通过控制能够保证所有消息被消费且仅被消费一次;有优秀的第三方 Kafka Web 管理界面Kafka-Manager;在日志领域比较成熟,被多家公司和多个开源项目使用;功能支持: 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用。

缺点:Kafka单机超过64 个队列/分区,Load会发生明显的飙高现象,队列越多,load越高,发送消息响应时间变长,使用短轮询方式,实时性取决于轮询间隔时间,消费失败不支持重试;支持消息顺序, 但是一台代理宕机后,就会产生消息乱序,社区更新较慢;

1.3.3 RocketMQ

RocketMQ 出自阿里巴巴的开源产品,用 Java 语言实现,在设计时参考了 Kafka,并做出了自己的一 些改进。被阿里巴巴广泛应用在订单,交易,充值,流计算,消息推送,日志流式处理,binglog分发等场景。

优点:单机吞吐量十万级,可用性非常高,分布式架构,消息可以做到 0 丢失,MQ功能较为完善,还是分布式的,扩展性好,支持10亿级别的消息堆积,不会因为堆积导致性能下降,源码是 java 我们可以自己阅读源码,定制自己公司的 MQ。

缺点:支持的客户端语言不多,目前是 java 及 c++,其中 c++不成熟;社区活跃度一般,没有在MQ 核心中去实现 JMS 等接口,有些系统要迁移需要修改大量代码。

1.3.4 RabbitMQ 

2007 年发布,是一个在 AMQP(高级消息队列协议)基础上完成的,可复用的企业消息系统,是当前最主流的消息中间件之一。 

优点:由于 erlang 语言的高并发特性,性能较好;吞吐量到万级,MQ功能比较完备,健壮、稳定、易用、跨平台、支持多种语言 如:Python、Ruby、.NET、Java、JMS、C、PHP、ActionScript、XMPP、STOMP 等,支持 AJAX文档齐全;开源提供的管理界面非常棒,用起来很好用,社区活跃度高;更新频率相当高。https://www.rabbitmq.com/news.html 

缺点:商业版需要收费,学习成本较高。

RabbitMQ详解_第6张图片

1.4 MQ 的选择 

1)Kafka 

Kafka 主要特点是基于 Pull 的模式来处理消息消费,追求高吞吐量,一开始的目的就是用于日志收集和传输,适合产生大量数据的互联网服务的数据收集业务。大型公司建议可以选用,如果有日志采集功能, 肯定是首选 kafka了。

2)RocketMQ 

天生为金融互联网领域而生,对于可靠性要求很高的场景,尤其是电商里面的订单扣款,以及业务削峰,在大量交易涌入时,后端可能无法及时处理的情况。RoketMQ在稳定性上可能更值得信赖,这些业务场景在阿里双 11已经经历了多次考验,如果你的业务有上述并发场景,建议可以选择 RocketMQ。 

3)RabbitMQ 

结合 erlang语言本身的并发优势,性能好时效性微秒级,社区活跃度也比较高,管理界面用起来十分 方便,如果你的数据量没有那么大,中小型公司优先选择功能比较完备的 RabbitMQ。

1.5 RabbitMQ 的起源

RabbitMQ 是采用 Erlang 语言实现 AMQP(Advanced Message Queuing Protocol,高级消息队列协议)的消息中间件,它最初起源于金融系统,用于在分布式系统中存储转发消息。

RabbitMQ 的具体特点可以概括为以下几点。 

  • 可靠性:RabbitMQ使用一些机制来保证可靠性,如持久化、传输确认及发布确认等。 
  • 灵活的路由:在消息进入队列之前,通过交换器来路由消息。对于典型的路由功能,RabbitMQ已经提供了一些内置的交换器来实现。针对更复杂的路由功能,可以将多个交换器绑定在一起,也可以通过插件机制来实现自己的交换器。 
  • 扩展性:多个RabbitMQ节点可以组成一个集群,也可以根据实际业务情况动态地扩展集群中节点。 
  • 高可用性:队列可以在集群中的机器上设置镜像,使得在部分节点出现问题的情况下队列仍然可用。 
  • 多种协议:RabbitMQ除了原生支持AMQP协议,还支持STOMP、MQTT等多种消息中间件协议。 
  • 多语言客户端:RabbitMQ几乎支持所有常用语言,比如Java、Python、Ruby、PHP、C#、JavaScript等。 
  • 管理界面:RabbitMQ提供了一个易用的用户界面,使得用户可以监控和管理消息、集群中的节点等。 
  • 插件机制:RabbitMQ提供了许多插件,以实现从多方面进行扩展,当然也可以编写自己的插件。


1.6 RabbitMQ 的安装及简单使用

因为 RabbitMQ 是由 Erlang 语言编写的,所以在安装RabbitMQ之前需要安装Erlang。建议采用较新版的Erlang,这样可以获得较多更新和改进。

1)  Win 10 安装 RabbitMQ

RabbitMQ 的安装与配置_单椒煜泽的博客-CSDN博客

2) Linux 下安装 RabbitMQ

(我的安装环境:CentOS 7)

① 下载需要的软件:rabbitmq3.8.8.zip-互联网文档类资源-CSDN下载

② 把这两个软件移动到 linux 相应的安装目录下

RabbitMQ详解_第7张图片

③ 安装:

rpm -ivh erlang-21.3-1.el7.x86_64.rpm

yum install socat -y

rpm -ivh rabbitmq-server-3.8.8-1.el7.noarch.rpm

④ 常用命令,按照以下顺序执行:

chkconfig rabbitmq-server on (添加开机启动 RabbitMQ 服务 )

/sbin/service rabbitmq-server start (添加启动服务)

/sbin/service rabbitmq-server status (查看服务状态)

RabbitMQ详解_第8张图片

/sbin/service rabbitmq-server stop (停止服务(选择执行))

rabbitmq-plugins enable rabbitmq_management (开启 web 管理插件)

用默认账号密码(guest)访问地址  http://192.168.10.6:15672/

如果这里地址打不开的话,可能是防火墙是打开的。(systemctl status firewalld)

RabbitMQ详解_第9张图片

那就关掉:systemctl stop firewalld

第一次登录居然发现没有权限:

RabbitMQ详解_第10张图片

那就添加一个新用户登录:rabbitmqctl add_user user_lyd 1234

设置用户角色:rabbitmqctl set_user_tags user_lyd administrator

设置用户权限:rabbitmqctl set_permissions -p "/" user_lyd ".*" ".*" ".*"  (用户 user_lyd 具有/vhost1 这个 virtual host 中所有资源的配置、写、读权限)

2. RabbitMQ 相关概念介绍

RabbitMQ 整体是一个生产者和一个消费者模型,主要负责接收、存储和转发消息。

RabbitMQ详解_第11张图片

1)Producer:生产者,就是投递消息的一方。 

生产者创建消息,然后发布到 RabbitMQ  中。消息一般可以包含 2 个部分:消息体和标签(Label)。消息体也可以称之为 payload,在实际应用中,消息体一般是一个带有业务逻辑结构的数据,比如一个 JSON 字符串。当然可以进一步对这个消息体进行序列化操作。消息的标签用来表述这条消息,比如一个交换器的名称和一个路由键。生产者把消息交由 RabbitMQ,RabbitMQ 之后会根据标签把消息发送给感兴趣的消费者(Consumer)。 

2)Consumer:消费者,就是接收消息的一方。 

消费者连接到  RabbitMQ 服务器,并订阅到队列上。当消费者消费一条消息时,只是消费消息的消息体(payload)。在消息路由的过程中,消息的标签会丢弃,存入到队列中的消息只有消息体,消费者也只会消费到消息体,也就不知道消息的生产者是谁,当然消费者也不需要知道。 

3)Broker:消息中间件的服务节点。 

对于 RabbitMQ 来说,一个 RabbitMQ Broker 可以简单地看作一个 RabbitMQ 服务节点,或者 RabbitMQ 服务实例。大多数情况下也可以将一个 RabbitMQ Broker 看作一台 RabbitMQ 服务器。 下图展示了生产者将消息存入 RabbitMQ Broker,以及消费者从 Broker 中消费数据的整个流程。

RabbitMQ详解_第12张图片

首先生产者将业务方数据进行可能的包装,之后封装成消息,发送(AMQP协议里这个动作对应的命令为Basic.Publish)到 Broker 中。消费者订阅并接收消息(AMQP协议里这个动作对应的命令为Basic.Consume或者Basic.Get),经过可能的解包处理得到原始的数据,之后再进行业务处理逻辑。

4)Queue:队列,是RabbitMQ的内部对象,用于存储消息。

RabbitMQ中消息都只能存储在队列中,这一点和Kafka这种消息中间件相反。Kafka将消息存储在topic(主题)这个逻辑层面,而相对应的队列逻辑只是topic实际存储文件中的位移标识。RabbitMQ的生产者生产消息并最终投递到队列中,消费者可以从队列中获取消息并消费。 

多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊(Round-Robin,即轮询)给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理,如下图所示。

RabbitMQ详解_第13张图片

多个消费者 RabbitMQ不支持队列层面的广播消费,如果需要广播消费,需要在其上进行二次开发,处理逻辑会变得异常复杂,同时也不建议这么做。

5)Exchange:交换器(通常也可以用大写的“X”来表示)

在上面我们暂时可以理解成生产者将消息投递到队列中,实际上这个在RabbitMQ中不会发生。真实情况是,生产者将消息发送到 Exchange(交换器),由交换器将消息路由到一个或者多个队列中。如果路由不到,或许会返回给生产者,或许直接丢弃。 

交换器的具体示意图如下图所示。  

RabbitMQ详解_第14张图片

交换器 RabbitMQ中的交换器有四种类型,不同的类型有着不同的路由策略。

6)Virtual host

出于多租户和安全因素设计的,把 AMQP 的基本组件划分到一个虚拟的分组中,类似于网络中的  namespace 概念。当多个不同的用户使用同一个 RabbitMQ server 提供的服务时,可以划分出多个 vhost,每个用户在自己的 vhost 创建 exchange/queue 等

7)RoutingKey:路由键。

生产者将消息发给交换器的时候,一般会指定一个 RoutingKey,用来指定这个消息的路由规则,而这个Routing Key需要与交换器类型和绑定键(BindingKey)联合使用才能最终生效。 

在交换器类型和绑定键(BindingKey)固定的情况下,生产者可以在发送消息给交换器时,通过指定RoutingKey来决定消息流向哪里。 

8)Binding:绑定。

RabbitMQ中通过绑定将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键(BindingKey),这样RabbitMQ就知道如何正确地将消息路由到队列了,如下图所示。

RabbitMQ详解_第15张图片

生产者将消息发送给交换器时,需要一个RoutingKey,当BindingKey和RoutingKey相匹配时,消息会被路由到对应的队列中。在绑定多个队列到同一个交换器的时候,这些绑定允许使用相同的BindingKey。BindingKey并不是在所有的情况下都生效,它依赖于交换器类型,比如fanout类型的交换器就会无视BindingKey,而是将消息路由到所有绑定到该交换器的队列中。 

在 direct 交换器类型下,RoutingKey 和 BindingKey 需要完全匹配才能使用,所以在这种情况下 RoutingKey 与 BindingKey 可以看作同一个东西。如:

channel.exchangeDeclare(EXCHANGE_NAME,"direct", true,false,null);
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,ROUTING_KEY);
String message = "Hello world!";
channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY,MessageProperties.PERSISTENT_TEXT_PLAIN,message.getBytes());

但是在 topic 交换器类型下,RoutingKey 和 BindingKey 之间需要做模糊匹配,两者并不是相同的。

大多数情况下习惯性地将 BindingKey 写成 RoutingKey,尤其是在使用 direct 类型的交换器的时候。本文后面的篇幅中也会将两者合称为路由键。

9)Connection

publisher/consumer 和 broker 之间的TCP 连接 。

10)Channel

如果每一次访问 RabbitMQ 都建立一个Connection,在消息量大的时候建立 TCP Connection 的开销将是巨大的,效率也较低。Channel 是在 connection 内部建立的逻辑连接,如果应用程序支持多线程,通常每个 thread 创建单独的 channel 进行通讯,AMQP method 包含了channel id 帮助客 户端和 message broker 识别 channel,所以channel之间是完全隔离的。

Channel 作为轻量级的 Connection 极大减少了操作系统建立 TCP connection 的开销。 

3. 消息队列

首先来一张消息队列的经典图,可以划分为三个角色: Producer, Queue, Consumer

RabbitMQ详解_第16张图片

  • Queue:为承载消息的容器,为什么是队列而不是栈呢?主要是因为绝大部分的场景,我们都是希望消息是先进先出,有顺序的
  • Producer:生产者,就是产生消息,并不断往队列塞的角色
  • Consumer:消费者,也就是不断从队列中获取消息的角色

3.1 实例理解消息队列

其实在生活中,这种模型用得非常多,就比如我们都会接触的网购快递,可以说是一个典型的消息队列的 case了:

商家不断的把商品扔给快递公司(注意不是直接将商品给买家),而快递公司则将商品根据地质分发对应的买家

对上面这个过程进行拆解,可以映射扮演的角色

  • 商品:Message,传递的消息,由商家投递给快递公司时,需要进行打包(一般Producer生产消息也会将实体数据进行封装)
  • 商家:Produer 生产者
  • 快递公司: Queue,消息的载体
  • 买家:Consumer 消费者

那么快递公司时怎么知道要把商品给对应的买家呢?根据包裹上的地址+电话

  • 同样消息队列也需要一个映射规则,实现Message和Consumer之间的路由

3.2 举例

RabbitMQ 队列_单椒煜泽的博客-CSDN博客

(队列其实只有一种队列,看有几个消费者连接而已。交换机常用的有 3 种)

1)简单队列

    
        
            com.rabbitmq
            amqp-client
            4.0.2
        

        
            org.slf4j
            slf4j-api
            1.7.10
        

        
            org.slf4j
            slf4j-log4j12
            1.7.5
        

        
            log4j
            log4j
            1.2.17
        

        
            junit
            junit
            4.11
        

    
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class ConnectionUtils {
    /*
     * 获取 MQ 的连接
     * */
    public static Connection getConnection() throws Exception {
        //1. 定义一个连接工厂
        ConnectionFactory factory = new ConnectionFactory();

        //2. 获取服务地址
        factory.setHost("127.0.0.1");

        //3. AMQP 5672
        factory.setPort(5672);

        //4. vhost
        factory.setVirtualHost("/vhost_lyd");

        //5. 用户名
        factory.setUsername("user_lyd");

        //6. 密码
        factory.setPassword("1234");

        return factory.newConnection();
    }
}
import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

public class Send {
    private static final String QUEUE_NAME = "testMaterialGroup";

    public static void main(String[] args) throws Exception {

        //1. 获取一个连接
        Connection connection = ConnectionUtils.getConnection();

        //2. 从连接中获取一个通道
        Channel channel = connection.createChannel();

        //3. 创建队列声明
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        String msg = "hello simple";

        channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());

        System.out.println("-----消费者发送消息:"+msg);

        channel.close();
        connection.close();

    }
}
import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;

import java.io.IOException;


public class Receive {
    private static final String QUEUE_NAME = "testMaterialGroup";

    public static void main(String[] args) throws Exception {

        //1. 获取连接
        Connection connection = ConnectionUtils.getConnection();

        //2. 创建通道
        Channel channel = connection.createChannel();

        //3. 队列声明
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            //获取到达的消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费者接收消息: " + msg);
            }
        };

        //4. 监听队列
        channel.basicConsume(QUEUE_NAME,true,consumer);

    }
}

2)工作队列 - 多个消费者监听一个队列(能者多劳模式)

1)channel.basicQos(1);  //保证每次只分发一个

(如果不写 channel.basicQos(1) ,则生产者的消息是轮询到两个消费者的,一人一个,有序发送,即使消费者的工作相率一样)

生产者:

import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @ClassName: Send
 * @Description: 消息有效期
 */
public class Send {

    private static final String QUEUE_NAME = "testMaterialGroup";

    public static void main(String[] args) throws Exception {
        //1. 获取一个连接
        Connection connection = ConnectionUtils.getConnection();

        //2. 从连接中获取一个通道
        Channel channel = connection.createChannel();

        //3. 声明队列
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息
        //限制发送给同一个消费者 不得超过一条信息
        channel.basicQos(1);

        //定义消息内容
        for(int i = 0; i <15; i++){
            String msg = "hello mq"+i;
            System.out.println("生产者发送消息:"+msg);

            //4. 发送消息
            channel.basicPublish("",QUEUE_NAME,null,msg.getBytes());

            //模拟发送消息延时,便于演示多个消费者竞争接受消息
            Thread.sleep(i*20);
        }

        //5. 关闭通道
        channel.close();
        //6. 关闭连接
        connection.close();
    }
}

消费者1:

import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Receive1 {
    private static final String QUEUE_NAME = "testMaterialGroup";

    public static void main(String[] args) throws Exception {
        //1. 获取连接
        Connection connection = ConnectionUtils.getConnection();

        //2. 创建通道
        final Channel channel = connection.createChannel();

        //3. 队列声明
        channel.queueDeclare(QUEUE_NAME,false,false,false,null);

        channel.basicQos(1); //保证每次只分发一个

        //4. 定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费1接收消息 " + msg);

                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    System.out.println("消费1 done");

                    //返回确认状态
                    channel.basicAck(envelope.getDeliveryTag(),false);
                }
            }
        };

        //5. 监听队列,手动返回完成状态
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME,autoAck,consumer);
    }
}

消费者2:

import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Receive2 {
    private static final String QUEUE_NAME = "testMaterialGroup";

    public static void main(String[] args) throws Exception {
        //1. 获取连接
        Connection connection = ConnectionUtils.getConnection();

        //2. 创建通道
        final Channel channel = connection.createChannel();

        //3. 队列声明
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.basicQos(1); //保证每次只分发一个

        //4. 定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费2接收消息 " + msg);

                System.out.println("消费2 done");

                //返回确认状态
                channel.basicAck(envelope.getDeliveryTag(), false);

            }
        };

        //5. 监听队列,手动返回完成状态
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
    }
}

RabbitMQ详解_第17张图片

RabbitMQ详解_第18张图片

可以看出消费者2效率高,所以得到的消息比消费者1多。

(如果生产者和消费者都设置了 channel.basicQos(N) ,就说明消费者发送了 N 条消息之后必须要让消费者确认之后才会发下面的消息。如果消费者已经拿到了 n 条数据

2)channel.basicQos(5); //每次分发5个需要确认

把生产者和两个消费者的 basicQos 方法改为每次发 5 个消息:

RabbitMQ详解_第19张图片

RabbitMQ详解_第20张图片

RabbitMQ详解_第21张图片

RabbitMQ详解_第22张图片

可以看出,如果 channel.basicQos(5) 写的是5,这说明每发 5 个消息需要确认,否则就不会再发给这个消费者了。上述例子消费者 2 一直未确认,所以收到了 5 条消息之后就阻塞了。还可以看出,其中几条消息阻塞是不会影响队列中后面的消息被消费的(所以消费者1 拿到了被阻塞消息后面的消息),只会影响被阻塞的这个消费者以及这几条消息。

4. AMQP 协议的介绍

从前面的内容可以了解到RabbitMQ是遵从AMQP协议的。RabbitMQ中的交换器、交换器类型、队列、绑定、路由键等都是遵循的AMQP协议中相应的概念。本文如无特殊说明,都以 AQMP 0-9-1 为基准说明。

AMQP协议本身包括三层。 

Module Layer:位于协议最高层,主要定义了一些供客户端调用的命令,客户端可以利用这些命令实现自己的业务逻辑。例如,客户端可以使用Queue.Declare命令声明一个队列或者使用Basic.Consume订阅消费一个队列中的消息。 

Session Layer:位于中间层,主要负责将客户端的命令发送给服务器,再将服务端的应答返回给客户端,主要为客户端与服务器之间的通信提供可靠性同步机制和错误处理。 

Transport Layer:位于最底层,主要传输二进制数据流,提供帧的处理、信道复用、错误检测和数据表示等。 

AMQP说到底还是一个通信协议,通信协议都会涉及报文交互,从low-level举例来说,AMQP本身是应用层的协议,其填充于TCP协议层的数据部分。而从high-level来说,AMQP是通过协议命令进行交互的。AMQP协议可以看作一系列结构化命令的集合,这里的命令代表一种操作,类似于HTTP中的方法(GET、POST、PUT、DELETE等)。

4.1 AMQP 生产者流转过程 

生产者部分代码:

        Connection connection = factory.newConnection(); //创建连接
        Channel channel = connection.createChannel(); // 创建信道
        String message = "Hello world";
        channel.basicPublish(EXCHANGE_NAME,ROUTING_KEY, MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());
        channel.close();
        connection.close();

当客户端与Broker建立连接的时候,会调用factory.newConnection方法,这个方法会进一步封装成Protocol Header 0-9-1的报文头发送给Broker,以此通知Broker本次交互采用的是AMQP 0-9-1协议,紧接着Broker返回Connection.Start来建立连接,在连接的过程中涉及Connection.Start/.Start-OK、Connection.Tune/.Tune-Ok、Connection.Open/.Open-Ok这6个命令的交互。 

当客户端调用connection.createChannel方法准备开启信道的时候,其包装Channel.Open命令发送给Broker,等待Channel.Open-Ok命令。 

当客户端发送消息的时候,需要调用channel.basicPublish方法,对应的AQMP命令为Basic.Publish,注意这个命令和前面涉及的命令略有不同,这个命令还包含了Content Header和Content Body。Content Header里面包含的是消息体的属性,例如,投递模式、优先级等,而Content Body包含消息体本身。 

当客户端发送完消息需要关闭资源时,涉及Channel.Close/.Close-Ok与Connection.Close/.Close-Ok的命令交互。详细流转过程如图下所示:

RabbitMQ详解_第23张图片

4.2 AMQP消费者流转过程 

消费端的关键代码如下:

        Connection connection = factory.newConnection(addresses); //创建连接
        final Channel channel = connection.createChannel(); // 创建信道
        DefaultConsumer consumer = new DefaultConsumer(channel);
        channel.basicQos(64);
        channel.basicConsume(QUEUE_NAME, consumer);
        channel.close();
        connection.close();

消费者客户端同样需要与 Broker 建立连接,与生产者客户端一样,协议交互同样涉及Connection.Start/.Start-Ok、Connection.Tune/.Tune-Ok和Connection.Open/.Open-Ok等,下中省略了这些步骤,可以上一张生产者的图。 

RabbitMQ详解_第24张图片

紧接着也少不了在Connection之上建立Channel,和生产者客户端一样,协议涉及Channel.Open/Open-Ok。 

如果在消费之前调用了channel.basicQos(int prefetchCount)的方法来设置消费者客户端最大能“保持”的未确认的消息数,那么协议流转会涉及Basic.Qos/.Qos-Ok这两个AMQP命令。

 在真正消费之前,消费者客户端需要向Broker发送Basic.Consume命令(即调用channel.basicConsume方法)将Channel置为接收模式,之后Broker回执Basic.Consume-Ok以告诉消费者客户端准备好消费消息。紧接着Broker向消费者客户端推送(Push)消息,即Basic.Deliver命令,有意思的是这个和Basic.Publish命令一样会携带Content Header和Content Body。 

消费者接收到消息并正确消费之后,向Broker发送确认,即Basic.Ack命令。 

在消费者停止消费的时候,主动关闭连接,这点和生产者一样,涉及Channel.Close/.Close-Ok和Connection.Close/.Close-Ok。

4.3 AMQP命令概览 

下面列举了AMQP 0-9-1协议主要的命令,包含名称、是否包含内容体(Content Body)、对应客户端中相应的方法及简要描述等四个维度进行说明,具体如下表所示:

RabbitMQ详解_第25张图片

RabbitMQ详解_第26张图片

RabbitMQ详解_第27张图片

RabbitMQ详解_第28张图片

5. RabbitMQ 运转流程 

在最初状态下,生产者发送消息的时候(可依照下图): 

RabbitMQ详解_第29张图片

(1)生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。

(2)生产者声明一个交换器,并设置相关属性,比如交换机类型、是否持久化等。

(3)生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等。

(4)生产者通过路由键将交换器和队列绑定起来。

(5)生产者发送消息至RabbitMQ Broker,其中包含路由键、交换器等信息。

(6)相应的交换器根据接收到的路由键查找相匹配的队列。 

(7)如果找到,则将从生产者发送过来的消息存入相应的队列中。 

(8)如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者。

(9)关闭信道。 

(10)关闭连接。 

消费者接收消息的过程: 

(1)消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)。 

(2)消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作。 

(3)等待RabbitMQ Broker回应并投递相应队列中的消息,消费者接收消息。 

(4)消费者确认(ack)接收到的消息。 

(5)RabbitMQ从队列中删除相应已经被确认的消息。 

(6)关闭信道。 

(7)关闭连接。 

如下图所示,我们又引入了两个新的概念:Connection和Channel。我们知道无论是生产者还是消费者,都需要和RabbitMQ Broker建立连接,这个连接就是一条TCP连接,也就是Connection。一旦TCP连接建立起来,客户端紧接着可以创建一个AMQP信道(Channel),每个信道都会被指派一个唯一的ID。信道是建立在Connection之上的虚拟连接,RabbitMQ处理的每条AMQP指令都是通过信道完成的。

RabbitMQ详解_第30张图片

我们完全可以直接使用Connection就能完成信道的工作,为什么还要引入信道呢?

试想这样一个场景,一个应用程序中有很多个线程需要从RabbitMQ中消费消息,或者生产消息,那么必然需要建立很多个Connection,也就是许多个TCP连接。然而对于操作系统而言,建立和销毁TCP连接是非常昂贵的开销,如果遇到使用高峰,性能瓶颈也随之显现。RabbitMQ采用类似NIO(Non-blocking I/O)的做法,选择TCP连接复用,不仅可以减少性能开销,同时也便于管理。 每个线程把持一个信道,所以信道复用了Connection的TCP连接。同时RabbitMQ可以确保每个线程的私密性,就像拥有独立的连接一样。当每个信道的流量不是很大时,复用单一的Connection可以在产生性能瓶颈的情况下有效地节省TCP连接资源。但是当信道本身的流量很大时,这时候多个信道复用一个Connection就会产生性能瓶颈,进而使整体的流量被限制了。此时就需要开辟多个Connection,将这些信道均摊到这些Connection中。

6. 交换机

6.1 交换机类型

消息队列中间件,也可以称为消息队列或者消息中间件。它一般有两种传递模式:点对点(P2P,Point-to-Point)模式和发布/订阅(Pub/Sub)模式点对点模式是基于队列的,消息生产者发送消息到队列,消息消费者从队列中接收消息,队列的存在使得消息的异步传输成为可能。发布订阅模式定义了如何向一个内容节点发布和订阅消息,这个内容节点称为主题(topic),主题可以认为是消息传递的中介,消息发布者将消息发布到某个主题,而消息订阅者则从主题中订阅消息。主题使得消息的订阅者与消息的发布者互相保持独立,不需要进行接触即可保证消息的传递,发布/订阅模式在消息的一对多广播时采用。 

RabbitMQ 常用的交换器类型有fanout、direct、topic、headers这四种。

1)fanout 。广播,它会把所有发送到该交换器的消息路由到所有与该交换器绑定的队列中。 

2)direct。定向,direct类型的交换器路由规则也很简单,它会把消息路由到那些BindingKey和RoutingKey完全匹配的队列中。 

以下图为例,交换器的类型为 direct,如果我们发送一条消息,并在发送消息的时候设置路由键为“warning”,则消息会路由到Queue1和Queue2:

channel.basicPublish(EXCHANGE_NAME, "warning",MessageProperties.PERSISTENT_TEXT_PLAIN, message.getBytes());

RabbitMQ详解_第31张图片

如果在发送消息的时候设置路由键为“info”或者“debug”,消息只会路由到Queue2。如果以其他的路由键发送消息,则消息不会路由到这两个队列中。 

3)topic。

通配符,将消息路由到 BindingKey 和 RoutingKey 相匹配的队列中, RoutingKey 和 BindingKey 都为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如“com.rabbitmq.client”、“java.util.concurrent”。 BindingKey中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多规格单词(可以是零个)。

以下图为例:

RabbitMQ详解_第32张图片

  • 路由键为“com.rabbitmq.client”的消息会同时路由到Queue1和Queue2; 
  • 路由键为“com.hidden.client”的消息只会路由到Queue2中; 
  • 路由键为“com.hidden.demo”的消息只会路由到Queue2中; 
  • 路由键为“java.rabbitmq.demo”的消息只会路由到Queue1中; 
  • 路由键为“java.util.concurrent”的消息将会被丢弃或者返回给生产者(需要设置mandatory参数),因为它没有匹配任何路由键。

消费者接收消息时,* 和 # 的区别:

“#” 可以代表多个词,“*” 只能代表一个词。

例如:

生产者发送消息:
channel.basicPublish(EXCHANGE_NAME,"goods.new.add",null,msg.getBytes());

消费者接收文件:
channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");  //可以收到
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"goods.*");  //收不到
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,"goods.new.*");  //可以收到

4)headers 

headers 类型的交换器不依赖于路由键的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。在绑定队列和交换器时制定一组键值对,当发送消息到交换器时,RabbitMQ会获取到该消息的headers(也是一个键值对的形式),对比其中的键值对是否完全匹配队列和交换器绑定时指定的键值对,如果完全匹配则消息会路由到该队列,否则不会路由到该队列。headers类型的交换器性能会很差,而且也不实用,基本上不会看到它的存在。

6.2 类型举例

6.2.1 订阅者模式中的分发模式(fanout)

这种模式下,消息会被所有消费者消费。就是说,只要是"绑定"到某个交换机的队列,都会收到生产者发送到该交换机的消息。

(转载连接:RabbitMQ (五) 订阅者模式之分发模式 ( fanout ) - 热敷哥 - 博客园)

生产者:

public class Send {
    private static final String EXCHANGE_NAME = "testFanoutExchange";
    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"fanout",false,false,null);

        for(int i = 0; i <10; i++){
            String msg = "hello mq"+i;
            System.out.println("生产者发送消息:"+msg);

            //4. 发送消息 第2个参数为路由键,这种模式显然不需要路由键了,因为我们是把消息发送到所有绑定到该交换机的队列.
            channel.basicPublish(EXCHANGE_NAME,"",null, msg.getBytes());

            System.out.println("----send"+msg);

        }

        channel.close();
        connection.close();
    }
}

消费者1:

public class Receive1 {
    private static final String EXCHANGE_NAME = "testFanoutExchange";
    private static final String QUEUE_NAME = "testFanoutGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //队列绑定交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME,"",null);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1]: " + msg);

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    channel.basicAck(envelope.getDeliveryTag(),false);
                    System.out.println("[1] done");
                }

            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);

    }
}

消费者2:

public class Receive2 {

    private static final String EXCHANGE_NAME = "testFanoutExchange";
    private static final String QUEUE_NAME = "testFanoutGroup2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //队列绑定交换机
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "", null);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2]: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[2] done");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(QUEUE_NAME, autoAck, consumer);
        
    }

}

RabbitMQ详解_第33张图片RabbitMQ详解_第34张图片RabbitMQ详解_第35张图片

消费者1和消费者2都绑定了交换机,所以都可以收到消息。

6.2.2 订阅者模式之路由模式 ( direct )

RabbitMQ (六) 订阅者模式之路由模式 ( direct ) - 热敷哥 - 博客园

刚刚的 分发模式 是 Exchange 把消息发送给所有绑定它的队列。

而路由模式中,队列与交换机的绑定,不能是任意绑定了,而是要指定一个RoutingKey(路由key)(可以绑定多个 );消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey。Exchange 不再把消息发送给每一个绑定它的队列,而是根据消息的 RoutingKey 进行判断,只有队列的 Routingkey 与消息的 Routingkey 完全一致,才会接收到消息。

public class Send {
    private static final String EXCHANGE_NAME = "testDirectExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();

        String msg = "hello mq";

        //把消息发送到交换机,交换机再转发到包含路由键"hello"的队列.
        channel.basicPublish(EXCHANGE_NAME, "hello", null, msg.getBytes());

        System.out.println("生产者发送消息:" + msg);

        channel.close();
        connection.close();
    }
}
public class Receive1 {
    private static final String EXCHANGE_NAME = "testDirectExchange";
    private static final String QUEUE_NAME = "testDirectQueue1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //声明交换机
        channel.exchangeDeclare(EXCHANGE_NAME, "direct", false, false, null);

        //队列绑定交换机,路由键为"hello"和"world"
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "hello", null);
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "world", null);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1]: " + msg);

                try {
                    Thread.sleep(10000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    channel.basicAck(envelope.getDeliveryTag(), false);
                    System.out.println("[1] done");
                }

            }
        };

        //4. 监听队列
        //自动应答 false
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }
}
public class Receive2 {

    private static final String EXCHANGE_NAME = "testDirectExchange";
    private static final String QUEUE_NAME = "testDirectQueue2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        //队列绑定交换机,路由键为"world"
        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "world", null);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2]: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[2] done");
            }
        };

        //4. 监听队列
        //自动应答 false
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }

}

这样就只会有消费者1能接收到消息,因为消费者2绑定的路由key不一样。
RabbitMQ详解_第36张图片

6.2.3 订阅者模式之主题模式 ( topic )

RabbitMQ (七) 订阅者模式之主题模式 ( topic ) - 热敷哥 - 博客园

Topic 类型与 Direct 相比,都是可以根据 RoutingKey 把消息路由到不同的队列。只不过 Topic 类型 Exchange 可以让队列在绑定 Routing key 的时候使用通配符!

public class Send {
    private static final String EXCHANGE_NAME = "testTopicExchange";

    public static void main(String[] args) throws Exception {

        Connection connection = ConnectionUtils.getConnection();

        Channel channel = connection.createChannel();
        
        String msg = "hello mq";
        System.out.println("生产者发送消息:" + msg);

        //4. 发送消息
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", null, msg.getBytes());

        System.out.println("----send" + msg);
        
        channel.close();
        connection.close();
    }
}
import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicQueue1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        //定义交换机
        channel.exchangeDeclare(EXCHANGE_NAME,"topic");

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[1]: " + msg);

                System.out.println("[1] done");

                channel.basicAck(envelope.getDeliveryTag(),false);
            }
        };

        //4. 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }
}
import com.janet.util.ConnectionUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

public class Receive2 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicQueue2";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, null);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.new.*");

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[2]: " + msg);
                try {
                    Thread.sleep(2000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                } finally {
                    System.out.println("[2] done");

                    channel.basicAck(envelope.getDeliveryTag(), false);
                }
            }
        };

        //4. 监听队列
        channel.basicConsume(QUEUE_NAME, false, consumer);

    }
}

RabbitMQ详解_第37张图片RabbitMQ详解_第38张图片RabbitMQ详解_第39张图片

7. RabbitMQ 消息的发送与确认

7.1 发送消息

如果要发送一个消息,可以使用 Channel 类的 basicPublish 方法。

/* 
         * 向server发布一条消息 
         * 参数1:exchange 交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
         * 参数2:routingKey  路由键,交换器根据路由键将消息存储到相应的队列之中。# 匹配0个或多个单词,* 匹配一个单词,在 topic exchange 做消息转发用
         * 参数3:mandatory 当 mandatory 参数设为 true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃。
         
* 参数4:immediate:当 immediate 参数设为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者。 RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持。
         * 参数5:BasicProperties :消息的基本属性集,其包含14个属性成员,分别有contentType、contentEncoding、headers(Map)、deliveryMode、priority、correlationId、replyTo、expiration、messageId、timestamp、type、userId、appId、clusterId。需要注意的是 BasicProperties.deliveryMode,1(nopersistent)非持久化,2(persistent)持久化。这里指的是消息的持久化,配合 channel(durable=true),queue(durable) 可以实现,即使服务器宕机,消息仍然保留。
         * 参数6:byte[] body:消息体(payload),真正需要发送的消息。

简单来说,mandatory 标志告诉服务器至少将该消息 route 到一个队列中,否则将消息返还给生产者;immediate 标志告诉服务器如果该消息关联的 queue 上有消费者,则马上将消息投递给它,如果所有 queue 都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。 
         */  

void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
            throws IOException;

对于basicPublish而言,有几个重载方法:

void basicPublish(String exchange, String routingKey, BasicProperties props,byte[] body) throws IOException; 

void basicPublish(String exchange, String routingKey, boolean mandatory,BasicProperties props, byte[] body) throws IOException; 

void basicPublish(String exchange, String routingKey, boolean mandatory,boolean immediate, BasicProperties props, byte[] body) throws IOException; 

比如发送一条内容为 “Hello World!” 的消息,参考如下: 

        String routingKey = "helloKey";
        byte[] msgBodyBytes = "Hello World!".getBytes();
        channel.basicPublish(EXCHANGE_NAME, routingKey, null, msgBodyBytes);

为了更好地控制发送,可以使用 mandatory 这个参数,或者可以发送一些特定属性的信息:  

        String routingKey = "helloKey";
        Boolean mandatory = true;
        byte[] msgBodyBytes = "Hello World!".getBytes();
        channel.basicPublish(EXCHANGE_NAME, routingKey, mandatory, MessageProperties.PERSISTENT_TEXT_PLAIN, msgBodyBytes);

上面这行代码发送了一条消息,这条消息的投递模式(delivery mode)设置为 2,即消息会被持久化(即存入磁盘)在服务器中。同时这条消息的优先级(priority)设置为1,content-type为“text/plain”。可以自己设定消息的属性:  

RabbitMQ详解_第40张图片

也可以发送一条带有headers的消息:

RabbitMQ详解_第41张图片

还可以发送一条带有过期时间(expiration)的消息:  

        String routingKey = "helloKey";
        
        byte[] msgBodyBytes = "Hello World!".getBytes();
        
        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .expiration("5000") //设置消息有效期5秒钟
                .build();
        channel.basicPublish(EXCHANGE_NAME, routingKey, properties, msgBodyBytes);

1)消息何去何从

① mandatory 参数

当 mandatory 参数设为 true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃。

那么生产者如何获取到没有被正确路由到合适队列的消息呢?这时候可以通过调用 channel.addReturnListener 来添加ReturnListener监听器实现。 

使用 mandatory 参数的关键代码如代码清单:

        channel.basicPublish(EXCHANGE_NAME, "", true, MessageProperties.PERSISTENT_TEXT_PLAIN, "mandatory test".getBytes());
        channel.addReturnListener(new ReturnListener() {
            public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties basicProperties, byte[] body) throws IOException {
                String message = new String(body);
                System.out.println("Basic.Return返回的结果是:" + message);
            }

        });

RabbitMQ详解_第42张图片

上面代码中生产者没有成功地将消息路由到队列,此时 RabbitMQ 会通过 Basic.Return 返回 “mandatory test” 这条消息,之后生产者客户端通过 ReturnListener 监听到了这个事件,上面代码的最后输出应该是 “Basic.Return返回的结果是:mandatory test”。 从AMQP协议层面来说,其对应的流转过程如下图所示:

RabbitMQ详解_第43张图片

② immediate 参数

当 immediate 参数设为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过 Basic.Return 返回至生产者。 

概括来说,mandatory 参数告诉服务器至少将该消息路由到一个队列中,否则将消息返回给生产者。immediate参数告诉服务器,如果该消息关联的队列上有消费者,则立刻投递;如果所有匹配的队列上都没有消费者,则直接将消息返还给生产者,不用将消息存入队列而等待消费者了。 

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

发送带 immediate 参数(immediate 参数设置为 true)的 Basic.Publish 客户端会报如下异常:  

RabbitMQ 服务端会报如下异常(查看RabbitMQ的运行日志,默认日志路径为$RABBITMQ _HOME/var/log/rabbitmq/rabbit@$HOSTNAME.log): 

③ 备份交换器 

备份交换器,英文名称为 Alternate Exchange,简称 AE,或者更直白地称之为“备胎交换器”。生产者在发送消息的时候如果不设置 mandatory 参数,那么消息在未被路由的情况下将会丢失;如果设置了 mandatory 参数,那么需要添加 ReturnListener 的编程逻辑,生产者的代码将变得复杂。如果既不想复杂化生产者的编程逻辑,又不想消息丢失,那么可以使用备份交换器,这样可以将未被路由的消息存储在 RabbitMQ 中,再在需要的时候去处理这些消息。 

可以通过在声明交换器(调用channel.exchangeDeclare方法)的时候添加 alternate-exchange 参数来实现,也可以通过策略(Policy)的方式实现。如果两者同时使用,则前者的优先级更高,会覆盖掉Policy的设置。

使用参数设置的关键代码如代码清单:

        HashMap argss = new HashMap<>();
        argss.put("alternate-exchange","myAe");
        channel.exchangeDeclare("normalExchange","direct",true,false,argss);//声明普通交换机
        
        channel.exchangeDeclare("myAe","fanout",true,false,null);  //声明备份交换机
        
        channel.queueDeclare("normalQueue",true,false,false,null);
        channel.queueBind("normalQueue","normalExchange","normalKey"); //普通队列绑定交换机
        
        channel.queueDeclare("unroutedQueue", true,false,false,null);
        channel.queueBind("unroutedQueue","myAe",""); //备份队列绑定备份交换机

上面的代码中声明了两个交换器 normalExchange 和 myAe,分别绑定了 normalQueue 和 unroutedQueue 这两个队列,同时将 myAe 设置为 normalExchange 的备份交换器。注意 myAe 的交换器类型为 fanout。

参考下图,如果此时发送一条消息到 normalExchange 上,当路由键等于 “normalKey” 的时候,消息能正确路由到 normalQueue 这个队列中。如果路由键设为其他值,比如“errorKey”,即消息不能被正确地路由到与 normalExchange 绑定的任何队列上,此时就会发送给 myAe,进而发送到 unroutedQueue 这个队列。

RabbitMQ详解_第44张图片
同样,如果采用 Policy 的方式来设置备份交换器,可以参考如下:

备份交换器其实和普通的交换器没有太大的区别,为了方便使用,建议设置为 fanout 类型,如若读者想设置为 direct 或者 topic 的类型也没有什么不妥。需要注意的是,消息被重新发送到备份交换器时的路由键和从生产者发出的路由键是一样的。 

考虑这样一种情况,如果备份交换器的类型是 direct,并且有一个与其绑定的队列,假设绑定的路由键是 key1,当某条携带路由键为 key2 的消息被转发到这个备份交换器的时候,备份交换器没有匹配到合适的队列,则消息丢失。如果消息携带的路由键为 key1,则可以存储到队列中。 

对于备份交换器,总结了以下几种特殊情况: 

  • 如果设置的备份交换器不存在,客户端和 RabbitMQ 服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器没有绑定任何队列,客户端和 RabbitMQ 服务端都不会有异常出现,此时消息会丢失。 
  • 如果备份交换器没有任何匹配的队列,客户端和 RabbitMQ 服务端都不会有异常出现,此时消息会丢失。
  • 如果备份交换器和 mandatory 参数一起使用,那么mandatory参数无效。

7.2 消费消息

RabbitMQ 的消费模式分两种:推(Push)模式和拉(Pull)模式。推模式采用 Basic.Consume 进行消费,而拉模式则是调用 Basic.Get 进行消费。

1)推模式 

在推模式中,可以通过持续订阅的方式来消费消息。接收消息一般通过实现 Consumer 接口或者继承 DefaultConsumer 类来实现。当调用与 Consumer 相关的API方法时,不同的订阅采用不同的消费者标签(consumerTag)来区分彼此,在同一个 Channel 中的消费者也需要通过唯一的消费者标签以作区分。

/*       消息消费完成确认
         * 参数1:queue 队列的名称
         * 参数2:autoAck 设置是否自动确认。建议设成false,即不自动确认。自动确认:只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费;手动确认:消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。如果选用自动确认,在消费者拿走消息执行过程中出现宕机时,消息可能就会丢失!!
         * 参数3:consumerTag 消费者标签,用来区分多个消费者
         * 参数4:noLocal 设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者
         * 参数5:exclusive 设置是否排他
         * 参数6:arguments 设置消费者的其他参数
         * 参数7:callback 设置消费者的回调函数。用来处理RabbitMQ推送过来的消息,比如DefaultConsumer,使用时需要客户端重写(override)其中的方法。
         */

String basicConsume(String queue, boolean autoAck, String consumerTag,boolean noLocal, boolean exclusive, Map arguments, Consumer callback)throws IOException; 

Channel 类中 basicConsume 方法有如下几种形式:

  • String basicConsume(String queue, Consumer callback) throws IOException; 
  •  String basicConsume(String queue, boolean autoAck, Consumer callback) throws IOException; 
  • String basicConsume(String queue, boolean autoAck, Maparguments, Consumer callback) throws IOException; 
  • String basicConsume(String queue, boolean autoAck, String consumerTag,Consumer callback) throws IOException; 
  • String basicConsume(String queue, boolean autoAck, String consumerTag,boolean noLocal, boolean exclusive, Map arguments, Consumer callback)throws IOException; 

关键消费代码如代码:

        boolean autoAck = false; //自动应答 false

        channel.basicQos(1);

        channel.basicConsume(QUEUE_NAME, autoAck,"myConsumerTag",new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 1 ]处理完成");

            }
        });

注意,上面代码中显式地设置 autoAck 为 false,然后在接收到消息之后进行显式 ack 操作(channel.basicAck),对于消费者来说这个设置是非常必要的,可以防止消息不必要地丢失。 

对于消费者客户端来说重写 handleDelivery 方法是十分方便的。更复杂的消费者客户端会重写更多的方法,具体如下:  

比如 handleShutdownSignal 方法,当 Channel 或者 Connection 关闭的时候会调用。再者,handleConsumeOk 方法会在其他方法之前调用,返回消费者标签。 

重写 handleCancelOk 和 handleCancel 方法,这样消费端可以在显式地或者隐式地取消订阅的时候调用。也可以通过 channel.basicCancel 方法来显式地取消一个消费者的订阅:

 channel.basicCancel(consumerTag); 

注意上面这行代码会首先触发 handleConsumerOk 方法,之后触发 handleDelivery 方法,最后才触发 handleCancelOk 方法。 

和生产者一样,消费者客户端同样需要考虑线程安全的问题。消费者客户端的这些 callback 会被分配到与 Channel 不同的线程池上,这意味着消费者客户端可以安全地调用这些阻塞方法,比如 channel.queueDeclare、channel.basicCancel等。 

每个 Channel 都拥有自己独立的线程。最常用的做法是一个 Channel 对应一个消费者,也就是意味着消费者彼此之间没有任何关联。当然也可以在一个 Channel 中维持多个消费者,但是要注意一个问题,如果 Channel 中的一个消费者一直在运行,那么其他消费者的 callback 会被“耽搁”。

2)拉模式

通过 channel.basicGet 方法可以单条地获取消息,其返回值是GetRespone。

GetResponse basicGet(String queue, boolean autoAck) throws IOException; 

其中 queue 代表队列的名称,如果设置 autoAck 为 false,那么同样需要调用 channel.basicAck 来确认消息已被成功接收。

        GetResponse getResponse = channel.basicGet(QUEUE_NAME, false);
        
        System.out.println(getResponse.getBody());
        
        channel.basicAck(getResponse.getEnvelope().getDeliveryTag(), false);

消费方式如图:

RabbitMQ详解_第45张图片

注意要点: 

Basic.Consume 将信道(Channel)置为接收模式,直到取消队列的订阅为止。在接收模式期间,RabbitMQ 会不断地推送消息给消费者,当然推送消息的个数还是会受到 Basic.Qos 的限制。如果只想从队列获得单条消息而不是持续订阅,建议还是使用 Basic.Get 进行消费。但是不能将 Basic.Get 放在一个循环里来代替 Basic.Consume,这样做会严重影响 RabbitMQ 的性能。如果要实现高吞吐量,消费者理应使用 Basic.Consume 方法。

订阅消费的方式建议通过继承 DefaultConsumer 类来实现,不要使用 QueueingConsumer 这个类来实现订阅消费。

QueueingConsumer 缺陷:

  • 内存溢出的问题,如果由于某些原因,队列之中堆积了比较多的消息,就可能导致消费者客户端内存溢出假死,于是发生恶性循环,队列消息不断堆积而得不到消化。 这个内存溢出的问题可以使用Basic.Qos来得到有效的解决,Basic.Qos可以限制某个消费者所保持未确认消息的数量,也就是间接地限制了QueueingConsumer中的LinkedBlockingQueue的大小。注意一定要在调用Basic.Consume之前调用Basic.Qos才能生效。
  • QueueingConsumer会拖累同一个Connection下的所有信道,使其性能降低; 
  • 同步递归调用QueueingConsumer会产生死锁; 
  • RabbitMQ的自动连接恢复机制(automatic connection recovery)不支持Queueing Consumer的这种形式; 
  • QueueingConsumer不是事件驱动的。 

为了避免不必要的麻烦,建议在消费的时候尽量使用继承DefaultConsumer的方式。

RabbitMQ之消息确认机制(事务+Confirm)

7.3 消费端的确认与拒绝 

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

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

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

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

RabbtiMQ的Web管理平台上可以看到当前队列中的“Ready”状态和“Unacknowledged”状态的消息数,分别对应上文中的等待投递给消费者的消息数和已经投递给消费者但是未收到确认信号的消息数:



在消费者接收到消息后,如果想明确拒绝当前的消息而不是确认,那么应该怎么做呢?RabbitMQ 在 2.0.0 版本开始引入了 Basic.Reject 这个命令,消费者客户端可以调用与其对应的 channel.basicReject 方法来告诉 RabbitMQ 拒绝这个消息。 Basic.Reject 命令一次只能拒绝一条消息,如果想要批量拒绝消息,则可以使用 Basic.Nack 这个命令。

7.4 消息的持久化

持久化可以提高 RabbitMQ 的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。RabbitMQ 的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。 

交换器的持久化是通过在声明交换机时 durable 参数置为 true 实现的。如果交换器不设置持久化,那么在 RabbitMQ 服务重启之后,相关的交换器元数据会丢失,不过消息不会丢失,只是不能将消息发送到这个交换器中了。对一个长期使用的交换器来说,建议将其置为持久化的。

队列的持久化是通过在声明队列时将 durable 参数置为 true 实现的。如果队列不设置持久化,那么在 RabbitMQ 服务重启之后,相关队列的元数据会丢失,此时数据也会丢失。正所谓“皮之不存,毛将焉附”,队列都没有了,消息又能存在哪里呢? 

队列的持久化能保证其本身的元数据不会因异常情况而丢失,但是并不能保证内部所存储的消息不会丢失。要确保消息不会丢失,需要将其设置为持久化。通过将消息的投递模式(BasicProperties 中的 deliveryMode 属性)设置为 2 即可实现消息的持久化前面示例中多次提及的 MessageProperties.PERSISTENT_TEXT_PLAIN 实际上是封装了这个属性:

  RabbitMQ详解_第46张图片

设置了队列和消息的持久化,当 RabbitMQ 服务重启之后,消息依旧存在。单单只设置队列持久化,重启之后消息会丢失;单单只设置消息的持久化,重启之后队列消失,继而消息也丢失。单单设置消息持久化而不设置队列的持久化显得毫无意义。 

注意要点:

可以将所有的消息都设置为持久化,但是这样会严重影响 RabbitMQ 的性能(随机)。写入磁盘的速度比写入内存的速度慢得不只一点点。对于可靠性不是那么高的消息可以不采用持久化处理以提高整体的吞吐量。在选择是否要将消息持久化时,需要在可靠性和吐吞量之间做一个权衡。 

将交换器、队列、消息都设置了持久化之后就能百分之百保证数据不丢失了吗?答案是否定的。 

① 首先从消费者来说,如果在订阅消费队列时将 autoAck 参数设置为 true,那么当消费者接收到相关消息之后,还没来得及处理就宕机了,这样也算数据丢失。

这种情况很好解决,将 autoAck 参数设置为 false,并进行手动确认。

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

这个问题怎么解决呢?这里可以引入 RabbitMQ 的镜像队列机制,相当于配置了副本,如果主节点(master)在此特殊时间内挂掉,可以自动切换到从节点(slave),这样有效地保证了高可用性,除非整个集群都挂掉。虽然这样也不能完全保证RabbitMQ消息不丢失,但是配置了镜像队列要比没有配置镜像队列的可靠性要高很多,在实际生产环境中的关键业务队列一般都会设置镜像队列。

还可以在发送端引入事务机制或者发送方确认机制来保证消息已经正确地发送并存储至 RabbitMQ 中,前提还要保证在调用 channel.basicPublish 方法的时候交换器能够将消息正确路由到相应的队列之中。

7.5 生产者确认消息

在使用 RabbitMQ 的时候,可以通过消息持久化操作来解决因为服务器的异常崩溃而导致的消息丢失,除此之外,我们还会遇到一个问题,当消息的生产者将消息发送出去之后,消息到底有没有正确地到达服务器呢?如果不进行特殊配置,默认情况下发送消息的操作是不会返回任何信息给生产者的,也就是默认情况下生产者是不知道消息有没有正确地到达服务器。如果在消息到达服务器之前已经丢失,持久化操作也解决不了这个问题,因为消息根本没有到达服务器,何谈持久化? 

RabbitMQ针对这个问题,提供了两种解决方式: 

  • 通过事务机制实现; 
  • 通过发送方确认(publisher confirm)机制实现。

7.5.1 事务机制 

RabbitMQ 客户端中与事务机制相关的方法有三个:channel.txSelect、channel.txCommit 和 channel.txRollback。channel.txSelect 用于将当前的信道设置成事务模式,channel.txCommit 用于提交事务,channel.txRollback 用于事务回滚。在通过channel.txSelect方法开启事务之后,我们便可以发布消息给RabbitMQ了,如果事务提交成功,则消息一定到达了RabbitMQ中,如果在事务提交执行之前由于RabbitMQ异常崩溃或者其他原因抛出异常,这个时候我们便可以将其捕获,进而通过执行channel.txRollback方法来实现事务回滚。

事务确实能够解决消息发送方和 RabbitMQ 之间消息确认的问题,只有消息成功被 RabbitMQ 接收,事务才能提交成功,否则便可在捕获异常之后进行事务回滚,与此同时可以进行消息重发。但是使用事务机制会“吸干” RabbitMQ 的性能(吞吐量大约降低了250倍,所以不推荐使用),那么有没有更好的方法既能保证消息发送方确认消息已经正确送达,又能基本上不带来性能上的损失呢?从AMQP协议层面来看并没有更好的办法,但是RabbitMQ提供了一个改进方案,即发送方确认机制。

7.5.2 发送方确认机制 

前面介绍了 RabbitMQ 可能会遇到的一个问题,即消息发送方(生产者)并不知道消息是否真正地到达了 RabbitMQ。随后了解到在 AMQP 协议层面提供了事务机制来解决这个问题,但是采用事务机制实现会严重降低 RabbitMQ 的消息吞吐量,这里就引入了一种轻量级的方式——发送方确认(publisher confirm)机制。 

生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。RabbitMQ 回传给生产者的确认消息中的 deliveryTag 包含了确认消息的序号,此外 RabbitMQ 也可以设置 channel.basicAck 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理,可以参考下图。注意辨别这里的确认和消费时候的确认之间的异同。  

RabbitMQ详解_第47张图片

事务机制在一条消息发送之后会使发送端阻塞,以等待 RabbitMQ 的回应,之后才能继续发送下一条消息。相比之下,发送方确认机制最大的好处在于它是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用程序便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack(Basic.Nack)命令,生产者应用程序同样可以在回调方法中处理该 nack 命令。 生产者通过调用channel.confirmSelect 方法(即 Confirm.Select 命令)将信道设置为 confirm模 式,之后 RabbitMQ 会返回 Confirm.Select-Ok 命令表示同意生产者将当前信道设置为 confirm 模式。所有被发送的后续消息都被 ack 或者 nack 一次,不会出现一条消息既被 ack 又被 nack 的情况,并且 RabbitMQ 也并没有对消息被 confirm 的快慢做任何保证。 下面看一下 publisher confirm 机制怎么运作,简要代码如代码清单:

RabbitMQ详解_第48张图片

如果发送多条消息,只需要将 channel.basicPublish 和 channel.waitForConfirms 方法包裹在循环里面即可,可以参考事务机制,不过不需要把 channel.confirmSelect 方法包裹在循环内部。 

在 publisher confirm 模式下发送多条消息的 AMQP 协议流转过程可以参考下图:

RabbitMQ详解_第49张图片

对于 channel.waitForConfirms 而言,在RabbitMQ客户端中它有4个同类的方法: 

  • (1) boolean waitForConfirms() throws InterruptedException; 
  • (2) boolean waitForConfirms(long timeout) throws InterruptedException,TimeoutException;
  • (3) void waitForConfirmsOrDie() throws IOException, InterruptedException; 
  • (4) void waitForConfirmsOrDie(long timeout) throws IOException, Interrupted Exception, TimeoutException; 

如果信道没有开启 publisher confirm 模式,则调用任何 waitForConfirms 方法都会报出 java.lang.IllegalStateException。对于没有参数的 waitForConfirms 方法来说,其返回的条件是客户端收到了相应的 Basic.Ack/.Nack 或者被中断。参数 timeout 表示超时时间,一旦等待 RabbitMQ 回应超时就会抛出 java.util.concurrent.TimeoutException 的异常。两个 waitForConfirmsOrDie 方法在接收到 RabbitMQ 返回的 Basic.Nack 之后会抛出 java.io.IOException。业务代码可以根据自身的特性灵活地运用这四种方法来保障消息的可靠发送。

注意要点: 

(1)事务机制和 publisher confirm 机制两者是互斥的,不能共存。如果企图将已开启事务模式的信道再设置为 publisher confirm 模式,RabbitMQ会报错:{amqp_error, precondition_failed, "cannot switch from tx to confirm mode", 'confirm.select'};或者如果企图将已开启 publisher confirm 模式的信道再设置为事务模式,RabbitMQ也会报错:{amqp_error, precondition_failed, "cannot switch from confirm to tx__mode", 'tx.select' }。 

(2)事务机制和 publisher confirm 机制确保的是消息能够正确地发送至 RabbitMQ,这里的“发送至 RabbitMQ ”的含义是指消息被正确地发往至 RabbitMQ 的交换器,如果此交换器没有匹配的队列,那么消息也会丢失。所以在使用这两种机制的时候要确保所涉及的交换器能够有匹配的队列。更进一步地讲,发送方要配合 mandatory 参数或者备份交换器一起使用来提高消息传输的可靠性。 

publisher confirm 的优势在于并不一定需要同步确认。这里我们改进了一下使用方式,总结有如下两种: 

  • 批量confirm方法:每发送一批消息后,调用channel.waitForConfirms方法,等待服务器的确认返回。 
  • 异步confirm方法:提供一个回调方法,服务端确认了一条或者多条消息后客户端会回调这个方法进行处理。 

批量 confirm 方法中,客户端程序需要定期或者定量(达到多少条),亦或者两者结合起来调用 channel.waitForConfirms 来等待RabbitMQ的确认返回。相比于前面示例中的普通confirm方法,批量极大地提升了confirm的效率,但是问题在于出现返回 Basic.Nack 或者超时情况时,客户端需要将这一批次的消息全部重发,这会带来明显的重复消息数量,并且当消息经常丢失时,批量 confirm 的性能应该是不升反降的。 

批量confirm方法的示例代码:

RabbitMQ详解_第50张图片

异步 confirm 方法的编程实现最为复杂。在客户端 Channel 接口中提供的 addConfirmListener 方法可以添加 ConfirmListener 这个回调接口,这个 ConfirmListener 接口包含两个方法:handleAck 和 handleNack,分别用来处理 RabbitMQ 回传的 Basic.Ack 和 Basic.Nack。在这两个方法中都包含有一个参数 deliveryTag(在 publisher confirm 模式下用来标记消息的唯一有序序号)。我们需要为每一个信道维护一个 “unconfirm” 的消息序号集合,每发送一条消息,集合中的元素加 1。每当调用 ConfirmListener 中的 handleAck 方法时,“unconfirm” 集合中删掉相应的一条(multiple 设置为 false)或者多条(multiple设置为true)记录。从程序运行效率上来看,这个 “unconfirm” 集合最好采用有序集合 SortedSet 的存储结构。事实上,Java 客户端 SDK 中的 waitForConfirms 方法也是通过 SortedSet 维护消息序号的。下列代码清单为我们演示了异步 confirm 的编码实现,其中的 confirmSet 就是一个 SortedSet 类型的集合。

RabbitMQ详解_第51张图片

最后我们将事务、普通 confirm、批量 confirm 和异步confirm这4种方式放到一起来比较一下彼此的QPS。

测试环境:客户端和Broker机器配置——CPU为24核、主频为2600Hz、内存为64GB、硬盘为1TB。客户端发送的消息体大小为10B,单线程发送,并且消息都进行持久化处理。

具体测试对比如下图:

RabbitMQ详解_第52张图片

可以看到批量 confirm 和异步 confirm 这两种方式所呈现的性能要比其余两种好得多。事务机制和普通 confirm 的方式吐吞量很低,但是编程方式简单,不需要在客户端维护状态(这里指的是维护 deliveryTag 及缓存未确认的消息)。批量 confirm 方式的问题在于遇到 RabbitMQ 服务端返回 Basic.Nack 需要重发批量消息而导致的性能降低。异步 confirm 方式编程模型最为复杂,而且和批量 confirm 方式一样需要在客户端维护状态。在实际生产环境中采用何种方式,这里就仁者见仁智者见智了,不过强烈建议读者使用异步confirm的方式。

7.5.3 消息的可靠投递

rabbitmq 整个消息投递的路径为:

producer ---> rabbitmq broker ---> exchange ---> queue---> consumer

  • 消息从 producer 到 exchange 则会返回一个 confirmCallback 。
  • 消息从 exchange --> queue 投递失败则会返回一个 returnCallback 。

我们将利用这两个 callback 控制消息的可靠性投递。

① 设置ConnectionFactory的publisher-confirms="true"开启确认模式。

使用rabbitTemplate.setConfirmCallback设置回调函数。当消息发送到exchange后回调confirm方法。在方法中判断ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。

    @Autowired
    private RabbitTemplate rabbitTemplate;


    //测试   Confirm 模式
    @Test
    public void testConfirm() {

        //定义回调
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            /**
             *
             * @param correlationData 相关配置信息
             * @param ack   exchange交换机 是否成功收到了消息。true 成功,false代表失败
             * @param cause 失败原因
             */
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                System.out.println("confirm方法被执行了....");

                //ack 为  true表示 消息已经到达交换机
                if (ack) {
                    //接收成功
                    System.out.println("接收成功消息" + cause);
                } else {
                    //接收失败
                    System.out.println("接收失败消息" + cause);
                    //做一些处理,让消息再次发送。
                }
            }
        });

        //进行消息发送
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message Confirm...");

        //进行睡眠操作
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

② 设置ConnectionFactory的publisher-returns="true"开启退回模式。

使用rabbitTemplate.setReturnCallback设置退回函数,当消息从exchange路由到queue失败后,如果设置了rabbitTemplate.setMandatory(true)参数,则会将消息退回给producer。并执行回调函数returnedMessage。

    //测试 return模式
    @Test
    public void testReturn() {

        //设置交换机处理失败消息的模式   为true的时候,消息达到不了 队列时,会将消息重新返回给生产者
        rabbitTemplate.setMandatory(true);

        //定义回调
        rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
            /**
             *
             * @param message   消息对象
             * @param replyCode 错误码
             * @param replyText 错误信息
             * @param exchange  交换机
             * @param routingKey 路由键
             */
            @Override
            public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
                System.out.println("return 执行了....");

                System.out.println("message:" + message);
                System.out.println("replyCode:" + replyCode);
                System.out.println("replyText:" + replyText);
                System.out.println("exchange:" + exchange);
                System.out.println("routingKey:" + routingKey);

                //处理
            }
        });
        
        //进行消息发送
        rabbitTemplate.convertAndSend("test_exchange_confirm", "confirm", "message return...");

        //进行睡眠操作
        try {
            Thread.sleep(5000);
        } catch (Exception e) {
            e.printStackTrace();
        }
    }

7.5.4 Consumer Ack

ack 指Acknowledge,确认。表示消费端收到消息后的确认方式。有三种确认方式:

  • 自动确认:acknowledge="none"
  • 手动确认:acknowledge="manual"
  • 根据异常情况确认:acknowledge="auto"

其中自动确认是指,当消息一旦被 Consumer 接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。如果设置了手动确认方式,则需要在业务处理成功后,调用 channel.basicAck(),手动签收,如果出现异常,则调用 channel.basicNack() 方法,让其自动重新发送消息。

如何保证消费的可靠性传输?

我们在使用消息队列的过程中,应该做到消息不能多消费,也不能少消费。可靠性传输,每种MQ都要从三个角度来分析:生产者弄丢数据、消息队列弄丢数据、消费者弄丢数据。

(1) 生产者丢数据

从生产者弄丢数据这个角度来看,RabbitMQ 提供事务机制发送方确认(publisher confirm)机制来确保生产者不丢消息。
事务机制机制就是说,发送消息前,开启事务(channel.txSelect),然后发送消息,如果发送过程中出现什么异常,事物就会回滚(channel.txRollback()),如果发送成功则提交事务(channel.txCommit())。然而缺点就是吞吐量下降了。(所以不建议使用)

因此,生产上用 publisher confirm 模式的居多。生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的 ID(从1开始),一旦消息被投递到所有匹配的队列之后,RabbitMQ 就会发送一个确认(Basic.Ack)给生产者(包含消息的唯一ID),这就使得生产者知晓消息已经正确到达了目的地了。如果消息和队列是可持久化的,那么确认消息会在消息写入磁盘之后发出。这种机制的好处在于它是异步的,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack(Basic.Nack)命令,生产者应用程序可以在回调方法中处理该 nack 命令。

除了以上两个,还有发送方的退回模式(publisher-returns),当 exchange --> queue 投递失败(比如投递 key 错误)则会返回一个 returnCallback 。

(2) 消息队列丢数据

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

如何持久化:

  • 交换器的持久化:在声明交换机时 durable 参数置为 true
  • 队列的持久化:在声明队列时将 durable 参数置为 true
  • 消息的持久化:发送消息时,消息的投递模式(BasicProperties 中的 deliveryMode 属性)设置为 2

这样设置以后,RabbitMQ就算挂了,重启后也能恢复数据

(3) 消费者丢数据

消费者丢数据一般是因为采用了自动确认消息模式。这种模式下,消费者会自动确认收到信息。这时 RahbitMQ 会立即将消息删除,这种情况下如果消费者出现异常而没能处理该消息,就会丢失该消息。至于解决方案,采用手动确认消息即可。

7.6 消费端拒绝消息

上面介绍了如何正确地消费消息。消费者客户端可以通过推模式或者拉模式的方式来获取并消费消息,当消费者处理完业务逻辑需要手动确认消息已被接收,这样 RabbitMQ 才能把当前消息从队列中标记清除。当然如果消费者由于某些原因无法处理当前接收到的消息,可以通过 channel.basicNack 或者 channel.basicReject 来拒绝掉。 

这里对于RabbitMQ消费端来说,还有几点需要注意: 

  • 消息分发; 
  • 消息顺序性; 
  • 弃用QueueingConsumer。

7.6.1 消息分发 

当 RabbitMQ 队列拥有多个消费者时,队列收到的消息将以轮询(round-robin)的分发方式发送给消费者。每条消息只会发送给订阅列表里的一个消费者。这种方式非常适合扩展,而且它是专门为并发程序设计的。如果现在负载加重,那么只需要创建更多的消费者来消费处理消息即可。 

很多时候轮询的分发机制也不是那么优雅。默认情况下,如果有 n 个消费者,那么 RabbitMQ 会将第 m 条消息分发给第m%n(取余的方式)个消费者,RabbitMQ 不管消费者是否消费并已经确认(Basic.Ack)了消息。如果某些消费者任务繁重,来不及消费那么多的消息,而某些其他消费者由于某些原因(比如业务逻辑简单、机器性能卓越等)很快地处理完了所分配到的消息,进而进程空闲,这样就会造成整体应用吞吐量的下降。 

所以这里就要用到 channel.basicQos(int prefetchCount) 这个方法,如前面所说,channel.basicQos 方法允许限制信道上的消费者所能保持的最大未确认消息的数量。 

举例说明,在订阅消费队列之前,消费端程序调用了channel.basicQos(5),之后订阅了某个队列进行消费。RabbitMQ 会保存一个消费者的列表,每发送一条消息都会为对应的消费者计数,如果达到了所设定的上限,那么 RabbitMQ 就不会向这个消费者再发送任何消息。直到消费者确认了某条消息之后,RabbitMQ 将相应的计数减1,之后消费者可以继续接收消息,直到再次到达计数上限。这种机制可以类比于 TCP/IP 中的“滑动窗口”。 

注意要点: Basic.Qos的使用对于拉模式的消费方式无效。 

channel.basicQos有三种类型的重载方法:

 (1) void basicQos(int prefetchCount) throws IOException; 

(2) void basicQos(int prefetchCount, boolean global) throws IOException; 

(3) void basicQos(int prefetchSize, int prefetchCount, boolean global) throws IOException; 

前面介绍的都只用到了 prefetchCount 这个参数,当 prefetchCount 设置为 0 则表示没有上限。还有 prefetchSize 这个参数表示消费者所能接收未确认消息的总体大小的上限,单位为 B,设置为 0 则表示没有上限。 

对于一个信道来说,它可以同时消费多个队列,当设置了 prefetchCount 大于 0 时,这个信道需要和各个队列协调以确保发送的消息都没有超过所限定的 prefetchCount 的值,这样会使 RabbitMQ 的性能降低,尤其是这些队列分散在集群中的多个 Broker 节点之中。RabbitMQ 为了提升相关的性能,在 AMQP 0-9-1 协议之上重新定义了 global 这个参数,对比如下表所示:

RabbitMQ详解_第53张图片

前面说的 channel.basicQos 方法的示例都是针对单个消费者的,而对于同一个信道上的多个消费者而言,如果设置了 prefetchCount 的值,那么都会生效。下面代码清单示例中有两个消费者,各自的能接收到的未确认消息的上限都为10。

RabbitMQ详解_第54张图片

如果在订阅消息之前,既设置了 global 为 true 的限制,又设置了 global 为 false 的限制,那么哪个会生效呢?RabbitMQ 会确保两者都会生效。举例说明,当前有两个队列 queue1 和 queue2:queue1 有 10 条消息,分别为 1 到 10;queue2 也有 10 条消息,分别为 11 到 20。有两个消费者分别消费这两个队列,如代码清单:

RabbitMQ详解_第55张图片

那么这里每个消费者最多只能收到 3 个未确认的消息,两个消费者能收到的未确认的消息个数之和的上限为 5。在未确认消息的情况下,如果 consumer1 接收到了消息 1、2 和 3,那么 consumer2 至多只能收到 11 和 12。如果像这样同时使用两种 global 的模式,则会增加 RabbitMQ 的负载,因为 RabbitMQ 需要更多的资源来协调完成这些限制。如无特殊需要,最好只使用 global 为 false 的设置,这也是默认的设置。

7.7 消息传输保障

消息可靠传输一般是业务系统接入消息中间件时首要考虑的问题,一般消息中间件的消息传输保障分为三个层级。 

  • At most once:最多一次。消息可能会丢失,但绝不会重复传输。 
  • At least once:最少一次。消息绝不会丢失,但可能会重复传输。 
  • Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次。 

RabbitMQ 支持其中的“最多一次”和“最少一次”。其中“最少一次”投递实现需要考虑以下这个几个方面的内容: 

(1)消息生产者需要开启事务机制或者publisher confirm机制,以确保消息可以可靠地传输到RabbitMQ中。 

(2)消息生产者需要配合使用mandatory参数或者备份交换器来确保消息能够从交换器路由到队列中,进而能够保存下来而不会被丢弃。 

(3)消息和队列都需要进行持久化处理,以确保RabbitMQ 服务器在遇到异常情况时不会造成消息丢失。 

(4)消费者在消费消息的同时需要将autoAck设置为false,然后通过手动确认的方式去确认已经正确消费的消息,以避免在消费端引起不必要的消息丢失。 

“最多一次”的方式就无须考虑以上那些方面,生产者随意发送,消费者随意消费,不过这样很难确保消息不会丢失。 

“恰好一次”是RabbitMQ目前无法保障的。考虑这样一种情况,消费者在消费完一条消息之后向RabbitMQ发送确认Basic.Ack命令,此时由于网络断开或者其他原因造成RabbitMQ并没有收到这个确认命令,那么RabbitMQ不会将此条消息标记删除。在重新建立连接之后,消费者还是会消费到这一条消息,这就造成了重复消费。再考虑一种情况,生产者在使用publisher confirm机制的时候,发送完一条消息等待RabbitMQ返回确认通知,此时网络断开,生产者捕获到异常情况,为了确保消息可靠性选择重新发送,这样RabbitMQ中就有两条同样的消息,在消费的时候,消费者就会重复消费。

 那么RabbitMQ有没有去重的机制来保证“恰好一次”呢?答案是并没有,不仅是RabbitMQ,目前大多数主流的消息中间件都没有消息去重机制,也不保障“恰好一次”。去重处理一般是在业务客户端实现,比如引入GUID(Globally Unique Identifier)的概念。针对GUID,如果从客户端的角度去重,那么需要引入集中式缓存,必然会增加依赖复杂度,另外缓存的大小也难以界定。建议在实际生产环境中,业务方根据自身的业务特性进行去重,比如业务消息本身具备幂等性,或者借助Redis等其他产品进行去重处理。

提升数据可靠性有以下一些途径:

  • 设置mandatory参数或者备份交换器(immediate参数已被淘汰);
  • 设置publisher confirm机制或者事务机制;
  • 设置交换器、队列和消息都为持久化;
  • 设置消费端对应的autoAck参数为false并在消费完消息之后再进行消息确认。

8.RabbitMQ - channel方法介绍

先介绍 RabbitMQ 的几个方法:

   8.1  channel.exchangeDeclare(EXCHANGE_NAME,"topic");

/*
         * 使用fanout类型创建的交换器
         * 参数1:exchange:交换机名称
         * 参数2:type:交换机类型( direct / topic / fanout )
         * 参数3:durable:设置是否持久化。true:可以将交换器存盘,服务器重启后不会丢失相关信息。 Exchange。警告:仅设置此选项,不代表消息持久化。即不保证重启后消息还在。
         * 参数4:autoDelete:设置是否自动删除。autoDelete 设置为 tru则表示自动删除。自动删除的前提是至少有一个队列或者交换器与这个交换器绑定,之后所有与这个交换器绑定的队列或者交换器都与此解绑。注意不能错误地把这个参数理解为:“当与此交换器连接的客户端都断开时,RabbitMQ会自动删除本交换器”。
         * 参数5:internal:设置是否是内置的。如果设置为true,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式。
         * 参数5:argument:其他一些结构化参数,比如alternate-exchange
         */

;Exchange.DeclareOk exchangeDeclare(String exchange, String type, boolean durable, boolean autoDelete,boolean internal, Map arguments) throws IOException;

   8.2 channel.queueDeclare(QUEUE_NAME, false, false, false, null)

/* 
         * 声明一个队列
         * 参数1:queue 队列名称
         * 参数2:durable 设置是否持久化。为true则设置队列为持久化。持久化的队列会存盘,在服务器重启的时候可以保证不丢失相关信息.
         * 参数3:exclusive 设置是否排他。为true则设置队列为排他的。如果一个队列被声明为排他队列,该队列仅对首次声明它的连接可见,并在连接断开时自动删除。这里需要注意三点:排他队列是基于连接(Connection)可见的,同一个连接的不同信道(Channel)是可以同时访问同一连接创建的排他队列;“首次”是指如果一个连接已经声明了一个排他队列,其他连接是不允许建立同名的排他队列的,这个与普通队列不同;即使该队列是持久化的,一旦连接关闭或者客户端退出,该排他队列都会被自动删除,这种队列适用于一个客户端同时发送和读取消息的应用场景。 
         * 参数4:autoDelete 设置是否自动删除。为true则设置队列为自动删除。自动删除的前提是:至少有一个消费者连接到这个队列,之后所有与这个队列连接的消费者都断开时,才会自动删除。不能把这个参数错误地理解为:“当连接到此队列的所有客户端断开时,这个队列自动删除”,因为生产者客户端创建这个队列,或者没有消费者客户端与这个队列连接时,都不会自动删除这个队列。 
         * 参数5:arguments  设置队列的其他一些参数,如 x-me s s age-ttl、x-expire s、x-max-length、x-max-length-bytes、x-dead-letter-exchange、x-deadletter-routing-key、x-max-priority 等。 注意要点: 生产者和消费者都能够使用queueDeclare来声明一个队列,但是如果消费者在同一个信道上订阅了另一个队列,就无法再声明队列了。必须先取消订阅,然后将信道置为“传输”模式,之后才能声明队列。

         */  

Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,Map arguments) throws IOException;

不带任何参数的 queueDeclare 方法默认创建一个由 RabbitMQ 命名的(类似这种 amq.genLhQz1gv3GhDOv8PIDabOXA 名称,这种队列也称之为匿名队列)、排他的、自动删除的、非持久化的队列。

   8.3 channel.queueBind(queueName, EXCHANGE_NAME, "black");

      /*
         * 将队列跟交换器进行绑定
         * queue:队列名称
         * exchange:交换机名称
         * routingKey:队列跟交换机绑定的键值
         */

Exchange.BindOk exchangeBind(String destination, String source, String routingKey) throws IOException;

 8.4  channel.exchangeBind(destination, source, routingKey,  arguments)

 交换器可以与队列绑定,交换器也可以与交换器绑定

Exchange.BindOk exchangeBind(String destination, String source, String routingKey, Map arguments) throws IOException; 

绑定之后,消息从 source 交换器转发到 destination 交换器,某种程度上来说 destination 交换器可以看作一个队列。

channel.exchangeDeclare("source","direct", false,true,null);
channel.exchangeDeclare("destination","fanout", false, true, null);
channel.exchangeBind("destination", "source", "exKey");
channel.queueDeclare("queue", false,false,true,null);
channel.queueBind("queue", "destination","") ;
channel.basicPublish("source","exKey", null,"exToExDemo".getBytes());

生产者发送消息至交换器 source 中,交换器 source 根据路由键找到与其匹配的另一个交换器 destination,并把消息转发到 destination 中,进而存储在 destination 绑定的队列 queue 中,可参考下图:

RabbitMQ详解_第56张图片

  8.5 channel.basicPublish(EXCHANGE_NAME,"goods.add",null,msg.getBytes());

/* 
         * 向server发布一条消息 
         * 参数1:exchange 交换器的名称,指明消息需要发送到哪个交换器中。如果设置为空字符串,则消息会被发送到RabbitMQ默认的交换器中。
         * 参数2:routingKey  路由键,交换器根据路由键将消息存储到相应的队列之中。# 匹配0个或多个单词,* 匹配一个单词,在 topic exchange 做消息转发用
         * 参数3:mandatory 当 mandatory 参数设为 true 时,交换器无法根据自身的类型和路由键找到一个符合条件的队列,那么 RabbitMQ 会调用 Basic.Return 命令将消息返回给生产者。当 mandatory 参数设置为 false 时,出现上述情形,则消息直接被丢弃。
         
* 参数4:immediate:当 immediate 参数设为 true 时,如果交换器在将消息路由到队列时发现队列上并不存在任何消费者,那么这条消息将不会存入队列中。当与路由键匹配的所有队列都没有消费者时,该消息会通过Basic.Return返回至生产者。 RabbitMQ 3.0 版本开始去掉了对 immediate 参数的支持。
         * 参数5:BasicProperties :消息的基本属性集,其包含14个属性成员,分别有contentType、contentEncoding、headers(Map)、deliveryMode、priority、correlationId、replyTo、expiration、messageId、timestamp、type、userId、appId、clusterId。需要注意的是 BasicProperties.deliveryMode,1(nopersistent)非持久化,2(persistent)持久化。这里指的是消息的持久化,配合 channel(durable=true),queue(durable) 可以实现,即使服务器宕机,消息仍然保留。
         * 参数6:byte[] body:消息体(payload),真正需要发送的消息。

简单来说,mandatory 标志告诉服务器至少将该消息 route 到一个队列中,否则将消息返还给生产者;immediate 标志告诉服务器如果该消息关联的 queue 上有消费者,则马上将消息投递给它,如果所有 queue 都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。 
         */  

void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
            throws IOException;

8.6  channel.basicQos(1);

/* 
         * 同一时刻服务器只会发一条消息给消费者(能者多劳模式:效率高的消费者消费消息多。可以用来进行负载均衡。)  (在消费端写)
         * 参数1: prefetchSize:0 
         * 参数2: prefetchCount:1,会告诉 RabbitMQ 不要同时给一个消费者推送多于 N 个消息,即一旦有 N 个消息还没有ack,则该 consumer 将 block 掉,直到有消息 ack。
         * 参数3: global:true \ false 是否将上面设置应用于 channel,简单点说,就是上面限制是 channel 级别的还是 consumer 级别

           备注:据说 prefetchSize 和 global 这两项,rabbitmq 没有实现,暂且不研究
         */

void basicQos(int prefetchSize, int prefetchCount, boolean global)

8.7 channel.basicConsume(TASK_QUEUE_NAME, false, consumer);

/*       消息消费完成确认
         * 参数1:queue 队列的名称
         * 参数2:autoAck 设置是否自动确认。建议设成false,即不自动确认。自动确认:只要消息从队列中获取,无论消费者获取到消息后是否成功消息,都认为是消息已经成功消费;手动确认:消费者从队列中获取消息后,服务器会将该消息标记为不可用状态,等待消费者的反馈,如果消费者一直没有反馈,那么该消息将一直处于不可用状态。如果选用自动确认,在消费者拿走消息执行过程中出现宕机时,消息可能就会丢失!!
         * 参数3:consumerTag 消费者标签,用来区分多个消费者
         * 参数4:noLocal 设置为true则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者
         * 参数5:exclusive 设置是否排他
         * 参数6:arguments 设置消费者的其他参数
         * 参数7:callback 设置消费者的回调函数。用来处理RabbitMQ推送过来的消息,比如DefaultConsumer,使用时需要客户端重写(override)其中的方法。
         */

String basicConsume(String queue, boolean autoAck, String consumerTag,boolean noLocal, boolean exclusive, Map arguments, Consumer callback)throws IOException; 

8.8 channel.basicAck(envelope.getDeliveryTag(),false)

当手动确认时,一定要在消息处理完成后,确认提交,加上如下代码:

       /*
         * 消息处理完成,手动确认提交
         * 参数1:deliveryTag 该消息的index
         * 参数2:multiple:是否批量。 true:将一次性 ack 所有小于 deliveryTag 的消息。
         */

void basicAck(long deliveryTag, boolean multiple)

使用场景:RabbitMQ详解(三)------RabbitMQ的五种队列 - YSOcean - 博客园 中的 work 模式

  8.9 channel.basicNack(delivery.getEnvelope().getDeliveryTag(), false, true)    

/*
         * 拒绝接收消息
         * 参数1:deliveryTag 消息的编号,它是一个64位的长整型值,最大值是9223372036854775807
         * 参数2:multiple:是否批量。设置为false则表示拒绝编号为deliveryTag的这一条消息,这时候basicNack和basicReject方法一样;multiple参数设置为 true 则表示拒绝 deliveryTag 编号之前所有未被当前消费者确认的消息。
         * 参数3:requeue:被拒绝的消息是否重新入队列
         */

void basicNack(long deliveryTag, boolean multiple, boolean requeue)

对于 requeue,AMQP 中还有一个命令 Basic.Recover 具备可重入队列的特性。其对应的客户端方法为: 

(1) Basic.RecoverOk basicRecover() throws IOException; 

(2) Basic.RecoverOk basicRecover(boolean requeue) throws IOException; 

这个 channel.basicRecover 方法用来请求 RabbitMQ 重新发送还未被确认的消息。如果 requeue 参数设置为 true,则未被确认的消息会被重新加入到队列中,这样对于同一条消息来说,可能会被分配给与之前不同的消费者。如果 requeue 参数设置为 false,那么同一条消息会被分配给与之前相同的消费者。默认情况下,如果不设置 requeue 这个参数,相当于channel.basicRecover(true),即 requeue 默认为 true。

关于消息被消费者拒绝后如何处理:

① 如果第三个参数是 false,且队列没设置死信队列,则删除消息。

② 如果第三个参数是 false,且队列设置死信队列,则消息进入死信队列。

③  如果第三个参数是 true,且队列设置死信队列。生产者发送 10 条消息,消费者 1 和消费者 2 加入方法channel.basicQos(4),消费者 1 正常接收并确认消息,消费者2拒绝接收数据(第三个参数 requeue 设置为 true,再入队列)并设置死信队列。测试结果如下:

RabbitMQ详解_第57张图片RabbitMQ详解_第58张图片RabbitMQ详解_第59张图片

可以看到消费者根据 channel.basicQos(N) 方法轮询先拿到各自的 N 条数据,然后消费者 1 先消费自己刚刚拿到的数据,再消费消费者 2 没拿到的数据,最后再消费 消费者 2 拒绝过的数据;

消费者 2 一直循环拒绝刚刚未确认的 N 条数据,消费者 1 每消费 1 条,消费者 2 则少循环 1 条。一直到队列被消费者 1 消费完。(消费者 1 消费 消费者 2 拒绝过的数据的顺序(就是消息1、3、5、7是如何排序的),有待考究

  8.10 channel.basicReject(delivery.getEnvelope().getDeliveryTag(), false)

/*
         * 拒绝接收消息
         * 参数1:deliveryTag 消息的编号,它是一个64位的长整型值,最大值是9223372036854775807
         * 参数2:requeue:被拒绝的消息是否重新入队列。如果requeue参数设置为true,则RabbitMQ会重新将这条消息存入队列,以便可以发送给下一个订阅的消费者;如果requeue参数设置为false,则RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
         */

void basicReject(long deliveryTag, boolean requeue)

channel.basicNack 与 channel.basicReject 的区别在于 basicNack 可以拒绝多条消息,而 basicReject 一次只能拒绝一条消息 

 8.11  String queueName = channel.queueDeclare().getQueue();

    /*
       * 获取到一个临时队列名称。
       * channel.queueDeclare():创建一个非持久化、独立、自动删除的队列名称
       * 此队列是临时的,随机的,一旦我们断开消费者,队列会立即被删除
       * 随机队列名,如amq.gen-jzty20brgko-hjmujj0wlg
       */

String queueName = channel.queueDeclare().getQueue();

9. 特殊队列

9.1 死信队列

DLX,全称为 Dead-Letter-Exchange,可以称之为死信交换器,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新被发送到另一个交换器中,这个交换器就是DLX,绑定DLX的队列就称之为死信队列。 

消息变成死信一般是由于以下几种情况:

  • 消息被消费者拒绝(Basic.Reject/Basic.Nack),并且设置 requeue 参数为 false(不重回队列); 
  • 消息过期且未被消费; 
  • 队列消息长度达到限制。 

DLX 也是一个正常的交换器,和一般的交换器没有区别,它能在任何的队列上被指定,实际上就是设置某个队列的属性。当这个队列中存在死信时,RabbitMQ 就会自动地将这个消息重新发布到设置的 DLX 上去,进而被路由到另一个队列,即死信队列。可以监听这个队列中的消息以进行相应的处理,这个特性与将消息的 TTL 设置为 0 配合使用可以弥补 immediate 参数的功能。 

通过在 channel.queueDeclare 方法中设置 x-dead-letter-exchange 参数来为这个队列添加 DLX,也可以为这个 DLX 指定路由键,如果没有特殊指定,则使用原队列的路由键:  

当然这里也可以通过Policy的方式设置:



下面创建一个队列,为其设置 TTL 和 DLX 等:

        // 创建死信交换机
        channel.exchangeDeclare("exchange.dlx","direct", true);
        // 创建普通交换机
        channel.exchangeDeclare("exchange.normal", "fanout", true);

        Map argss = new HashMap<>();
        argss.put("x-message-ttl",10000) ;
        argss.put("x-dead-letter-exchange", "exchange.dlx");
        argss.put("x-dead-letter-routing-key", "routingkey");
        // 创建普通队列,队列过期时间为 10 秒,且该队列存在死信交换机为 exchange.dlx,该队列的消息过期后放到死信交换机的路由键为 "routingkey"。
        channel.queueDeclare("queue.normal", true, false, false, argss);

        // 普通交换机与队列绑定
        channel.queueBind("queue,normal", "exchange.normal","");
        // 声明死信队列绑定死信交换机,路由键为 "routingkey"
        channel.queueDeclare("queue.dlx", true, false, false, null);
        channel. queueBind("queue.dlx", "exchange.dlx", "routingkey");

        // 往交换机 exchange.normal 发送消息 "dlx",路由键为 "rk"
        channel.basicPublish ( "exchange.normal", "rk", MessageProperties.PERSISTENT_TEXT_PLATN, "dlx". getBytes());

这里创建了两个交换器 exchange.normal 和 exchange.dlx,分别绑定两个队列 queue.normal 和 queue.dlx。 

由 Web 管理页面可以看出,两个队列都被标记了“D”,这个是 durable 的缩写,即设置了队列持久化。queue.normal 这个队列还配置了 TTL、DLX 和 DLK,其中 DLX 指的是 x-dead-letter-routing-key 这个属性

生产者首先发送一条携带路由键为 “rk” 的消息,然后经过交换器 exchange.normal 顺利地存储到队列 queue.normal 中。由于队列 queue.normal 设置了过期时间为 10s,在这 10s 内没有消费者消费这条消息,那么判定这条消息为过期。由于设置了 DLX,过期之时,消息被丢给交换器 exchange.dlx 中,这时找到与 exchange.dlx 匹配的队列 queue.dlx,最后消息被存储在 queue.dlx 这个死信队列中。

对于 RabbitMQ 来说,DLX 是一个非常有用的特性。它可以处理异常情况下,消息不能够被消费者正确消费(消费者调用了Basic.Nack 或者 Basic.Reject)而被置入死信队列中的情况。DLX 配合 TTL 使用还可以实现延迟队列的功能。

9.2 延迟队列

延迟队列存储的对象是对应的延迟消息,所谓“延迟消息”是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。 

延迟队列的使用场景有很多,比如: 

  • 在订单系统中,一个用户下单之后通常有 30 分钟的时间进行支付,如果 30 分钟之内没有支付成功,那么这个订单将进行异常处理,这时就可以使用延迟队列来处理这些订单了。 
  • 用户希望通过手机远程遥控家里的智能设备在指定的时间进行工作。这时候就可以将用户指令发送到延迟队列,当指令设定的时间到了再将指令推送到智能设备。 

在 AMQP 协议中,或者 RabbitMQ 本身没有直接支持延迟队列的功能,但是可以通过前面所介绍的 DLX 和 TTL 模拟出延迟队列的功能。 

在上面那个图中,不仅展示的是死信队列的用法,也是延迟队列的用法,对于 queue.dlx 这个死信队列来说,同样可以看作延迟队列。假设一个应用中需要将每条消息都设置为10秒的延迟,生产者通过 exchange.normal 这个交换器将发送的消息存储在 queue.normal 这个队列中。消费者订阅的并非是 queue.normal 这个队列,而是 queue.dlx 这个队列。当消息从 queue.normal 这个队列中过期之后被存入 queue.dlx 这个队列中,消费者就恰巧消费到了延迟 10 秒的这条消息。 

在真实应用中,对于延迟队列可以根据延迟时间的长短分为多个等级,一般分为 5 秒、10 秒、30 秒、1 分钟、5 分钟、10 分钟、30 分钟、1 小时这几个维度,当然也可以再细化一下。 

参考下图,为了简化说明,这里只设置了 5 秒、10 秒、30 秒、1 分钟这四个等级。根据应用需求的不同,生产者在发送消息的时候通过设置不同的路由键,以此将消息发送到与交换器绑定的不同的队列中。这里队列分别设置了过期时间为 5 秒、10 秒、30 秒、1 分钟,同时也分别配置了 DLX 和相应的死信队列。当相应的消息过期时,就会转存到相应的死信队列(即延迟队列)中,这样消费者根据业务自身的情况,分别选择不同延迟等级的延迟队列进行消费。

RabbitMQ详解_第60张图片

10. RabbitMQ 队列的参数

转载连接:RabbitMQ (八) 队列的参数详解 - 热敷哥 - 博客园

/* 
         * 声明一个队列
         * 参数1:queue 队列名称
         * 参数2:durable 队列是否持久化。false:队列在内存中,服务器挂掉后,队列就没了。true:服务器重启后,队列将会重新生成。注意:只是队列持久化,不代表队列中的消息持久化!
         * 参数3:exclusive 队列是否专属。专属的范围针对的是连接,也就是说,一个连接下面的多个信道是可见的。对于其他连接是不可见的,连接断开后,该队列会被删除。注意,不是信道断开,是连接断开,并且,就算设置成了持久化,也会删除。
         * 参数4:autoDelete 如果所有消费者都断开连接了,是否自动删除。如果还没有消费者从该队列获取过消息或者监听该队列,那么该队列不会删除。只有在有消费者从该队列获取过消息后,该队列才有可能自动删除(当所有消费者都断开连接,不管消息是否获取完)
         * 参数5:arguments  队列的配置

         */  

channel.queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map arguments)

对于第5个参数: arguments ,可以设置队列的属性.

它的类型是一个键值对集合 ,大致写法如下:

RabbitMQ详解_第61张图片

队列的属性可以在 web 控制台添加队列的地方看到:

RabbitMQ详解_第62张图片

一共10个:

  • Message TTL : 消息过期时间
  • Auto expire : 队列有效期
  • Max length : 队列可以容纳的消息的最大条数
  • Max length bytes : 队列可以容纳的消息的最大字节数
  • Overflow behaviour : 队列中的消息溢出后如何处理
  • Dead letter exchange : 溢出的消息需要发送到绑定该死信交换机的队列
  • Dead letter routing key : 溢出的消息需要发送到绑定该死信交换机,并且路由键匹配的队列
  • Maximum priority : 最大优先级
  • Lazy mode : 懒人模式
  • Master locator : 

队列一旦声明,参数将无法更改、添加、删除。要改变一个队列的参数,只有两种办法:

  • 删除该队列,重新创建
  • 换个名字,创建一个新的队列

我先声明一个队列:channel.queueDeclare(QUEUE_NAME, false, false, false, null);

RabbitMQ详解_第63张图片

可以看到队列已经生成了,此时我企图重新声明这个队列来改变可持久性:channel.queueDeclare(QUEUE_NAME, true, false, false, null);

但是会报错:

Caused by: com.rabbitmq.client.ShutdownSignalException: channel error; protocol method: #method(reply-code=406, reply-text=PRECONDITION_FAILED - inequivalent arg 'durable' for queue 'testTopicGroup1' in vhost '/vhost_lyd': received 'true' but current is 'false', class-id=50, method-id=10)

所以队列一旦声明,参数是无法更改的。

10.1 Message TTL : 消息过期时间

TTL,Time to Live,即过期时间。RabbitMQ 可以对消息和队列设置过期时间。

1)设置消息的 TTL

目前有两种方法可以设置消息的 TTL。第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。第二种方法是对消息本身进行单独设置,每条消息的 TTL 可以不同。如果两种方法一起使用,则消息的 TTL 以两者之间较小的那个数值为准。消息在队列中的生存时间一旦超过设置的 TTL 值时,就会变成“死信”(Dead Message),消费者将无法再收到该消息(这点不是绝对的)。

① 通过队列属性设置消息 TTL 的方法是在 channel.queueDeclare 方法中加入 x-message-ttl 参数实现的,这个参数的单位是毫秒。

 示例代码如代码清单:

        HashMap argss = new HashMap<>();
        argss.put("x-message-ttl", 10000); //设置队列里面的消息过期时间10秒

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

查看控制台的队列:TTL 表示设置了消息的过期时间。

RabbitMQ详解_第64张图片

给消息设置了 10 秒的过期时间,下图可以看出,消息 10 秒之后就没了。

RabbitMQ详解_第65张图片

同时也可以通过 Policy 的方式来设置 TTL,示例如下:  

rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --apply-to queues

还可以通过调用 HTTP API 接口设置:  

curl -i -u guest:guest -H "content-type:application/json"-X PUT -d'{"auto_delete":false,"durable":true,"arguments":{"x-message-ttl": 60000}}' http://localhost:15672/api/queues/{vhost}/{queuename}

如果不设置 TTL,则表示此消息不会过期;如果将 TTL 设置为 0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃,这个特性可以部分替代 RabbitMQ 3.0 版本之前的 immediate 参数,之所以部分代替,是因为 immediate 参数在投递失败时会用 Basic.Return 将消息返回(这个功能可以用死信队列来实现)。 

② 针对每条消息设置 TTL 的方法是在 channel.basicPublish 方法中加入 expiration 的属性参数,单位为毫秒。 关键代码如代码清单:

        AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
                .deliveryMode(2) //持久化
                .expiration("5000") //设置消息有效期5秒钟
                .build();
        channel.basicPublish(EXCHANGE_NAME, "routingKey", mandatory(true/false), properties, "ttlTestMessage".getBytes());

还可以通过HTTP API接口设置:  

RabbitMQ详解_第66张图片

对于第一种设置队列 TTL 属性的方法,一旦消息过期,就会从队列中抹去,而在第二种方法中,即使消息过期,也不会马上从队列中抹去,因为每条消息是否过期是在即将投递到消费者之前判定的。 

为什么这两种方法处理的方式不一样?因为第一种方法里,队列中已过期的消息肯定在队列头部,RabbitMQ 只要定期从队头开始扫描是否有过期的消息即可。而第二种方法里,每条消息的过期时间不同,如果要删除所有过期消息势必要扫描整个队列,所以不如等到此消息即将被消费时再判定是否过期,如果过期再进行删除即可。

还有,消息设置的过期时间是指消费者未拿到消息之前,如果消费者已经拿到了消息,并且未确认,那么消息就会一直阻塞,不会消失。(无论是消息队列整体设置过期时间还是消息单独设置过期时间都是这样)

2)设置队列的 TTL

通过 channel.queueDeclare 方法中的 x-expires 参数可以控制队列被自动删除前处于未使用状态的时间。未使用的意思是队列上没有任何的消费者,队列也没有被重新声明,并且在过期时间段内也未调用过 Basic.Get 命令。

设置队列里的 TTL 可以应用于类似 RPC 方式的回复队列,在 RPC 中,许多队列会被创建出来,但是却是未被使用的。 

RabbitMQ 会确保在过期时间到达后将队列删除当队列在指定的时间内没有被使用(访问)就会被删除),但是不保障删除的动作有多及时。在 RabbitMQ 重启后,持久化的队列的过期时间会被重新计算。 

用于表示过期时间的 x-expires 参数以毫秒为单位,并且服从和 x-message-ttl 一样的约束条件,不过不能设置为 0。比如该参数设置为 1000,则表示该队列如果在 1 秒钟之内未使用则会被删除。 

下面的代码清单演示了创建一个过期时间为30分钟的队列: 

        HashMap argss = new HashMap<>();
        argss.put("x-expires", 1800000); //设置队列的过期时间为 30 分钟,30分钟之后没用就会删除
        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

10.2 Max length : 队列可以容纳的消息的最大条数

队列可以容纳的消息的最大条数,超过这个条数,队列头部的消息将会被丢弃。

测试 : 我们设置消费者队列最多只能容纳 1 条消息,生产者一次性发送 10 条消息。

生产者:

        for (int i = 0; i < 10; i++) {
            String msg = "hello mq" + i;
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);
        }

消费者:

public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        HashMap argss = new HashMap<>();
//        argss.put("x-message-ttl", 10000); //设置队列里面的消息过期时间10秒
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 1 ]处理完成");

            }
        });

    }
}

先启动消费者,再启动生产者:

RabbitMQ详解_第67张图片RabbitMQ详解_第68张图片

当生产者发送消息的时候,消费者已经连接队列等待消费了,所以第一条消息进来的时候,队列中有且仅有一条消息,并推给消费者消费。当后面的数据发送之后,由于队列中最多只能有一条消息,所以遵循 “先进先出(丢)” 的原则,消息1-8都被丢弃了,最后剩下消息 9 退给消费者。

所以我们猜测,如果是生产者先发送消息,再启动消费者,消费者只会拿到第 9 条消息,其他的消息会被丢掉。

启动生产者,再启动消费者:

RabbitMQ详解_第69张图片RabbitMQ详解_第70张图片

结果确实是这样子的。如果设置了 Max length 为 n,说明队列里面任何时候都有且仅有 n 条数据,且先进先出(丢)。

10.3 Max length bytes : 队列可以容纳的消息的最大字节数

队列可以容纳的消息的最大字节数,超过这个字节数,队列头部的消息将会被丢弃。

消费者:

        HashMap argss = new HashMap<>();
        argss.put("x-max-length-bytes", 10); //设置队列中的消息的最大字节数

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

1)发送一条 18 个字节的消息(我们采用UTF8编码,1个汉字占3个字节),先启动消费者,再启动生产者:

生产者:

            channel.exchangeDeclare(EXCHANGE_NAME, "topic", true);
            String msg = "旧故里草木深";
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());

            System.out.println("生产者发送消息:" + msg);

这里是消费者先待机,然后生产者再发消息,消息被消费了。

2)消费者同时发送两条消息,第一条字节数 9,第二条字节数 18。先启动生产者,再启动消费者:

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木深".getBytes());
        System.out.println("生产者发送消息:" + "草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧故里草木深".getBytes());
        System.out.println("生产者发送消息:" + "旧故里草木深");

发现队列里面一条数据也没有,全都扔掉了。队列发现字节数超了,先扔第一条,第二条也超了,所以又删了第二条。

3)把上两个消息的顺序换掉,先启动生产者,再启动消费者:

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧故里草木深".getBytes());
        System.out.println("生产者发送消息:" + "旧故里草木深");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草木深".getBytes());
        System.out.println("生产者发送消息:" + "草木深");

发现只有一条数据了,第一条被删掉了。

结论:如果设置了 Max length bytes 参数为 n,说明队列里面所有消息的字节数都不会超过 n,遵循先进先出(丢)的原理,而且丢消息是一整条丢。(如果设置了死信队列,那么就进入死信队列)(如果消费者在生产者发消息的时候一直待机,且队列里面已经没有消息了,那么生产者刚发送的消息一下就会发给消费者消费了,设置的队列字节长度就不包括已经发送给消费者的消息,包括未确认的)

10.4 Overflow behaviour : 队列中的消息溢出后如何处理

队列中的消息溢出时,如何处理这些消息:要么丢弃队列头部的消息(drop-head ),要么丢掉后面生产者发送过来的所有消息(reject-publish)

1)设置队列中最大允许 1 条消息存在,并且丢掉后面生产者发送过来的所有消息(reject-publish)。生产者一次发送 5 条消息。

        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "旧啊啊啊啊".getBytes());
        System.out.println("生产者发送消息:" + "旧啊啊啊啊");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "故".getBytes());
        System.out.println("生产者发送消息:" + "故");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "里".getBytes());
        System.out.println("生产者发送消息:" + "里");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "草".getBytes());
        System.out.println("生产者发送消息:" + "草");
        channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, "木".getBytes());
        System.out.println("生产者发送消息:" + "木");
public class Receive1 {
    private static final String EXCHANGE_NAME = "testTopicExchange";
    private static final String QUEUE_NAME = "testTopicGroup1";
    private static final String DLX_EXCHANGE_NAME = "dlxExchange";
    private static final String DEX_QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.exchangeDeclare(DLX_EXCHANGE_NAME, "fanout", true, true, null); //声明死信交换机
        channel.queueDeclare(DEX_QUEUE_NAME, true, false, false, null); //声明死信队列
        channel.queueBind(DEX_QUEUE_NAME, DLX_EXCHANGE_NAME, "null"); //死信队列和死信交换机绑定

        HashMap argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "reject-publish");  //队列中的消息溢出时,如何处理这些消息:要么丢弃队列头部的消息(drop-head ),要么拒绝接收后面生产者发送过来的所有消息(reject-publish)
//        argss.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

//        channel.queueDelete(QUEUE_NAME); //创建队列前先删除队列,避免已存在队列造成误差

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        channel.queueBind(QUEUE_NAME, EXCHANGE_NAME, "goods.#");

        channel.basicQos(1); //每个消费者发送确认消息之前,消息队列不发送下一个消息到消费者,一次只能处理一个消息

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);

                System.out.println("[消费者 1 ]处理完成");

            }
        });

    }
}

RabbitMQ详解_第71张图片

消费者消费了第 1 条消息,其他的消息丢掉了了。

2)设置队列中最大允许 1 条消息存在,并且丢弃队列头部的消息(drop-head )。生产者一次发送 5 条消息。

        HashMap argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "drop-head"); //设置队列中的消息的最大字节数

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

RabbitMQ详解_第72张图片

消费者消费了最后 1 条消息,前面的消息丢掉了

10.5 Dead letter exchange 死信交换机

死信交换机前面已经讲很多了,这里就直接举个例子。

在前一部分中,我们给队列设置了 “x-overflow”的值,丢弃队列头部的消息(drop-head ),或者丢掉后面生产者发送过来的所有消息(reject-publish)

1)当 argss.put("x-overflow", "drop-head"); 时,设置死信交换机

        HashMap argss = new HashMap<>();
        argss.put("x-max-length", 1); //队列的长度 设置队列中的消息的最大条数为 1 条,超过1条,则遵循队列的"先进先出(丢)"原则.
        argss.put("x-overflow", "drop-head"); //设置队列中的消息的最大字节数
        argss.put("x-dead-letter-exchange", DLX_EXCHANGE_NAME);//设置DLX

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);
public class Receive3 {
    private static final String DEX_QUEUE_NAME = "dexGroup"; //死信队列

    public static void main(String[] args) throws Exception {
        Connection connection = ConnectionUtils.getConnection();
        final Channel channel = connection.createChannel();

        channel.basicQos(1);

        //定义一个消费者
        Consumer consumer = new DefaultConsumer(channel) {
            // 消息到达,触发这个消息
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("消费者 3 接收到死信队列中的消息: " + msg);

                channel.basicAck(envelope.getDeliveryTag(), false);
                System.out.println("[消费者 3 ] 确认消费消息");
            }
        };

        //4. 监听队列
        boolean autoAck = false; //自动应答 false
        channel.basicConsume(DEX_QUEUE_NAME, autoAck, consumer);

    }
}

看队列中如果消息条数超过了设置的 1,那么扔掉的数据是否会进死信队列:

RabbitMQ详解_第73张图片RabbitMQ详解_第74张图片RabbitMQ详解_第75张图片

说明前面扔掉的消息确实进了死信队列。

2)当 argss.put("x-overflow", "reject-publish"); 时,设置死信交换机

argss.put("x-overflow", "reject-publish"); 

RabbitMQ详解_第76张图片

嗯,如果是设置了 “reject-publish”,消息就直接扔掉了,就算有死信队列也不会进入死信队列。

10.6 Dead letter routing key : 绑定死信交换机,并且指定路由键

如果普通队列设置了死信交换机,Dead letter routing key 就是这个死信交换机的路由键。也可以为这个 DLX 指定路由键,如果没有特殊指定,则使用原队列的路由键。

10.7 Maximum priority : 最大优先级

RabbitMQ 在 3.5.0 版本的时候实现了优先级队列,可以在声明队列时设置该队列中的消息的优先级最大值,这个参数必须是一个在 1 到 255 的整数。发布消息的时候,可以指定消息的优先级,优先级高的先被消费。如果没有设置该参数,那么该队列不支持消息优先级功能,也就是说,就算发布消息的时候传入了优先级的值,也不会起什么作用。

可以通过 RabbitMQ 管理界面配置队列的优先级属性,如下图的 x-max-priority:

RabbitMQ详解_第77张图片

也可以通过代码实现:

Map args = new HashMap();
args.put("x-max-priority", 10);
channel.queueDeclare(QUEUE_NAME, true, false, false, args);

配置了队列优先级的属性之后,可以在管理页面看到 Pri 的标记:

在发送的消息中设置消息本身的优先级,如下:

AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
builder.priority(5);
AMQP.BasicProperties properties = builder.build();
channel.basicPublish(EXCHANGE_NAME,"routingKey",properties,msg.getBytes());

结论:

① 队列可以设置最大优先级N(1-255的整数),消息也可以设置优先级。如果要保证消息按照优先级消费,那么队列和消息都要设置优先级。如果只有消息设置了优先级,而消息进入的队列没有设置最大优先级,那么这些消息设置的优先级就不会起效果,依然还是按照消息进入队列的顺序消费。

② 在队列设置最大优先级的前提下,设置优先级的消息比没有设置优先级的消息先消费

③ 优先级大的消息先消费。

④  消息设置的优先级 i 必须小于所在队列的最大优先级 N 才会起效果,否则 i >= N 的消息不会按照优先级排序,还是按照入队列的顺序消费。而 i= N 的消息比 i

⑤ 要想设置的优先级有意义,必须保证生产者的速度 > 消费者的速度,队列中有消息堆积才会排序,不然生产者发来一条就被消费者消费了,队列中至多只有一条消息,根本不会去排序。

10.8 Lazy mode : 懒人模式

设置队列为懒人模式。该模式下的队列会先将交换机推送过来的消息(尽可能多的)保存在磁盘上,以减少内存的占用。当消费者开始消费的时候才加载到内存中。如果没有设置懒人模式,队列则会直接利用内存缓存,以最快的速度传递消息。

生产者:

        for (int i = 0; i < 10; i++) {
            String msg = "hello mq" + i;
            // 发送消息
            channel.basicPublish(EXCHANGE_NAME, "goods.new.add", true, false, null, msg.getBytes());
            System.out.println("生产者发送消息:" + msg);

        }

消费者:

        HashMap argss = new HashMap<>();
        argss.put("x-queue-mode", "lazy");

        //声明队列
        channel.queueDeclare(QUEUE_NAME, false, false, false, argss);

        ...

        boolean autoAck = false; //自动应答 false
        //定义一个消费者
        channel.basicConsume(QUEUE_NAME, autoAck, new DefaultConsumer(channel){
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                String msg = new String(body, "utf-8");
                System.out.println("[消费者 1 ]接收到消息: " + msg);

                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }finally {
                    channel.basicAck(envelope.getDeliveryTag(), false);

                    System.out.println("[消费者 1 ]处理完成");
                }

            }
        });

当生产者发送 10 条消息后,观察控制台:

可以非常清楚看到,内存中是没有任何消息的。总数和已准备都是 90B

90B 怎么来的? " hello mq i" 一共 9 个字节,一共 10 条,9*10 = 90

启动消费者开始消费,内存中开始有消息了:

而且消息好像是一块拿到内存中的,不是一条一条拿。

11. Spring 整合RabbitMQ

11.1 添加依赖

        
            org.springframework.boot
            spring-boot-starter-amqp
            1.5.2.RELEASE
        

11.2 添加配置

server:
  port: 8888
spring:
    rabbitmq:
        host: 10.12.14.90
        port: 5672
        username: janet
        password: 123456
        virtual-host: /test
        listener:
            simple:
                acknowledge-mode: manual
canal:
    client:
        instances:
            example:
                host: localhost
                port: 11111
                batchSize: 1000

#mq配置
exchangeName: materialActivityTopicExchange
queueName: materialActivityTopicQueue
bindingKey: material.confirmed.activity

deadExchangeName: deadMaterialActivityTopicExchange
deadQueueName: deadMaterialActivityTopicQueue
deadBindingKey: material.confirmed.activity.dead

11.3 声明交换机、队列及绑定

如果在RabbitMQ web端已经提前创建好交换机和队列,并且已经进行绑定了的话,这一步是可以省略的。

package com.janet.mq;

import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.HashMap;
import java.util.Map;

/**
 * @Description MQ交换机、队列声明绑定工具类
 * @Date 2021/6/25
 * @Author Janet
 */
@Configuration
public class RabbitMQConfig {

    @Value("${exchangeName}")
    private String exchangeName;

    @Value("${queueName}")
    private String queueName;  //队列名称

    @Value("${bindingKey}")
    private String bindingKey;

    @Value("${deadExchangeName}")
    private String deadExchangeName; //死信交换机

    @Value("${deadQueueName}")
    private String deadQueueName; //死信队列

    @Value("${deadBindingKey}")
    private String deadBindingKey; //死信队列绑定路由键

    //声明交换机(topic类型)
    @Bean("materialActivityTopicExchange")
    public Exchange topicExchange(){
        return ExchangeBuilder.topicExchange(exchangeName).durable(true).build();
    }

    //声明队列以及绑定改队列的死信交换机
    @Bean("materialActivityTopicQueue")
    public Queue itemQueue(){
        Map args = new HashMap<>(2);
        // 声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", deadExchangeName);
        // 声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", deadBindingKey);
        //队列过期时间
//        args.put("x-message-ttl", 60000);//1min

        return QueueBuilder.durable(queueName).withArguments(args).build();
    }

    //绑定队列和交换机
    @Bean
    public Binding itemQueueExchange(@Qualifier("materialActivityTopicQueue") Queue queue,
                                     @Qualifier("materialActivityTopicExchange") Exchange exchange){
        return BindingBuilder.bind(queue).to(exchange).with(bindingKey).noargs();
    }

    //声明死信交换机(direct类型)
    @Bean("deadMaterialActivityTopicExchange")
    public Exchange deadExchange(){
        return ExchangeBuilder.directExchange(deadExchangeName).durable(true).build();
    }

    //声明死信队列
    @Bean("deadMaterialActivityTopicQueue")
    public Queue deadQueue(){
        return QueueBuilder.durable(deadQueueName).build();
    }

    //死信交换机和死信队列绑定
    @Bean
    public Binding deadQueueExchange(@Qualifier("deadMaterialActivityTopicQueue") Queue deadQueue,
                                     @Qualifier("deadMaterialActivityTopicExchange") Exchange deadExchange){
        return BindingBuilder.bind(deadQueue).to(deadExchange).with(deadBindingKey).noargs();
    }

}

11.4 生产者发送消息

package com.janet.mq;

import org.apache.commons.lang.SerializationUtils;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

/**
 * @Description 生产者发送消息
 * @Date 2021/6/30
 * @Author Janet
 */

@Component
public class Product {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Value("${exchangeName}")
    private String exchange;

    @Value("${bindingKey}")
    private String routKey;

    /**
     * 情况1:消息是传字节数组
     * rabbitMQ111监听了消息
     */
    public void sendMessage(MQMessageActivityModel msg) {
        //sendMessage,msg:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null)
        System.out.println("sendMessage,msg:" + msg);
        byte[] bytes = SerializationUtils.serialize(msg);
        rabbitTemplate.convertAndSend(exchange, routKey, bytes);

    }

    /**
     * 情况2:消息是 MQMessageActivityModel对象
     * rabbitMQ222监听了消息
     */
    public void sendActivityModelMessage(MQMessageActivityModel msg) {
        //sendActivityModelMessage,msg:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null)
        System.out.println("sendActivityModelMessage,msg:" + msg);
        rabbitTemplate.convertAndSend(exchange, routKey, msg);
    }

    /**
     * 情况3:消息是字符串
     * rabbitMQ333 监听了消息
     */
    public void sendStringMessage(String msg) {
        //sendStringMessage,msg:Janet
        System.out.println("sendStringMessage,msg:" + msg);
        rabbitTemplate.convertAndSend(exchange, routKey, msg);

    }


    /**
     * 发送有过期时间的消息
     * @param msg 发送的消息
     * @param expiration 过期时间,单位为毫秒
     */
    public void sendTtlMessage(String msg, String expiration) {
        MessagePostProcessor messagePostProcessor = message -> {
            message.getMessageProperties().setExpiration(expiration); //单位为毫秒
            return message;
        };
        System.out.println("sendTtlMessage,msg:" + msg + ",date:" + new Date());
        rabbitTemplate.convertAndSend(exchange, routKey, msg, messagePostProcessor);

    }

}

11.5 消费者监听消息

下面监听方法中,message中的body也是和msg一样的消息。

用 convertAndSend 方法发送消息的话,发送什么类型的消息都可以的,监听消息的时候用同种方法转换就行。(具体发送时都换转换成字节数组进行传输)

package com.janet.mq;

import com.rabbitmq.client.Channel;
import org.apache.commons.lang.SerializationUtils;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

import java.io.IOException;

/**
 * @Description 监听队列的消费者
 */
@Component
@RabbitListener(queues = "${queueName}")
public class Customer {

    /**
     * 情况1:消息是传字节数组
     *
     * @param msg
     * @param channel
     * @param message
     * @throws IOException
     */
    @RabbitHandler
    public void receiveMessage(byte[] msg, Channel channel, Message message) throws IOException {
        MQMessageActivityModel msg1 = (MQMessageActivityModel) SerializationUtils.deserialize(msg);
        MQMessageActivityModel msg2 = (MQMessageActivityModel) SerializationUtils.deserialize(message.getBody());
        //rabbitMQ111----------消费者接收到消息,msg1:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null),msg2:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null)
        System.out.println("rabbitMQ111----------消费者接收到消息,msg1:" + msg1 + ",msg2:" + msg2);

        // 手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        // channel.basicNack(message.getMessageProperties().getDeliveryTag(), false,false); //拒绝消息
        System.out.println("rabbitMQ111----------消费者成功消费消息");
    }

    /**
     * 情况2:消息是 MQMessageActivityModel对象
     */
    @RabbitHandler
    public void receiveMessage(MQMessageActivityModel msg, Channel channel, Message message) throws IOException {
        MQMessageActivityModel msg1 = (MQMessageActivityModel) SerializationUtils.deserialize(message.getBody());
        //rabbitMQ222----------消费者接收到消息,msg1:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null),msg:MQMessageActivityModel(schemaName=cloud, tableName=student, activityId=123, confirmOrder=null)
        System.out.println("rabbitMQ222----------消费者接收到消息,msg1:" + msg1 + ",msg:" + msg);

        // 手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("rabbitMQ222----------消费者成功消费消息");
    }


    /**
     * 情况3:消息是字符串
     */
    @RabbitHandler
    public void receiveMessage(String msg, Channel channel, Message message) throws IOException {

        String msg1 = new String(message.getBody(), "UTF-8");
        //rabbitMQ333----------消费者接收到消息,msg1:Janet,msg:Janet
        System.out.println("rabbitMQ333----------消费者接收到消息,msg1:" + msg1 + ",msg:" + msg);

        // 手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("rabbitMQ333----------消费者成功消费消息");

    }

}

SpringBoot 整合 RabbitMQ - 原来是晴天啊 - 博客园

@RabbitListener 和 @RabbitHandler 的使用

@RabbitListener 注解是指定某方法作为消息消费的方法,例如监听某 Queue 里面的消息。

① @RabbitListener标注在方法上,直接监听指定的队列,此时接收的参数需要与发送市类型一致

@Component
public class ActivityQueueListener {
	//监听的队列名
    @RabbitListener(queues = "queueName")
    public void MessageQueueHandlerManualAck(String msg, Message message, Channel channel) {
        System.out.println("消费者监听消息:" + msg);

        //手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
    }
 
}

② @RabbitListener 可以标注在类上面,需配合 @RabbitHandler 注解一起使用。@RabbitListener 标注在类上面表示当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。

@Component
@RabbitListener(queues = "queueName")
public class ActivityQueueListener {
 
    @RabbitHandler
    public void processMessage(String message) {
        System.out.println(message);
    }
 
    @RabbitHandler
    public void processMessage(byte[] message) {
        System.out.println(new String(message));
    }
    
}

11.6 发送有过期时间的消息

生产者:

@Component
public class Product {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Value("${exchangeName}")
    private String exchange;

    @Value("${bindingKey}")
    private String routKey;


    /**
     * 发送有过期时间的消息
     * @param msg 发送的消息
     * @param expiration 过期时间,单位为毫秒
     */
    public void sendTtlMessage(String msg, String expiration) {
        MessagePostProcessor messagePostProcessor = message -> {
            message.getMessageProperties().setExpiration(expiration); //单位为毫秒
            return message;
        };
        System.out.println("sendTtlMessage,msg:" + msg + ",date:" + new Date()+ ",expiration:" + expiration);
        rabbitTemplate.convertAndSend(exchange, routKey, msg, messagePostProcessor);
    }

}

监听普通队列的消费者:

@Component
@RabbitListener(queues = "${queueName}")
public class Customer {

    @RabbitHandler
    public void receiveMessage(String msg, Channel channel, Message message) throws IOException {
        System.out.println("普通消费者消费者接收到消息,msg:" + msg + ",date:" + new Date());

        // 手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("普通消费者成功消费消息");

    }

}

① 有消费者监听普通队列(queues = "${queueName}"),生产者发送有过期时间的消息:

sendTtlMessage,msg:Janet,date:Wed Jan 11 11:23:51 CST 2023,expiration:10000
普通消费者消费者接收到消息,msg:Janet,date:Wed Jan 11 11:23:51 CST 2023
普通消费者成功消费消息

消费者在生产者发送消息后立即收到消息并消费。

② 消费者不监听普通队列,让消费者监听普通队列的死信队列(queues = "${deadQueueName}")

监听死信队列的消费者:

@Component
public class DeadCustomer {

    @RabbitListener(queues = "${deadQueueName}")
    public void receiveDeadMessage(String msg, Channel channel, Message message) throws IOException {

        System.out.println("监听到死信队列中的消息------message:" + msg + ",date:" + new Date());

        // 手动确认消息
        channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
        System.out.println("消费者成功死信队列中的消息");

    }
}

生产者发送有过期时间的消息:

sendTtlMessage,msg:Janet,date:Wed Jan 11 11:22:18 CST 2023,expiration:10000
监听到死信队列中的消息------message:Janet,date:Wed Jan 11 11:22:28 CST 2023
消费者成功死信队列中的消息

10秒钟后,消费者监听到死信队列的消息。说明只要有消费者监听普通队列,消息设不设置过期时间都没用到,消息是立即投递到消费者的。只有不监听普通队列,监听死信队列,设置的过期时间才有效果(普通队列的消息过期之后发送到死信队列)。

③ 还是消费者不监听普通队列,让消费者监听普通队列的死信队列。

生产者连续发送两条过期时间升序的消息:

sendTtlMessage,msg:Janet111,date:Wed Jan 11 11:25:06 CST 2023,expiration:10000
sendTtlMessage,msg:Janet222,date:Wed Jan 11 11:25:12 CST 2023,expiration:15000
监听到死信队列中的消息------message:Janet111,date:Wed Jan 11 11:25:16 CST 2023
消费者成功死信队列中的消息
监听到死信队列中的消息------message:Janet222,date:Wed Jan 11 11:25:27 CST 2023
消费者成功死信队列中的消息

发送的两条消息过期时间是升序的,消费者也是可以成功在过期时间消费两条消息,第一条10秒后,第二条15秒后。

④ 生产者连续发送两条过期时间降序的消息:

sendTtlMessage,msg:Janet333,date:Wed Jan 11 11:29:24 CST 2023,expiration:10000  //过期时间为10秒,按道理应该在29分34秒接收到消息
sendTtlMessage,msg:Janet444,date:Wed Jan 11 11:29:30 CST 2023,expiration:5000   //过期时间为15秒,按道理应该在29分35秒接收到消息
监听到死信队列中的消息------message:Janet333,date:Wed Jan 11 11:29:34 CST 2023   //及时接收到消息
消费者成功死信队列中的消息
监听到死信队列中的消息------message:Janet444,date:Wed Jan 11 11:29:35 CST 2023   //及时接收到消息
消费者成功死信队列中的消息


sendTtlMessage,msg:Janet777,date:Wed Jan 11 11:37:53 CST 2023,expiration:15000  //过期时间为15秒,按道理应该在38分08秒接收到消息
sendTtlMessage,msg:Janet888,date:Wed Jan 11 11:38:02 CST 2023,expiration:5000  //过期时间为5秒,按道理应该在38分07秒接收到消息
监听到死信队列中的消息------message:Janet777,date:Wed Jan 11 11:38:08 CST 2023   //及时接收到消息
消费者成功死信队列中的消息
监听到死信队列中的消息------message:Janet888,date:Wed Jan 11 11:38:08 CST 2023   //未及时接收到消息,应该在38分07秒接收到消息,但是现在在08分接收到消息
消费者成功死信队列中的消息


sendTtlMessage,msg:Janet999,date:Wed Jan 11 11:38:55 CST 2023,expiration:15000  //过期时间为15秒,按道理应该在39分10秒接收到消息
sendTtlMessage,msg:Janet100,date:Wed Jan 11 11:39:04 CST 2023,expiration:2000  //过期时间为10秒,按道理应该在39分06秒接收到消息
监听到死信队列中的消息------message:Janet999,date:Wed Jan 11 11:39:10 CST 2023   //及时接收到消息
消费者成功死信队列中的消息
监听到死信队列中的消息------message:Janet100,date:Wed Jan 11 11:39:10 CST 2023   //未及时接收到消息,应该在39分06秒接收到消息,但是现在在10分接收到消息
消费者成功死信队列中的消息

说明队列前一条消息会阻塞后一条消息的出队列。即使设置了过期时间,要是前一条消息没有出队列,后面的消息也会一直阻塞,直到前一条消息出队列,后面的才会判断过期时间出队列。因为每条消息是否过期是在即将投递到消费者之前判定的。 

12. RabbitMQ 的 RPC 实现

RPC,是 Remote Procedure Call 的简称,即远程过程调用。它是一种通过网络从远程计算机上请求服务,而不需要了解底层网络的技术。RPC的主要功用是让构建分布式计算更容易,在提供强大的远程调用能力时不损失本地调用的语义简洁性。 

通俗点来说,假设有两台服务器 A 和 B,一个应用部署在 A 服务器上,想要调用 B 服务器上应用提供的函数或者方法,由于不在同一个内存空间,不能直接调用,需要通过网络来表达调用的语义和传达调用的数据。 

RPC 的协议有很多,比如最早的 CORBA、Java RMI、WebService 的 RPC 风格、Hessian、Thrift 甚至还有 Restful API。 

一般在 RabbitMQ 中进行 RPC 是很简单。客户端发送请求消息,服务端回复响应的消息。为了接收响应的消息,我们需要在请求消息中发送一个回调队列(参考下面代码中的replyTo)。可以使用默认的队列,具体示例代码如下:

对于代码中涉及的 BasicProperties 这个类,在上面有写,其包含14个属性(contentType、contentEncoding、headers(Map)、deliveryMode、priority、correlationId、replyTo、expiration、messageId、timestamp、type、userId、appId、clusterId),这里就用到两个属性。 

  • replyTo:通常用来设置一个回调队列。 
  • correlationId:用来关联请求(request)和其调用RPC之后的回复(response)。 

如果像上面的代码中一样,为每个 RPC 请求创建一个回调队列,则是非常低效的。但是幸运的是这里有一个通用的解决方案——可以为每个客户端创建一个单一的回调队列。 

这样就产生了一个新的问题,对于回调队列而言,在其接收到一条回复的消息之后,它并不知道这条消息应该和哪一个请求匹配。这里就用到correlationId这个属性了,我们应该为每一个请求设置一个唯一的correlationId。之后在回调队列接收到回复的消息时,可以根据这个属性匹配到相应的请求。如果回调队列接收到一条未知correlationId的回复消息,可以简单地将其丢弃。 

你有可能会问,为什么要将回调队列中的位置消息丢弃而不是仅仅将其看作失败?这样可以针对这个失败做一些弥补措施。参考下图,考虑这样一种情况,RPC服务器可能在发送给回调队列(amq.gen-LhQz1gv3GhDOv8PIDabOXA)并且在确认接收到请求的消息(rpc_queue中的消息)之后挂掉了,那么只需重启下RPC服务器即可,RPC服务会重新消费rpc_queue队列中的请求,这样就不会出现RPC服务端未处理请求的情况。这里的回调队列可能会收到重复消息的情况,这需要客户端能够优雅地处理这种情况,并且RPC请求也需要保证其本身是幂等的(补充:消费者消费消息一般是先处理业务逻辑,再使用Basic.Ack确认已接收到消息以防止消息不必要地丢失)。  

RabbitMQ详解_第78张图片

根据上图所示,RPC的处理流程如下:

(1)当客户端启动时,创建一个匿名的回调队列(名称由RabbitMQ自动创建,图4-7中的回调队列为amq.gen-LhQz1gv3GhDOv8PIDabOXA)。 

(2)客户端为RPC请求设置2个属性:replyTo用来告知RPC服务端回复请求时的目的队列,即回调队列;correlationId用来标记一个请求。 

(3)请求被发送到rpc_queue队列中。 

(4)RPC服务端监听rpc_queue队列中的请求,当请求到来时,服务端会处理并且把带有结果的消息发送给客户端。接收的队列就是replyTo设定的回调队列。 

(5)客户端监听回调队列,当有消息时,检查correlationId属性,如果与请求匹配,那就是结果了。 

下面沿用RabbitMQ官方网站的一个例子来做说明,RPC客户端通过RPC来调用服务端的方法以便得到相应的斐波那契值。 

首先是服务端的关键代码,代码清单:

RabbitMQ详解_第79张图片

RPC 客户端关键代码:

RabbitMQ详解_第80张图片

13. RabbitMQ 的权限

13.1 多租户与权限

每一个 RabbitMQ 服务器都能创建虚拟的消息服务器,我们称之为虚拟主机(virtual host),简称为 vhost。每一个 vhost 本质上是一个独立的小型 RabbitMQ 服务器,拥有自己独立的队列、交换器及绑定关系等,并且它拥有自己独立的权限。vhost 就像是虚拟机与物理服务器一样,它们在各个实例间提供逻辑上的分离,为不同程序安全保密地运行数据,它既能将同一个 RabbitMQ 中的众多客户区分开,又可以避免队列和交换器等命名冲突。vhost 之间是绝对隔离的,无法将 vhost1 中的交换器与 vhost2 中的队列进行绑定,这样既保证了安全性,又可以确保可移植性。如果在使用 RabbitMQ 达到一定规模的时候,建议用户对业务功能、场景进行归类区分,并为之分配独立的 vhost。 

vhost 是 AMQP 概念的基础,客户端在连接的时候必须制定一个 vhost。RabbitMQ 默认创建的 vhost 为 “/”。

AMQP 协议中并没有指定权限在 vhost 级别还是在服务器级别实现,由具体的应用自定义。在 RabbitMQ 中,权限控制则是以 vhost 为单位的。当创建一个用户时,用户通常会被指派给至少一个 vhost,并且只能访问被指派的 vhost 内的队列、交换器和绑定关系等。因此,RabbitMQ 中的授予权限是指在 vhost 级别对用户而言的权限授予。 

13.2 用户管理

未完待续--

14. RabbitMQ 集群搭建

1)先要在部署集群的服务器上安装 MQ,这里我已经安装好了两台。

2)统一两台服务器 erlang.cookie 文件中 cookie 值。 将第一台服务器中的 .erlang.cookie 同步到第二台中。

scp /var/lib/rabbitmq/.erlang.cookie 192.168.1.93:/var/lib/rabbitmq/.erlang.cookie #192.168.1.93是我第二台部署rabbitMQ的ip

如果.erlang.cookie文件没在那个目录,可以使用命令  find / -name '.erlang.cookie' 找到文件位置。

如果scp命令搞不了,可以直接看下第一台服务器的.erlang.cookie里面的key是啥,直接复制到第二台服务器中。

可以看到我第一台服务器里面的key是ZRMDSXXMWDNJGLAOTADM,所以在第二台服务器上执行命令: vim /var/lib/rabbitmq/.erlang.cookie ,修改文件内容为ZRMDSXXMWDNJGLAOTADM。在修改时如果遇到权限问题,可执行命令 chmod 600 /var/lib/rabbitmq/.erlang.cookie 修改文件的权限为可写,修改内容完成后,执行命令 chmod 400 /var/lib/rabbitmq/.erlang.cookie 把文件再次改成只读的。

我当时还遇到这个错:

Error when reading /var/lib/rabbitmq/.erlang.cookie: eacces

这里提示的,eaccess 错误就是权限问题,新复制过来的cookie ,当前不可用,需要授权。我是通过 rpm 安装的 rabbitmq,执行:

chown rabbitmq:rabbitmq /var/lib/rabbitmq/.erlang.cookie

3)待续-


我另一篇文章是关于RabbitMQ基础功能测试的:

Rabbit MQ 操作测试_单椒煜泽的博客-CSDN博客


参考:

《RabbitMQ 实战指南》

分布式之消息队列复习精讲

RabbitMq基础教程之基本概念

Rabbit MQ的通信过程

RabbitMQ的应用场景以及基本原理介绍

rabbitmq-channel方法介绍

RabbitMQ BasicGet与BasicConsume的区别 - SupPilot - 博客园

请教!rabbitMQ怎么确认同一个队列所有消费者收到消息?_liuji0517的博客-CSDN博客_rabbitmq一个队列多个消费者

延迟队列

Spring Cloud Stream RabbitMQ 延迟消息实现_xiegwei的博客-CSDN博客

rabbitmq channel参数详解 - zhangxuhui - 博客园

Spring Cloud Stream 进阶配置——使用延迟队列实现“定时关闭超时未支付订单”_多隆的博客-CSDN博客_springcloud stream延迟队列
@RabbitListener和@RabbitHandler的使用_sliver1836的博客-CSDN博客_@rabbithandler

(13)RabbitMQ实现延迟队列的两种方式_没头脑遇到不高兴的博客-CSDN博客_rabbitmq延迟队列实现

(12)RabbitMQ的TTL(消息有效期)和DLX(死信交换机/队列)_没头脑遇到不高兴的博客-CSDN博客_dlk dlx

你可能感兴趣的:(分布式,面试,#,消息队列,RabbitMQ,为什么要使用RabbitMQ,RabbitMQ的方法,RabbitMQ,消息确认机制)