RabbitMQ入门到精通

RabbitMQ

1. 消息中间件概述

1.1. 为什么学习消息队列

电子商务应用中,经常需要对庞大的海量数据进行监控,随着网络技术和软件开发技术的不断提高,在实战开发中MQ的使用与日俱增,特别是RabbitMQ在分布式系统中存储转发消息,可以保证数据不丢失,也能保证高可用性,即集群部署的时候部分机器宕机可以继续运行。在大型电子商务类网站,如京东、淘宝、去哪儿等网站有着深入的应用 。

队列的主要作用是消除高并发访问高峰,加快网站的响应速度

在不使用消息队列的情况下,用户的请求数据直接写入数据库,在高并发的情况下,会对数据库造成巨大的压力,同时也使得系统响应延迟加剧。

1.2. 什么是消息中间件

MQ全称为Message Queue, 消息队列(MQ)是一种应用程序对应用程序的通信方法。

介绍:消息队列就是基础数据结构中的“先进先出”的一种数据机构。想一下,生活中买东西,需要排队,先排的人先买消费,就是典型的“先进先出”。
在这里插入图片描述

**消息传递:**指的是程序之间通过消息发送数据进行通信,而不是通过直接调用彼此来通信,直接调用通常是用于诸如远程过程调用的技术。

**排队:**指的是应用程序通过队列来通信。

业务场景说明:

消息队列在大型电子商务类网站,如京东、淘宝、去哪儿等网站有着深入的应用,为什么会产生消息队列?有几个原因:

不同进程(process)之间传递消息时,两个进程之间耦合程度过高,改动一个进程,引发必须修改另一个进程,为了隔离这两个进程,在两进程间抽离出一层(一个模块),所有两进程之间传递的消息,都必须通过消D,单独修改某一个进程,不会影响另一个;

不同进程(process)之间传递消息时,为了实现标准化,将消息的格式规范化了,并且,某一个进程接受的消息太多,一下子无法处理完,并且也有先后顺序,必须对收到的消息进行排队,因此诞生了事实上的消息队列;

在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高系统的吞吐量

1.3. 消息队列应用场景

首先我们先说一下消息中间件的主要的作用:

[1]异步处理

有些服务间调用是异步的,例如 A 调用 B,B 需要花费很长时间执行,但是 A 需要知道 B 什么时候可 以执行完,以前一般有两种方式,A 过一段时间去调用 B 的查询 api 查询。或者 A 提供一个 callback api, B 执行完之后调用 api 通知 A 服务。这两种方式都不是很优雅,使用消息总线,可以很方便解决这个问题, A 调用 B 服务后,只需要监听 B 处理完成的消息,当 B 处理完成后,会发送一条消息给 MQ,MQ 会将此 消息转发给 A 服务。这样 A 服务既不用循环调用 B 的查询 api,也不用提供 callback api。同样 B 服务也不 用做这些操作。A 服务还能及时的得到异步处理成功的消息。

[2]解耦服务

以电商应用为例,应用中有订单系统、库存系统、物流系统、支付系统。用户创建订单后,如果耦合调用库存系统、物流系统、支付系统,任何一个子系统出了故障,都会造成下单操作异常。当转变成基于消息队列的方式后,系统间调用的问题会减少很多,比如物流系统因为发生故障,需要几分钟来修复。在这几分钟的时间里,物流系统要处理的内存被缓存在消息队列中,用户的下单操作可以正常完成。当物流系统恢复后,继续处理订单信息即可,中单用户感受不到物流系统的故障, 提升系统的可用性。

在这里插入图片描述

[3]流量削峰

举个例子,如果订单系统最多能处理一万次订单,这个处理能力应付正常时段的下单时绰绰有余,正常时段我们下单一秒后就能返回结果。但是在高峰期,如果有两万次下单操作系统是处理不了的,只能限制订单超过一万后不允许用户下单。使用消息队列做缓冲,我们可以取消这个限制,把一秒内下的订单分散成一段时间来处理,这时有些用户可能在下单十几秒后才能收到下单成功的操作,但是比不能下单的体验要好。

上面的三点是我们使用消息中间件最主要的目的.

1.3.1. 应用解耦

传统模式:
在这里插入图片描述

传统模式的缺点:

  • 系统间耦合性太强,如上图所示,系统A在代码中直接调用系统B和系统C的代码,如果将来D系统接入,系统A还需要修改代码,过于麻烦!

中间件模式:
在这里插入图片描述

中间件模式的的优点:

  • 将消息写入消息队列,需要消息的系统自己从消息队列中订阅,从而系统A不需要做任何修改。

1.3.2. 异步处理

场景说明:用户注册后,需要发注册邮件和注册短信,传统的做法有两种

  • 串行的方式

  • 并行的方式

(1) 串行方式:

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

在这里插入图片描述

(2) 并行方式:

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

在这里插入图片描述

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

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

在这里插入图片描述

由此可以看出,引入消息队列后,用户的响应时间就等于写入数据库的时间+写入消息队列的时间(可以忽略不计),

引入消息队列后处理后,响应时间是串行的3分之1,是并行的2分之1。

传统模式的缺点:

  • 一些非必要的业务逻辑以同步的方式运行,太耗费时间。

中间件模式的的优点:

  • 将消息写入消息队列,非必要的业务逻辑以异步的方式运行,加快响应速度

1.3.3. 流量削峰

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

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

传统模式

如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。

在这里插入图片描述

传统模式的缺点:

  • 并发量大的时候,所有的请求直接怼到数据库,造成数据库连接异常

中间件模式:

消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。

在这里插入图片描述

中间件模式的的优点:

系统A慢慢的按照数据库能处理的并发量,从消息队列中慢慢拉取消息。在生产中,这个短暂的高峰期积压是允许的。

流量削峰也叫做削峰填谷

使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在 3消费完积压的消息,这就叫做**“填谷”**

1.3.4. 什么是QPS,PV

QPS即每秒查询率,是对一个特定的查询服务器在规定时间内所处理流量多少的衡量标准。

每秒查询率

因特网上,经常用每秒查询率来衡量域名系统服务器的机器的性能,即为QPS。

或者理解:每秒的响应请求数,也即是最大吞吐能力

计算关系:

QPS = 并发量 / 平均响应时间

并发量 = QPS * 平均响应时间

原理:每天80%的访问集中在20%的时间里,这20%时间叫做峰值时间

公式:( 总PV数 * 80% ) / ( 每天秒数 * 20% ) = 峰值时间每秒请求数(QPS) 。

机器:峰值时间每秒QPS / 单台机器的QPS = 需要的机器 。

1.3.5. 什么是PV , UV , PR

① 什么是pv

PV(page view),即页面浏览量,或点击量;通常是衡量一个网络新闻频道或网站甚至一条网络新闻的主要指标。

对pv的解释是,一个访问者在24小时(0点到24点)内到底看了你网站几个页面。这里需要强调:同一个人浏览你网站同一个页面,不重复计算pv量,点100次也算1次。说白了,pv就是一个访问者打开了你的几个页面。

PV之于网站,就像收视率之于电视,从某种程度上已成为投资者衡量商业网站表现的最重要尺度。

pv的计算:当一个访问者访问的时候,记录他所访问的页面和对应的IP,然后确定这个IP今天访问了这个页面没有。如果你的网站到了23点,单纯IP有60万条的话,每个访问者平均访问了3个页面,那么pv表的记录就要有180万条。

② 什么是uv
uv(unique visitor),指访问某个站点或点击某条新闻的不同IP地址的人数。

在同一天内,uv只记录第一次进入网站的具有独立IP的访问者,在同一天内再次访问该网站则不计数。独立IP访问者提供了一定时间内不同观众数量的统计指标,而没有反应出网站的全面活动。

③ 什么是PR值
 PR值,即PageRank,网页的级别技术,用来标识网页的等级/重要性。级别从1到10级,10级为满分。PR值越高说明该网页越受欢迎(越重要)。

例如:一个PR值为1的网站表明这个网站不太具有流行度,而PR值为7到10则表明这个网站非常受欢迎(或者说极其重要)。

1.4. AMQP 和 JMS

MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。

1.4.1. AMQP

AMQP是一种高级消息队列协议(Advanced Message Queuing Protocol),更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式**。

1.4.2. JMS

JMS即**Java消息服务(JavaMessage Service)**应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。

1.4.3. AMQP 与 JMS 区别

  • JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
  • JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
  • JMS规定了两种消息模式;而AMQP的消息模式更加丰富

1.5. 消息队列产品

市场上常见的消息队列有如下:

  • ActiveMQ:基于JMS

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

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

  • ZeroMQ:基于C语言开发

  • RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好

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

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

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

  • RocketMQ:基于JMS,阿里巴巴产品

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

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

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

  • Kafka:类似MQ的产品;分布式消息系统,高吞吐量

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

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

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

在这里插入图片描述

1.6. MQ的选择

1.Kafka

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

2.RocketMQ

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

3.RabbitMQ

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

1.7. RabbitMQ

RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。

RabbitMQ 是一个消息中间件:它接受并转发消息。你可以把它当做一个快递站点,当你要发送一个包裹时,你把你的包裹放到快递站,快递员最终会把你的快递送到收件人那里,按照这种逻辑 RabbitMQ 是一个快递站,一个快递员帮你传递快件。 RabbitMQ 与快递站的主要区别在于,它不处理快件而是接收,存储和转发消息数据。

RabbitMQ官方地址:http://www.rabbitmq.com/

RabbitMQ提供了6种模式简单模式,work模式 ,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ;暂不作介绍);

官网对应模式介绍:https://www.rabbitmq.com/getstarted.html

在这里插入图片描述

1.7.1. RabbitMQ简介

AMQP,即 Advanced
Message Queuing Protocol(高级消息队列协议),是一个网络协议,是应用层协议的一个开放标准,为面向消息的中间件设计。基于此协议的客户端与消息中间件可传递消息,并不受客户端/中间件不同产品,不同的开发语言等条件的限制。2006年,AMQP 规范发布。类比HTTP。

在这里插入图片描述

2007年,Rabbit 技术公司基于 AMQP 标准开发的 RabbitMQ 1.0 发布。RabbitMQ 采用 Erlang 语言开发。Erlang 语言由 Ericson 设计,专门为开发高并发和分布式系统的一种语言,在电信领域使用广泛。

RabbitMQ 基础架构如下图:

在这里插入图片描述

1.7.2. 四大核心概念

生产者

​ 产生数据发送消息的程序是生产者

交换机

​ 交换机是 RabbitMQ 非常重要的一个部件,一方面它接收来自生产者的消息,另一方面它将消息推送到队列中。交换机必须确切知道如何处理它接收到的消息,是将这些消息推送到特定队列还是推送到多个队列,亦或者是把消息丢弃,这个得有交换机类型决定

队列

​ 队列是 RabbitMQ 内部使用的一种数据结构, 尽管消息流经 RabbitMQ 和应用程序,但它们只能存储在队列中。队列仅受主机的内存和磁盘限制的约束,本质上是一个大的消息缓冲区。许多生产者可以将消息发送到一个队列,许多消费者可以尝试从一个队列接收数据。这就是我们使用队列的方式

消费者

​ 消费与接收具有相似的含义。消费者大多时候是一个等待接收消息的程序。 请注意生产者,消费者和消息中间件很多时候并不在同一机器上。同一个应用程序既可以是生产者又是可以是消费者。

1.7.3. RabbitMQ 核心部分

在这里插入图片描述

1.7.4. RabbitMQ 相关概念

在这里插入图片描述

Broker:接收和分发消息的应用,RabbitMQ Server就是 Message Broker

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

Connection:publisher/consumer 和 broker 之间的 TCP 连接

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

Exchange:message 到达 broker 的第一站,根据分发规则,匹配查询表中的 routing key,分发消息到queue 中去。常用的类型有:direct (point-to-point)topic (publish-subscribe) and fanout (multicast)

Queue:存储消息的容器,消息最终被送到这里,等待 consumer 取走

Binding:exchange 和 queue 之间的虚拟连接,binding 中可以包含 routing key。Binding 信息被保存到 exchange 中的查询表中,用于 message 的分发依据

2. 安装及配置RabbitMQ

2.1. 下载和安装

2.1.1. 下载

1.下载Erlang的rpm包

RabbitMQ是Erlang语言编写,所以Erang环境必须要有,注:Erlang环境一定要与RabbitMQ版本匹配:

https://www.rabbitmq.com/which-erlang.html

2.下载socat的rpm包

rabbitmq安装依赖于socat,所以需要下载socat。

socat下载地址:http://repo.iotti.biz/CentOS/7/x86_64/socat-1.7.3.2-5.el7.lux.x86_64.rpm

3.下载RabbitMQ的rpm包

RabbitMQ下载地址:https://www.rabbitmq.com/download.html(根据自身需求及匹配关系,下载对应rpm包)rabbitmq-server-3.8.1-1.el7.noarch.rpm

也可从github等网站下载。https://hub.fastgit.org/rabbitmq/rabbitmq-server/releases/

2.1.2. 安装

1.安装Erlang、Socat、RabbitMQ
rpm -ivh erlang-21.3.8.9-1.el7.x86_64.rpm
rpm -ivh socat-1.7.3.2-1.el6.lux.x86_64.rpm

在安装rabbitmq之前需要先安装socat,否则,报错。

可以采用yum安装方式:yum install socat,我们这里采用rpm安装方式

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

/usr/lib/rabbitmq/bin/

2.启用管理插件
rabbitmq-plugins enable rabbitmq_management
3.启动RabbitMQ
systemctl start rabbitmq-server.service
systemctl status rabbitmq-server.service
systemctl restart rabbitmq-server.service
systemctl stop rabbitmq-server.service 
4.查看进程
ps -ef | grep rabbitmq

2.1.3. 测试

关闭防火墙:

systemctl stop firewalld.service

在web浏览器中输入地址:http://虚拟机ip:15672/

输入默认账号密码: guest : guest,guest用户默认不允许远程连接。
在这里插入图片描述

增加自定义账号

  • 添加管理员账号密码:

    rabbitmqctl add_user admin admin
    
  • 分配账号角色:

    rabbitmqctl set_user_tags admin administrator
    
  • 设置用户权限 :

    set_permissions [-p <vhostpath>] <user> <conf> <write> <read> 
    
    rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*" 
    
  • 用户user_admin具有/vhost1这个virtual host中所有资源的配置、写、读权限

  • 修改密码:

    rabbitmqctl change_password admin 123456
    
  • 查看用户列表:

    rabbitmqctl list_users
    

使用新账号登录,成功界面

在这里插入图片描述

管理界面标签页介绍

  • overview:概览

  • connections:无论生产者还是消费者,都需要与RabbitMQ建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况

  • channels:通道,建立连接后,会形成通道,消息的投递获取依赖通道。

  • Exchanges:交换机,用来实现消息的路由

  • Queues:队列,即消息队列,消息存放在队列中,等待消费,消费后被移除队列。

端口:

  • 5672:rabbitMq的编程语言客户端连接端口
  • 15672:rabbitMq管理界面端口
  • 25672:rabbitMq集群的端口

2.1.4. 卸载

rpm -qa | grep rabbitmq
rpm -e rabbitmq-server

2.2. 管理界面

2.2.1. 添加用户

如果不使用guest,我们也可以自己创建一个用户:

在这里插入图片描述

1、 超级管理员(administrator)

可登录管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

2、 监控者(monitoring)

可登录管理控制台,同时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)

3、 策略制定者(policymaker)

可登录管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息。

4、 普通管理者(management)

仅可登录管理控制台,无法看到节点信息,也无法对策略进行管理。

5、 其他

无法登录管理控制台,通常就是普通的生产者和消费者。

2.2.2. 创建Virtual Hosts

虚拟主机:类似于mysql中的database。他们都是以“/”开头
在这里插入图片描述

2.2.3. 设置权限

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

3. RabbitMQ入门

官网: https://www.rabbitmq.com/

需求:使用简单模式完成消息传递

步骤:

① 创建工程(生产者、消费者)

② 分别添加依赖

③ 编写生产者发送消息

④ 编写消费者接收消息

3.1. 搭建示例工程

3.1.1. 创建工程

创建父工程项目:rabbitmq-demo-parent

在这里插入图片描述

创建项目:rabbitmq-producer
在这里插入图片描述

创建项目:rabbitmq-consumer

在这里插入图片描述

3.1.2. 添加依赖

往父工程rabbitmq的pom.xml文件中添加如下依赖:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.crazer</groupId>
    <artifactId>rabbitmq-demo-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>rabbitmq-producer</module>
        <module>rabbitmq-consumer</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>com.rabbitmq</groupId>
            <artifactId>amqp-client</artifactId>
            <version>5.16.0</version>
        </dependency>
    </dependencies>
</project>

3.2. 编写生产者

编写消息生产者 com.crazer.rabbitmq.simple.SimpleProducer

package com.crazer.rabbitmq.simple;

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

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:18
 * @version: 1.0.0
 * @Description: 简单模式
 */
public class SimpleProducer {
    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 主机地址
        connectionFactory.setHost("192.168.187.128");
        // 连接端口;默认为 5672
        connectionFactory.setPort(5672);
        // 虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/");
        // 连接用户名;默认为guest
        connectionFactory.setUsername("admin");
        // 连接密码;默认为guest
        connectionFactory.setPassword("123456");

        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 创建频道
        Channel channel = connection.createChannel();
        // 声明(创建)队列
        /**
         * queue      参数1:队列名称
         * durable    参数2:是否定义持久化队列,当mq重启之后,还在
         * exclusive  参数3:是否独占本次连接
         *                  ① 是否独占,只能有一个消费者监听这个队列
         *                  ② 当connection关闭时,是否删除队列
         * autoDelete 参数4:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
         * arguments  参数5:队列其它参数
         */
        channel.queueDeclare("simple_queue", true, false, false, null);
        // 要发送的信息
        String msg = "你好;小兔子!";
        /**
         * exchange:交换机名称,如果没有指定则使用默认Default Exchage
         * routingKey:路由key,简单模式可以传递队列名称
         * props:配置信息
         * body:消息内容
         */
        channel.basicPublish("", "simple_queue", null, msg.getBytes());
        System.out.println("已发送消息:" + msg);

        // 关闭资源
        channel.close();
        connection.close();
    }
}

运行程序:http://192.168.6.100:15672

在执行上述的消息发送之后;可以登录rabbitMQ的管理控制台,可以发现队列和其消息:
在这里插入图片描述
在这里插入图片描述

3.3. 编写消费者

编写消息的消费者 com.crazer.rabbitmq.simple.SimpleConsumer

package com.crazer.rabbitmq.simple;

import com.rabbitmq.client.*;
import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:25
 * @version: 1.0.0
 * @Description: 简单模式
 */
public class SimpleConsumer {
    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 主机地址
        connectionFactory.setHost("192.168.187.128");
        // 连接端口;默认为 5672
        connectionFactory.setPort(5672);
        // 虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/");
        // 连接用户名;默认为guest
        connectionFactory.setUsername("admin");
        // 连接密码;默认为guest
        connectionFactory.setPassword("123456");

        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 创建频道
        Channel channel = connection.createChannel();
        // 声明(创建)队列【可以忽略不写】
        /**
         * queue      参数1:队列名称
         * durable    参数2:是否定义持久化队列,当mq重启之后,还在
         * exclusive  参数3:是否独占本次连接
         *                  ① 是否独占,只能有一个消费者监听这个队列
         *                  ② 当connection关闭时,是否删除队列
         * autoDelete 参数4:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
         * arguments  参数5:队列其它参数
         */
        channel.queueDeclare("simple_queue", true, false, false, null);

        // 接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
             /*
                回调方法,当收到消息后,会自动执行该方法
                1. consumerTag:标识
                2. envelope:获取一些信息,交换机,路由key...
                3. properties:配置信息
                4. body:数据
            */

            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消费者标识:" + consumerTag);
                System.out.println("交换机名称:" + envelope.getExchange());
                System.out.println("路由Key:" + envelope.getRoutingKey());
                System.out.println("消息序号:" + envelope.getDeliveryTag());
                System.out.println("消息内容:" + new String(body));
            }
        };

        /*
            basicConsume(String queue, boolean autoAck, Consumer callback)
            参数:
                1. queue:队列名称
                2. autoAck:是否自动确认 ,类似咱们发短信,发送成功会收到一个确认消息
                3. callback:回调对象
         */
        // 消费者类似一个监听程序,主要是用来监听消息
        channel.basicConsume("simple_queue", true, consumer);

        // 关闭资源
        // channel.close();
        // connection.close();
    }
}

运行程序

在这里插入图片描述

3.4. 小结

上述的入门案例中中其实使用的是如下的简单模式:

在这里插入图片描述

在上图的模型中,有以下概念:

  • P:生产者,也就是要发送消息的程序
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。

4. AMQP

4.1. 相关概念介绍

AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。

RabbitMQ是AMQP协议的Erlang的实现。

概念说明
连接Connection一个网络连接,比如TCP/IP套接字连接。
信道Channel多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。
客户端ClientAMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。
服务节点Broker消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器。
端点AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。
消费者Consumer一个从消息队列里请求消息的客户端程序。
生产者Producer一个向交换机发布消息的客户端应用程序。

4.2. RabbitMQ运转流程

在入门案例中:

  • 生产者发送消息

    1. 生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
    2. 声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
    3. 将路由键(空字符串)与队列绑定起来;
    4. 发送消息至RabbitMQ Broker;
    5. 关闭信道;
    6. 关闭连接;
  • 消费者接收消息

    1. 消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
    2. 向Broker 请求消费相应队列中的消息,设置相应的回调函数;
    3. 等待Broker投递响应队列中的消息,消费者接收消息;
    4. 确认(ack,自动确认)接收到的消息;
    5. RabbitMQ从队列中删除相应已经被确认的消息;
    6. 关闭信道;
    7. 关闭连接;

在这里插入图片描述

5. RabbitMQ 持久化

5.1. 概念

如何保障当 RabbitMQ 服务停掉以后消息生产者发送过来的消息不丢失。默认情况下 RabbitMQ 退出或由于某种原因崩溃时,它忽视队列和消息,除非告知它不要这样做。确保消息不会丢失需要做两件事: 我们需要将队列和消息都标记为持久化

5.2. 队列实现持久化

之前我们创建的队列都是非持久化的, rabbitmq 如果重启的化,该队列就会被删除掉,如果要队列实现持久化 需要在声明队列的时候把 durable 参数设置为持久化。

/**
 * queue      参数1:队列名称
 * durable    参数2:是否定义持久化队列,当mq重启之后,还在
 * exclusive  参数3:是否独占本次连接
 *                  ① 是否独占,只能有一个消费者监听这个队列
 *                  ② 当connection关闭时,是否删除队列
 * autoDelete 参数4:是否在不使用的时候自动删除队列,当没有consumer时,自动删除
 * arguments  参数5:队列其它参数
 */
channel.queueDeclare("task_queue", true, false, false, null);

但是需要注意的就是如果之前声明的队列不是持久化的,需要把原先队列先删除,或者重新创建一个持久化的队列,不然就会出现错误

在这里插入图片描述

以下为控制台中持久化与非持久化队列的 UI 显示区

在这里插入图片描述

这个时候即使重启 rabbitmq 队列也依然存在

5.3. 消息实现持久化

要想让消息实现持久化需要在消息生产者修改代码, MessageProperties.PERSISTENT_TEXT_PLAIN 添加这个属性。

在这里插入图片描述

将消息标记为持久化并不能完全保证不会丢失消息。尽管它告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候但是还没有存储完,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。持久性保证并不强,但是对于我们的简单任务队列而言,这已经绰绰有余了。如果需要更强有力的持久化策略, 参考后边课件发布确认章节。

5.4. 不公平分发

在最开始的时候我们学习到 RabbitMQ 分发消息采用的轮训分发,但是在某种场景下这种策略并不是很好,比方说有两个消费者在处理任务,其中有个消费者 1 处理任务的速度非常快,而另外一个消费者 2处理速度却很慢,这个时候我们还是采用轮训分发的化就会到这处理速度快的这个消费者很大一部分时间处于空闲状态,而处理慢的那个消费者一直在干活,这种分配方式在这种情况下其实就不太好,但是RabbitMQ 并不知道这种情况它依然很公平的进行分发。为了避免这种情况,我们可以设置参数 channel.basicQos(1);

在这里插入图片描述
在这里插入图片描述
在这里插入图片描述

意思就是如果这个任务我还没有处理完或者我还没有应答你,你先别分配给我,我目前只能处理一个任务,然后 rabbitmq 就会把该任务分配给没有那么忙的那个空闲消费者,当然如果所有的消费者都没有完成手上任务,队列还在不停的添加新任务,队列有可能就会遇到队列被撑满的情况,这个时候就只能添加新的 worker 或者改变其他存储任务的策略。

5.5. 预取值

本身消息的发送就是异步发送的,所以在任何时候, channel 上肯定不止只有一个消息另外来自消费者的手动确认本质上也是异步的。因此这里就存在一个未确认的消息缓冲区,因此希望开发人员能限制此缓冲区的大小,以避免缓冲区里面无限制的未确认消息问题。这个时候就可以通过使用 basic.qos 方法设置“预取计数”值来完成的。 **该值定义通道上允许的未确认消息的最大数量。**一旦数量达到配置的数量,RabbitMQ 将停止在通道上传递更多消息,除非至少有一个未处理的消息被确认,例如,假设在通道上有未确认的消息 5、 6、 7, 8,并且通道的预取计数设置为 4,此时 RabbitMQ 将不会在该通道上再传递任何消息,除非至少有一个未应答的消息被 ack。比方说 tag=6 这个消息刚刚被确认 ACK, RabbitMQ 将会感知这个情况到并再发送一条消息。消息应答和 QoS 预取值对用户吞吐量有重大影响。通常,增加预取将提高向消费者传递消息的速度。 虽然自动应答传输消息速率是最佳的,但是,在这种情况下已传递但尚未处 的消息的数量也会增加,从而增加了消费者的 RAM 消耗(随机存取存储器)应该小心使用具有无限预处理的自动确认模式或手动确认模式,消费者消费了大量的消息如果没有确认的话,会导致消费者连接节点的内存消耗变大,所以找到合适的预取值是一个反复试验的过程,不同的负载该值取值也不同 100 到 300 范围内的值通常可提供最佳的吞吐量,并且不会给消费者带来太大的风险。预取值为 1 是最保守的。当然这将使吞吐量变得很低,特别是消费者连接延迟很严重的情况下,特别是在消费者连接等待时间较长的环境中。对于大多数应用来说,稍微高一点的值将是最佳的。

在这里插入图片描述

6. 发布确认

6.1. 发布确认原理

生产者将信道设置成 confirm 模式,一旦信道进入 confirm 模式, 所有在该信道上面发布的消息都将会被指派一个唯一的 ID(从 1 开始),一旦消息被投递到所有匹配的队列之后, broker就会发送一个确认给生产者(包含消息的唯一 ID),这就使得生产者知道消息已经正确到达目的队列了,如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出, broker 回传给生产者的确认消息中 delivery-tag 域包含了确认消息的序列号,此外 broker 也可以设置basic.ack 的 multiple 域,表示到这个序列号之前的所有消息都已经得到了处理。

confirm 模式最大的好处在于他是异步的,一旦发布一条消息,生产者应用程序就可以在等信道返回确认的同时继续发送下一条消息,当消息最终得到确认之后,生产者应用便可以通过回调方法来处理该确认消息,如果 RabbitMQ 因为自身内部错误导致消息丢失,就会发送一条 nack 消息,生产者应用程序同样可以在回调方法中处理该 nack 消息。

6.2. 发布确认的策略

6.2.1. 开启发布确认的方法

发布确认默认是没有开启的,如果要开启需要调用方法 confirmSelect,每当你要想使用发布确认,都需要在 channel 上调用该方法。

Channel channel = connection.createChannel();

channel.confirmSelect();

6.2.2. 单个确认发布

这是一种简单的确认方式,它是一种同步确认发布的方式,也就是发布一个消息之后只有它被确认发布,后续的消息才能继续发布,waitForConfirmsOrDie(long)这个方法只有在消息被确认的时候才返回,如果在指定时间范围内这个消息没有被确认那么它将抛出异常。

这种确认方式有一个最大的缺点就是:发布速度特别的慢, 因为如果没有确认发布的消息就会阻塞所有后续消息的发布,这种方式最多提供每秒不超过数百条发布消息的吞吐量。当然对于某些应用程序来说这可能已经足够了。

package com.crazer.rabbitmq.confirm;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:50
 * @version: 1.0.0
 * @Description: 发布确认模式
 * 1、单个确认
 * 2、批量确认
 * 3、异步批量确认
 */
public class ConfirmMessageProducer {
    public static int MESSAGE_COUNT = 1000; // 批量发消息的个数

    public static void main(String[] args) throws Exception {
        // 单个确认
        ConfirmMessageProducer.publishMessageIndividually();    // 发布1000个单独确认消息,耗时753ms
    }

    // 单个确认
    public static void publishMessageIndividually() throws Exception {
        // 获取连接
        Connection connection = RabbitUtils.getConnection();
        // 创建信道
        Channel channel = connection.createChannel();

        // 创建队列
        String queueName = "confirm_queue1";
        channel.queueDeclare(queueName, true, false, false, null);
        // 开启发布确认
        channel.confirmSelect();
        // 开始时间
        long start = System.currentTimeMillis();

        // 批量发消息
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String msg = "来了," + i;
            channel.basicPublish("", queueName, null, msg.getBytes());
            // 单个消息就马上进行发布确认
            boolean flag = channel.waitForConfirms();
            if (flag) {
                System.out.println("消息发生成功!");
            }
        }

        // 结束时间
        long end = System.currentTimeMillis();

        System.out.println("发布" + MESSAGE_COUNT + "个单独确认消息,耗时" + (end - start) + "ms");
        // 释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

6.2.3. 批量确认发布

上面那种方式非常慢,与单个等待确认消息相比,先发布一批消息然后一起确认可以极大地提高吞吐量,当然这种方式的缺点就是:当发生故障导致发布出现问题时, 不知道是哪个消息出现问题了, 我们必须将整个批处理保存在内存中,以记录重要的信息而后重新发布消息。当然这种方案仍然是同步的,也一样阻塞消息的发布。

package com.crazer.rabbitmq.confirm;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:50
 * @version: 1.0.0
 * @Description: 发布确认模式
 * 1、单个确认
 * 2、批量确认
 * 3、异步批量确认
 */
public class ConfirmMessageProducer {
    public static int MESSAGE_COUNT = 1000; // 批量发消息的个数

    public static void main(String[] args) throws Exception {
        // 单个确认
        // ConfirmMessageProducer.publishMessageIndividually();    // 发布1000个单独确认消息,耗时753ms
        // 批量确认
        ConfirmMessageProducer.publishMessageBatch(); // 发布1000个批量确认消息,耗时58ms
        ConfirmMessageProducer.publishMessageBatch1(); // 发布1000个批量确认消息,耗时91ms
    }
    // 批量确认
    public static void publishMessageBatch() throws Exception {
        // 获取连接
        Connection connection = RabbitUtils.getConnection();
        // 创建信道
        Channel channel = connection.createChannel();

        // 创建队列
        String queueName = "confirm_queue2";
        channel.queueDeclare(queueName, true, false, false, null);
        // 开启发布确认
        channel.confirmSelect();
        // 开始时间
        long start = System.currentTimeMillis();

        // 批量确认消息大小
        int batchSize = 1000;
        // 未确认消息个数
        int outstandingMessageCount = 0;
        // 批量发消息 批量发布确认
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String msg = "来了," + i;
            channel.basicPublish("", queueName, null, msg.getBytes());
            if (i % batchSize == 0) {
                // 发布确认
                channel.waitForConfirms();
            }
        }

        // 结束时间
        long end = System.currentTimeMillis();

        System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - start) + "ms");
        // 释放资源
        RabbitUtils.closeConnection(connection, channel);
    }

    public static void publishMessageBatch1() throws Exception {
        // 获取连接
        Connection connection = RabbitUtils.getConnection();
        // 创建信道
        Channel channel = connection.createChannel();

        // 创建队列
        String queueName = "confirm_queue2";
        channel.queueDeclare(queueName, true, false, false, null);
        // 开启发布确认
        channel.confirmSelect();
        // 开始时间
        long start = System.currentTimeMillis();

        // 批量确认消息大小
        int batchSize = 1000;
        // 未确认消息个数
        int outstandingMessageCount = 0;
        // 批量发消息 批量发布确认
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String msg = "来了," + i;
            channel.basicPublish("", queueName, null, msg.getBytes());
            outstandingMessageCount++;
            if (outstandingMessageCount == batchSize) {
                // 发布确认
                channel.waitForConfirms();
                outstandingMessageCount = 0;
            }
        }
        // 为了确保还有剩余没有确认消息 再次确认
        if (outstandingMessageCount > 0) {
            channel.waitForConfirms();
        }
        // 结束时间
        long end = System.currentTimeMillis();

        System.out.println("发布" + MESSAGE_COUNT + "个批量确认消息,耗时" + (end - start) + "ms");
        // 释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

6.2.4. 异步确认发布

异步确认虽然编程逻辑比上两个要复杂,但是性价比最高,无论是可靠性还是效率都没得说,他是利用回调函数来达到消息可靠性传递的,这个中间件也是通过函数回调来保证是否投递成功,下面就让我们来详细讲解异步确认是怎么实现的。

在这里插入图片描述

package com.crazer.rabbitmq.confirm;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;

import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:50
 * @version: 1.0.0
 * @Description: 发布确认模式
 * 1、单个确认
 * 2、批量确认
 * 3、异步批量确认
 */
public class ConfirmMessageProducer {
    public static int MESSAGE_COUNT = 1000; // 批量发消息的个数

    public static void main(String[] args) throws Exception {
        // 异步批量确认
        ConfirmMessageProducer.handlePublishConfirmsAsynchronously(); // 发布1000个异步批量确认消息,耗时43ms
    }

    // 异步批量确认
    public static void handlePublishConfirmsAsynchronously() throws Exception {
        // 获取连接
        Connection connection = RabbitUtils.getConnection();
        // 创建信道
        Channel channel = connection.createChannel();

        // 创建队列
        String queueName = "confirm_queue3";
        channel.queueDeclare(queueName, true, false, false, null);
        // 开启发布确认
        channel.confirmSelect();
        /**
         * 消息确认成功 回调函数
         * 1.消息序列号
         * 2.true 可以确认小于等于当前序列号的消息
         *  false 确认当前序列号消息
         */
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            System.out.println("确认的消息:" + deliveryTag);
        };
        // 消息确认失败 回调函数
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            System.out.println("未被确认的消息:" + deliveryTag);
        };
        /**
         * 添加一个异步确认的监听器
         * 1.确认收到消息的回调
         * 2.未收到消息的回调
         */
        channel.addConfirmListener(ackCallback, nackCallback);

        // 开始时间
        long start = System.currentTimeMillis();
        // 批量发消息 批量发布确认
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String msg = "来了," + i;
            channel.basicPublish("", queueName, null, msg.getBytes());
        }

        // 结束时间
        long end = System.currentTimeMillis();

        System.out.println("发布" + MESSAGE_COUNT + "个异步批量确认消息,耗时" + (end - start) + "ms");
        // 释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

6.2.5. 如何处理异步未确认消息

最好的解决的解决方案就是把未确认的消息放到一个基于内存的能被发布线程访问的队列,比如说用 ConcurrentLinkedQueue 这个队列在 confirm callbacks 与发布线程之间进行消息的传递。

package com.crazer.rabbitmq.confirm;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.ConfirmCallback;
import com.rabbitmq.client.Connection;

import java.util.concurrent.ConcurrentNavigableMap;
import java.util.concurrent.ConcurrentSkipListMap;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:50
 * @version: 1.0.0
 * @Description: 发布确认模式
 * 1、单个确认
 * 2、批量确认
 * 3、异步批量确认
 */
public class ConfirmMessageProducer {
    public static int MESSAGE_COUNT = 1000; // 批量发消息的个数

    public static void main(String[] args) throws Exception {
        // 异步批量确认
        ConfirmMessageProducer.handlePublishConfirmsAsynchronously(); // 发布1000个异步批量确认消息,耗时43ms
    }

    // 异步批量确认
    public static void handlePublishConfirmsAsynchronously() throws Exception {
        // 获取连接
        Connection connection = RabbitUtils.getConnection();
        // 创建信道
        Channel channel = connection.createChannel();

        // 创建队列
        String queueName = "confirm_queue3";
        channel.queueDeclare(queueName, true, false, false, null);
        // 开启发布确认
        channel.confirmSelect();
        /**
         * 线程安全有序的一个哈希表,适用于高并发的情况
         * 1.轻松的将序号与消息进行关联
         * 2.轻松批量删除条目 只要给到序列号
         * 3.支持并发访问
         */
        ConcurrentSkipListMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();

        /**
         * 消息确认成功 回调函数
         * 1.消息序列号
         * 2.true 可以确认小于等于当前序列号的消息
         *  false 确认当前序列号消息
         */
        ConfirmCallback ackCallback = (deliveryTag, multiple) -> {
            // 删除已经确认的消息,剩下的就是未确认的消息
            if (multiple) {
                // 返回的是小于等于当前序列号的未确认消息 是一个 map
                ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(deliveryTag, true);
                // 清除该部分未确认消息
                confirmed.clear();
            } else {
                // 只清除当前序列号的消息
                outstandingConfirms.remove(deliveryTag);
            }
            System.out.println("确认的消息:" + deliveryTag);
        };
        // 消息确认失败 回调函数
        ConfirmCallback nackCallback = (deliveryTag, multiple) -> {
            String message = outstandingConfirms.get(deliveryTag);
            System.out.println("发布的消息" + message + "未被确认,序列号" + deliveryTag);
        };
        /**
         * 添加一个异步确认的监听器
         * 1.确认收到消息的回调
         * 2.未收到消息的回调
         */
        channel.addConfirmListener(ackCallback, nackCallback);

        // 开始时间
        long start = System.currentTimeMillis();
        // 批量发消息 批量发布确认
        for (int i = 0; i < MESSAGE_COUNT; i++) {
            String msg = "来了," + i;
            /**
             * channel.getNextPublishSeqNo()获取下一个消息的序列号
             * 通过序列号与消息体进行一个关联
             * 全部都是未确认的消息体
             */
            outstandingConfirms.put(channel.getNextPublishSeqNo(), msg);
            channel.basicPublish("", queueName, null, msg.getBytes());
        }

        // 结束时间
        long end = System.currentTimeMillis();

        System.out.println("发布" + MESSAGE_COUNT + "个异步批量确认消息,耗时" + (end - start) + "ms");
        // 释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

6.2.6. 以上 3 种发布确认速度对比

单独发布消息

​ 同步等待确认, 简单,但吞吐量非常有限。

批量发布消息

​ 批量同步等待确认, 简单,合理的吞吐量, 一旦出现问题但很难推断出是那条消息出现了问题。
异步处理

​ 最佳性能和资源使用,在出现错误的情况下可以很好地控制,但是实现起来稍微难些

public static void main(String[] args) throws Exception {
    // 单个确认
    // ConfirmMessageProducer.publishMessageIndividually();    // 发布1000个单独确认消息,耗时753ms
    // 批量确认
    // ConfirmMessageProducer.publishMessageBatch(); // 发布1000个批量确认消息,耗时58ms
    // ConfirmMessageProducer.publishMessageBatch1(); // 发布1000个批量确认消息,耗时91ms
    // 异步批量确认
    ConfirmMessageProducer.handlePublishConfirmsAsynchronously(); // 发布1000个异步批量确认消息,耗时43ms
}

7. RabbitMQ工作模式

7.1. Work queues工作队列模式

7.1.1. 模式说明

在这里插入图片描述

Work Queues与入门程序的简单模式相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。

应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度

7.1.2. 代码

Work Queues与入门程序的简单模式的代码是几乎一样的;可以完全复制,并复制多一个消费者进行多个消费者同时消费消息的测试。

1)抽取工具类
package com.crazer.utils;

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

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:36
 * @version: 1.0.0
 * @Description: Rabbit工具类
 */
public class RabbitUtils {
    public static Connection getConnection() throws Exception {
        // 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 主机地址
        connectionFactory.setHost("192.168.187.128");
        // 连接端口;默认为 5672
        connectionFactory.setPort(5672);
        // 虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/");
        // 连接用户名;默认为guest
        connectionFactory.setUsername("admin");
        // 连接密码;默认为guest
        connectionFactory.setPassword("123456");
        // 创建连接
        Connection connection = connectionFactory.newConnection();
        return connection;
    }

    public static void closeConnection(Connection connection, Channel channel) throws Exception {
        if (channel != null) {
            channel.close();
        }
        if (connection != null) {
            connection.close();
        }
    }
}
2)生产者
package com.crazer.rabbitmq.work;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:39
 * @version: 1.0.0
 * @Description: 工作队列模式
 */
public class WorkProducer {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();

        // 创建队列
        channel.queueDeclare("work_queue", true, false, false, null);
        // 发信息
        for (int i = 0; i < 10; i++) {
            String msg = "工作队列模式的消息......" + i;
            channel.basicPublish("", "work_queue", null, msg.getBytes());
        }
        RabbitUtils.closeConnection(connection, channel);
    }
}

在这里插入图片描述

3)消费者1
package com.crazer.rabbitmq.work;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:43
 * @version: 1.0.0
 * @Description: 工作队列模式
 */
public class WorkConsumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("1号消费者获取消息---> " + new String(body));
            }
        };

        channel.basicConsume("work_queue", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}
4)消费者2

Consumer2Consumer1类一样。省略。

运行两个消费者

7.1.3. 测试

启动两个消费者,然后再启动生产者发送消息;到IDEA的两个消费者对应的控制台查看是否竞争性的接收到消息。

在这里插入图片描述

7.1.4. 小结

  1. 在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
  2. Work Queues 对于任务过重或任务较多情况使用工作队列可以提高任务处理的速度。

7.2. 订阅模式类型

订阅模式示例图:

在这里插入图片描述

前面2个案例中,只有3个角色:·

  • P:生产者,也就是要发送消息的程序
  • C:消费者:消息的接受者,会一直等待消息到来。
  • queue:消息队列,图中红色部分

而在订阅模型中,多了一个exchange角色,而且过程略有变化:

  • P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)

  • C:消费者,消息的接受者,会一直等待消息到来。

  • Queue:消息队列,接收消息、缓存消息。

  • Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型

    • Fanout:广播,将消息交给所有绑定到交换机的队列
    • Direct:定向,把消息交给符合指定routing key 的队列
    • Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列

Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!

7.3. Publish/Subscribe发布与订阅模式

7.3.1. 模式说明

在这里插入图片描述

发布订阅模式:
1、每个消费者监听自己的队列。
2、生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收到消息

7.3.2. 代码

1)生产者
package com.crazer.rabbitmq.fanout;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:50
 * @version: 1.0.0
 * @Description: 发布与订阅模式
 */
public class FanoutProducer {
    public static void main(String[] args) throws Exception {
        // 1.获取连接
        Connection connection = RabbitUtils.getConnection();
        // 2.创建信道
        Channel channel = connection.createChannel();
        /*
            exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
            参数:
            1. exchange:交换机名称
            2. type:交换机类型
                DIRECT("direct"),:定向
                FANOUT("fanout"),:扇形(广播),发送消息到每一个与之绑定队列。
                TOPIC("topic"),通配符的方式
                HEADERS("headers");参数匹配
            3. durable:是否持久化
            4. autoDelete:自动删除
            5. internal:内部使用。 一般false
            6. arguments:参数
        */
        // 3.创建交换机
        String exchangeName = "fanout_exchange";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.FANOUT, true, false, false, null);
        // 4.创建队列
        String queue1Name = "fanout_queue1";
        String queue2Name = "fanout_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 5.绑定队列和交换机
        /*
            queueBind(String queue, String exchange, String routingKey)
            参数:
                1. queue:队列名称
                2. exchange:交换机名称
                3. routingKey:路由键,绑定规则
            如果交换机的类型为fanout ,routingKey设置为""
        */
        channel.queueBind(queue1Name, exchangeName, "");
        channel.queueBind(queue2Name, exchangeName, "");
        // 6.发送消息
        String body = "发布订阅模式发送的信息......";
        channel.basicPublish(exchangeName, "", null, body.getBytes());
        // 7.释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

在这里插入图片描述

2)消费者1
package com.crazer.rabbitmq.fanout;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:57
 * @version: 1.0.0
 * @Description: 发布与订阅模式
 */
public class FanoutConsumer1 {
    public static void main(String[] args)throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("1号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("fanout_queue1", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}
3)消费者2
package com.crazer.rabbitmq.fanout;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 19:57
 * @version: 1.0.0
 * @Description: 发布与订阅模式
 */
public class FanoutConsumer2 {
    public static void main(String[] args)throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("2号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("fanout_queue2", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}

7.3.3. 测试

启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达广播的效果。

在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges选项卡,点击 fanout_exchange 的交换机,可以查看到如下的绑定:

在这里插入图片描述

在这里插入图片描述

7.3.4. 小结

交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。

发布订阅模式与工作队列模式的区别

1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。

2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。

3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。

7.4. Routing路由模式

7.4.1. 模式说明

路由模式特点:

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

在这里插入图片描述

图解:

  • P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
  • C1:消费者,其所在队列指定了需要routing key 为 error 的消息
  • C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息

7.4.2. 代码

在编码上与 Publish/Subscribe 发布与订阅模式 的区别是交换机的类型为:Direct,还有队列绑定交换机的时候需要指定routing key。

1)生产者
package com.crazer.rabbitmq.routing;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 14:49
 * @version: 1.0.0
 * @Description: 路由模式
 */
public class RoutingProducer {
    public static void main(String[] args) throws Exception {
        // 1.获取连接
        Connection connection = RabbitUtils.getConnection();
        // 2.创建信道
        Channel channel = connection.createChannel();

        // 3.创建交换机
        String exchangeName = "direct_exchange";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.DIRECT, true, false, false, null);

        // 4.创建队列
        String queue1Name = "direct_queue1";
        String queue2Name = "direct_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);

        // 5.队列绑定交换机
        channel.queueBind(queue1Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "info");
        channel.queueBind(queue2Name, exchangeName, "error");
        channel.queueBind(queue2Name, exchangeName, "warning");

        // 6.发送消息
        String body = "发布路由模式发送的信息......";
        channel.basicPublish(exchangeName, "warning", null, body.getBytes());

        // 7.释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

在这里插入图片描述
在这里插入图片描述

2)消费者1
package com.crazer.rabbitmq.routing;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 14:44
 * @version: 1.0.0
 * @Description: 路由模式
 */
public class RoutingConsumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();

        Channel channel = connection.createChannel();

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("1号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("direct_queue1", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}
3)消费者2
package com.crazer.rabbitmq.routing;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 14:44
 * @version: 1.0.0
 * @Description: 路由模式
 */
public class RoutingConsumer2 {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();

        Channel channel = connection.createChannel();

        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("2号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("direct_queue2", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}

7.4.3. 测试

启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果。

在这里插入图片描述

7.4.4. 小结

Routing模式要求队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列。

7.5. Topics通配符模式

7.5.1. 模式说明

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

Routingkey一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如:item.insert

通配符规则:

#:匹配零个或多个词

*:匹配不多不少恰好1个词

举例:

item.#:能够匹配item.insert.abc 或者 item.insert
item.*:只能匹配item.insert

在这里插入图片描述
在这里插入图片描述

图解:

  • 红色Queue:绑定的是usa.# ,因此凡是以 usa.开头的routing key 都会被匹配到
  • 黄色Queue:绑定的是#.news ,因此凡是以 .news结尾的 routing key 都会被匹配

7.5.2. 代码

1)生产者

使用topic类型的Exchange,发送消息的routing key有3种: order.info

package com.crazer.rabbitmq.topic;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 20:09
 * @version: 1.0.0
 * @Description: 通配符模式
 */
public class TopicsProducer {
    public static void main(String[] args) throws Exception {
        // 1.获取连接
        Connection connection = RabbitUtils.getConnection();
        // 2.创建信道
        Channel channel = connection.createChannel();
        // 3.创建交换机
        String exchangeName = "topic_exchange";
        channel.exchangeDeclare(exchangeName, BuiltinExchangeType.TOPIC, true, false, false, null);
        // 4.创建队列
        String queue1Name = "topic_queue1";
        String queue2Name = "topic_queue2";
        channel.queueDeclare(queue1Name, true, false, false, null);
        channel.queueDeclare(queue2Name, true, false, false, null);
        // 5.队列绑定交换机
        /*
             参数:
             1. queue:队列名称
             2. exchange:交换机名称
             3. routingKey:路由键,绑定规则
             如果交换机的类型为fanout ,routingKey设置为""
         */
        channel.queueBind(queue1Name, exchangeName, "*.orange.*");
        channel.queueBind(queue2Name, exchangeName, "*.*.rabbit");
        channel.queueBind(queue2Name, exchangeName, "lazy.#");
        // 6.发送消息
        String body = "发布通配符模式发送的信息......";
        channel.basicPublish(exchangeName, "lazy.orange.666", null, body.getBytes());

        // 7.释放资源
        RabbitUtils.closeConnection(connection, channel);
    }
}

运行程序

在这里插入图片描述

在这里插入图片描述

2)消费者1

接收一种类型的消息:*.orange.*

package com.crazer.rabbitmq.topic;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 14:44
 * @version: 1.0.0
 * @Description: 通配符模式
 */
public class TopicsConsumer1 {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("1号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("topic_queue1", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}
3)消费者2

接收两种类型的消息:*.*.rabbitlazy.#

package com.crazer.rabbitmq.topic;

import com.crazer.utils.RabbitUtils;
import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 14:44
 * @version: 1.0.0
 * @Description: 通配符模式
 */
public class TopicsConsumer2 {
    public static void main(String[] args) throws Exception {
        Connection connection = RabbitUtils.getConnection();
        Channel channel = connection.createChannel();
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("2号消费者获取消息---> " + new String(body));
            }
        };
        channel.basicConsume("topic_queue2", true, consumer);

        // RabbitUtils.closeConnection(connection,channel);
    }
}

7.5.3. 测试

启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果;并且这些routing key可以使用通配符。

在这里插入图片描述

7.5.4. 小结

Topic主题模式可以实现 Publish/Subscribe 发布与订阅模式Routing 路由模式 的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活。

7.6. 模式总结

RabbitMQ工作模式:
1、简单模式 HelloWorld
一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-2XZ0famV-1666789504748)(img/在这里插入图片描述

2、工作队列模式 Work Queue
一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)

在这里插入图片描述

3、发布订阅模式 Publish/subscribe
需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列

在这里插入图片描述

4、路由模式 Routing
需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

在这里插入图片描述

5、通配符模式 Topic
需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列

在这里插入图片描述

8. Spring 整合RabbitMQ

创建父工程项目:spring-rabbitmq-parent

在这里插入图片描述

8.1. 搭建生产者工程

8.1.1. 创建工程spring-rabbitmq-producer

8.1.2. 添加依赖

修改pom.xml文件内容为如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-rabbitmq-parent</artifactId>
        <groupId>com.crazer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-rabbitmq-producer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.3.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.22</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
    </dependencies>
</project>

8.1.3. 配置整合

1 创建rabbitmq.properties连接参数等配置文件;

rabbitmq.host=192.168.187.128
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=123456
rabbitmq.virtual-host=/

2 创建 spring-rabbitmq.xml 整合配置文件;

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>
    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>
    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>
    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Simple简单模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <!--
        自动声明队列
        id 对象在Spring容器中的唯-标识
        name 队列名称
        auto-declare="true" 自动声明,队列不存在的时候才创建
    -->
    <rabbit:queue id="spring_simple_queue11" name="spring_simple_queue1" auto-declare="true"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Work工作队列模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_work_queue11" name="spring_work_queue1" auto-declare="true"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Publish/Subscribe发布与订阅模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <!--自动声明队列-->
    <rabbit:queue id="spring_fanout_queue_11" name="spring_fanout_queue_1" auto-declare="true"/>
    <rabbit:queue id="spring_fanout_queue_22" name="spring_fanout_queue_2" auto-declare="true"/>

    <!--创建交换机-->
    <rabbit:fanout-exchange name="spring_fanout_exchange">
        <!--绑定队列到该交换机-->
        <rabbit:bindings>
            <!--queue指向的是队列在Spring容器中的唯一标识(id)-->
            <rabbit:binding queue="spring_fanout_queue_11"></rabbit:binding>
            <rabbit:binding queue="spring_fanout_queue_22"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:fanout-exchange>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Routing路由模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_direct_queue_11" name="spring_direct_queue_1" auto-declare="true"/>
    <rabbit:queue id="spring_direct_queue_22" name="spring_direct_queue_2" auto-declare="true"/>
    <!--创建交换机-->
    <rabbit:direct-exchange name="spring_direct_exchange">
        <!--绑定队列到该交换机-->
        <rabbit:bindings>
            <!--queue指向的是队列在Spring容器中的唯一标识(id)-->
            <rabbit:binding queue="spring_direct_queue_11" key="error"/>
            <rabbit:binding queue="spring_direct_queue_22" key="info"/>
            <rabbit:binding queue="spring_direct_queue_22" key="error"/>
            <rabbit:binding queue="spring_direct_queue_22" key="waring"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Topics通配符模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_topic_queue_11" name="spring_topic_queue_1" auto-declare="true"/>
    <rabbit:queue id="spring_topic_queue_22" name="spring_topic_queue_2" auto-declare="true"/>
    <!--创建交换机-->
    <rabbit:topic-exchange name="spring_topic_exchange">
        <rabbit:bindings>
            <rabbit:binding queue="spring_topic_queue_11" pattern="*.orange.*"/>
            <rabbit:binding queue="spring_topic_queue_22" pattern="*.*.rabbit"/>
            <rabbit:binding queue="spring_topic_queue_22" pattern="lazy.#"/>
        </rabbit:bindings>
    </rabbit:topic-exchange>
</beans>

8.1.4. 发送消息

创建测试文件 ProducerTest.java

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 20:40
 * @version: 1.0.0
 * @Description: TODO
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class ProducerTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 简单模式
     * -使用的是默认的交换机(类型direct)
     * -绑定的路由key是队列名
     */
    @Test
    public void simple() {
        String msg = "简单模式的信息...";
        rabbitTemplate.convertAndSend("spring_simple_queue1", msg.getBytes());
    }

    /**
     * 工作队列模式
     * -使用的是默认的交换机(类型direct)
     * -绑定的路由key是队列名
     */
    @Test
    public void work() {
        for (int i = 0; i < 10; i++) {
            String msg = "工作队列模式的信息..." + i;
            rabbitTemplate.convertAndSend("spring_work_queue1", msg.getBytes());
        }
    }

    /**
     * 发布订阅模式
     * -使用的交换机(类型fanout)
     * -不需要使用路由key,定义为""
     */
    @Test
    public void fanout() {
        for (int i = 0; i < 10; i++) {
            String msg = "发布订阅模式的信息..." + i;
            rabbitTemplate.convertAndSend("spring_fanout_exchange", "", msg.getBytes());
        }
    }

    /**
     * 路由模式
     * -使用的交换机(类型direct)
     * -使用路由key
     */
    @Test
    public void direct() {
        String msg = "发布路由模式的信息...";
        rabbitTemplate.convertAndSend("spring_direct_exchange", "error", msg.getBytes());
    }

    /**
     * 通配符模式
     * -使用的交换机(类型topic)
     * -使用路由key
     */
    @Test
    public void topic() {
        String msg = "发布通配符模式的信息...";
        rabbitTemplate.convertAndSend("spring_topic_exchange", "www.orange.rabbit", msg.getBytes());
    }
}

8.2. 搭建消费者工程

8.2.1. 创建工程spring-rabbitmq-consumer

8.2.2. 添加依赖

修改pom.xml文件内容为如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-rabbitmq-parent</artifactId>
        <groupId>com.crazer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-rabbitmq-consumer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.22</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.3.5</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.22</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>
    </dependencies>
</project>

8.2.3. 配置整合

创建rabbitmq.properties连接参数等配置文件;

rabbitmq.host=192.168.187.128
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=123456
rabbitmq.virtual-host=/

创建spring-rabbitmq.xml 整合配置文件;

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
       xmlns:context="http://www.springframework.org/schema/context"
       xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <rabbit:listener-container connection-factory="connectionFactory">
        <!--
            使用监听器监听一个队列(消费者)
            ref 监听器在Spring容器中的唯-标识
            queue-names 队列名称
        -->
        <rabbit:listener ref="simpleListener" queue-names="spring_simple_queue1"/>
        <!--工作队列模式-->
        <rabbit:listener ref="workListener1" queue-names="spring_work_queue1"/>
        <rabbit:listener ref="workListener2" queue-names="spring_work_queue1"/>
        <!--发布订阅模式-->
        <rabbit:listener ref="fanoutListener1" queue-names="spring_fanout_queue_1"/>
        <rabbit:listener ref="fanoutListener2" queue-names="spring_fanout_queue_2"/>
        <!--路由模式-->
        <rabbit:listener ref="diretListener1" queue-names="spring_direct_queue_1"/>
        <rabbit:listener ref="diretListener2" queue-names="spring_direct_queue_2"/>
        <!--通配符模式-->
        <rabbit:listener ref="topicListener1" queue-names="spring_topic_queue_1"/>
        <rabbit:listener ref="topicListener2" queue-names="spring_topic_queue_2"/>
    </rabbit:listener-container>
</beans>

8.2.4. 消息监听器

1)简单模式监听器

创建SimpleListener.java

package com.crazer.listener.simple;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 20:47
 * @version: 1.0.0
 * @Description: 简单模式
 */
@Component
public class SimpleListener implements MessageListener {
    @Override
    public void onMessage(Message message) {
        System.out.println("消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
2)工作队列模式监听器

创建WorkListener1.javaWorkListener2.java【WorkListener2与WorkListener1代码相同,不再重复】

package com.crazer.listener.work;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 16:40
 * @version: 1.0.0
 * @Description: 工作队列模式
 */
@Component
public class WorkListener1 implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("1号消费者,消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
3)发布与订阅模式监听器

创建FanoutListener1.javaFanoutListener2.java【FanoutListener2与FanoutListener1代码相同,不再重复】

package com.crazer.listener.fanout;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 16:40
 * @version: 1.0.0
 * @Description: 发布与订阅模式
 */
@Component
public class FanoutListener1 implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("1号消费者,消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
4)路由模式监听器

创建DiretListener1.javaDiretListener2.java【DiretListener2与DiretListener1代码相同,不再重复】

package com.crazer.listener.direct;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 16:40
 * @version: 1.0.0
 * @Description: 路由模式
 */
@Component
public class DiretListener1 implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("1号消费者,消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
5)通配符模式监听器

创建TopicListener1.javaTopicListener2.java【TopicListener2与TopicListener1代码相同,不再重复】

package com.crazer.listener.topic;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/11 16:40
 * @version: 1.0.0
 * @Description: 通配符模式
 */
@Component
public class TopicListener1 implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("1号消费者,消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}

8.2.5. 编写测试类

package com.crazer;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 20:54
 * @version: 1.0.0
 * @Description: TODO
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class ConsumerTest {

    @Test
    public void start() {
        while (true) {

        }
    }
}

9. RabbitMQ高级特性

9.1. 消息的可靠投递

消息丢失的三个环节

在这里插入图片描述

在使用 RabbitMQ 的时候,作为消息发送方希望杜绝任何消息丢失或者投递失败场景。RabbitMQ 为我们提供了两种方式用来控制消息的投递可靠性模式

  • confirm 确认模式
  • return 退回模式

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

producer—>rabbitmq broker—>exchange—>queue—>consumer

l 消息从 producer 到 exchange 则会返回一个 confirmCallback 。

l 消息从 exchange–>queue 投递失败则会返回一个 returnCallback 。

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

9.1.1. 提供者代码实现confirm 确认模式、return 退回模式

① 创建项目 spring-rabbitmq-producer

② 添加pom文件

注意:

spring-rabbit的版本如果为:2.2.22.RELEASE版本以上就无法在定义rabbitmq connectionFactory中使用publisher-confirms="true",应该换成confirm-type="CORRELATED"

  • none:表示禁用发布确认模式,默认即此。
  • correlated:表示成功发布消息到交换器后会触发的回调方法。
  • simple:类似 correlated,并且支持 waitForConfirms()waitForConfirmsOrDie() 方法的调用。

在这里插入图片描述

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-rabbitmq-advancedfeatures-parent</artifactId>
        <groupId>com.crazer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-rabbitmq-producer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.3.23</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.4.7</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.13.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.3.23</version>
        </dependency>
    </dependencies>

</project>

③ 在resource 文件夹下面添加 配置文件 rabbitmq.properties

rabbitmq.host=192.168.187.128
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=123456
rabbitmq.virtual-host=/

④ 在resource 文件夹下面添加 配置文件 spring-rabbitmq-confirm-return.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <!--
    spring-rabbit的版本如果为:2.3版本以下,用此配置
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
                               publisher-confirms="true"
                               publisher-returns="true"/>
    -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"
                               confirm-type="CORRELATED"
                               publisher-returns="true"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>


    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Routing路由模式~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_confirm_queue_11" name="spring_confirm_queue_1" auto-declare="true"/>
    <rabbit:queue id="spring_confirm_queue_22" name="spring_confirm_queue_2" auto-declare="true"/>
    <!--创建交换机-->
    <rabbit:direct-exchange name="spring_exchange_confirm">
        <!--绑定队列到该交换机-->
        <rabbit:bindings>
            <!--queue指向的是队列在Spring容器中的唯一标识(id)-->
            <rabbit:binding queue="spring_confirm_queue_11" key="error"/>
            <rabbit:binding queue="spring_confirm_queue_22" key="info"/>
            <rabbit:binding queue="spring_confirm_queue_22" key="error"/>
            <rabbit:binding queue="spring_confirm_queue_22" key="waring"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>

⑤ 创建测试类 , 添加确认模式

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 8:27
 * @version: 1.0.0
 * @Description: 高级特性 - 确认模式、退回机制
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class ProducerConfirmAndReturnTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 生产者消息丢失问题
     * confirm 确认模式
     */
    @Test
    public void testConfirm() {
        String msg = "发布路由模式的信息...";
        /**
         * 确认模式:
         * 步骤:
         * 1. 确认模式开启:ConnectionFactory中开启publisher-confirms="true"
         * 2. 在rabbitTemplate定义ConfirmCallBack回调函数
         */
        rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
            @Override
            public void confirm(CorrelationData correlationData, boolean ack, String cause) {
                if (ack) {
                    // 接收成功
                    System.out.println("接收成功消息:" + cause);
                } else {
                    // 接收失败
                    System.out.println("接收失败消息:" + cause);
                    // 做一些处理,让消息再次发送。
                }
            }
        });
        // 发生失败
        rabbitTemplate.convertAndSend("spring_confirm_exchange123", "error", msg.getBytes());
        // 发生成功
        rabbitTemplate.convertAndSend("spring_confirm_exchange", "error", msg.getBytes());
    }
}

运行程序

在这里插入图片描述

⑥ 添加回退模式

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 8:27
 * @version: 1.0.0
 * @Description: 高级特性 - 确认模式、退回机制
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class ProducerConfirmAndReturnTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 生产者消息丢失问题(交换机 > 队列)
     * return 退回机制
     *
     * spring-rabbit的版本如果为:2.3版本及以上,用此配置
     */
    @Test
    public void testReturn() {
        String msg = "发布路由模式的信息...";
        /**
         * 回退模式: 当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack
         * 步骤:
         * 1. 开启回退模式:publisher-returns="true"
         * 2. 设置ReturnCallBack
         * 3. 设置Exchange处理消息的模式:
         *      1). 如果消息没有路由到Queue,则丢弃消息(默认)
         *      2). 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
         *            rabbitTemplate.setMandatory(true);
         */
        rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback(){
            @Override
            public void returnedMessage(ReturnedMessage returned) {
                System.out.println("消息对象->" + new String(returned.getMessage().getBody()));
                System.out.println("错误码->" + returned.getReplyCode());
                System.out.println("错误信息->" + returned.getReplyText());
                System.out.println("交换机->" + returned.getExchange());
                System.out.println("路由键->" + returned.getRoutingKey());
            }
        });
        // 设置强制处理,交换机转发消息给队列失败后,将消息退回发送方(生产者),交给ReturnCallback
        rabbitTemplate.setMandatory(true);
        // 发生失败
        rabbitTemplate.convertAndSend("spring_confirm_exchange", "86844", msg.getBytes());
        // 发生成功
        rabbitTemplate.convertAndSend("spring_confirm_exchange", "error", msg.getBytes());
    }

    /**
     * 生产者消息丢失问题(交换机 》队列)
     * return 退回机制
     *
     * spring-rabbit的版本如果为:2.3版本以下,用此配置
     */
    @Test
    public void testReturn_OldMenthod() {
        String msg = "发布路由模式的信息...";
        /**
         * 回退模式: 当消息发送给Exchange后,Exchange路由到Queue失败时 才会执行 ReturnCallBack
         * 步骤:
         * 1. 开启回退模式:publisher-returns="true"
         * 2. 设置ReturnCallBack
         * 3. 设置Exchange处理消息的模式:
         *      1). 如果消息没有路由到Queue,则丢弃消息(默认)
         *      2). 如果消息没有路由到Queue,返回给消息发送方ReturnCallBack
         *            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("消息对象->" + new String(message.getBody()));
                System.out.println("错误码->" + replyCode);
                System.out.println("错误信息->" + replyText);
                System.out.println("交换机->" + exchange);
                System.out.println("路由键->" + routingKey);
            }
        });

        // 设置强制处理,交换机转发消息给队列失败后,将消息退回发送方(生产者),交给ReturnCallback
        rabbitTemplate.setMandatory(true);
        // 发生失败
        rabbitTemplate.convertAndSend("spring_confirm_exchange", "86844", msg.getBytes());
        // 发生成功
        rabbitTemplate.convertAndSend("spring_confirm_exchange", "error", msg.getBytes());

    }
}

运行程序

在这里插入图片描述

9.1.2. 消息的可靠投递小结

  • 设置 ConnectionFactorypublisher-confirms="true"【2.3版本以下被淘汰】或者confirm-type="CORRELATED" 开启 确认模式。
  • 使用 rabbitTemplate.setConfirmCallback 设置回调函数。当消息发送到 exchange 后回调 confirm 方法。在方法中判断 ack,如果为true,则发送成功,如果为false,则发送失败,需要处理。
  • 设置 ConnectionFactorypublisher-returns="true" 开启 退回模式。
  • 使用 rabbitTemplate.setReturnCallback【2.3版本以下被淘汰】或者rabbitTemplate.setReturnsCallback 设置退回函数,当消息从exchange 路由到 queue 失败后,如果设置了 rabbitTemplate.setMandatory(true) 参数,则会将消息退回给 producer并执行回调函数returnedMessage

9.1.3. Consumer Ack

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

有三种确认方式:

  • ​ 自动确认:acknowledge=“none”
  • ​ 手动确认:acknowledge=“manual”
  • ​ 根据异常自动处理:acknowledge=“auto” (默认)

其中自动确认是指,当消息一旦被Consumer接收到,则自动确认收到,并将相应 message 从 RabbitMQ 的消息缓存中移除。但是在实际业务处理中,很可能消息接收到,业务处理出现异常,那么该消息就会丢失。

如果设置了手动确认方式,则需要在业务处理成功后,调用channel.basicAck(),手动签收,如果出现异常,则调用channel.basicNack()方法,让其自动重新发送消息。

如果xml里没有配置acknowledge,则是默认配置acknowledge=“auto”,根据异常自动处理, 如果此时消费者抛出异常,消息会返回该队列并发送给其他消费者,如没有其他消费者,则会继续发到该消费者(此时会产生无限循环)。

生产者:

① 在 resource 文件夹下面新建 rabbitmq.properties 文件 和 spring-rabbitmq-byconsumer.xml 文件

spring-rabbitmq-byconsumer.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~Consumer Ack~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_ack_queue11" name="spring_ack_queue1" auto-declare="true"/>
</beans>

② 编写生产者测试类

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 21:31
 * @version: 1.0.0
 * @Description: Consumer Ack
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-byconsumer.xml")
public class ProducerByConsumerTest {

    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void t1() {
        String msg = "123哈哈哈";
        rabbitTemplate.convertAndSend("spring_ack_queue1", msg.getBytes());
    }
}

消费者:

① 创建项目 spring-rabbitmq-consumer

② 添加pom文件

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <parent>
        <artifactId>spring-rabbitmq-parent</artifactId>
        <groupId>com.crazer</groupId>
        <version>1.0-SNAPSHOT</version>
    </parent>
    <modelVersion>4.0.0</modelVersion>

    <artifactId>spring-rabbitmq-consumer</artifactId>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-context</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.amqp</groupId>
            <artifactId>spring-rabbit</artifactId>
            <version>2.1.8.RELEASE</version>
        </dependency>

        <dependency>
            <groupId>junit</groupId>
            <artifactId>junit</artifactId>
            <version>4.12</version>
        </dependency>

        <dependency>
            <groupId>org.springframework</groupId>
            <artifactId>spring-test</artifactId>
            <version>5.1.7.RELEASE</version>
        </dependency>
    </dependencies>
</project>

③ 在 resource 文件夹下面新建 rabbitmq.properties 文件 和 spring-rabbitmq-byconsumer.xml 文件

rabbitmq.properties 文件

rabbitmq.host=192.168.187.128
rabbitmq.port=5672
rabbitmq.username=admin
rabbitmq.password=123456
rabbitmq.virtual-host=/

spring-rabbitmq-byconsumer.xml 文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <!--
        ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
        有三种确认方式:
        •	自动确认:acknowledge=“none”
        •	手动确认:acknowledge=“manual”
        •	根据异常自动处理:acknowledge=“auto”  (默认)
        prefetch="1" 消费者每次只接收1条消息,处理完后再接收下一条
    -->
    <rabbit:listener-container connection-factory="connectionFactory"
                               acknowledge="manual">
        <!--自动确认-监听器-->
        <!--<rabbit:listener ref="ackListener" queue-names="spring_simple_queue1"/>-->
        <!--手动确认-监听器-->
        <rabbit:listener ref="ackListener1" queue-names="spring_simple_queue1"/>
    </rabbit:listener-container>
</beans>
9.1.3.1. 自动确认

选择启用 自动确认-监听器 <rabbit:listener ref="ackListener" queue-names="spring_simple_queue1"/>

④ 添加监听器

package com.crazer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 9:12
 * @version: 1.0.0
 * @Description: 自动确认-监听器
 */
@Component
public class AckListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        int i = 1 / 0;

        System.out.println("消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
9.1.3.2. 手动确认

选择启用 手动确认-监听器 <rabbit:listener ref="ackListener1" queue-names="spring_simple_queue1"/>

④ 添加监听器

package com.crazer.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 9:12
 * @version: 1.0.0
 * @Description: 手动确认-监听器
 * 1、<rabbit:listener-container> 配置 acknowledge="manual"
 * 2、监听器实现的接口ChannelAwareMessageListener
 * 3.手动确认
 * channel.basicAck 确认
 * channel.basicNack 拒绝
 */
@Component
public class AckListener1 implements ChannelAwareMessageListener {

    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("消息内容:" + new String(message.getBody()));

        // 获取消息传递标记
        long deliveryTag = message.getMessageProperties().getDeliveryTag();
        try {
            // 处理业务逻辑
            System.out.println("处理业务逻辑......");

            // 模拟异常
            int i = 1 / 0;

            // 手动签收
            /**
             * deliveryTag 表示收到的标签
             * multiple 如果为true表示可以签收所有的消息
             * channel.basicAck(5, true) 确认序号<=5的所有消息
             * channel.basicAck(5,false) 磅认序号=5的消息
             */
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            System.out.println("出现异常......");
            e.printStackTrace();
            // 拒绝签收
            /**
             * deliveryTag 表示收到的标签
             * multiple 是否批量处理
             * requeue 是否重回队列
             *      设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag, true, false);
            // 失败后,根据业务需求具体处理
            // 例如:如果是重要消息,重试3次,3次还是失败则存入数据库,等待人工解决.
            // channel.basicNack(deliveryTag, true, false);
        }
    }
}

⑤ 添加测试类

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 19:02
 * @version: 1.0.0
 * @Description: Consumer Ack
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq.xml")
public class ConsumerTest {
    @Test
    public void start() {
        while (true) {

        }
    }
}

测试【自动确认-监听器AckListener】运行测试类 ,会一直监听消息 ,查看后台 http://192.168.187.128:15672/#/queues

在这里插入图片描述

测试【手动确认-监听器AckListener1】当程序报错,程序会拒绝签收,直到修改错误,修改上面的监听器,注释 除 0 错误 ,重新运行程序

在这里插入图片描述

在这里插入图片描述

9.1.3.3. Consumer Ack 小结

在rabbit:listener-container标签中设置acknowledge属性,设置ack方式 none:自动确认,manual:手动确认

如果在消费端没有出现异常,则调用channel.basicAck(deliveryTag,true);方法确认签收消息

如果出现异常,则在catch中调用 basicNack,拒绝消息,让MQ重新发送消息。

消息应答的方法

  • Channel.basicAck(用于肯定确认)
    • RabbitMQ 已知道该消息并且成功的处理消息,可以将其丢弃了
  • Channel.basicNack(用于否定确认)
  • Channel.basicReject(用于否定确认)
    • 与 Channel.basicNack 相比少一个参数
    • 不处理该消息了直接拒绝,可以将其丢弃了

Multiple 的解释

multiple 的 true 和 false 代表不同意思

  • true 代表批量应答 channel 上未应答的消息
    • 比如说 channel 上有传送 tag 的消息 5,6,7,8 当前 tag 是 8 那么此时
    • 5-8 的这些还未应答的消息都会被确认收到消息应答
  • false 同上面相比
    • 只会应答 tag=8 的消息 5,6,7 这三个消息依然不会被确认收到消息应答

在这里插入图片描述

9.2. 消费端限流

在这里插入图片描述

① 在项目 spring-rabbitmq-consumer ,新建 com.crazer.listener.QosListener

package com.crazer.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 21:56
 * @version: 1.0.0
 * @Description: Consumer 限流机制
 * 1. 确保消息被确认。不确认是不继续处理其他消息的
 * 2. listener-container配置属性
 * prefetch = 1,表示消费端每次从mq拉去一条消息来消费,直到手动确认消费完毕后,才会继续拉取下一条消息。
 */
@Component
public class QosListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("消息内容:" + new String(message.getBody()));
    }
}

② 修改spring-rabbitmq-qos.xml 配置文件

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <!--
        ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
        有三种确认方式:
        •  自动确认:acknowledge=“none”
        •  手动确认:acknowledge=“manual”
        •  根据异常自动处理:acknowledge=“auto”  (默认)
        prefetch="1" 消费者每次只接收1条消息,处理完后再接收下一条
    -->
    <rabbit:listener-container connection-factory="connectionFactory"
                               acknowledge="manual"
                                prefetch="1">

        <rabbit:listener ref="qosListener" queue-names="spring_ack_queue1"/>
    </rabbit:listener-container>
</beans>

③ 测试消费者限流ConsumerQosTest

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 19:02
 * @version: 1.0.0
 * @Description: 测试消费者限流
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-qos.xml")
public class ConsumerQosTest {
    @Test
    public void start() {
        while (true) {

        }
    }
}

运行消费者,等待消息….

④ 在项目 spring-rabbitmq-producer , ProducerQosTest 测试方法

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 测试消费者限流
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-byconsumer.xml")
public class ProducerQosTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSend() {
        for (int i = 0; i < 10; i++) {
            String msg = "生产者发生消息--->消费者限流," + i;
            rabbitTemplate.convertAndSend("spring_ack_queue1", "confirm", "message confirm");
        }
    }
}

运行提供者

在这里插入图片描述

⑤ 查看后台 ,有 9 条消息待消费 ,有 1 条消息未确认

在这里插入图片描述

⑥ 修改消费者配置文件 ,去掉 prefetch="1" 会发现一次就可以消费所有消息

在这里插入图片描述

运行消费者测试类 ConsumerQosTest

在这里插入图片描述

⑦ 修改 QosListener , 添加手动签收方法 ,这样就可以确认消费限流

在这里插入图片描述

运行程序

在这里插入图片描述

消费端限流小结:

  • <rabbit:listener-container>中配置 prefetch 属性设置消费端一次拉取多少条消息
  • 消费端的必须确认才会继续处理其他消息。

9.3. TTL

TTL 全称 Time To Live(存活时间/过期时间)。

当消息到达存活时间后,还没有被消费,会被自动清除。

RabbitMQ可以对消息设置过期时间,也可以对整个队列(Queue)设置过期时间。

在这里插入图片描述

9.3.1. 控制后台演示消息过期

① 修改管理后台界面,增加队列

参数:表示过期时间,单位毫秒 ,10000表示10秒

在这里插入图片描述

② 增加交换机

在这里插入图片描述

③ 绑定队列

在这里插入图片描述

④ 发送消息

Delivery mode:2-Persistent表示需要进行持久化

在这里插入图片描述

⑤ 查看消息,可以看到消息,但十秒之后,消息自动消失,因为我们设置了十秒消息过期

在这里插入图片描述

在这里插入图片描述

9.3.2. 代码实现

9.3.2.1. 队列统一过期

修改 spring-rabbitmq-producer 项目的 配置文件 spring-rabbitmq-ttl.xml

给队列设置参数:x-message-ttl

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~TTL 队列~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_ttl_queue" name="spring_ttl_queue" auto-declare="true">
        <!--设置队列参数-->
        <rabbit:queue-arguments>
            <!--
              设置x-message-ttl队列的过期时间
              默认情况下value-type的类型是String类型,但时间的类型是number类型,所以需要设置成integer类型
            -->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
        </rabbit:queue-arguments>
    </rabbit:queue>
</beans>

在测试类ProducerTTLTest中,添加测试方法,发送消息

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: TTL队列 设置有效时间
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-ttl.xml")
public class ProducerTTLTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 1.队列设置过期时间
     */
    @Test
    public void testSend() {
        String msg = "生产者发生消息--->TTL队列";
        rabbitTemplate.convertAndSend("spring_ttl_queue", msg);
    }
}

查看控制台,发现有1条消息,10秒之后自动过期

在这里插入图片描述

在这里插入图片描述

9.3.2.2. 消息过期
package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: TTL队列 设置有效时间
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-ttl.xml")
public class ProducerTTLTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 2.消息设置过期时间
     * TTL:过期时间
     * 1. 队列统一过期
     * 2. 消息单独过期
     * 如果设置了消息的过期时间,也设置了队列的过期时间,它以时间短的为准。
     */
    @Test
    public void testSend2() {
        String msg = "123哈哈哈";
        // 通过消息处理器,设置消息的过期时间
        MessagePostProcessor processor = new MessagePostProcessor() {
            @Override
            public Message postProcessMessage(Message message) throws AmqpException {
                // 1.设置message的信息
                // 第二个方法:消息的过期时间 ,5秒之后过期
                message.getMessageProperties().setExpiration("5000");
                // 2.返回该消息
                return message;
            }
        };
        rabbitTemplate.convertAndSend("spring_ttl_queue", (Object) msg, processor);
    }
}

运行程序,发现有1条消息,5秒之后自动过期

在这里插入图片描述

在这里插入图片描述

9.4. 死信队列

死信队列,英文缩写:DLX 。DeadLetter Exchange(死信交换机),当消息成为Dead message后,可以被重新发送到另一个交换机,这个交换机就是DLX。

什么是死信队列

先从概念解释上搞清楚这个定义,死信,顾名思义就是无法被消费的消息,字面意思可以这样理解,一般来说,producer将消息投递到broker或者直接到queue里了,consumer从queue取出消息进行消费,但某些时候由于特定的原因导致queue中的某些消息无法被消费,这样的消息如果没有后续的处理,就变成了死信,有死信,自然就有了死信队列;

在这里插入图片描述

消息成为死信的三种情况:

  1. 队列消息数量到达限制;比如队列最大只能存储10条消息,而发了11条消息,根据先进先出,最先发的消息会进入死信队列。
  2. 消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false;
  3. 原队列存在消息过期设置,消息到达超时时间未被消费;

死信的处理方式

死信的产生既然不可避免,那么就需要从实际的业务角度和场景出发,对这些死信进行后续的处理,常见的处理方式大致有下面几种,

① 丢弃,如果不是很重要,可以选择丢弃

② 记录死信入库,然后做后续的业务分析或处理

③ 通过死信队列,由负责监听死信的应用程序进行处理

综合来看,更常用的做法是第三种,即通过死信队列,将产生的死信通过程序的配置路由到指定的死信队列,然后应用监听死信队列,对接收到的死信做后续的处理,

队列绑定死信交换机:

给队列设置参数: x-dead-letter-exchangex-dead-letter-routing-key

在这里插入图片描述

9.4.1. 过期时间代码实现

修改生产者项目的配置文件 spring-rabbitmq-dl.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~死信 队列~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <!--
      死信队列:
          1. 声明正常的队列(spring_normal_queue)和交换机(spring_normal_exchange)
          2. 声明死信队列(spring_dl_queue)和死信交换机(spring_dl_exchange)
          3. 正常队列绑定死信交换机
              设置两个参数:
                  * x-dead-letter-exchange:死信交换机名称
                  * x-dead-letter-routing-key:发送给死信交换机的routingkey
    -->
    <!--普通队列-->
    <rabbit:queue id="spring_normal_queue" name="spring_normal_queue" auto-declare="true">
        <rabbit:queue-arguments>
            <!--设置队列的过期时间 ttl-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
            <!--设置队列的长度限制x-max-length-->
            <!--<entry key="x-max-length" value="10" value-type="java.lang.Integer"/>-->

            <!--死信交换机-->
            <!--x-dead-letter-exchange:死信交换机名称-->
            <entry key="x-dead-letter-exchange" value="spring_dl_exchange"/>
            <!--x-dead-letter-routing-key:发送给死信交换机的routingkey-->
            <entry key="x-dead-letter-routing-key" value="info_dl"></entry>

        </rabbit:queue-arguments>
    </rabbit:queue>
    <!--普通交换机-->
    <rabbit:direct-exchange name="spring_normal_exchange">
        <rabbit:bindings>
            <rabbit:binding queue="spring_normal_queue" key="info"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--死信队列-->
    <rabbit:queue id="spring_dl_queue" name="spring_dl_queue" auto-declare="true"/>
    <!--死信交换机-->
    <rabbit:direct-exchange name="spring_dl_exchange">
        <rabbit:bindings>
            <rabbit:binding queue="spring_dl_queue" key="info_dl"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>

在测试类中,添加如下方法,进行测试

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 死信队列
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-dl.xml")
public class ProducerDLTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 过期时间代码实现
     */
    @Test
    public void testSend() {
        String msg = "生产者发生消息--->死信队列,";
        rabbitTemplate.convertAndSend("spring_normal_exchange", "info", msg);
    }
}

运行测试,查看管理台界面

在这里插入图片描述

9.4.2. 长度限制代码实现

修改生产者项目的配置文件 spring-rabbitmq-dl.xml ,添加设置队列的长度限制``x-max-length`

<entry key="x-max-length" value="10" value-type="java.lang.Integer"/>

在这里插入图片描述

修改测试类,添加测试方法

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 死信队列
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-dl.xml")
public class ProducerDLTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 长度限制代码实现
     */
    @Test
    public void testSendAndMaxLength() {
        for (int i = 0; i < 15; i++) {
            String msg = "生产者发生消息--->死信队列," + i;
            rabbitTemplate.convertAndSend("spring_normal_exchange", "info", msg);
        }
    }
}

运行测试方法,进行测试

在这里插入图片描述

9.4.3. 测试消息拒收

修改生产者项目的配置文件 spring-rabbitmq-dl.xml ,取消设置队列的过期时间 ttl设置队列的长度限制x-max-length

在这里插入图片描述

生产者工程编写发生消息的测试代码,ProducerDLTest

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 死信队列
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-dl.xml")
public class ProducerDLTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    /**
     * 测试消费者-消息拒收
     */
    @Test
    public void testSendToConsumer() {
        String msg = "生产者发生消息--->测试消费者-消息拒收";
        rabbitTemplate.convertAndSend("spring_normal_exchange", "info", msg);
    }
}

消费者工程 创建 com.crazer.listener.DlxListener

package com.crazer.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/15 11:26
 * @version: 1.0.0
 * @Description: 消费者-消息拒收
 */
@Component
public class DlxListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("消息内容:" + new String(message.getBody()));
        // 获取消息传递标记
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            // 处理业务逻辑
            System.out.println("处理业务逻辑......");
            // 模拟异常
            int i = 1 / 0;
            // 手动签收
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            System.out.println("出现异常......");
            e.printStackTrace();
            // 拒绝签收
            /**
             * deliveryTag 表示收到的标签
             * multiple 是否批量处理
             * requeue 是否重回队列
             *      设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag, true, false);
        }
    }
}

创建并修改消费者配置文件 spring-rabbitmq-dl.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <!--
        ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
        有三种确认方式:
        •  自动确认:acknowledge=“none”
        •  手动确认:acknowledge=“manual”
        •  根据异常自动处理:acknowledge=“auto”  (默认)
        prefetch="1" 消费者每次只接收1条消息,处理完后再接收下一条
    -->
    <rabbit:listener-container connection-factory="connectionFactory"
                               acknowledge="manual"
                               prefetch="1">
        <!--监听普通队列-->
        <rabbit:listener ref="dlxListener" queue-names="spring_normal_queue"/>
    </rabbit:listener-container>
</beans>

消费者工程编写测试类代码,ConsumerDLTest

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 19:02
 * @version: 1.0.0
 * @Description: 测试消费者-消息拒收
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-dl.xml")
public class ConsumerDLTest {
    @Test
    public void start() {
        while (true) {

        }
    }
}

发送消息,运行程序,查看后台管理界面

在这里插入图片描述

死信队列小结

  1. 死信交换机和死信队列和普通的没有区别
  2. 当消息成为死信后,如果该队列绑定了死信交换机,则消息会被死信交换机重新路由到死信队列
  3. 消息成为死信的三种情况:
    • 队列消息长度(数量)到达限制;
    • 消费者拒接消费消息,并且不重回队列;
    • 原队列存在消息过期设置,消息到达超时时间未被消费;

9.5. 延迟队列

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

场景:在订单系统中,一个用户下单之后通常有30分钟的时间进行支付,如果30分钟之内没有支付成功,那么这个订单将进行取消处理。这时就可以使用延时队列将订单信息发送到延时队列。

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。
  2. 新用户注册成功30分钟后,发送短信问候。

实现方式:

  1. 延迟队列

在这里插入图片描述

很可惜,在RabbitMQ中并未提供延迟队列功能。

但是可以使用:TTL+死信队列 组合实现延迟队列的效果。

在这里插入图片描述

9.5.1. 代码实现

9.5.1.1. 生产者

修改生产者代码 ,修改生产者配置文件 spring-rabbitmq-delay.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~延迟 队列~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <!--
      死信队列:
          1. 声明正常的队列(spring_delay_normal_queue)和交换机(spring_delay_normal_exchange)
          2. 声明死信队列(spring_delay_queue)和死信交换机(spring_delay_exchange)
          3. 正常队列绑定死信交换机
              设置两个参数:
                  * x-dead-letter-exchange:死信交换机名称
                  * x-dead-letter-routing-key:发送给死信交换机的routingkey
    -->
    <!--普通队列-->
    <rabbit:queue id="spring_delay_normal_queue" name="spring_delay_normal_queue" auto-declare="true">
        <rabbit:queue-arguments>
            <!--设置队列的过期时间 ttl-->
            <entry key="x-message-ttl" value="10000" value-type="java.lang.Integer"/>
            <!--设置队列的长度限制x-max-length-->
            <!--<entry key="x-max-length" value="10" value-type="java.lang.Integer"/>-->

            <!--死信交换机-->
            <!--x-dead-letter-exchange:死信交换机名称-->
            <entry key="x-dead-letter-exchange" value="spring_delay_exchange"/>
            <!--x-dead-letter-routing-key:发送给死信交换机的routingkey-->
            <entry key="x-dead-letter-routing-key" value="info_delay"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>
    <!--普通交换机-->
    <rabbit:direct-exchange name="spring_delay_normal_exchange">
        <rabbit:bindings>
            <rabbit:binding queue="spring_delay_normal_queue" key="info"></rabbit:binding>
        </rabbit:bindings>
    </rabbit:direct-exchange>

    <!--死信队列-->
    <rabbit:queue id="spring_delay_queue" name="spring_delay_queue" auto-declare="true"/>
    <!--死信交换机-->
    <rabbit:direct-exchange name="spring_delay_exchange">
        <rabbit:bindings>
            <rabbit:binding queue="spring_delay_queue" key="info_delay"/>
        </rabbit:bindings>
    </rabbit:direct-exchange>
</beans>

修改生产者类ProducerDelayTest,添加测试方法

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 延迟队列
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-delay.xml")
public class ProducerDelayTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendToConsumer() {
        String msg = "生产者发生消息--->延迟队列";
        rabbitTemplate.convertAndSend("spring_delay_normal_exchange", "info", msg);
    }
}

运行程序创建延时队列

9.5.1.2. 消费者

修改消费者项目,添加 com.crazer.listener.DelayListener

package com.crazer.listener;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/15 11:26
 * @version: 1.0.0
 * @Description: 延迟队列
 */
@Component
public class DelayListener implements ChannelAwareMessageListener {
    @Override
    public void onMessage(Message message, Channel channel) throws Exception {
        System.out.println("消息内容:" + new String(message.getBody()));
        // 获取消息传递标记
        long deliveryTag = message.getMessageProperties().getDeliveryTag();

        try {
            // 处理业务逻辑
            System.out.println("处理业务逻辑......");
            // 手动签收
            channel.basicAck(deliveryTag, true);
        } catch (Exception e) {
            System.out.println("出现异常......");
            e.printStackTrace();
            // 拒绝签收
            /**
             * deliveryTag 表示收到的标签
             * multiple 是否批量处理
             * requeue 是否重回队列
             *      设置为true,则消息重新回到queue,broker会重新发送该消息给消费端
             */
            channel.basicNack(deliveryTag, true, false);
        }
    }
}

修改消费者配置文件 spring-rabbitmq-delay.xml

注意:延迟队列监听的是 死信队列

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <!--
        ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
        有三种确认方式:
        •  自动确认:acknowledge=“none”
        •  手动确认:acknowledge=“manual”
        •  根据异常自动处理:acknowledge=“auto”  (默认)
        prefetch="1" 消费者每次只接收1条消息,处理完后再接收下一条
    -->
    <rabbit:listener-container connection-factory="connectionFactory"
                               acknowledge="manual"
                               prefetch="1">
        <!--监听死信队列-->
        <rabbit:listener ref="delayListener" queue-names="spring_delay_queue"/>
    </rabbit:listener-container>
</beans>

运行消费者测试类ConsumerDelayTest

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 19:02
 * @version: 1.0.0
 * @Description: 延迟队列
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-delay.xml")
public class ConsumerDelayTest {
    @Test
    public void start() {
        while (true) {

        }
    }
}

在这里插入图片描述

9.5.2. 延时队列优化

9.5.2.1. 代码架构图

在这里新增了一个队列 QC,绑定关系如下,该队列不设置 TTL 时间

在这里插入图片描述

9.5.2.2. 配置文件类代码
@Component
public class MsgTtlQueueConfig {
    public static final String Y_DEAD_LETTER_EXCHANGE = "Y";
    public static final String QUEUE_C = "QC";
    //声明队列 C 死信交换机
    @Bean("queueC")
    public Queue queueB(){
        Map<String, Object> args = new HashMap<>(3);
        //声明当前队列绑定的死信交换机
        args.put("x-dead-letter-exchange", Y_DEAD_LETTER_EXCHANGE);
        //声明当前队列的死信路由 key
        args.put("x-dead-letter-routing-key", "YD");
        //没有声明 TTL 属性
        return QueueBuilder.durable(QUEUE_C).withArguments(args).build();
    }
    //声明队列 B 绑定 X 交换机
    @Bean
    public Binding queuecBindingX(@Qualifier("queueC") Queue queueC,
    							@Qualifier("xExchange") DirectExchange xExchange){
    	return BindingBuilder.bind(queueC).to(xExchange).with("XC");
    }
}
9.5.2.3. 消息生产者代码
@GetMapping("sendExpirationMsg/{message}/{ttlTime}")
public void sendMsg(@PathVariable String message,@PathVariable String ttlTime) {
    rabbitTemplate.convertAndSend("X", "XC", message, correlationData ->{
    correlationData.getMessageProperties().setExpiration(ttlTime);
    return correlationData;
    });
    log.info("当前时间: {},发送一条时长{}毫秒 TTL 信息给队列 C:{}", new Date(),ttlTime, message);
}

发起请求
http://localhost:8080/ttl/sendExpirationMsg/你好 1/20000
http://localhost:8080/ttl/sendExpirationMsg/你好 2/2000

在这里插入图片描述

看起来似乎没什么问题,但是在最开始的时候,就介绍过如果使用在消息属性上设置 TTL 的方式,消息可能并不会按时“死亡“,因为 RabbitMQ 只会检查第一个消息是否过期,如果过期则丢到死信队列,如果第一个消息的延时时长很长,而第二个消息的延时时长很短,第二个消息并不会优先得到执行。

9.5.3. Rabbitmq 插件实现延迟队列

上文中提到的问题,确实是一个问题,如果不能实现在消息粒度上的 TTL,并使其在设置的 TTL 时间及时死亡,就无法设计成一个通用的延时队列。那如何解决呢,接下来我们就去解决该问题。

9.5.3.1. 安装延时队列插件

在官网上下载 https://www.rabbitmq.com/community-plugins.html,下载rabbitmq_delayed_message_exchange 插件,然后解压放置到 RabbitMQ 的插件目录。

进入 RabbitMQ 的安装目录下的 plgins 目录,执行下面命令让该插件生效,然后重启RabbitMQ
/usr/lib/rabbitmq/lib/rabbitmq_server-3.8.8/plugins
rabbitmq-plugins enable rabbitmq_delayed_message_exchange

在这里插入图片描述

在这里插入图片描述

9.5.3.2. 代码架构图

在这里新增了一个队列 delayed.queue,一个自定义交换机 delayed.exchange,绑定关系如下:

在这里插入图片描述

9.5.3.3. 配置文件类代码

在我们自定义的交换机中,这是一种新的交换类型,该类型消息支持延迟投递机制 消息传递后并不会立即投递到目标队列中,而是存储在 mnesia(一个分布式数据系统)表中,当达到投递时间时,才投递到目标队列中。

@Configuration
public class DelayedQueueConfig {
    public static final String DELAYED_QUEUE_NAME = "delayed.queue";
    public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
    public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
    @Bean
    public Queue delayedQueue() {
    	return new Queue(DELAYED_QUEUE_NAME);
    }
    //自定义交换机 我们在这里定义的是一个延迟交换机
    @Bean
    public CustomExchange delayedExchange() {
        Map<String, Object> args = new HashMap<>();
        //自定义交换机的类型
        args.put("x-delayed-type", "direct");
        return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false,args);
    }
    @Bean
    public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
    							@Qualifier("delayedExchange") CustomExchange delayedExchange) {
    	return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
    }
}
9.5.3.4. 消息生产者代码
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message,@PathVariable Integer delayTime) {
    rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message,correlationData ->{
    correlationData.getMessageProperties().setDelay(delayTime);
    return correlationData;});
    log.info(" 当 前 时 间 : {}, 发 送 一 条 延 迟 {} 毫 秒 的 信 息 给 队 列 delayed.queue:{}", new
    Date(),delayTime, message);
}
9.5.3.5. 消息消费者代码
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message){
    String msg = new String(message.getBody());
    log.info("当前时间: {},收到延时队列的消息: {}", new Date().toString(), msg);
}

发起请求:
http://localhost:8080/ttl/sendDelayMsg/come on baby1/20000
http://localhost:8080/ttl/sendDelayMsg/come on baby2/2000

在这里插入图片描述

第二个消息被先消费掉了,符合预期

9.5.4. 总结

延时队列在需要延时处理的场景下非常有用,使用 RabbitMQ 来实现延时队列可以很好的利用RabbitMQ 的特性,如:消息可靠发送、消息可靠投递、死信队列来保障消息至少被消费一次以及未被正确处理的消息不会被丢弃。另外,通过 RabbitMQ 集群的特性,可以很好的解决单点故障问题,不会因为单个节点挂掉导致延时队列不可用或者消息丢失。

当然,延时队列还有很多其它选择,比如利用 Java 的 DelayQueue,利用 Redis 的 zset,利用 Quartz或者利用 kafka 的时间轮,这些方式各有特点,看需要适用的场景

9.6. 优化级队列

9.6.1. 使用场景

​ 在我们系统中有一个订单催付的场景,我们的客户在天猫下的订单,淘宝会及时将订单推送给我们,如果在用户设定的时间内未付款那么就会给用户推送一条短信提醒,很简单的一个功能对吧,但是, tmall商家对我们来说,肯定是要分大客户和小客户的对吧,比如像苹果,小米这样大商家一年起码能给我们创造很大的利润,所以理应当然,他们的订单必须得到优先处理,而曾经我们的后端系统是使用 redis 来存放的定时轮询,大家都知道 redis 只能用 List 做一个简简单单的消息队列,并不能实现一个优先级的场景,所以订单量大了后采用 RabbitMQ 进行改造和优化,如果发现是大客户的订单给一个相对比较高的优先级,否则就是默认优先级。

9.6.2. 如何添加

a.控制台页面添加

在这里插入图片描述

在这里插入图片描述

b.队列中代码添加优先级【生产者】

1)普通方式:

Map<String, Object> params = new HashMap();
params.put("x-max-priority", 10);
channel.queueDeclare("hello", true, false, false, params);

2)Spring整合方式:

创建spring-rabbitmq-priority.xml配置文件

<!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~优化级队列~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
<rabbit:queue id="spring_priority_queue" name="spring_priority_queue" auto-declare="true">
    <rabbit:queue-arguments>
        <!--
                队列中代码添加优先级
                官方允许是0-255
                从此设置10,允许优化级范围为0-10,不要设置过大,浪费CPU和内存
            -->
        <entry key="x-max-priority" value="10"></entry>
    </rabbit:queue-arguments>
</rabbit:queue>

c.消息中代码添加优先级【生产者】

1)普通方式:

AMQP.BasicProperties properties = new
AMQP.BasicProperties().builder().priority(5).build();

2)Spring整合方式:

MessagePostProcessor processor = new MessagePostProcessor() {
    @Override
    public Message postProcessMessage(Message message) throws AmqpException {
        MessageProperties messageProperties = message.getMessageProperties();
        messageProperties.setPriority(5);
        return message;
    }
};

d.注意事项
要让队列实现优先级需要做的事情有如下事情:队列需要设置为优先级队列,消息需要设置消息的优先级,消费者需要等待消息已经发送到队列中才去消费因为,这样才有机会对消息进行排序

9.6.3. 实战

9.6.3.1. 普通方式

a.生产者

package com.crazer.simple;


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

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

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 优化级队列
 */
public class ProducerPriorityTest1 {
    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 主机地址
        connectionFactory.setHost("192.168.187.128");
        // 连接端口;默认为 5672
        connectionFactory.setPort(5672);
        // 虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/");
        // 连接用户名;默认为guest
        connectionFactory.setUsername("admin");
        // 连接密码;默认为guest
        connectionFactory.setPassword("123456");

        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 创建频道
        Channel channel = connection.createChannel();

        Map<String, Object> arguments = new HashMap();
        arguments.put("x-max-priority", 10);
        // 声明(创建)队列
        channel.queueDeclare("normal_priority_queue", true, false, false, arguments);

        for (int i = 0; i < 10; i++) {
            String msg = "生产者发生消息--->优化级队列," + i;
            if (i == 5) {
                AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
                channel.basicPublish("", "normal_priority_queue", properties, msg.getBytes());
            }else {
                channel.basicPublish("", "normal_priority_queue", null, msg.getBytes());
            }
        }

        // 关闭资源
        channel.close();
        connection.close();
    }
}

b.消费者

package com.crazer.simple;


import com.rabbitmq.client.*;

import java.io.IOException;

/**
 * @Author: Crazer
 * @Date: 2022/10/16 1:11
 * @version: 1.0.0
 * @Description: 优化级队列
 */
public class ConsumerPriorityTest1 {
    public static void main(String[] args) throws Exception {
        // 创建连接工厂
        ConnectionFactory connectionFactory = new ConnectionFactory();
        // 主机地址
        connectionFactory.setHost("192.168.187.128");
        // 连接端口;默认为 5672
        connectionFactory.setPort(5672);
        // 虚拟主机名称;默认为 /
        connectionFactory.setVirtualHost("/");
        // 连接用户名;默认为guest
        connectionFactory.setUsername("admin");
        // 连接密码;默认为guest
        connectionFactory.setPassword("123456");

        // 创建连接
        Connection connection = connectionFactory.newConnection();
        // 创建频道
        Channel channel = connection.createChannel();

        // 接收消息
        DefaultConsumer consumer = new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("消息内容:" + new String(body));
                System.out.println("消息序号:" + envelope.getDeliveryTag());
            }
        };
        // 消费者类似一个监听程序,主要是用来监听消息
        channel.basicConsume("normal_priority_queue", true, consumer);

        // 关闭资源
        channel.close();
        connection.close();
    }
}

在这里插入图片描述

9.6.3.1. Spring整合方式

a.生产者

配置spring-rabbitmq-priority.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!-- 管理器,用于管理交换机与队列-->
    <rabbit:admin connection-factory="connectionFactory"/>

    <!--创建RabbitTemplate对象存入Spring容器,用于发送消息-->
    <rabbit:template id="rabbitTemplate" connection-factory="connectionFactory"/>

    <!--~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~优化级队列~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~~-->
    <rabbit:queue id="spring_priority_queue" name="spring_priority_queue" auto-declare="true">
        <rabbit:queue-arguments>
            <!--
                队列中代码添加优先级
                官方允许是0-255
                从此设置10,允许优化级范围为0-10,不要设置过大,浪费CPU和内存
            -->
            <entry key="x-max-priority" value="10"></entry>
        </rabbit:queue-arguments>
    </rabbit:queue>
</beans>

生产者测试类

package com.crazer.simple;

import com.rabbitmq.client.AMQP;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.AmqpException;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 22:00
 * @version: 1.0.0
 * @Description: 优化级队列
 */
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-priority.xml")
public class ProducerPriorityTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void testSendToConsumer() {
        for (int i = 0; i < 10; i++) {
            String msg = "生产者发生消息--->优化级队列," + i;
            if (i == 5) {
                // 消息中代码添加优先级
                MessagePostProcessor processor = new MessagePostProcessor() {
                    @Override
                    public Message postProcessMessage(Message message) throws AmqpException {
                        MessageProperties messageProperties = message.getMessageProperties();
                        messageProperties.setPriority(5);
                        return message;
                    }
                };
                rabbitTemplate.convertAndSend("", "spring_priority_queue", msg, processor);
            } else {
                rabbitTemplate.convertAndSend("spring_priority_queue",  msg);
            }
        }
    }
}

b.消费者

优先级队列监听器com.crazer.listener.PriorityListener

package com.crazer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/14 9:12
 * @version: 1.0.0
 * @Description: 优化级队列
 */
@Component
public class PriorityListener implements MessageListener {

    @Override
    public void onMessage(Message message) {
        System.out.println("消息内容:" + new String(message.getBody()));
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}

配置spring-rabbitmq-priority.xml

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
       xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:rabbit="http://www.springframework.org/schema/rabbit"
       xmlns:context="http://www.springframework.org/schema/context"
       xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd http://www.springframework.org/schema/rabbit http://www.springframework.org/schema/rabbit/spring-rabbit.xsd http://www.springframework.org/schema/context https://www.springframework.org/schema/context/spring-context.xsd">
    <!--加载配置文件-->
    <context:property-placeholder location="classpath:rabbitmq.properties"/>

    <!--扫描注解包-->
    <context:component-scan base-package="com.crazer.listener"/>
    <!-- 定义rabbitmq connectionFactory -->
    <rabbit:connection-factory id="connectionFactory"
                               host="${rabbitmq.host}"
                               port="${rabbitmq.port}"
                               username="${rabbitmq.username}"
                               password="${rabbitmq.password}"
                               virtual-host="${rabbitmq.virtual-host}"/>

    <!--监听器容器,管理所有的监听器-->
    <!--
        ack指Acknowledge,确认。 表示消费端收到消息后的确认方式。
        有三种确认方式:
        •  自动确认:acknowledge=“none”
        •  手动确认:acknowledge=“manual”
        •  根据异常自动处理:acknowledge=“auto”  (默认)
        prefetch="1" 消费者每次只接收1条消息,处理完后再接收下一条
    -->
    <rabbit:listener-container connection-factory="connectionFactory"
                               acknowledge="manual">
        <!--监听普通队列-->
        <rabbit:listener ref="priorityListener" queue-names="spring_priority_queue"/>
    </rabbit:listener-container>
</beans>

消费者测试类

package com.crazer.simple;

import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;

/**
 * @Author: Crazer
 * @Date: 2022/10/16 1:11
 * @version: 1.0.0
 * @Description: 优化级队列
 */
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(locations = "classpath:spring-rabbitmq-priority.xml")
public class ConsumerPriorityTest {
    @Test
    public void start() {
        while (true) {

        }
    }
}

在这里插入图片描述

9.7. 惰性队列

9.7.1. 使用场景

​ RabbitMQ 从 3.6.0 版本开始引入了惰性队列的概念。惰性队列会尽可能的将消息存入磁盘中,而在消费者消费到相应的消息时才会被加载到内存中,它的一个重要的设计目标是能够支持更长的队列,即支持更多的消息存储。当消费者由于各种各样的原因(比如消费者下线、宕机亦或者是由于维护而关闭等)而致使长时间内不能消费消息造成堆积时,惰性队列就很有必要了。

​ 默认情况下,当生产者将消息发送到 RabbitMQ 的时候,队列中的消息会尽可能的存储在内存之中,这样可以更加快速的将消息发送给消费者。即使是持久化的消息,在被写入磁盘的同时也会在内存中驻留一份备份。当 RabbitMQ 需要释放内存的时候,会将内存中的消息换页至磁盘中,这个操作会耗费较长的时间,也会阻塞队列的操作,进而无法接收新的消息。虽然 RabbitMQ 的开发者们一直在升级相关的算法,但是效果始终不太理想,尤其是在消息量特别大的时候。

在这里插入图片描述

9.7.2. 两种模式

队列具备两种模式: default 和 lazy。默认的为 default 模式,在 3.6.0 之前的版本无需做任何变更。 lazy模式即为惰性队列的模式,可以通过调用 channel.queueDeclare 方法的时候在参数中设置,也可以通过Policy 的方式设置,如果一个队列同时使用这两种方式设置的话,那么 Policy 的方式具备更高的优先级。

如果要通过声明的方式改变已有队列的模式的话,那么只能先删除队列,然后再重新声明一个新的。在队列声明的时候可以通过x-queue-mode参数来设置队列的模式,取值为defaultlazy。下面示例中演示了一个惰性队列的声明细节:

Map<String, Object> args = new HashMap<String, Object>();
args.put("x-queue-mode", "lazy");
channel.queueDeclare("myqueue", false, false, false, args);   

在这里插入图片描述

9.7.3. 内存开销对比

在这里插入图片描述

在发送 1 百万条消息,每条消息大概占 1KB 的情况下,普通队列占用内存是 1.2GB,而惰性队列仅仅占用 1.5MB

10. RabbitMQ集成微服务

10.1. RabbitMQ与SpringBoot整合

10.1.1. 创建父工程

项目名字 :springboot-rabbitmq-parent

在这里插入图片描述

添加依赖

修改pom.xml文件内容为如下:

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>

    <groupId>com.crazer</groupId>
    <artifactId>springboot-rabbitmq-parent</artifactId>
    <packaging>pom</packaging>
    <version>1.0-SNAPSHOT</version>
    <modules>
        <module>springboot-rabbitmq-producer</module>
        <module>springboot-rabbitmq-consumer</module>
    </modules>

    <properties>
        <maven.compiler.source>8</maven.compiler.source>
        <maven.compiler.target>8</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>2.7.4</version>
    </parent>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-amqp</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
</project>

10.1.2. 搭建生产者工程

实现步骤:

  1. 创建生产者SpringBoot工程
  2. 引入依赖坐标
  3. 编写yml配置,基本信息配置
  4. 定义交换机,队列以及绑定关系的配置类
  5. 注入RabbitTemplate,调用方法,完成消息发送
10.1.2.1. 创建工程

项目名字:springboot-rabbitmq-producer

10.1.2.2 添加依赖

这里父工程已经配置了,无须再修改pom.xml文件

10.1.2.3 配置整合

创建application.yml连接参数等配置文件;

spring:
  rabbitmq:
    host: 192.168.187.128
    port: 5672
    virtual-host: /
    username: admin
    password: 123456
10.1.2.4. 创建配置类
package com.crazer.config;

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

/**
 * @Author: Crazer
 * @Date: 2022/10/15 13:47
 * @version: 1.0.0
 * @Description: RabbitMQ配置类
 */
@Configuration
public class RabbitMQConfig {
    public static final String EXCHANGE_NAME = "boot_direct_exchange";
    public static final String QUEUE_NAME = "boot_queue";

    // 创建队列
    @Bean // 将方法的返回值存入Spring容器,唯-标识为方法名
    public Queue bootQueue() {
        // 创建一个持久化队列,队列名 boot_queue
        return QueueBuilder.durable(QUEUE_NAME).build();
    }

    // 创建交换机
    @Bean
    public Exchange bootExchange() {
        return ExchangeBuilder.directExchange(EXCHANGE_NAME).build();
    }

    // 3. 队列和交互机绑定关系 Binding
    /*
        1. 知道哪个队列
        2. 知道哪个交换机
        3. routing key
        noargs():表示不指定参数
     */
    @Bean // 方法有参数列表时,使用Autowired根据类型自动注入, @Qualifier指定唯一标识注入
    public Binding bindQueueExchange(@Qualifier("bootQueue") Queue queue,
                                     @Qualifier("bootExchange") Exchange exchange) {
        return BindingBuilder.bind(queue).to(exchange).with("info").noargs();
    }
}
10.1.2.5 创建入口类
package com.crazer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 14:37
 * @version: 1.0.0
 * @Description: 生产者
 */
@SpringBootApplication
public class ProducerApllication {
    public static void main(String[] args) {
        SpringApplication.run(ProducerApllication.class, args);
    }
}
10.1.2.6. 发送消息

创建测试文件 com.crazer.ProducerTest.java

package com.crazer;

import com.crazer.config.RabbitMQConfig;
import org.junit.jupiter.api.Test;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

/**
 * @Author: Crazer
 * @Date: 2022/10/15 13:52
 * @version: 1.0.0
 * @Description: 生产者测试类
 */
@SpringBootTest
public class ProducerTest {
    @Autowired
    private RabbitTemplate rabbitTemplate;

    @Test
    public void test() {
        String msg = "Hello, world!";
        rabbitTemplate.convertAndSend(RabbitMQConfig.EXCHANGE_NAME, "info", msg);
    }
}

运行测试类,发送消息

在这里插入图片描述

10.1.3. 搭建消费者工程

实现步骤

  1. 创建消费者SpringBoot工程
  2. 引入start,依赖坐标
  3. 编写yml配置,基本信息配置
  4. 定义监听类,使用@RabbitListener注解完成队列监听。
10.1.3.1. 创建工程

项目名字: springboot-rabbitmq-consumer

10.1.3.2 添加依赖

这里父工程已经配置了,无须再修改pom.xml文件

10.1.3.3. 配置整合

创建application.yml连接参数等配置文件;

spring:
  rabbitmq:
    host: 192.168.187.128
    port: 5672
    virtual-host: /
    username: admin
    password: 123456
10.1.3.4. 消息监听器

队列监听器

创建 com.atguigu.listener.RabbimtMQListener

package com.crazer.listener;

import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;

/**
 * @Author: Crazer
 * @Date: 2022/10/15 13:50
 * @version: 1.0.0
 * @Description: 队列监听器
 */
@Component
public class RabbimtMQListener {
    @RabbitListener(queues = "boot_queue")
    public void listenerQueue(Message message) {
        System.out.println("消息内容:" + new String(message.getBody()));
        System.out.println("交换机名称:" + message.getMessageProperties().getReceivedExchange());
        System.out.println("路由Key:" + message.getMessageProperties().getReceivedRoutingKey());
        System.out.println("队列名称:" + message.getMessageProperties().getConsumerQueue());
        System.out.println("消息序号:" + message.getMessageProperties().getDeliveryTag());
    }
}
10.1.3.5 创建入口类
package com.crazer;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

/**
 * @Author: Crazer
 * @Date: 2022/10/13 14:37
 * @version: 1.0.0
 * @Description: 消费者
 */
@SpringBootApplication
public class ConsumerApllication {
    public static void main(String[] args) {
        SpringApplication.run(ConsumerApllication.class, args);
    }
}
10.1.3.6. 运行消费者applicaion:

在这里插入图片描述

10.1.4. 小结:

  • SpringBoot提供了快速整合RabbitMQ的方式
  • 基本信息在yml中配置,队列交互机以及绑定关系在配置类中使用Bean的方式配置
  • 生产端直接注入RabbitTemplate完成消息发送
  • 消费端直接使用@RabbitListener完成消息接收

11. 消息百分百成功投递

谈到消息的可靠性投递,无法避免的,在实际的工作中会经常碰到,比如一些核心业务需要保障消息不丢失,接下来我们看一个可靠性投递的流程图,说明可靠性投递的概念:

在这里插入图片描述

Step 1: 首先把消息信息(业务数据)存储到数据库中,紧接着,我们再把这个消息记录也存储到一张消息记录表里(或者另外一个同源数据库的消息记录表)

Step 2:发送消息到MQ Broker节点(采用confirm方式发送,会有异步的返回结果)

Step 3、4:生产者端接受MQ Broker节点返回的Confirm确认消息结果,然后进行更新消息记录表里的消息状态。比如默认Status = 0 当收到消息确认成功后,更新为1即可!

Step 5:但是在消息确认这个过程中可能由于网络闪断、MQ Broker端异常等原因导致 回送消息失败或者异常。这个时候就需要发送方(生产者)对消息进行可靠性投递了,保障消息不丢失,100%的投递成功!(有一种极限情况是闪断,Broker返回的成功确认消息,但是生产端由于网络闪断没收到,这个时候重新投递可能会造成消息重复,需要消费端去做幂等处理)所以我们需要有一个定时任务,(比如每5分钟拉取一下处于中间状态的消息,当然这个消息可以设置一个超时时间,比如超过1分钟 Status = 0 ,也就说明了1分钟这个时间窗口内,我们的消息没有被确认,那么会被定时任务拉取出来)

Step 6:接下来我们把中间状态的消息进行重新投递 retry send,继续发送消息到MQ ,当然也可能有多种原因导致发送失败

Step 7:我们可以采用设置最大努力尝试次数,比如投递了3次,还是失败,那么我们可以将最终状态设置为Status = 2 ,最后 交由人工解决处理此类问题(或者把消息转储到失败表中)。

11.1. 数据库文件

-- ----------------------------
-- Table structure for broker_message_log
-- ----------------------------
DROP TABLE IF EXISTS `broker_message_log`;
CREATE TABLE `broker_message_log` (
  `message_id` varchar(255) NOT NULL COMMENT '消息唯一ID',
  `message` varchar(4000) NOT NULL COMMENT '消息内容',
  `try_count` int(4) DEFAULT '0' COMMENT '重试次数',
  `status` varchar(10) DEFAULT '' COMMENT '消息投递状态 0投递中,1投递成功,2投递失败',
  `next_retry` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP COMMENT '下一次重试时间',
  `create_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  `update_time` timestamp NOT NULL DEFAULT '0000-00-00 00:00:00' ON UPDATE CURRENT_TIMESTAMP,
  PRIMARY KEY (`message_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8;

-- ----------------------------
-- Table structure for t_order
-- ----------------------------
DROP TABLE IF EXISTS `t_order`;
CREATE TABLE `t_order` (
  `id` int(11) NOT NULL AUTO_INCREMENT,
  `name` varchar(255) DEFAULT NULL,
  `message_id` varchar(255) DEFAULT NULL,
  PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2018091102 DEFAULT CHARSET=utf8;

12. 幂等性

幂等性指一次和多次请求某一个资源,对于资源本身应该具有同样的结果。也就是说,其任意多次执行对资源本身所产生的影响均与一次执行的影响相同。

在这里插入图片描述

12.1. 概念

用户对于同一操作发起的一次请求或者多次请求的结果是一致的,不会因为多次点击而产生了副作用。举个最简单的例子,那就是支付,用户购买商品后支付,支付扣款成功,但是返回结果的时候网络异常,此时钱已经扣了,用户再次点击按钮,此时会进行第二次扣款,返回结果成功,用户查询余额发现多扣钱了,流水记录也变成了两条。在以前的单应用系统中,我们只需要把数据操作放入事务中即可,发生错误立即回滚,但是再响应客户端的时候也有可能出现网络中断或者异常等等

12.2. 消息重复消费

消费者在消费 MQ 中的消息时, MQ 已把消息发送给消费者,消费者在给 MQ 返回 ack 时网络中断,故 MQ 未收到确认信息,该条消息会重新发给其他的消费者,或者在网络重连后再次发送给该消费者,但实际上该消费者已成功消费了该条消息,造成消费者消费了重复的消息。

12.3. 解决思路

MQ 消费者的幂等性的解决一般使用全局 ID 或者写个唯一标识比如时间戳 或者 UUID 或者订单消费者消费 MQ 中的消息也可利用 MQ 的该 id 来判断,或者可按自己的规则生成一个全局唯一 id,每次消费消息时用该 id 先判断该消息是否已消费过。

12.4. 消费端的幂等性保障

在海量订单生成的业务高峰期,生产端有可能就会重复发生了消息,这时候消费端就要实现幂等性,这就意味着我们的消息永远不会被消费多次,即使我们收到了一样的消息。业界主流的幂等性有两种操作:a.唯一 ID+指纹码机制,利用数据库主键去重, b.利用 redis 的原子性去实现。

在MQ中指,消费多条相同的消息,得到与消费该消息一次相同的结果。

消息幂等性保障 乐观锁机制

生产者发送消息:

id=1,money=500,version=1

消费者接收消息

id=1,money=500,version=1
id=1,money=500,version=1

消费者需要保证幂等性:第一次执行SQL语句

第一次执行:version=1
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1

消费者需要保证幂等性:第二次执行SQL语句

第二次执行:version=2
update account set money = money - 500 , version = version + 1
where id = 1 and version = 1

12.5. 唯一 ID+指纹码机制

指纹码:我们的一些规则或者时间戳加别的服务给到的唯一信息码,它并不一定是我们系统生成的,基本都是由我们的业务规则拼接而来,但是一定要保证唯一性,然后就利用查询语句进行判断这个 id 是否存在数据库中,优势就是实现简单就一个拼接,然后查询判断是否重复;劣势就是在高并发时,如果是单个数据库就会有写入性能瓶颈当然也可以采用分库分表提升性能,但也不是我们最推荐的方式。

12.6. Redis 原子性

利用 redis 执行 setnx 命令,天然具有幂等性。从而实现不重复消费

13. RabbitMQ集群

摘要:实际生产应用中都会采用消息队列的集群方案,如果选择RabbitMQ那么有必要了解下它的集群方案原理

一般来说,如果只是为了学习RabbitMQ或者验证业务工程的正确性那么在本地环境或者测试环境上使用其单实例部署就可以了,但是出于MQ中间件本身的可靠性、并发性、吞吐量和消息堆积能力等问题的考虑,在生产环境上一般都会考虑使用RabbitMQ的集群方案。

13.1. 集群方案的原理

RabbitMQ这款消息队列中间件产品本身是基于Erlang编写,Erlang语言天生具备分布式特性(通过同步Erlang集群各节点的magic cookie来实现)。因此,RabbitMQ天然支持Clustering。这使得RabbitMQ本身不需要像ActiveMQ、Kafka那样通过ZooKeeper分别来实现HA方案和保存集群的元数据。集群是保证可靠性的一种方式,同时可以通过水平扩展以达到增加消息吞吐量能力的目的。

在这里插入图片描述

13.2. 单机多实例部署

由于某些因素的限制,有时候你不得不在一台机器上去搭建一个rabbitmq集群,这个有点类似zookeeper的单机版。真实生成环境还是要配成多机集群的。有关怎么配置多机集群的可以参考其他的资料,这里主要论述如何在单机中配置多个rabbitmq实例。

主要参考官方文档:https://www.rabbitmq.com/clustering.html

首先确保RabbitMQ运行没有问题

[root@localhost ~]# systemctl start rabbitmq-server.service
[root@localhost ~]# systemctl status rabbitmq-server.service

停止rabbitmq服务

[root@localhost ~]# systemctl stop rabbitmq-server.service

启动三个节点做集群演示:

由于web管理插件端口占用,所以还要指定其web插件占用的端口号。

[root@localhost ~]# RABBITMQ_NODE_PORT=5672 RABBITMQ_NODENAME=rabbit1 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15672}]"  rabbitmq-server -detached
[root@localhost ~]# RABBITMQ_NODE_PORT=5673 RABBITMQ_NODENAME=rabbit2 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15673}]"  rabbitmq-server -detached
[root@localhost ~]# RABBITMQ_NODE_PORT=5674 RABBITMQ_NODENAME=rabbit3 RABBITMQ_SERVER_START_ARGS="-rabbitmq_management listener [{port,15674}]"  rabbitmq-server -detached

启动三个节点后,分别访问三个节点,后台管理页面,看看是否OK.

停止服务命令(不要执行这三个命令,否则还得执行上面三个命令):

rabbitmqctl -n rabbit1 stop
rabbitmqctl -n rabbit2 stop
rabbitmqctl -n rabbit3 stop

rabbit1操作作为主节点:

[root@localhost ~]# rabbitmqctl -n rabbit1 stop_app  
Stopping node rabbit1@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit1 reset     
Resetting node rabbit1@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit1 start_app
Starting node rabbit1@localhost ...

rabbit2操作为从节点:

[root@localhost ~]# rabbitmqctl -n rabbit2 stop_app
Stopping node rabbit2@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit2 reset
Resetting node rabbit2@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit2 join_cluster rabbit1
Clustering node rabbit2@localhost with rabbit1 ...
[root@localhost ~]# rabbitmqctl -n rabbit2 start_app
Starting node rabbit2@localhost ...

rabbit3操作为从节点:

[root@localhost ~]# rabbitmqctl -n rabbit3 stop_app
Stopping node rabbit3@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit3 reset
Resetting node rabbit3@localhost ...
[root@localhost ~]# rabbitmqctl -n rabbit3 join_cluster rabbit1
Clustering node rabbit3@localhost with rabbit1@super ...
[root@localhost ~]# rabbitmqctl -n rabbit3 start_app
Starting node rabbit3@localhost ...

查看集群状态:

[root@localhost ~]# rabbitmqctl -n rabbit1 cluster_status
Cluster status of node rabbit1@localhost ...
Basics
 
Cluster name: rabbit1@localhost
 
Disk Nodes
 
rabbit1@localhost
rabbit2@localhost
rabbit3@localhost
 
Running Nodes
 
rabbit1@localhost
rabbit2@localhost
rabbit3@localhost
 
Versions
 
rabbit1@localhost: RabbitMQ 3.8.1 on Erlang 21.3.8.9
rabbit2@localhost: RabbitMQ 3.8.1 on Erlang 21.3.8.9
rabbit3@localhost: RabbitMQ 3.8.1 on Erlang 21.3.8.9
 
Alarms
 
(none)
 
Network Partitions
 
(none)
 
Listeners
 
Node: rabbit1@localhost, interface: [::], port: 25672, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit1@localhost, interface: [::], port: 5672, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit1@localhost, interface: [::], port: 15672, protocol: http, purpose: HTTP API
Node: rabbit2@localhost, interface: [::], port: 25673, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit2@localhost, interface: [::], port: 5673, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit2@localhost, interface: [::], port: 15673, protocol: http, purpose: HTTP API
Node: rabbit3@localhost, interface: [::], port: 25674, protocol: clustering, purpose: inter-node and CLI tool communication
Node: rabbit3@localhost, interface: [::], port: 5674, protocol: amqp, purpose: AMQP 0-9-1 and AMQP 1.0
Node: rabbit3@localhost, interface: [::], port: 15674, protocol: http, purpose: HTTP API
 
Feature flags
 
Flag: drop_unroutable_metric, state: enabled
Flag: empty_basic_get_metric, state: enabled
Flag: implicit_default_bindings, state: enabled
Flag: quorum_queue, state: enabled
Flag: virtual_host_metadata, state: enabled

web监控:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-40p2B9Qk-1666789504887)(img/45.png)]

rabbitmqctl -n rabbit1 add_user admin admin
rabbitmqctl -n rabbit1 set_user_tags admin administrator
rabbitmqctl -n rabbit1 change_password admin 123456

13.3. 集群管理

rabbitmqctl join_cluster {cluster_node} [–ram]

将节点加入指定集群中。在这个命令执行前需要停止RabbitMQ应用并重置节点。

rabbitmqctl cluster_status

显示集群的状态。

rabbitmqctl change_cluster_node_type {disc|ram}

修改集群节点的类型。在这个命令执行前需要停止RabbitMQ应用。

rabbitmqctl forget_cluster_node [–offline]

将节点从集群中删除,允许离线执行。

rabbitmqctl update_cluster_nodes {clusternode}

在集群中的节点应用启动前咨询clusternode节点的最新信息,并更新相应的集群信息。这个和join_cluster不同,它不加入集群。考虑这样一种情况,节点A和节点B都在集群中,当节点A离线了,节点C又和节点B组成了一个集群,然后节点B又离开了集群,当A醒来的时候,它会尝试联系节点B,但是这样会失败,因为节点B已经不在集群中了。rabbitmqctl update_cluster_nodes -n A C可以解决这种场景。

rabbitmqctl cancel_sync_queue [-p vhost] {queue}

取消队列queue同步镜像的操作。

rabbitmqctl set_cluster_name {name}

设置集群名称。集群名称在客户端连接时会通报给客户端。Federation和Shovel插件也会有用到集群名称的地方。集群名称默认是集群中第一个节点的名称,通过这个命令可以重新设置。

13.4. RabbitMQ镜像集群配置

上面已经完成RabbitMQ默认集群模式,但并不保证队列的高可用性,尽管交换机、绑定这些可以复制到集群里的任何一个节点,但是队列内容不会复制。虽然该模式解决一项目组节点压力,但队列节点宕机直接导致该队列无法应用,只能等待重启,所以要想在队列节点宕机或故障也能正常应用,就要复制队列内容到集群里的每个节点,必须要创建镜像队列。

镜像队列是基于普通的集群模式的,然后再添加一些策略,所以你还是得先配置普通集群,然后才能设置镜像队列,我们就以上面的集群接着做。

设置的镜像队列可以通过开启的网页的管理端Admin->Policies,也可以通过命令。

在这里插入图片描述

  • Name:策略名称
  • Pattern:匹配的规则,如果是匹配所有的队列,是^
  • Definition:使用ha-mode模式中的all,也就是同步所有匹配的队列。问号链接帮助文档。

在这里插入图片描述

13.5. 负载均衡-HAProxy

HAProxy提供高可用性、负载均衡以及基于TCP和HTTP应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括Twitter,Reddit,StackOverflow,GitHub在内的多家知名互联网公司在使用。HAProxy实现了一种事件驱动、单一进程模型,此模型支持非常大的并发连接数。

https://www.haproxy.org/

在这里插入图片描述

13.5.1. 安装HAProxy

//下载依赖包
yum install gcc vim wget
//上传haproxy源码包; -C解压到指定的目录
tar -zxvf haproxy-2.3.14.tar.gz -C /usr/local
//进入目录、进行编译、安装
cd /usr/local/haproxy-2.3.14
// make 表示编译;TARGET=linux31 表示CentOS7系统;PREFIX=/usr/local/haproxy指定安装路径
// TARGET=linux310,内核版本,使用uname -r查看内核,如:3.10.0-514.el7,此时该参数就为linux310;
make TARGET=linux310 PREFIX=/usr/local/haproxy
make install PREFIX=/usr/local/haproxy
mkdir /etc/haproxy
//添加用户组:-r 创建一个系统组;-g 组ID
groupadd -r -g 149 haproxy
//添加用户:-g 新账户组的名称;-r 创建一个系统用户;-s 新用户的登录shell; -u 新账户的用户ID
useradd -g haproxy -r -s /sbin/nologin -u 149 haproxy
//创建haproxy配置文件
vim /etc/haproxy/haproxy.cfg

13.5.2. 配置HAProxy

配置文件路径:/etc/haproxy/haproxy.cfg

#全局配置
global
    #日志输出配置,所有日志都记录在本机,通过local0输出
    log 127.0.0.1 local0 info
    #最大连接数
    maxconn 5120
    #改变当前的工作目录
    chroot /usr/local/haproxy
    #以指定的UID运行haproxy进程
    uid 99
    #以指定的GID运行haproxy进程
    gid 99
    #以守护进程方式运行haproxy
    daemon
    quiet
    nbproc 20
    #当前进程PID文件
    pidfile /var/run/haproxy.pid
#默认配置
defaults
    #应用全局的日志配置
    log global
    #默认的模式mode{tcp|http|health}
    mode tcp
    #日志类别
    option tcplog
    #不记录检查检查日志信息
    option dontlognull
    #3次失败则认为服务不可用
    retries 3
    option redispatch
    #每个进程可用的最大连接数
    maxconn 2000   
#绑定配置
listen rabbitmq_cluster
    bind *:5677
    #配置TCP模式
    mode tcp
    #balance url_param userid
    #balance url_param session_id check_post 64
    #balance hdr(User-Agent)
    #balance hdr(host)
    #balance hdr(Host) use_domain_only
    #balance rdp-cookie
    #balance leastconn
    #balance source //ip
    #简单的轮询
    balance roundrobin
    #server rabbit1 定义服务内部标识,
    #127.0.0.1:5672 服务连接IP和端口,
    #check inter 5000 定义每隔多少毫秒检查服务是否可用,
    #rise 2 服务故障后需要多少次检查才能被再次确认可用,
    #fall 2 经历多次失败的检查检查后,haproxy才会停止使用此服务
    #weight 1 定义服务权重
    server rabbit1 192.168.6.100:5672 check inter 5000 rise 2 fall 2 weight 1
    server rabbit2 192.168.6.100:5673 check inter 5000 rise 2 fall 2 weight 1
    server rabbit3 192.168.6.100:5674 check inter 5000 rise 2 fall 2 weight 1
#haproxy监控页面地址
listen stats
    bind 192.168.6.100:8100
    mode http
    option httplog
    stats enable
    stats uri /rabbitmq-stats
    stats refresh 5s

启动HAproxy负载

/usr/local/haproxy/sbin/haproxy -f /etc/haproxy/haproxy.cfg

//查看haproxy进程状态

ps -ef | grep haproxy  

访问如下地址对mq节点进行监控

springboot yml文件中访问mq集群地址:

spring: 
  rabbitmq: 
    host: 192.168.6.100
    port: 5677
    username: admin
    password: 123456
virtual-host: /
#addresses: 192.168.6.100:5672,192.168.6.100:5673,192.168.6.100:5674

在这里插入图片描述

14. Haproxy+Keepalive 实现高可用负载均衡

14.1. 整体架构图

在这里插入图片描述

14.2. Haproxy 实现负载均衡

HAProxy 提供高可用性、负载均衡及基于 TCPHTTP 应用的代理,支持虚拟主机,它是免费、快速并且可靠的一种解决方案,包括 Twitter,Reddit,StackOverflow,GitHub 在内的多家知名互联网公司在使用。HAProxy 实现了一种事件驱动、单一进程模型,此模型支持非常大的井发连接数。

扩展 nginx,lvs,haproxy 之间的区别: http://www.ha97.com/5646.html

14.2.1. 搭建步骤

1.下载 haproxy(在 node1 和 node2)

yum -y install haproxy

2.修改 node1 和 node2 的

haproxy.cfgvim /etc/haproxy/haproxy.cfg
需要修改红色 IP 为当前机器 IP

在这里插入图片描述

3.在两台节点启动 haproxy

haproxy -f /etc/haproxy/haproxy.cfg
ps -ef | grep haproxy

4.访问地址

http://10.211.55.71:8888/stats

14.3. Keepalived 实现双机(主备)热备

试想如果前面配置的 HAProxy 主机突然宕机或者网卡失效,那么虽然 RbbitMQ 集群没有任何故障但是对于外界的客户端来说所有的连接都会被断开结果将是灾难性的为了确保负载均衡服务的可靠性同样显得十分重要,这里就要引入 Keepalived 它能够通过自身健康检查、资源接管功能做高可用(双机热备),实现故障转移。

14.3.1. 搭建步骤

1.下载 keepalived

yum -y install keepalived

2.节点 node1 配置文件

vim /etc/keepalived/keepalived.conf
把资料里面的 keepalived.conf 修改之后替换

3.节点 node2 配置文件

需要修改 global_defs 的 router_id,如:nodeB
其次要修改 vrrp_instance_VI 中 state 为"BACKUP";
最后要将 priority 设置为小于 100 的值

4.添加 haproxy_chk.sh

(为了防止 HAProxy 服务挂掉之后 Keepalived 还在正常工作而没有切换到 Backup 上,所以这里需要编写一个脚本来检测 HAProxy 务的状态,当 HAProxy 服务挂掉之后该脚本会自动重启
HAProxy 的服务,如果不成功则关闭 Keepalived 服务,这样便可以切换到 Backup 继续工作)
vim /etc/keepalived/haproxy_chk.sh(可以直接上传文件)
修改权限 chmod 777 /etc/keepalived/haproxy_chk.sh

5.启动 keepalive 命令(node1 和 node2 启动)

systemctl start keepalived

6.观察 Keepalived 的日志

tail -f /var/log/messages -n 200

7.观察最新添加的 vip

ip add show

8.node1 模拟 keepalived 关闭状态

systemctl stop keepalived

9.使用 vip 地址来访问 rabbitmq 集群

14.4. Federation Exchange

14.4.1. 使用它的原因

​ (broker 北京), (broker 深圳)彼此之间相距甚远,网络延迟是一个不得不面对的问题。有一个在北京的业务(Client 北京) 需要连接(broker 北京),向其中的交换器 exchangeA 发送消息,此时的网络延迟很小,(Client 北京)可以迅速将消息发送至 exchangeA 中,就算在开启了 publisherconfirm 机制或者事务机制的情况下,也可以迅速收到确认信息。此时又有个在深圳的业务(Client 深圳)需要向 exchangeA 发送消息,那么(Client 深圳) (broker 北京)之间有很大的网络延迟, (Client 深圳) 将发送消息至 exchangeA 会经历一定的延迟,尤其是在开启了 publisherconfirm 机制或者事务机制的情况下, (Client 深圳) 会等待很长的延迟时间来接收(broker 北京)的确认信息,进而必然造成这条发送线程的性能降低,甚至造成一定程度上的阻塞。

​ 将业务(Client 深圳)部署到北京的机房可以解决这个问题,但是如果(Client 深圳)调用的另些服务都部署在深圳,那么又会引发新的时延问题,总不见得将所有业务全部部署在一个机房,那么容灾又何以实现?这里使用 Federation 插件就可以很好地解决这个问题.

在这里插入图片描述

14.4.2. 搭建步骤

1.需要保证每台节点单独运行

2.在每台机器上开启 federation 相关插件

rabbitmq-plugins enable rabbitmq_federation
rabbitmq-plugins enable rabbitmq_federation_management

在这里插入图片描述

3.原理图(先运行 consumer 在 node2 创建 fed_exchange)

在这里插入图片描述

4.在 downstream(node2)配置 upstream(node1)

在这里插入图片描述

5.添加 policy

在这里插入图片描述

6.成功的前提

在这里插入图片描述

14.5. Federation Queue

14.5.1. 使用它的原因

联邦队列可以在多个 Broker 节点(或者集群)之间为单个队列提供均衡负载的功能。一个联邦队列可以连接一个或者多个上游队列(upstream queue),并从这些上游队列中获取消息以满足本地消费者消费消息的需求。

14.5.2. 搭建步骤

1.原理图

在这里插入图片描述

2.添加 upstream(同上)

3.添加 policy

在这里插入图片描述

14.6. Shovel

14.6.1. 使用它的原因

Federation 具备的数据转发功能类似, Shovel 够可靠、持续地从一个 Broker 中的队列(作为源端,即source)拉取数据并转发至另一个 Broker 中的交换器(作为目的端,即 destination)。作为源端的队列和作为目的端的交换器可以同时位于同一个 Broker,也可以位于不同的 Broker 上。 Shovel 可以翻译为"铲子",是一种比较形象的比喻,这个"铲子"可以将消息从一方"铲子"另一方。 Shovel 行为就像优秀的客户端应用程序能够负责连接源和目的地、负责消息的读写及负责连接失败问题的处理。

14.6.2. 搭建步骤

1.开启插件(需要的机器都开启)

rabbitmq-plugins enable rabbitmq_shovel 
rabbitmq-plugins enable rabbitmq_shovel_management

在这里插入图片描述

2.原理图(在源头发送的消息直接回进入到目的地队列)

在这里插入图片描述

3.添加 shovel 源和目的地

在这里插入图片描述

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值