一、rabbitmq的使用场景

1、高并发的流量削峰

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

2、应用解耦

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

3、异步处理

有些服务间调用是异步的,例如 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 服务还能及时的得到异步处理成功的消息。

4、分布式事务

以订单服务为例,传统的方式为单体应用,支付、修改订单状态、创建物流订单三个步骤集成在一个服务中,因此这三个步骤可以放在一个jdbc事务中,要么全成功,要么全失败。而在微服务的环境下,会将三个步骤拆分成三个服务,例如:支付服务,订单服务,物流服务。三者各司其职,相互之间进行服务间调用,但这会带来分布式事务的问题,因为三个步骤操作的不是同一个数据库,导致无法使用jdbc事务管理以达到一致性。而 MQ 能够很好的帮我们解决分布式事务的问题,有一个比较容易理解的方案,就是二次提交。基于MQ的特点,MQ作为二次提交的中间节点,负责存储请求数据,在失败的情况可以进行多次尝试,或者基于MQ中的队列数据进行回滚操作,是一个既能保证性能,又能保证业务一致性的方案,如下图所示:

5、数据分发

MQ 具有发布订阅机制,不仅仅是简单的上游和下游一对一的关系,还有支持一对多或者广播的模式,并且都可以根据规则选择分发的对象。这样一份上游数据,众多下游系统中,可以根据规则选择是否接收这些数据,能达到很高的拓展性。

二、rabbitmq环境搭建

1、工具准备

RabbitMQ是由erlang语言开发,所以安装环境需要安装 erlang

  • erlang-21.3.8.21-1.el7.x86_64.rpm erlang环境

  • rabbitmq-server-3.8.8-1.el7.noarch.rpm rabbit安装

2、环境搭建

本人使用的是 阿里云服务器 没有的话也可以使用虚拟机… 事先使用连接工具上传了文件

本人喜欢把工具都安装在 /usr/wsm 目录下:

解压安装:

ok ,安装完毕了解一些 RabbitMQ 命令:

  # 启动服务
systemctl start rabbitmq-server # 查看服务状态
systemctl status rabbitmq-server # 开机自启动
systemctl enable rabbitmq-server # 停止服务
systemctl stop rabbitmq-server # 重启服务
systemctl restart rabbitmq-server

注意:这里只是把RabbitMQ 服务给搭建好了,为了方便操作我们还需要安装一个web控制面板

  # 安装web控制面板
rabbitmq-plugins enable rabbitmq_management # 安装完毕以后,重启服务即可
systemctl restart rabbitmq-server # 访问 http://服务器ip:15672 ,用默认账号密码(guest)登录,出现权限问题
# 默认情况只能在 localhost 本机下访问,所以需要添加一个远程登录的用户
# 创建账号和密码: admin 123456
rabbitmqctl add_user admin 123456 # 设置用户角色,用户级别: administrator monitoring policymaker managment
rabbitmqctl set_user_tags admin administrator # 为用户添加资源权限
# set_permissions [-p <vhostpath>] <user> <conf> <write> <read> # 添加配置、写、读权限
rabbitmqctl set_permissions -p "/" admin ".*" ".*" ".*" ##### 扩展一些命令:#####
关闭应用的命令为: rabbitmqctl stop_app
清除的命令为: rabbitmqctl reset
重新启动命令为: rabbitmqctl start_app

如果是阿里云的服务器 别忘记开启端口 还有 关闭防火墙~

主要端口介绍:阿里云建议将这些都打开~

1、4369 – erlang发现口

2、5672 – client端通信口

3、15672 – 管理界面ui端口

4、25672 – server间内部通信口

三、访问 rabbitmq 管理页面

  • Overview:概览 RabbitMQ 的整体情况,也可以查看集群各个节点的信息 情况 MQ 各个端口映射信息

  • Connection:【连接】 生产者--消费者 和 RabbitMQ服务器之间建立的TCP连接

  • Channel:【信道】 是TCP里面的虚拟连接。例如:Connection相当于电缆,Channel相当于独立光纤束,一条TCP连接中可以创建多条信道,增加连接效率。无论是发布消息、接收消息、订阅队列都是通过信道完成的

  • Exchanage:【交换机】 用来接收生产者发送的消息,并根据分发规则,将这些消息分发给服务器中的队列中。不同的交换机有不同的分发规则

  • Queue:【消息队列】 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。消息一直在队列里面,等待消费者链接到这个 队列将其取走。

  • Admin:这里管理着,MQ 所有的操作用户~

1、Overview

2、Connections

  • Name: 连接名 点击连接名, 还可以查看详细的信息~

  • User name: 当前连接登录MQ 的用户

  • State: 当前连接的状态,running 运行 idle 空闲

  • SSL|TLS: 是否使用的是 SSL 或 TLS协议

  • Peotocol: AMQP 0-9-1 指的是AMQP 的协议版本号

  • Channels: 当前连接创建通道的 通道总数

  • From client: 每秒发出的消息数

  • To client: 每秒接收的消息数

3、Channels【信道】

记录各个连接【Connections】的信道:一个连接【Connections】 可以有多个信道【Channels】 多个通道通过多线程实现,不相互干扰 我们在 信道中创建:队列 交换机 ...

生产者的通道一般使用完之后会立马关闭,消费者是一直监听的…

  • Channel: 通道名称

  • User Name: 该通道,创建者 用户名

  • Model: 通道的确认模式 C confirm模式 T 表示事务

  • State: 通道当前的状态 running 运行 idie 空闲

  • Unconfirmed: 待确认的消息数

  • Prefetch: 预先载入

Prefetch 表示每个消费者最大的能承受的未确认消息数目,简单来说就是用来指定一个消费者一次可以从 RabbitMQ 中获取多少条消息并缓存在消费者中,

一旦消费者的缓冲区满了,RabbitMQ 将会停止投递新的消息到该消费者中直到它发出有消息被 ack 了

消费者负责不断处理消息,不断 ack,然后只要 UnAcked 数少于 Prefetch * consumer 数目,RabbitMQ 就不断将消息投递过去

  • Unacker: 待 ack 的消息数

  • publish: 消息生产者发送消息的 速率

  • confirm: 消息生产者确认消息的 速率

  • unroutable: drop 表示消息,未被接收,且已经删除的消息

  • deliver / get 消息消费者获取消息的 速率

  • ack: 消息消费者 ack 消息的速率. MQ 的 ACK机制:100%消息消费!

4、Exchange

  • Virtual Host:

    • 表示 这个交换机属于哪个虚拟目录。一个虚拟目录下 exchange的名字不能重复。

    • 而在不同虚拟目录下可以有同名的exchange交换机。 类似于不同的文件夹概念。

  • Name:交换机名字

  • Type:交换机的类型,常见的有fanout、direct、topic、headers这四种

Direct(直接交换机)

路由键与队列名完全匹配交换机,此类类型交换机,通过RoutingKey路由键将交换机和队列进行绑定,消息被发送到Exchange时需要根据消息的RoutingKey进行匹配,将消息发送到完全匹配到此RoutingKey的队列

Fanout(广播交换机)

此种交换机,会将消息分发给所有绑定了此交换机的队列,此时RoutingKey参数无效,无需依赖路由键

Topic(主题交换机,又称为通配符交换机)

Topic,主题类型交换机,此种交换机与Direct类似,也需要通过RountingKey路由键进行匹配分发,区别在于Topic可以进行模糊匹配,Direct是精准匹配

   1、Topic,将RoutingKey通过"."来分为多个部分

   2、"*":代表一个部分

   3、"#":代表0个或多个部分(如果绑定的路由键为"#"时,则接受所有消息,因为路由键所有都匹配)

Headers(头交换机)

header匹配AMQP消息的header而不是RoutingKey路由键,此外header交换机和direct交换机完全一致,但性能差很多,目前几乎用不到

消费方指定的headers中必须包含一个"x-match"的键

键"x-match"的值有2个

  x-match: all:表示所有的键值对都匹配才能接收到消息

  x-match: any:表示只要有键值对匹配就能接收到消息

  • Durability: 持久化特性 (transient 临时的, durable 持久化的), 默认为持久化

  • Auto Delete : 是否自动删除, 默认为false

    • true:当没有消费者连接时,队列会被删除,期间生产者发送到队列的消息会丢失

    • false:没有消费者连接时,队列不会被删除,期间生产者发送到队列的消息不会丢失

  • internal:设置是否是RabbitMQ内部使用,默认false。如果设置为 true ,则表示是内置的交换器,客户端程序无法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种方式

  • Arguments :表示其他自定义参数。

5、Queue

  • Name: 表示消息队列的名称

  • Type: 消息队列的类型…

  • Features:表示消息队列的特性,D 表示消息队列持久化

  • State:表示当前队列的状态,running 表示运行中;idle 表示空闲

  • Ready:表示待消费的消息总数

  • Unacked:表示待应答的消息总数

  • Total:表示消息总数 Ready+Unacked

  • incoming:表示消息进入的速率

  • deliver/get:表示获取消息的速率

  • ack:表示消息应答的速

Add a new queue

  • virtual host:虚拟目录

  • Name:队列名称

  • Durability: 持久化特性 (transient 临时的, durable 持久化的)

  • Auto Delete : 是否自动删除

    • true:当没有消费者连接时,队列会被删除,期间生产者发送到队列的消息会丢失

    • false:没有消费者连接时,队列不会被删除,期间生产者发送到队列的消息不会丢失

  • arguments: 其他参数

      Message-ttl
      消息的最大存活时间,单位毫秒, 当超过时间后消息会被丢弃
    默认消息存活时间为永久存在 Auto-expires
      队列过期时间,当auto delete设置为true时,才会生效 Max-length
    队列存放最大就绪消息数量,超过限制时,从头部丢弃消息
    默认最大数量限制与操作系统有关。 Max-length-bytes
    队列存放的所有消息总大小,超过限制时,从头部丢弃消息 Overflow-behaviour
    消息超出最大数量时,溢出行为: drop-head 或 reject-publish
    (drop-head:头部丢弃, reject-publish拒绝生产者发布消息) Dead-letter-exchange
    当队列满时,被拒绝的消息,或者消息过期时,将被重新发布到死信交换机上。 Dead-letter-routing-key
    消息被发布到死信交换机时,如果没设置这个路由键,则将使用消息的原始路由键, 比如,消息发送到
    exchange=contract.exchange 路由键 routeKey = contract.info
    当该队列满时,如果 x-dead-letter-exchange=contract.dead.exchange
    没有指定x-dead-letter-routing-key时,会将消息发送到队列为
    exchange=contract.dead.exchange
    routeKey = contract.info 消息的原始路由键即时,消息原本要发送到的队列绑定路由键名 Max-priority
    列支持的消息最大优先级数,没设置时,队列不支持消息优先级 Lazy-mode
    将队列设置为惰性模式,将消息保存在磁盘上,如果没设置,将保存内存缓存上以尽快传递消息 Master-locator
    将队列设置为 master 位置模式,确定队列 master 在节点集群上声明时的定位规则。

6、Admin

添加用户

上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

  • 超级管理员(administrator):可登陆管理控制台,可查看所有的信息,并且可以对用户,策略(policy)进行操作。

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

  • 策略制定者(policymaker):可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息(上图红框标识的部分)。

  • 普通管理者(management):仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。

  • 其他:无法登陆管理控制台,通常就是普通的生产者和消费者。

创建虚拟主机(Virtual Hosts)

创建好虚拟主机,我们还要给用户添加访问权限:

四、rabbitmq 工作原理

  • Producer:【消息的生产者】 一个向交换机发布消息的客户端应用程序。

  • Connection: 【连接】 生产者-消费者 和 RabbitMQ服务器之间建立的TCP连接。

  • Channel:【信道】 是TCP里面的虚拟连接。例如:Connection相当于电缆,Channel相当于独立光纤束,一条TCP连接中可以创建多条信道,增加连接效率。无论是发布消息、接收消息、订阅队列都是通过信道完成的。

  • Broker: 消息队列服务器实体。即RabbitMQ服务器

  • Virtual Host:【虚拟主机】 出于多租户和安全因素设计的,把AMQP的基本组件划分到一个虚拟的分组中。每个vhost本质上就是一个mini版的RabbitMQ服务器,拥有自己的队列、交换机、绑定和权限机制。当多个不同的用户使用同一个RabbitMQ服务器时,可以划分出多个虚拟主机。RabbitMQ默认的虚拟主机路径是 /

  • Exchange:【交换机】 用来接收生产者发送的消息,并根据分发规则,将这些消息分发给服务器中的队列中。不同的交换机有不同的分发规则。

  • Queue:【消息队列】 用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。消息一直在队列里面,等待消费者链接到这个 队列将其取走。

  • Binding:【绑定】 消息队列和交换机之间的虚拟连接,绑定中包含路由规则,绑定信息保存到交换机的路由表中,作为消息的分发依据。

  • Consumer:【消息的消费者】 表示一个从消息队列中取得消息的客户端应用程序。

1、生产者发送消息流程:

1、生产者和Broker建立 Connection 连接。

2、生产者和Broker建立通道。

3、生产者通过通道消息发送给Broker,由Exchange将消息进行转发。

4、Exchange将消息转发到指定的Queue(队列)

2、消费者接收消息流程::

1、消费者和Broker建立 Connection 连接

2、消费者和Broker建立通道

3、消费者监听指定的Queue(队列)

4、当有消息到达Queue时Broker默认将消息推送给消费者。

5、消费者接收到消息。

6、ack回复

五、rabbitMQ五种消息模型

RabbitMQ 作为功能丰富的消息中间件,支持多种消息传递模式,以下是其主要支持的消息模式及详细说明:

1、简单模式(Simple)

结合示意图案例分析: RabbitMQ是一个消息代理,它接受和转发消息。我们可以把它抽象成一个货运仓库,当商家把商品打包放进仓库后,可以确定快递员最后一定会把快递送到收件人手里。

示意图解释:

  • P: 生产者, 一个发送消息的用户应用程序。

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

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

  • Routing Key: 等于Queue队列名

  • 不需要设置交换机(使用默认的交换机【AMQP default】 且 交换机类型为direct)

简单模式特点:

  • 1、一个生产者对应一个消费者,通过队列进行消息传递。

  • 2、使用默认的交换机 且 交换机类型为direct

  • 3、单个生产者、单个队列、单个消费者

代码示例:

channel.queue_declare(queue='simple_queue')
channel.basic_publish(exchange='', routing_key='simple_queue', body='消息内容')

生产者发送消息 Producer

  public class Producer {
// 定义队列名称
private final static String QUEUE_NAME = "simple_queue"; public static void main(String[] args) throws IOException, TimeoutException { // 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、建立信道
Channel channel = connection.createChannel(); // 3、创建队列,如果队列存在,则使用该队列, 声明一个队列是幂等的 且只有当队列不存在时才会被创建
/**
* 参数1:队列名
* 参数2:是否持久化,true表示MQ重启后队列还在。
* 参数3:是否私有化,false表示所有消费者都可以访问,true表示只有第一次拥有它的消费者才能访问
* 参数4:是否自动删除,true表示不再使用队列时自动删除队列(当没有消费者时,就自动删除)
* 参数5:其他额外参数
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null); // 4、发送消息
String message = "Hello RabbitMQ"; /**
* 参数1:交换机名,""表示默认交换机
* 参数2:路由键,简单模式就是队列名
* 参数3:其他额外参数
* 参数4:要传递的消息字节数组
*/
channel.basicPublish("",QUEUE_NAME, null, message.getBytes()); // 5、关闭信道和连接
channel.close();
connection.close();
System.out.println("===消息发送成功===");
}
}

消费者消费消息 Consumer Producer

  public class Consumer {
public static void main(String[] args) throws IOException, TimeoutException { // 1、建立连接工厂
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、建立信道
Channel channel = connection.createChannel(); // 3、监听队列
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8"); /*
回调方法:当收到消息时,会自动执行该方法
consumerTag: 标识
envelope: 获取一些信息,如:交换机、路由key
properties: 配置信息
*/
System.out.println("consumerTag:"+consumerTag);
System.out.println("envelope:"+envelope);
System.out.println("RoutingKey:"+envelope.getRoutingKey());
System.out.println("properties:"+properties);
System.out.println("接受消息为:" + message);
}
}; /**
* 参数1:监听的队列名
* 参数2:是否自动签收,如果设置为false,则需要手动确认消息已收到,否则MQ会一直发送消息
* 参数3:Consumer的实现类,重写该类方法表示接受到消息后如何消费
*/
channel.basicConsume("simple_queue", true, consumer);
}
}

2、工作队列模式(Work Queue)

示意图解释:

  • P:生产者, 一个发送消息的用户应用程序。

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

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

  • 不需要设置交换机(使用默认的交换机 且 交换机类型为direct)

工作队列模式特点:

  • 一对多消息分发

  • 单个生产者、单个队列、多个消费者

  • 用于任务分发和负载均衡

生产者发送消息 Producer

  public class Producer {
// 定义队列名称
private final static String QUEUE_NAME = "work_queue"; public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、创建信道
Channel channel = connection.createChannel(); // 3、创建队列、并持久化
channel.queueDeclare("work_queue", true, false, false, null); // 4、发送大量消息
/**
* 参数3:表示该消息为持久化消息,即除了保存到内存还会保存到磁盘中
*/
for (int i = 0; i < 30; i++) {
channel.basicPublish("", "work_queue", MessageProperties.PERSISTENT_TEXT_PLAIN,
("你好,你有新快递编号为:"+i).getBytes());
} // 6、关闭资源
channel.close();
connection.close();
}
}

消费者消费消息 Consumer

Consumer 创建三个消费者:ConsumerOne、ConsumerTwo、ConsumerThree 因为三个消费者的代码大致相同,这里只贴ConsumerOne的代码,ConsumerTwo、ConsumerThree改下类文件和输出信息即可

public class ConsumerOne {
public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立连接工厂
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、建立信道
Channel channel = connection.createChannel(); // 3、监听队列处理消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("消费者One 消费消息:" + message);
}
}; channel.basicConsume("work_queue", true, consumer);
}
}

总结

1、工作队列模式 使用了MessageProperties.PERSISTENT_TEXT_PLAIN 来设置消息持久化,目的是为了保证数据安全可靠不丢失。但是,事与愿违。消息虽然被标记为持久化却并不能完全保证消息不会丢失。尽管MessageProperties.PERSISTENT_TEXT_PLAIN 告诉 RabbitMQ 将消息保存到磁盘,但是这里依然存在当消息刚准备存储在磁盘的时候;可能存在还没有存储完的情况,消息还在缓存的一个间隔点。此时并没有真正写入磁盘。因此持久性保证并不强,进而需要引入发布确认、ACK。本篇幅不做详解。

2、多个消费者进行消息消费,因为消息是轮询平均发送给消费者。可能会有某个消费者Slow;因为要处理其他复杂的业务逻辑,其消费的效率相对其他消费者比较慢,这个就会照成当其他消费者已经消费完处于空闲状态时,因平均分配原则,队列任会继续把消息发给 Slow 处于忙碌状态,大大降低了系统的性能。正确的做法的是“能劳者多劳;消费越快的,让其消费的越多”。本篇幅不做详解

3、发布订阅模式(Publish/Subscribe)

交换机有哪些类型:

1、Fanout:广播,将消息交给所有绑定到交换机的队列

2、Direct:定向,把消息交给符合指定routing key 的队列

3、Topic:通配符是最为常有用的一种,交换机把消息交给符合routing pattern(路由模式)的队列

发布订阅模式特点:

  • 一对多消息广播

  • 使用Fanout Exchange

  • 所有绑定队列都会收到相同消息

示意图解释:

  • P:生产者, 一个发送消息的用户应用程序。

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

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

  • Exchange:交换机(X),一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。发布订阅模式默认使用 fanout 类型交换机

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

发布订阅模式示例:

  channel.exchange_declare(exchange='logs', exchange_type='fanout')
channel.basic_publish(exchange='logs', routing_key='', body='广播消息')

生产者发送消息 Producer

  public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、创建信道
Channel channel = connection.createChannel(); // 3、创建交换机
/**
* 参数1:交换机名
* 参数2:交换机类型
* 参数3:交换机持久化
*/
channel.exchangeDeclare("exchange_fanout", BuiltinExchangeType.FANOUT,true); // 4、创建队列
// 短信
channel.queueDeclare("SEND_MESSAGE", true, false, false, null);
// 邮件
channel.queueDeclare("SEND_MAIL", true, false, false, null);
// 站内信
channel.queueDeclare("SEND_STATION", true, false, false, null); // 5、交换机绑定队列
/**
* 参数1:队列名
* 参数2:交换机名
* 参数3:路由关键字,发布订阅模式写 ""即可
*/
channel.queueBind("SEND_MAIL", "exchange_fanout", ""); channel.queueBind("SEND_MESSAGE", "exchange_fanout", ""); channel.queueBind("SEND_STATION", "exchange_fanout", ""); // 6、发送消息
for (int i = 0; i < 10; i++) {
channel.basicPublish("exchange_fanout", "", null, ("尊敬的Vip用户,秒杀商品开抢了!"+i).getBytes());
} // 7、关闭资源
channel.close(); connection.close();
}
}

消费者消费消息 Consumer

创建三个消费者:邮件消费者(ConsumerMail)、短信消费者(ConsumerMessage)、站内信消费者(ConsumerStation)

1、邮件消费者(ConsumerMail)

public class ConsumerMail {
public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、建立信道
Channel channel = connection.createChannel(); // 3、监听队列
channel.basicConsume("SEND_MAIL", true, new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body,"UTF-8");
System.out.println("接收邮件:"+message);
}
});
}
}

2、短信消费者(ConsumerMessage)

public class ConsumerMessage {
public static void main(String[] args) throws IOException, TimeoutException {
...
// 3、监听队列
channel.basicConsume("SEND_MESSAGE", true, new DefaultConsumer(channel) { @Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body,"UTF-8");
System.out.println("接收短信消息:"+message);
}
});
}
}

3、站内信消费者(ConsumerStation)

public class ConsumerStation {
public static void main(String[] args) throws IOException, TimeoutException {
...
// 3、监听队列
channel.basicConsume("SEND_STATION", true, new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String message = new String(body, "UTF-8");
System.out.println("接收站内信消息:"+message);
}
});
}
}

代码写完以后,先启动生产者创建队列生产消息,将消息转发给交换机,再由交换机把消息发给绑定该交换机的队列中去,等待消费者获取消费。

总结:订阅模式中,多个消费者同时订阅一个队列,该队列会轮询地把消息平均分配给每个消费者,这也就是标准的工作队列模式的模型。通过前面的 demo工程可知,

我们在使用发布订阅模式时,所有消息都会发送到绑定的队列中,但很多时候,不是所有消息都无差别的发布到所有队列中,这无形当中就会照成不必要的资源浪费。为了解决这个问题,路由模式就诞生了。

4、路由模式(Routing)

路由模式特点:

  • 选择性接收消息

  • 使用 Direct Exchange

  • 基于精确的 Routing key匹配

模式说明:

  • 队列与交换机的绑定,不能是任意绑定了,而是要指定一个 RoutingKey(路由键)

  • 消息的发送方在向 Exchange 发送消息时,也必须指定消息的 RoutingKey

  • Exchange 不再把消息交给每一个绑定的队列,而是根据消息的 Routing Key 进行判断,只有队列的Routingkey 与消息的 Routing key 完全一致,才会接收到消息

示意图解释:

  • P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key

  • C1:消费者,其所在队列指定了需要 routing key 为 error 的消息

  • C2:消费者,其所在队列指定了需要 routing key 为 info、error、warning 的消息

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

  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列,Routing 路由模式默认使用 direct 类型交换机

说明:路由(Routing)模式是发布订阅模式的升级版。我们知道发布订阅模式是无条件地将所有消息分发给所有消费者队列,每个队列中都有相同的消息;

路由模式,由上图很容易理解,每个队列消息会因为绑定的路由不同而不同。

特点:

  • 1、每个队列绑定一个路由关键字RoutingKey,生产者将带有RoutingKey的消息发送给交换机,交换机再根据路由 RoutingKey关键字将消息定向发送到指定的队列中;

  • 2、默认使用 direct 交换机。

应用场景:

  • 1、如在电商网站的促销活动中,双十一搞促销活动会把促销消息发布到所有队列中去;而一些小的促销活动为了节约成本,只发布到站内信队列。

  • 2、为了节省磁盘空间,需要将重要的错误消息引导到日志文件,同时仍然能够在控制台上打印输出所有日志消息。

路由模式代码示例:

channel.exchange_declare(exchange='direct_logs', exchange_type='direct')
channel.basic_publish(exchange='direct_logs', routing_key='error', body='错误日志')

生产者发送消息 Producer

  public class Producer {
// 定义交换机名称
private final static String exchange_name= "exchange_name"; // 大促销路由 RoutingKey
private final static String big_rout_key= "big"; // 小促销路由 RoutingKey
private final static String small_rout_ket= "small"; public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、创建信道
Channel channel = connection.createChannel(); // 3、建立交换机
channel.exchangeDeclare(exchange_name, BuiltinExchangeType.DIRECT, true); // 4、创建队列
channel.queueDeclare("message2_queue", true, false, false, null);
channel.queueDeclare("station2_queue", true, false, false, null);
channel.queueDeclare("email2_queue", true, false, false, null); // 5、交换机通过 RoutingKey 关键字绑定队列
channel.queueBind("message2_queue", exchange_name, big_rout_key);
channel.queueBind("email2_queue", exchange_name, big_rout_key); channel.queueBind("station2_queue", exchange_name, small_rout_ket); // 6、发送消息
channel.basicPublish(exchange_name, big_rout_key, null, ("双十一大促销活动--全场买一送一").getBytes()); channel.basicPublish(exchange_name, small_rout_ket, null, ("小促销活动--满1000立减200").getBytes()); // 7、关闭资源
channel.close();
connection.close();
}
}

创建三个消费者:邮件消费者(ConsumerMail)、短信消费者(ConsumerMessage)、站内信消费者(ConsumerStation);直接用 发布订阅模式的代码,修改下监听的队列即可 。

邮件消费者(ConsumerMail)

public class ConsumerMail {
public static void main(String[] args) throws IOException, TimeoutException {
...
// 3、监听队列 SEND_MAIL2
channel.basicConsume("SEND_MAIL2", true, new DefaultConsumer(channel) {
...
});
}
}

短信消费者(ConsumerMessage)

public class ConsumerMessage {
public static void main(String[] args) throws IOException, TimeoutException {
...
// 3、监听队列 SEND_MESSAGE2
channel.basicConsume("SEND_MESSAGE2", true, new DefaultConsumer(channel) {
...
});
}
}

站内信消费者(ConsumerStation)

public class ConsumerMessage {
public static void main(String[] args) throws IOException, TimeoutException {
...
// 3、监听队列 SEND_STATION2
channel.basicConsume("SEND_STATION2", true, new DefaultConsumer(channel) {
...
});
}
}

生产者业务流程执行的结果如下:

总结:路由模式是一种精准的匹配,只有设置了 Routing Key 后消息才能进行分发

5、通配符模式,又称为主题模式(Topic)

主题模式特点:

  • 基于模式的路由

  • 使用Topic Exchange

  • 支持通配符匹配(*和#)

模式说明:

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

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

  • 通配符规则:# 匹配一个或多个词,* 匹配不多不少恰好1个词,例如:item.# 能够匹配 item.insert.abc 或者 item.insert,item.* 只能匹配 item.insert

示意图解释:

  • P:生产者,向 Exchange 发送消息,发送消息时,会指定一个routing key

  • C1:消费者,其所在队列指定了需要 routing key 通配符为 .orange. 的消息

  • C2:消费者,其所在队列指定了需要 routing key 通配符为 ..rabbite、Lazy.# 的消息

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

  • X:Exchange(交换机),接收生产者的消息,然后把消息递交给与 routing key 完全匹配的队列,Topics 通配符模式默认使用 topic 类型交换机

图解:

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

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

说明:通配符模式(Topic)是在路由模式的基础上升级,给队列绑定带通配符的路由关键字,只要消息的RoutingKey 能实现通配符匹配而不再是固定的字符串,就会将消息转发到该队列。通配符模式比路由模式更灵活

特点:

1、消息设置RoutingKey时,RoutingKey由多个单词构成,中间以 . 分割。

2、队列设置RoutingKey时,#可以匹配任意多个单词,*可以匹配任意一个单词。

3、使用 topic 交换机

通配符规则:# 匹配一个或多个词,* 匹配有且仅有1个词。

主题模式示例:

  channel.exchange_declare(exchange='topic_logs', exchange_type='topic')
channel.basic_publish(exchange='topic_logs', routing_key='system.error', body='系统错误')

生产者发送消息 Producer

  public class Producer {

      private final static String ROUTE_NAME = "exchange_topic";

      public static void main(String[] args) throws IOException, TimeoutException {
// 1、建立工厂连接
Connection connection = ConnectionFactoryUtil.getConnection(); // 2、建立信道
Channel channel = connection.createChannel(); // 3、建立交换机
channel.exchangeDeclare(ROUTE_NAME, BuiltinExchangeType.TOPIC, true); // 4、创建队列
channel.queueDeclare("SEND_MAIL3", true, false, false, null);
channel.queueDeclare("SEND_MESSAGE3", true, false, false, null);
channel.queueDeclare("SEND_STATION3", true, false, false, null); // 5、交换机绑定队列
channel.queueBind("SEND_MAIL3",ROUTE_NAME,"#.big.#");
channel.queueBind("SEND_MESSAGE3",ROUTE_NAME,"#.middle.#");
channel.queueBind("SEND_STATION3",ROUTE_NAME,"#.small.#"); // 6、发送消息
channel.basicPublish(ROUTE_NAME, "big.middle", null, ("双十一大促销活动--全场买一送一").getBytes());
channel.basicPublish(ROUTE_NAME, "small",null, ("小促销活动--满1000立减200").getBytes()); // 7、关闭资源
channel.close();
connection.close(); }
}

6、RabbitMQ 的工作模式小结

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

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

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

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

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

六、springboot整合rabbitmq

生产者

1、设置 JDK 版本信息、添加项目所需的 jar 依赖

  <properties>
<java.version>1.8</java.version>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
</properties> <parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.5</version>
<relativePath/>
</parent> <dependencies>
<!-- rabbitmq 依赖 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
</dependencies>

2、编写配置文件 application.yml

  server:
# 项目访问的端口号
port: 9001
spring:
application:
# 项目名称
name: provider-rabbitmq
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: / #日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'

3、编写配置类绑定交换机和队列的控制器放到 Spring 容器里

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitConfig {
// 指定交换机名称
private final String EXCHANGE_NAME = "boot_topic_exchange"; // 指定队列名
private final String QUEUE_NAME = "boot_queue"; // 创建交换机
@Bean("bootExchange")
public Exchange getExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) // 是否持久化
.build();
} // 创建队列
@Bean("bootQueue")
public Queue getMessageQueue() {
return new Queue(QUEUE_NAME);
} // 创建交换机绑定队列
@Bean
public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.happyNewYear.#") // 通配符模式 要匹配的路由键 RoutingKey
.noargs();
}
}

4、编写生产者生产消息

  import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("bootRabbitMq")
public class RabbitProvider {
// 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearMessage")
public String sendMessage() {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("boot_topic_exchange", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
}
}

5、启动 SpringBoot 后 打开浏览器窗口执行 http://127.0.0.1:9001/bootRabb

消费者

6、设置 JDK 版本信息、添加项目所需的 jar 依赖;pom.xml 与 springBootProdiverRabbitMq 项目一致,这里就不再重复贴代码了

7、编写配置文件 application.yml 与 springBootProdiverRabbitMq 项目一致,唯一不同的是 项目访问的端口号和项目名称

server:
# 项目访问的端口号
port: 9002
spring:
application:
# 项目名称
name: consumer-rabbitmq

8、编写消费者

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.util.Map; @Component
@RabbitListener(queues = "boot_queue") // 监听队列
public class RabbitConsumer { @RabbitHandler // @RabbitListener 当有收到消息的时候,就交给 @RabbitHandler 的方法处理,根据接受的参数类型进入具体的方法中。
public void listenMessage(Map messageContent) {
System.out.println("topicReceiver消费者收到新年祝福:" + messageContent.toString());
}
}

9、消费者自动监听队列当队列里有消息时就会消费,此时,我们多次执行http://127.0.0.1:9001/bootRabbitMq/sendNewYearMessage 消费者的控制台则会打印多条消息记录

七、rabbitmq的消息可靠投递

我们知道 RabbitMQ 消息投递路径是:

可以看到在这个过程中,每个环节都有可能因某些故障导致消息传递失败,在项目开发出现这种消息无法传递的情况,对系统数据支撑的安全和可靠是致命的。所以如何才能保证 MQ 消息可靠、无误、准确地传递是本篇章的重点。

从上述流程我们可以得知:消息从生产者到打消费者,经过两次网络传输,并且在RabbitMQ服务器中进行路由,因此我们能知道整个流程中可能会出现三种消息丢失场景:

1、生产者发送消息到RabbitMQ服务器的过程中出现消息丢失,可能是网络波动未收到消息,又或者是服务器宕机

2、RabbitMQ服务器消息持久化出现消息丢失。消息发送到RabbitMQ之后未能及时存储完成持久化,RabbitMQ服务器出现宕机重庆,消息出现丢失

3、消费者拉取消息过程以及拿到消息之后出现消息丢失,消费者从RabbitMQ服务器获取到消息过程出现网络波动等问题可能出现消息丢失;消费者拿到消息后但消费者未能正常消费,导致丢失,可能是消费者出现处理异常又或者是消费者宕机

针对上述三种消息丢失场景,RabbitMQ提供了相应的解决方案,confirm消息确认机制(生产者),消息持久化机制(RabbitMQ服务),ACK事务机制(消费者)

从图中可以总结出以下几点:

1、确认模式(confirm):可以监听消息是否从生产者成功传递到交换机。

2、退回模式(return):可以监听消息是否从交换机成功传递到队列。

3、消费者消息确认(Ack):可以监听消费者是否成功处理消息。

生产者

a、生产者 application.yml 配置文件:

  server:
# 项目访问的端口号
port: 9003
spring:
application:
# 项目名称
name: reliable-provider
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: / #日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'

b、添加生成者的配置类 RabbitConfig.java 创建交换机和队列的绑定加入到 Spring 容器里

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitConfig {
// 指定交换机名称
private final String EXCHANGE_NAME = "reliable_exchange"; // 指定队列名
private final String QUEUE_NAME = "reliable_queue"; // 创建交换机
@Bean("bootExchange")
public Exchange getExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) // 是否持久化
.build();
} // 创建队列
@Bean("bootQueue")
public Queue getMessageQueue() {
return new Queue(QUEUE_NAME);
} // 创建交换机绑定队列
@Bean
public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.happyNewYear.#") // 通配符模式 要匹配的路由键 RoutingKey
.noargs();
}
}

c、创建生产者消息的发送的类 ReliableProvider.java

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class ReliableProvider { // 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearMessage")
public String sendMessage() {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
}
}

d、创建生产者消息的发送的类 ReliableProvider.java

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class ReliableProvider { // 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearMessage")
public String sendMessage() {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
}
}

e、启动 生产者服务,成功后在浏览器上输入 http://127.0.0.1:9003/provider/sendNewYearMessage 生产消息并 查看RabbitMQ 管控台是否有消息生成

d、前面的创建步骤完成,且能生产者能正常发送消息,但存在一下问题

首先假如没有确认模式,当生产者发送消息到交换机,发送期间或因某种不可控因素又或者是交换机故障等等。消息并没有正在成功发送到交换机上,此时,消息创建就会失败。但是,这个过程用户是无感知的,程序员也不知道是什么故障情况。如果在生产者把消息推到交换机上这个过程做个监听,是不是就一目了然了呢

验证场景

首先我们清空下当前队列 reliable_queue 已经存在的数据,然后在生产者这个类,把发送消息改成一个不存在的交换机,然后再看下运行结果。

// rabbitTemplate.convertAndSend("reliable_exchange", "happyNewYear", messageMap);

// 模拟确认模式 使交换机故障让消息无法发送到交换机上
rabbitTemplate.convertAndSend("xxxx", "happyNewYear", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";

重启服务后,浏览器访问下生产者地址,在RabbitMQ 控制台就会发现没有消息产出了。

下面就开始实施部署 “确认模式” 的代码来启动对生产者发送消息到交换机的监控。

1、修改生产者配置 application.yml 文件开启确认模式

  ...
spring:
...
# 配置RabbitMQ
rabbitmq:
...
virtual-host: /
# 开启生产者确认
publisher-confirm-type: correlated

2、配置编写确认模式回调函数 RabbitConfirmConfig.java

  import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitConfirmConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory); // 设置开启 Mandatory 强制执行调用回调函数
rabbitTemplate.setMandatory(true); // 消息到达Broker回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 被调用的回调方法
* @param correlationData 相关配置信息
* @param ack 交换机是否成功收到消息 可以根据 ack 做相关的业务逻辑处理
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack) {
// 控制台 打印输出内容
System.out.println("生产者已成功将消息推送到交换机");
log.info("消息到达Exchange成功, ID: {}", correlationData.getId());
} else {
// 控制台 打印输出内容
log.error("消息到达Exchange失败, ID: {}, 原因: {}", correlationData.getId(), cause);
System.out.println("ConfirmInfo: "+"相关配置信息: "+correlationData);
System.out.println("ConfirmInfo: "+"确认结果: "+ack);
System.out.println("ConfirmInfo: "+"原因: "+cause); // 可做针对性地业务逻辑处理,例如:让消息重发、发送邮件通知程序员、做日志等等。
}
}
}); return rabbitTemplate;
}
}

3、重启服务发起生产者生产消息的请求、并观察控制台

生产者推送信息到交换失败的情况:

到此,确认模式的代码部署已完成,当生产者推送消息到交换机失败时,可以马上进行监控,并把结果反馈到负责人手里,进行排查处理解决问题。但是,我们从前文已经了解到光有确认模式是不足以保证消息从生产者到消费者的传输与消费都是成功的。从交换机拿到消息后,再次把消息发送给队列,这个过程中也是会有失败风险。这就引入了接下来需要部署的 “退回模式” 用于监听消息从交换机到队列的传输结果。

退回模式

1、修改生产者配置 application.yml 文件开启确认模式

...
spring:
...
# 配置RabbitMQ
rabbitmq:
...
# 开启生产者确认
publisher-confirm-type: correlated
# 开启回退模式(当消息无法路由到队列时返回给生产者)
publisher-returns: true

2、配置编写退回模式回调函数 RabbitConfirmConfig.java

  import org.springframework.amqp.core.ReturnedMessage;
...
@Configuration
public class RabbitConfirmConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
...
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
...
}); // 消息未路由到队列回调
rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* 被调用的回调方法
* @param returnedMessage 消息主题内容对象
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("ReturnInfo: "+"消息对象:"+returnedMessage.getMessage());
System.out.println("ReturnInfo: "+"错误码:"+returnedMessage.getReplyCode());
System.out.println("ReturnInfo: "+"错误信息:"+returnedMessage.getReplyText());
System.out.println("ReturnInfo: "+"交换机:"+returnedMessage.getExchange());
System.out.println("ReturnInfo: "+"路由键:"+returnedMessage.getRoutingKey()); log.error("消息未路由到队列, 消息: {}, 回应码: {}, 回应信息: {}, 交换机: {}, 路由键: {}",
new String(returned.getMessage().getBody()),
returned.getReplyCode(),
returned.getReplyText(),
returned.getExchange(),
returned.getRoutingKey()); // 可做针对性地业务逻辑处理,例如:发送邮件通知程序员、做日志等等。
}
}); return rabbitTemplate;
}
}

3、模拟交换机推送消息到队列错误测试,把生产者 ReliableProvider.java 路由键改成 xxxx

...
// 模拟确认模式 使交换机故障让消息无法发送到交换机上
// rabbitTemplate.convertAndSend("xxxx", "happyNewYear", messageMap);
// 模拟退回模式 修改不存在的路由键 使交换机无法通过路由键把消息发送到队列中
rabbitTemplate.convertAndSend("reliable_exchange", "xxxx", messageMap);
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>新年祝福已发送</p>";
...

4、重启服务发起生产者生产消息的请求、并观察控制台

消息持久化机制(RabbitMQ服务)

持久化机制是指将消息存储到磁盘,以保证在RabbitMQ服务器宕机或重启时,消息不会丢失

使用方法:

  • 生产者通过将消息的 delivery_mode属性设置为2,将消息标记为持久化

  • 队列也需要进行持久化设置,确保队列在RabbitMQ服务器重启后任然存在,经典队列需要将durable属性设置为true

注意事项:持久化机制会影响性能,因此在需要确保消息不丢失的场景下使用

Ack 模式

在RabbitMQ中,消费者接收到消息后会向队列发送确认签收的消息,只有确认签收的消息才会被移除队列。这种机制称为消费者消息确认(Consumer Acknowledge,简称Ack)。类似快递员派送快递也需要我们签收,否则一直存在于快递公司的系统中。

消息分为自动确认和手动确认。自动确认指消息只要被消费者接收到,不关心消息是否处理成功,则自动签收,并将消息从队列中移除。但是在实际开发中,收到消息后可能业务处理出现异常,那么消息就会丢失。此时需要设置手动签收,即在业务处理成功再通知签收消息,如果出现异常,则拒签消息,让消息依然保留在队列当中。

有三种确认方式:

  • 自动确认:acknowledge="none"

  • 手动确认:acknowledge="manual"

  • 根据异常情况确认:acknowledge="auto",(这种方式使用麻烦,不作讲解)

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

常,则调用channel.basicNack()方法,让其自动重新发送消息。

1、自动确认

前面所有的 demo 工程都是自动确认,这里就不再贴代码演示了。“自动确认模式”对消费者而言不管消费者是否成功处理本次消息的投递,都会自动认为本次投递已经被正确处理,消息会被移除该队列。所以对于这种情况,如果消费者处理消费逻辑时抛出异常,此时的消费者其实是并没有真正把此消息处理成功,这样的结果就相当于丢失了此消息。

对于这种情况一般我们都是使用try catch捕获异常后,记录日志来追踪数据,这样找出对应数据后再做后续的业务处理。但是,这种处理方式效率明显很低。

2、手动确认

手动确认有以下几种模式:

  • basicAck():用于肯定确认

  • basicNack():用于拒绝确认

  • basicReject():用于拒绝确认

案例

第一种方式 application.yml 的配置方式:

1、消费者 application.yml 配置文件,并开启手动签收:

  server:
# 项目访问的端口号
port: 9004
spring:
application:
# 项目名称
name: reliable-consumer
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /
# 消费者开启手动签收
listener:
simple:
acknowledge-mode: manual #日志格式
logging:
pattern:
console: '%d{HH:mm:ss.SSS} %clr(%-5level) --- [%-15thread] %cyan(%-50logger{50}):%msg%n'

2、创建消费者类 RabbitConsumerOfDeploy.java 编写消费者逻辑代码

  import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Map; @Component
public class RabbitConsumerOfDeploy {
// 监听队列
@RabbitListener(queues = "reliable_queue") public void listenMessage(Message message, Channel channel) throws Exception { // 消息投递序号,消息每次投递该值都会+1
long deliveryTag = message.getMessageProperties().getDeliveryTag(); byte[] body = message.getBody();
try { ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
String messageId = msgMap.get("messageId");
String messageData = msgMap.get("messageContent");
String createTime = msgMap.get("sendTime");
objectInputStream.close();
System.out.println("rabbitConsumer: messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue()); /**
* 签收消息
* 参数1:消息投递序号
* 参数2:是否一次可以签收多条消息,true:是;false:否
*/
channel.basicAck(deliveryTag, true); } catch (Exception e) {
/**
* 拒签消息
* 参数1:消息投递序号
* 参数2:是否一次可以拒签多条消息,true:是;false:否
* 参数3:拒签后消息是否重回队列,true:重回;false:不重回
*/
System.out.println("消息消费失败!");
channel.basicNack(deliveryTag, true, true);
e.printStackTrace();
}
}
}

第二种方式 实现 ChannelAwareMessageListener 接口(和RabbitConsumerOfDeploy.java 有很多的相似处) :

1、编写 RabbitConsumer.java

  import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.listener.api.ChannelAwareMessageListener;
import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Map; @Component
public class RabbitConsumer implements ChannelAwareMessageListener {
@Override
public void onMessage(Message message, Channel channel) throws Exception { // 消息投递序号,消息每次投递该值都会+1
long deliveryTag = message.getMessageProperties().getDeliveryTag(); try {
byte[] body = message.getBody();
ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
String messageId = msgMap.get("messageId");
String messageData = msgMap.get("messageContent");
String createTime = msgMap.get("sendTime");
objectInputStream.close();
System.out.println("rabbitConsumer: messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue()); /**
* 签收消息
* 参数1:消息投递序号
* 参数2:是否一次可以签收多条消息,true:是;false:否
*/
channel.basicAck(deliveryTag, true);
}catch (Exception e) { /**
* 拒签消息
* 参数1:消息投递序号
* 参数2:是否一次可以拒签多条消息,true:是;false:否
* 参数3:拒签后消息是否重回队列,true:重回队列;false:不重回队列
*/
System.out.println("消息消费失败!");
channel.basicNack(deliveryTag, true, true);
e.printStackTrace();
}
}
}

2、编写Ack配置类 AckConfig.java 注入到容器的配置方式,不需要通过application.yml配置方式开启:

  import com.demo.reliable.consumer.RabbitConsumer;
import org.springframework.amqp.core.AcknowledgeMode;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.listener.SimpleMessageListenerContainer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class AckConfig {
@Autowired
private CachingConnectionFactory connectionFactory; @Autowired
private RabbitConsumer rabbitConsumer; @Bean
public SimpleMessageListenerContainer simpleMessageListenerContainer() {
SimpleMessageListenerContainer listenerContainer = new SimpleMessageListenerContainer(connectionFactory); // 设置消费者个数,当前设置为1
listenerContainer.setConcurrentConsumers(1);
listenerContainer.setMaxConcurrentConsumers(1); // 开启手动ACK
listenerContainer.setAcknowledgeMode(AcknowledgeMode.MANUAL); // 设置一个已存在的队列
listenerContainer.setQueueNames("reliable_queue"); listenerContainer.setMessageListener(rabbitConsumer); return listenerContainer;
}
}

3、注释掉 配置类 application.yml 的手动签收配置

server:
...
spring:
...
rabbitmq:
...
# 开启手动签收
# listener:
# simple:
# acknowledge-mode: manual

两种方式代码的部署就此完成

接下来我们使用第二种方式来是模拟异常实现 ACK 手动确认的拒签场景

1、修改 RabbitConsumer.java

  try {
int i = 1/0; //模拟处理消息出现 触发ACK
...
}catch (Exception e) {
...
}

2、生产者生成一条消息并重启消费者服务

经过异常模拟执行后,很清晰地看出当消费者消费出现异常时,RabbitMQ 控制台则拒签消息,让消息重回队列中进行下一次消费。

该消息就会一直处于这样 Unacked 状态进行循环下去(消费-入列-消费-入列)。除非异常解除后才会停止循环,开始正常消费消息。当我们停止消费者时,消息

会重新发放,Unacked 变为0,Ready 变为1。

这里,如果把消费者 RabbitConsumer.java 的 basicNack 修改成 basicReject 结果也是一样,那么手动确认的三种模式: basicAck()、 basicNack()、basicReject()的使用方法和区别做一个归纳总结

RabbitMQ 的 basicNack 和 basicReject 详解

一、基本概念

1、basicReject 方法

  • 功能:拒绝单条消息

  • 特点:

    • 只能拒绝一条消息

    • 可以控制是否重新入队(requeue)

    • 不支持批量操作

2、basicNack 方法

  • 功能:拒绝单条或多条消息

  • 特点:

    • 可以拒绝单条或多条消息

    • 可以控制是否重新入队(requeue)

    • 支持批量操作

    • 是 basicReject 的增强版

二、方法签名对比

需要注意的是:手动Ack如果处理方式不对会发生一些问题。

1、没有及时ack,或者程序出现bug,所有的消息将被存在unacked中,消耗内存如果忘记了ack,那么后果很严重。当Consumer退出时,Message会重新分发。然后RabbitMQ会占用越来越多的内存,由于 RabbitMQ会长时间运行,因此这个 “内存泄漏” 是致命的。

2、如果使用basicNack,将消费失败的消息重新塞进队列的头部,则会造成死循环。(解决basicNack造成的消息循环循环消费的办法是为队列设置“回退队列”,设置回退队列和阀值,如设置队列为q1,阀值为2,则在rollback两次后将消息转入q1)

综上,对于手动ack的使用注意以下三点:

1、在消费者端一定要进行ack,或者是nack,可以放在try方法块的finally中执行

2、可以对消费者的异常状态进行捕捉,根据异常类型选择ack,或者nack抛弃消息,nack再次尝试

3、对于nack的再次尝试,是进入到队列头的,如果一直是失败的状态,将会造成阻塞。所以最好是专门投递到“死信队列”,死信队列 后面再详细讲解。

完整可靠性检查清单

生产者端:

  • 配置 publisher-confirm-type: correlated

  • 配置 publisher-returns: true

  • 实现 ConfirmCallback 和 ReturnsCallback

  • 消息设置 PERSISTENT 持久化模式

  • 为每条消息设置唯一ID

Broker端:

  • 交换机声明为持久化(durable=true)

  • 队列声明为持久化(durable=true)

  • 配置死信队列处理失败消息

  • 生产环境配置镜像队列

消费者端:

  • 配置 acknowledge-mode: manual

  • 实现正确的手动ACK/NACK逻辑

  • 设置合理的 prefetch 值(通常设为1)

  • 实现消费者幂等处理

增强措施:

  • 实现消息发送前落库

  • 定时任务检查未确认消息

  • 实现消息轨迹追踪

  • 建立定期对账机制

八、rabbitmq的高级特性

RabbitMQ 高级特性包括包括消息限流、实现限流不公平分发、消息存活时间、队列的优先级。因为有这些高级特性的存在使得它在不同的应用场景里面对各种问题都能有一个比较好的解决方案。例如:生产者生产大量的消息,但是消费者获取消息后要处理相关业务需要一定的时间,并不能很快地去消费这些大量的消息;此时,这些大数据量的消息就会冲击

消费者,照成消费端处理业务达到瓶颈而崩溃,为了解决这个问题,可以把消息暂时存放在MQ当中,然后再按照一定的速度把消息发送给消费者来保护消费端正常处理业务。

1、rabbitmq的的高级特性之消费限流

生产者 interdictProvider 项目

①、添加配置文件 application.yml

server:
# 项目访问的端口号
port: 9006
spring:
application:
# 项目名称
name: reliable-provider
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /
# 开启生产者确认
publisher-confirm-type: correlated
# 开启返回模式(当消息无法路由到队列时返回给生产者)
publisher-returns: true

②、创建配置文件 RabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

package com.demo.interdict.config;
import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitConfig { // 指定交换机名称
private final String EXCHANGE_NAME = "interdict_exchange"; // 指定队列名
private final String QUEUE_NAME = "interdict_queue"; // 创建交换机
@Bean("bootExchange")
public Exchange getExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) // 是否持久化
.build();
} // 创建队列
@Bean("bootQueue")
public Queue getMessageQueue() {
return new Queue(QUEUE_NAME);
} // 创建交换机绑定队列
@Bean
public Binding bindingExchangeQueue(@Qualifier("bootExchange") Exchange exchange, @Qualifier("bootQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.happyNewYear.#") // 通配符模式 要匹配的路由键 RoutingKey
.noargs();
}
}

③、编写拥有确认模式和退回模式的配置文件 RabbitConfirmConfig.java

  import org.springframework.amqp.core.ReturnedMessage;
import org.springframework.amqp.rabbit.connection.ConnectionFactory;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitConfirmConfig {
@Bean
public RabbitTemplate createRabbitTemplate(ConnectionFactory connectionFactory) {
RabbitTemplate rabbitTemplate = new RabbitTemplate();
rabbitTemplate.setConnectionFactory(connectionFactory); // 设置开启 Mandatory 强制执行调用回调函数
rabbitTemplate.setMandatory(true); rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* 被调用的回调方法
* @param correlationData 相关配置信息
* @param ack 交换机是否成功收到消息 可以根据 ack 做相关的业务逻辑处理
* @param cause 失败原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(ack) {
// 控制台 打印输出内容
System.out.println("生产者已成功将消息推送到交换机");
} else {
// 控制台 打印输出内容
System.out.println("ConfirmInfo: "+"相关配置信息: "+correlationData);
System.out.println("ConfirmInfo: "+"确认结果: "+ack);
System.out.println("ConfirmInfo: "+"原因: "+cause); // 可做针对性地业务逻辑处理,例如:让消息重发、发送邮件通知程序员、做日志等等。
}
}
}); rabbitTemplate.setReturnsCallback(new RabbitTemplate.ReturnsCallback() {
/**
* 消息发送成功时,被调用的回调方法
* @param returnedMessage 消息主题内容对象
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
System.out.println("ReturnInfo: "+"消息对象:"+returnedMessage.getMessage());
System.out.println("ReturnInfo: "+"错误码:"+returnedMessage.getReplyCode());
System.out.println("ReturnInfo: "+"错误信息:"+returnedMessage.getReplyText());
System.out.println("ReturnInfo: "+"交换机:"+returnedMessage.getExchange());
System.out.println("ReturnInfo: "+"路由键:"+returnedMessage.getRoutingKey()); // 可做针对性地业务逻辑处理,例如:发送邮件通知程序员、做日志等等。
}
}); return rabbitTemplate;
}
}

④、编写生产者代码生产100条消息

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class interdictProvider { // 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearMessage")
public String sendMessage() {
int m=0;
for (int i = 1; i <= 100; i++) {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信"; // 消息主题内容 String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", messageMap);
m++;
}
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
}
}

⑤、启动测试生产者,浏览器 请求 http://127.0.0.1:9006/provider/sendNewYearMessage 生成100消息到 MQ

消费者 interdictConsumer 项目

①、添加配置文件 application.yml

spring:
application:
# 项目名称
name: reliable-consumer
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /
# 消费者必须开启手动签收
listener:
simple:
acknowledge-mode: manual

②、编写消费者

  import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Map;
import java.util.concurrent.TimeUnit; @Component
public class InterdictConsumer {
@RabbitListener(queues = "interdict_queue")
public void listenMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
byte[] body = message.getBody();
try { ObjectInputStream objectInputStream = new ObjectInputStream(new ByteArrayInputStream(body));
Map<String,String> msgMap = (Map<String,String>) objectInputStream.readObject();
String messageId = msgMap.get("messageId");
String messageData = msgMap.get("messageContent");
String createTime = msgMap.get("sendTime");
objectInputStream.close();
System.out.println("rabbitConsumer: messageId:"+messageId+" messageData:"+messageData+" createTime:"+createTime);
System.out.println("消费的队列名:"+message.getMessageProperties().getConsumerQueue()); /**
* 签收消息
* 参数1:消息投递序号
* 参数2:是否一次可以签收多条消息,true:是;false:否
*/
channel.basicAck(deliveryTag, true);
} catch (Exception e) {
/**
* 拒签消息
* 参数1:消息投递序号
* 参数2:是否一次可以拒签多条消息,true:是;false:否
* 参数3:拒签后消息是否重回队列,true:重回;false:不重回
*/
System.out.println("消息消费失败!");
channel.basicNack(deliveryTag, true, true);
e.printStackTrace();
}
}
}

演示在没有限流的情况下,消费端消费消息的状态。

可以看到所有消息都会堆积到 Unacked 未签收中。如果存在当有大量消息产出并堆积到消费者,很可能就会照成特征:内存溢 出或泄露导致系统瘫痪而不可用。如何解决这个问题呢?那就需要需要用到消费端限流机制。

配置限流机制

修改配置文件 application.yml

添加配置 spring.rabbitmq.listener.simple.prefetch 为 10

  spring:
application:
# 项目名称
name: reliable-consumer
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /
# 消费者必须开启手动签收
listener:
simple:
# 限流机制必须开启手动签收
acknowledge-mode: manual # 消费者最多拉取10条消息进行消费,当签收后不满10条则继续拉取消息
prefetch: 10

演示在限流的情况下,消费端消费消息的状态。

总结:当开启限流后,不会有大量消息堆积到消费端;然当有大量消息进来时,有且只有一定量 prefetch 设定数量值的消息堆在 Unacked 中,当签收后 Unacked 中的值不满 prefetch 设定值时就会自动拉取(演示的案例中,Unacked 会一直保持消息条数的数值为10的状态),直至消息消费完;充分保护了消费端正常运行签收消费消息。

2、rabbitmq的的高级特性之设置消息存活时间

在某些应用场景中,某些消息只是临时通知的但却一直没有被消费者消费,这种消息就会一直存在MQ当中,浪费资源空间。这时就可以给消息设定存活时间。RabbitMQ就有这种功能特性,它可以设置消息的存活时间(Time To Live,简称TTL),当消息到达存活时间后还没有被消费,会被移出队列。

有两种设置模式:

1、对队列的所有消息设置存活时间(相当于给队列设置了有效时间);

2、对某条消息设置存活时间。

a、对队列的所有消息设置存活时间

i、新增一个绑定队列和交换机的配置 RabbitSecondConfig.java 并给队列里的消息设置存活时间,设置的方法和代码块见 getSecondMessageQueue() 方法。

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitSecondConfig {
// 指定交换机名称
private final String EXCHANGE_NAME = "live_exchange"; // 指定队列名
private final String QUEUE_NAME = "live_queue"; // 创建交换机
@Bean("bootSecondExchange")
public Exchange getSecondExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) // 是否持久化
.build();
} // 创建队列
@Bean("bootSecondQueue")
public Queue getSecondMessageQueue() {
return QueueBuilder
.durable(QUEUE_NAME)
.ttl(15000) // 设定该队列里所有消息的存活时间是 15秒
.build();
} // 创建交换机绑定队列
@Bean
public Binding bindingSecondExchangeQueue(@Qualifier("bootSecondExchange") Exchange exchange, @Qualifier("bootSecondQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.happyNewYears.#") // 通配符模式 要匹配的路由键 RoutingKey
.noargs();
}
}

ii、新增生产者创建消息的方法,修改生产者 interdictProvider.java;生产者生产消息请求的路径为:http://127.0.0.1:9006/provider

  import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class interdictProvider {
// 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearMessage")
public String sendMessage() {
...
} /**
* 生产者设定消息存活时间
* @return
*/
@GetMapping("/sendNewYearMessageOfTtl")
public String sendSecondMessage() {
int m=0;
// 生产20条消息
for (int i = 1; i <= 20; i++) {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("live_exchange", "happyNewYears", messageMap);
m++;
}
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
}
}

iii、重启启动器,在浏览器中输入地址并发起请求,注意观察 RabbitMQ的控制台

b、对某条消息设置存活时间

i、修改 interdictProvider.java 新增消息生成者 sendTtlMessage() 方法。生产者生产消息请求的路径为:http://127.0.0.1:9006/provider

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class interdictProvider { // 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/sendNewYearTtlMessage")
public String sendTtlMessage() {
int m=0;
// 生产5条消息
for (int i = 1; i <= 5; i++) {
// 设置消息主题
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime);
messageMap.put("expiration",20); if(i == 4) {
/*
* 模拟生成一条具有有效时间为1秒的消息
* */
// 设置消息属性
MessageProperties messageProperties = new MessageProperties(); // 设置消息存活时间
messageProperties.setExpiration("1000"); // 创建消息对象
Message message = new Message(messageContent.getBytes(StandardCharsets.UTF_8), messageProperties); // 发送消息
rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", message);
} else {
/*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("interdict_exchange", "happyNewYear", messageMap);
}
m++;
}
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
}
}

总结:如果在队列和指定的消息都设置了有效存活时间,则以时间短的为准,谁的有效时间短谁的优先级就高。这里说到优先级,引入一个问题可能有些小伙伴们有遇到这样类似的场景:某个大型商城APP需要给用户推送促销活动消息,登录系统领取抵扣券先到先得领完截止。

老板说了要优先给VIP的用户推送。这个时候就要考虑到消息优先级的问题,先给VIP用户发,然后再给普通用户发。那消息的优先级该如何设置?

3、rabbitmq的的高级特性之消息优先级

i、新增一个绑定队列和交换机的配置 RabbitPriorityConfig.java

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class RabbitPriorityConfig { // 定义交换机
private final String EXCHANGE_NAME = "priority_exchange"; // 定义队列
private final String QUEUE_NAME = "priority_queue"; //创建交换机
@Bean(value = "bootPriorityExchange")
public Exchange getPriorityExchange() {
return ExchangeBuilder
.topicExchange(EXCHANGE_NAME)
.durable(true) // 持久化
.build();
} // 创建队列
@Bean(value = "bootPriorityQueue")
public Queue getPriorityQueue() {
return QueueBuilder
.durable(QUEUE_NAME)
.maxPriority(10) // 设置优先级参数值
.build();
} // 创建交换机绑定队列
@Bean
public Binding bindingPriorityExchangeQueue(@Qualifier("bootPriorityExchange") Exchange exchange,@Qualifier("bootPriorityQueue") Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.cxActive.#")
.noargs();
}
}

ii、新增生产者创建消息的方法,修改生产者 interdictProvider.java;生产者生产消息请求的路径为:http://127.0.0.1:9006/provider

  @RestController
@RequestMapping("provider")
public class interdictProvider {
// 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; /**
* 消息优先级
* @return
*/
@GetMapping("/sendCxActiveMessage")
public String sendPriorityMessage() {
// 数据模拟 定义10、12为vip 用户
Map<Integer, String> vips = new HashMap<>();
vips.put(10, "张三");
vips.put(12, "李四"); // 生产消息
MessageProperties messageProperties = new MessageProperties();
String messageContent = "";
int m=0;
for (int i = 1; i <= 50; i++) {
String userName = vips.get(i);
if(userName == null){
messageContent = "限时活动,进入个人中心领红包了,先到先得,领完为止。";
} else {
messageContent = "尊贵的 VIP用户"+userName+ "您好,即可登录APP 进入个人中心领取全场 1000的通用红包,先到先得,领完为止。";
messageProperties.setPriority(10);
}
Message message = new Message(messageContent.getBytes(StandardCharsets.UTF_8), messageProperties);
rabbitTemplate.convertAndSend("priority_exchange","cxActive", message);
m++;
}
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"条消息已发送</p>";
}
}

iii、新增消费者

  import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.IOException; @Component
public class InterdictPriorityConsumer {
@RabbitListener(queues = "priority_queue")
public void listenMessage(Message message, Channel channel) throws IOException {
System.out.println(new String(message.getBody())); channel.basicAck(message.getMessageProperties().getDeliveryTag(),true);
}
}

注意:优先级设置的过多,会消耗更多的CPU资源,因此,推荐优先级的值不超过10。

九、rabbitmq的死信队列和延迟队列

死信队列概念:在MQ中,死信队列是用于存储未能被正常消费的消息的特殊队列,相当于电脑中垃圾回收站。而在 RabbitMQ中,由于有交换机的概念,

实际是将死信发送给了死信交换机(Dead Letter Exchange,简称DLX)。死信交换机和死信队列和普通的没有区别

消息成为死信队列的情况:

  • 消息被消费者拒绝 (basic.reject 或 basic.nack) 且 requeue=false

  • 消息在队列中的存活时间 (TTL) 过期

  • 队列达到最大长度限制

延迟队列概念:延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。【经典案例:用户下单,30分钟后。订单未支付状态则会自动取消。】

死信队列

生产者 deadProvider 项目

①、添加配置文件 application.yml

server:
# 项目访问的端口号
port: 9007
spring:
application:
# 项目名称
name: dead-provider
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /

②、创建配置文件 DeadRabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class DeadRabbitConfig { // 定义死信交换机
private final String DEAD_EXCHANGE = "dead_exchange"; // 定义死信队列
private final String DEAD_QUEUE = "dead_queue"; // 普通交换机
private final String ORDINARY_EXCHANGE = "ordinary_exchange"; // 普通队列
private final String ORDINARY_QUEUE = "ordinary_queue"; // 普通交换机
@Bean(ORDINARY_EXCHANGE)
public Exchange ordinaryExchange() {
return ExchangeBuilder
.topicExchange(ORDINARY_EXCHANGE)
.durable(true) // 持久化
.build();
} // 普通队列
@Bean(ORDINARY_QUEUE)
public Queue ordinaryQueue() {
return QueueBuilder
.durable(ORDINARY_QUEUE)
.deadLetterExchange(DEAD_EXCHANGE) // 绑定死信交换机
.deadLetterRoutingKey("deadRouting") // 死信队列路由关键字
.ttl(15000) // 消息存活时间 15秒
.maxLength(10) // 队列最大长度
.build();
} // 普通交换机绑定普通队列
@Bean
public Binding bindOrdinaryQueue(@Qualifier(ORDINARY_EXCHANGE) Exchange exchange, @Qualifier(ORDINARY_QUEUE) Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.happyNewYear.#")
.noargs();
} // 死信交换机
@Bean(DEAD_EXCHANGE)
public Exchange deadExchange() {
return ExchangeBuilder
.topicExchange(DEAD_EXCHANGE)
.durable(true) // 持久化
.build();
} // 死信队列
@Bean(DEAD_QUEUE)
public Queue deadQueue() {
return QueueBuilder
.durable(DEAD_QUEUE)
.build();
} // 死信交换机绑定死信队列
@Bean
public Binding bindDeadQueue(@Qualifier(DEAD_EXCHANGE) Exchange exchange, @Qualifier(DEAD_QUEUE) Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.deadRouting.#")
.noargs();
} }

③、创建生产者 DeadProvider.java

import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;
import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class DeadProvider { // 注入 RabbitTemplate 工具类
@Autowired
private RabbitTemplate rabbitTemplate; // 模拟演示当消息长度达到限制后,剩余消息进入死信
@GetMapping("productMaxLengthMessage")
public String productMaxLengthMessage() {
return null;
} // 模拟演示当消息过期后进行死信
@GetMapping("productTtlMessage")
public String productTtlMessage() {
return null;
} // 模拟演示消费者拒签后,消息进入死信
@GetMapping("productRefuseMessage")
public String productRefuseMessage() {
return null;
} protected String productMessage(int num) {
int m = 0;
for (int i = 1; i <= num; i++) {
String messageId = String.valueOf(UUID.randomUUID()); // 随机一个消息 ID
String messageContent = "快过年了,提前祝你新年快乐。第 "+i+"封信"; // 消息主题内容
String sendTime = LocalDateTime.now().format(DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss"));
HashMap<String, Object> messageMap = new HashMap<>();
messageMap.put("messageId", messageId);
messageMap.put("messageContent", messageContent);
messageMap.put("sendTime", sendTime); /*
* 发送消息
* 参数1:交换机名称
* 参数2:路由 routeKey
* 参数3:消息主题内容
*/
rabbitTemplate.convertAndSend("ordinary_exchange", "happyNewYear", messageMap);
m++;
}
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>第"+m+"封新年祝福信已发送</p>";
}
}

消费者 deadConsumer 项目

①、添加配置文件 application.yml

spring:
application:
# 项目名称
name: dead-consumer
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /

②、创建消费者 DeadConsumer.java

package com.demo.dead.consumer;

import com.rabbitmq.client.Channel;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; @Component
public class DeadConsumer { @RabbitListener(queues = "ordinary_queue")
public void listenMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag(); }
}

死信一:队列消息长度到达限制

模拟演示当消息长度达到限制后,剩余消息进入死信。在配置文件 DeadRabbitConfig.java 给普通交换机ordinary_exchange 设置了交换机 最多只能产出10条消息,

超过10条后的消息将进入死信。修改生产者 DeadProvider.java生产消息的方案 productMaxLengthMessage()

  // 模拟演示当消息长度达到限制后,剩余消息进入死信
@GetMapping("productMaxLengthMessage")
public String productMaxLengthMessage() {
return this.productMessage(100);
}

重新启动生产者启动器,并在浏览器中访问 http://127.0.0.1:9007/provider

死信二:消息到达存活时间未被消费

当消息在配置文件 DeadRabbitConfig.java预设的有效时间未被消费将进入死信队列

修改生产者 DeadProvider.java生产消息的方案 productTtlMessage()

// 模拟演示当消息过期后进行死信
@GetMapping("productTtlMessage")
public String productTtlMessage() {
return this.productMessage(10);
}

重新启动生产者启动器,删除已存在的队列并在浏览器中访问 http://127.0.0.1:9007/provider

死信三:消费者拒接消费消息,basicNack/basicReject,并且不把消息重新放入原目标队列,requeue=false

修改 消费者 DeadConsumer.java

@Component
public class DeadConsumer { @RabbitListener(queues = "ordinary_queue")
public void listenMessage(Message message, Channel channel) throws Exception {
long deliveryTag = message.getMessageProperties().getDeliveryTag();
channel.basicNack(deliveryTag, true, false); // 消息拒签
}
}

启动消费者 并在浏览器中运行 http://127.0.0.1:9007/provider

十、rabbitmq的延迟队列

延迟队列是指消息在发送后不会立即被消费,而是在指定的延迟时间后才可供消费者获取的特殊队列。常见应用场景包括:

  • 订单超时未支付自动取消(30分钟延迟)

  • 预约提醒(提前1小时通知)

  • 重试机制(失败后延迟5秒重试)

RabbitMQ 实现延迟队列的方案

方案1:TTL + 死信队列(兼容所有版本)

实现原理:

  • 为消息或队列设置TTL(Time To Live)

  • 消息过期后通过死信交换器路由到处理队列

  • 消费者从处理队列获取"延迟后"的消息

SpringBoot订单生产者(orderProvider)

①、SpringBoot 整合RabbitMQ的 pom.xml 文件依赖一致如前言部分创建死信队列demo 时相同

②、添加配置文件 application.yml

server:
# 项目访问的端口号
port: 9010
spring:
application:
# 项目名称
name: order-provider
# 配置RabbitMQ
rabbitmq:
host: 127.0.0.1
port: 5672
username: logic
password: 123456
# 虚拟主机
virtual-host: /

③、创建配置文件 OrderRabbitConfig.java 编写交换机、队列的绑定加入到 Spring 容器中

  import org.springframework.amqp.core.*;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration; @Configuration
public class OrderRabbitConfig { // 定义普通正常的交换机
private final String ORDER_EXCHANGE = "order_exchange"; // 定义普通正常的队列
private final String ORDER_QUEUE = "order_queue"; // 定义死信过期过期的交换机
private final String EXPIRE_EXCHANGE = "expire_exchange"; // 定义死信过期过期的队列
private final String EXPIRE_QUEUE = "expire_queue"; // 正常订单交换机
@Bean(ORDER_EXCHANGE)
public Exchange orderExchange() {
return ExchangeBuilder
.topicExchange(ORDER_EXCHANGE)
.durable(true)
.build();
} // 正常订单队列
@Bean(ORDER_QUEUE)
public Queue orderQueue() {
return QueueBuilder
.durable(ORDER_QUEUE)
.ttl(15000) // 正常队列消息存活时间模拟订单 30分钟为 10秒
.deadLetterExchange(EXPIRE_EXCHANGE) // 绑定死信过期交换机
.deadLetterRoutingKey("expire_orderRouting") // 绑定死信过期交换机的路由关键字
.build(); } // 正常订单交换机和队列绑定
@Bean
public Binding bindExchangeQueue(@Qualifier(ORDER_EXCHANGE) Exchange exchange,@Qualifier(ORDER_QUEUE) Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.orderRouting.#")
.noargs();
} // 死信过期交换机
@Bean(EXPIRE_EXCHANGE)
public Exchange expireExchange() {
return ExchangeBuilder
.topicExchange(EXPIRE_EXCHANGE)
.durable(true)
.build();
} // 死信过期队列
@Bean(EXPIRE_QUEUE)
public Queue expireQueue() {
return QueueBuilder
.durable(EXPIRE_QUEUE)
.build();
} // 死信过期交换机和队列进行绑定
@Bean
public Binding bindExpireExchangeQueue(@Qualifier(EXPIRE_EXCHANGE) Exchange exchange,@Qualifier(EXPIRE_QUEUE) Queue queue) {
return BindingBuilder
.bind(queue)
.to(exchange)
.with("#.expire_orderRouting.#")
.noargs();
}
}

④、创建生产者 OrderProviderController.java

  import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController; import java.util.HashMap;
import java.util.UUID; @RestController
@RequestMapping("provider")
public class OrderProviderController {
@Autowired
private RabbitTemplate rabbitTemplate; @GetMapping("/makeOrder")
public String makeOrder() {
String orderId = String.valueOf(UUID.randomUUID()); // 生成随机订单号
String orderInfo = "迈巴赫 S480";
HashMap<String, String> orderMap = new HashMap<>();
orderMap.put("orderId", orderId);
orderMap.put("orderInfo", orderInfo);
rabbitTemplate.convertAndSend("order_exchange","orderRouting",orderMap);
System.out.println("付款中...");
return "<p style=color:blue;text-align:center;top:20px;font-size:28px;>下单成功,订单号为:"+orderId+"</p>";
}
}

⑤、创建消费者 OrderConsumerController.java监听消息

  import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.ByteArrayInputStream;
import java.io.ObjectInputStream;
import java.util.Map; @Component
public class OrderConsumerController {
// 监听队列
@RabbitListener(queues = "expire_queue")
public void listenOrder(Message message) throws Exception {
byte[] body = message.getBody();
ObjectInputStream orderStream = new ObjectInputStream(new ByteArrayInputStream(body));
Map<String, String> orderObj = (Map<String, String>) orderStream.readObject();
String orderId = orderObj.get("orderId");
String orderInfo = orderObj.get("orderInfo");
System.out.println("付款成功,喜提一辆 "+orderInfo+",订单号为:"+orderId);
}
}

⑥、启动生产者,浏览器输入 http://127.0.0.1:9010/provider/makeOrder 观察RabbitMQ 控制台和终端

基于死信队列的延迟队列的使用看着使用也是很简单,但是一个消息要进入到死信队列,必须得满足当前这个消息消费到队列的顶端时才会进入到死信队列。所以,在使用死信队列实现延迟队列时,定会遇到这样的问题:RabbitMQ只会移除队列顶端的过期消息,如果第一个消息的存活时长较长,而第二个消息的存活时长较短,则第二个消息并不会及时执行,这样就很影响业务的正常运行。RabbitMQ 虽本身不能使用延迟队列,但是为了解决这个问题,官方提供了延迟队列插件,安装后可直接使用延迟队列。

延迟队列,即消息进入队列后不会立即被消费,只有到达指定时间后,才会被消费。

需求:

  1. 下单后,30分钟未支付,取消订单,回滚库存。

  2. 新用户注册成功7天后,发送短信问候。

实现方式:

  1. 定时器

  2. 延迟队列

十一、消息可靠性投递

提出问题:故障情况1

提出问题:故障情况2

提出问题:故障情况3

故障情况1:消息没有发送到消息队列

  • 解决思路A:在生产者端进行确认,具体操作中我们会分别针对 交换机 和 队列来确认,如果没有成功发送到消息队列服务器上,那就可以尝试重新发送

  • 解决思路B:为目标交换机指定备份交换机,当目标交换机投递失败时,把消息投递至备份交换机

故障情况2:消息队列服务器宕机导致内存中消息丢失

  • 解决思路:消息持久化到硬盘上,哪怕服务器重启也不会导致消息丢失

故障情况3:消费端宕机或抛异常导致消息没有成功被消费

  • 消费端消费消息成功,给服务器返回ACK信息,然后消息队列删除该消息

  • 消费端消费消息失败,给服务器端返回NACK信息,同时把消息恢复为待消费的状态,这样就可以再次取回消息,重试一次(当然,这就需要消费端接口支持幂等性)

故障情况1-解决思路A,执行案例

YAML配置

注意:publisher-confirm-type 和 publisher-returns 是两个必须要增加的配置,如果没有则本节功能不生效

创建配置类

1、目标

2、API说明

3、配置类代码

4、代码

  package com.atguigu.mq.config;

  import jakarta.annotation.PostConstruct;
import lombok.extern.slf4j.Slf4j;
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.stereotype.Component; @Component
@Slf4j
public class MQProducerAckConfig implements RabbitTemplate.ConfirmCallback, RabbitTemplate.ReturnsCallback{ @Autowired
private RabbitTemplate rabbitTemplate; @PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
rabbitTemplate.setReturnsCallback(this);
} @Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
log.info("消息发送到交换机成功!数据:" + correlationData);
} else {
log.info("消息发送到交换机失败!数据:" + correlationData + " 原因:" + cause);
}
} @Override
public void returnedMessage(ReturnedMessage returned) {
log.info("消息主体: " + new String(returned.getMessage().getBody()));
log.info("应答码: " + returned.getReplyCode());
log.info("描述:" + returned.getReplyText());
log.info("消息使用的交换器 exchange : " + returned.getExchange());
log.info("消息使用的路由键 routing : " + returned.getRoutingKey());
}
}

故障情况2--解决思路,持久化的交换机 和 队列,执行案例

我们其实不必专门创建持久化的交换机和队列,因为它们默认就是持久化的。接下来我们只需要确认一下:存放到队列中,尚未被消费端取走的消息,是否会随着RabbitMQ服务器重启而丢失 ?

1、发送消息

运行以前的发送消息方法即可,不过要关掉消费端程序

2、在管理界面查看消息

3、重启RabbitMQ服务器

docker restart rabbitmq

4、再次查看消息

故障情况3--解决思路,消费端消费消息成功,给服务器返回ACK信息;消费端消费消息失败,给服务器端返回NACK信息,执行案例

1、ACK

ACK是acknowledge的缩写,表示已确认

2、默认情况

默认情况下,消费端取回消息后,默认会自动返回ACK确认消息,所以在前面的测试中消息被消费端消费之后,RabbitMQ得到ACK确认信息就会删除消息

但实际开发中,消费端根据消息队列投递的消息执行对应的业务,未必都能执行成功,如果希望能够多次重试,那么默认设定就不满足要求了,所以还是要修改成手动确认

3、YAML

增加针对监听器的设置:

4、创建监听器类

5、在接收消息的方法上应用注解

  // 修饰监听方法
@RabbitListener(
// 设置绑定关系
bindings = @QueueBinding( // 配置队列信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
value = @Queue(value = QUEUE_NAME, durable = "true", autoDelete = "false"), // 配置交换机信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
exchange = @Exchange(value = EXCHANGE_DIRECT, durable = "true", autoDelete = "false"), // 配置路由键信息
key = {ROUTING_KEY}
))
public void processMessage(String dataString, Message message, Channel channel) { }

6、接收消息方法内部逻辑

  • 业务处理成功:手动返回ACK信息,表示消息成功消费

  • 业务处理失败:手动返回NACK信息,表示消息消费失败。此时有两种后续操作供选择:

    • 把消息重新放回消息队列,RabbitMQ会重新投递这条消息,那么消费端将重新消费这条消息——从而让业务代码再执行一遍

    • 不把消息放回消息队列,返回reject信息表示拒绝,那么这条消息的处理就到此为止

7、相关API

8、完整代码示例

    package com.atguigu.mq.listener;

    import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.Exchange;
import org.springframework.amqp.rabbit.annotation.Queue;
import org.springframework.amqp.rabbit.annotation.QueueBinding;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component; import java.io.IOException; @Component
@Slf4j
public class MyMessageListener { public static final String EXCHANGE_DIRECT = "exchange.direct.order";
public static final String ROUTING_KEY = "order";
public static final String QUEUE_NAME = "queue.order"; // 修饰监听方法
@RabbitListener(
// 设置绑定关系
bindings = @QueueBinding( // 配置队列信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
value = @Queue(value = QUEUE_NAME, durable = "true", autoDelete = "false"), // 配置交换机信息:durable 设置为 true 表示队列持久化;autoDelete 设置为 false 表示关闭自动删除
exchange = @Exchange(value = EXCHANGE_DIRECT, durable = "true", autoDelete = "false"), // 配置路由键信息
key = {ROUTING_KEY}
))
public void processMessage(String dataString, Message message, Channel channel) throws IOException { // 1、获取当前消息的 deliveryTag 值备用
long deliveryTag = message.getMessageProperties().getDeliveryTag(); try {
// 2、正常业务操作
log.info("消费端接收到消息内容:" + dataString); // System.out.println(10 / 0); // 3、给 RabbitMQ 服务器返回 ACK 确认信息
channel.basicAck(deliveryTag, false);
} catch (Exception e) { // 4、获取信息,看当前消息是否曾经被投递过
Boolean redelivered = message.getMessageProperties().getRedelivered(); if (!redelivered) {
// 5、如果没有被投递过,那就重新放回队列,重新投递,再试一次
channel.basicNack(deliveryTag, false, true);
} else {
// 6、如果已经被投递过,且这一次仍然进入了 catch 块,那么返回拒绝且不再放回队列
channel.basicReject(deliveryTag, false);
} }
}
}

9、要点总结

  • 要点1:把消息确认模式改为手动确认

  • 要点2:调用Channel对象的方法返回信息

    • ACK:Acknowledgement,表示消息处理成功
    • NACK:Negative Acknowledgement,表示消息处理失败
    • Reject:拒绝,同样表示消息处理失败
  • 要点3:后续操作

    • requeue为true:重新放回队列,重新投递,再次尝试
    • requeue为false:不放回队列,不重新投递
  • 要点4:deliveryTag 消息的唯一标识,查找具体某一条消息的依据

10、流程梳理

十二、消费端限流

1、思路

  • 生产者发送100个消息

  • 对照两种情况:

    • 消费端没有设置prefetch参数:100个消息被全部取回

    • 消费端设置prefetch参数为1:100个消息慢慢取回

2、测试

A、未使用prefetch

B、设定prefetch

①、YAML配置

spring:
rabbitmq:
host: 192.168.200.100
port: 5672
username: guest
password: 123456
virtual-host: /
listener:
simple:
acknowledge-mode: manual
prefetch: 1 # 设置每次最多从消息队列服务器取回多少消息

②、测试流程

十三、消息超时

  • 给消息设定一个过期时间,超过这个时间没有被取走的消息就会被删除

  • 我们可以从两个层面来给消息设定过期时间:

    • 队列层面:在队列层面设定消息的过期时间,并不是队列的过期时间。意思是这个队列中的消息全部使用同一个过期时间

    • 消息本身:给具体的某个消息设定过期时间

  • 如果两个层面都做了设置,那么哪个时间短,哪个生效

1、队列层面设置

A、设置

B、测试

2、消息层面设置

A、设置

import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessagePostProcessor; @Test
public void testSendMessageTTL() { // 1、创建消息后置处理器对象
MessagePostProcessor messagePostProcessor = (Message message) -> { // 设定 TTL 时间,以毫秒为单位
message.getMessageProperties().setExpiration("5000"); return message;
}; // 2、发送消息
rabbitTemplate.convertAndSend(
EXCHANGE_DIRECT,
ROUTING_KEY,
"Hello atguigu", messagePostProcessor);
}

B、查看效果

十四、死信 和 死信队列

1、死信

  • 概念:当一个消息无法被消费,它就变成了死信

  • 死信产生的原因大致有下面三种:

    • 拒绝:消费者拒接消息,basicNack() 或 basicReject(),并且不把消息重新放入原目标队列,requeue=false

    • 溢出:队列中消息数量到达限制。比如队列最大只能存储10条消息,且现在已经存储了10条,此时如果再发送一条消息进来,根据先进先出原则,队列中最早的消息会变成死信

    • 超时:原队列存在消息过期设置,消息到达超时时间未被消费

  • 死信的处理方式大致有下面三种:

    • 丢弃:对不重要的消息直接丢弃,不做处理

    • 入库:把死信写入数据库,日后处理

    • 监听:消息变成死信后进入死信队列,我们专门设置消费端监听死信队列,做后续处理(通常采用)

2、测试相关准备

A、创建死信交换机和死信队列

常规设定即可,没有特殊设置:

  • 死信交换机:exchange.dead.letter.video

  • 死信队列:queue.dead.letter.video

  • 死信路由键:routing.key.dead.letter.video

B、创建正常交换机和正常队列

C、Java代码中的相关常量声明

public static final String EXCHANGE_NORMAL = "exchange.normal.video";
public static final String EXCHANGE_DEAD_LETTER = "exchange.dead.letter.video"; public static final String ROUTING_KEY_NORMAL = "routing.key.normal.video";
public static final String ROUTING_KEY_DEAD_LETTER = "routing.key.dead.letter.video"; public static final String QUEUE_NORMAL = "queue.normal.video";
public static final String QUEUE_DEAD_LETTER = "queue.dead.letter.video";

3、场景一:消费端拒收消息

A、发送消息的代码

@Test
public void testSendMessageButReject() {
rabbitTemplate
.convertAndSend(
EXCHANGE_NORMAL,
ROUTING_KEY_NORMAL,
"测试死信情况1:消息被拒绝");
}

B、接收消息的代码

C、执行结果

4、场景二:消息数量超过队列容纳极限

A、发送消息的代码

@Test
public void testSendMultiMessage() {
for (int i = 0; i < 20; i++) {
rabbitTemplate.convertAndSend(
EXCHANGE_NORMAL,
ROUTING_KEY_NORMAL,
"测试死信情况2:消息数量超过队列的最大容量" + i);
}
}

B、接收消息的代码

C、执行效果

5、场景三:消息超时未消费

A、发送消息的代码

B、执行效果

十五、延迟队列

1、应用场景

2、实现思路

• 方案1:借助消息超时时间 + 死信队列(就是刚刚我们测试的例子)

• 方案2:给RabbitMQ安装插件

十六、事务消息

1、分析图

2、总结

• 在生产者端使用事务消息 和 消费端没有关系

• 在生产者端使用事务消息仅仅是控制事务内的消息是否发送

• 提交事务就把事务内所有消息都发送到交换机

• 回滚事务则事务内任何消息都不会被发送

十七、惰性队列

定义:惰性队列:未设置惰性模式时队列的持久化机制

创建队列时,在Durability这里有两个选项可以选择

  • Durable:持久化队列,消息会持久化到硬盘上

  • Transient:临时队列,不做持久化操作,broker重启后消息会丢失

  • 那么Durable队列在存入消息之后,是否是立即保存到硬盘呢 ?

十八、优先级队列

优先级队列:机制说明

• 默认情况:基于队列先进先出的特性,通常来说,先入队的先投递

• 设置优先级之后:优先级高的消息更大几率先投递

• 关键参数:x-max-priority

优先级队列:消息的优先级设置

• RabbitMQ允许我们使用一个正整数给消息设定优先级

• 消息的优先级数值取值范围:1~255

• RabbitMQ官网建议在1~5之间设置消息的优先级(优先级越高,占用CPU、内存等资源越多)

优先级队列:队列的优先级设置

• 队列在声明时可以指定参数:x-max-priority

• 默认值:0 此时消息即使设置优先级也无效

• 指定一个正整数值:消息的优先级数值不能超过这个值

优先级队列,执行案例

1、创建相关资源

A、创建交换机

exchange.test.priority

B、创建队列

queue.test.priority

x-max-priority

C、队列绑定交换机

2、发送消息

  • 不要启动消费者程序,让多条不同优先级的消息滞留在队列中

  • 第一次发送优先级为1的消息

  • 第二次发送优先级为2的消息

  • 第三次发送优先级为3的消息

  • 先发送的消息优先级低,后发送的消息优先级高,将来看看消费端是不是先收到优先级高的消息

①、第一次发送优先级为1的消息

②、第二次发送优先级为2的消息

③、第三次发送优先级为3的消息

3、监听器

  package com.atguigu.mq.listener;

  import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.*;
import org.springframework.stereotype.Component; @Slf4j
@Component
public class MyMessageProcessor { public static final String QUEUE_PRIORITY = "queue.test.priority"; @RabbitListener(queues = {QUEUE_PRIORITY})
public void processPriorityMessage(String data, Message message, Channel channel) throws IOException {
log.info(data); channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} }

4、测试效果

rabbitmq学习与总结的更多相关文章

  1. RabbitMQ学习系列(四): 几种Exchange 模式

    上一篇,讲了RabbitMQ的具体用法,可以看看这篇文章:RabbitMQ学习系列(三): C# 如何使用 RabbitMQ.今天说些理论的东西,Exchange 的几种模式. AMQP协议中的核心思 ...

  2. RabbitMQ学习系列(三): C# 如何使用 RabbitMQ

    上一篇已经讲了Rabbitmq如何在Windows平台安装,还不了解如何安装的朋友,请看我前面几篇文章:RabbitMQ学习系列一:windows下安装RabbitMQ服务 , 今天就来聊聊 C# 实 ...

  3. RabbitMQ学习总结 第三篇:工作队列Work Queue

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  4. RabbitMQ学习总结 第一篇:理论篇

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  5. RabbitMQ学习总结 第二篇:快速入门HelloWorld

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  6. RabbitMQ学习总结 第四篇:发布/订阅 Publish/Subscribe

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  7. RabbitMQ学习总结 第五篇:路由Routing

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  8. RabbitMQ学习总结 第六篇:Topic类型的exchange

    目录 RabbitMQ学习总结 第一篇:理论篇 RabbitMQ学习总结 第二篇:快速入门HelloWorld RabbitMQ学习总结 第三篇:工作队列Work Queue RabbitMQ学习总结 ...

  9. RabbitMQ学习笔记(五) Topic

    更多的问题 Direct Exchange帮助我们解决了分类发布与订阅消息的问题,但是Direct Exchange的问题是,它所使用的routingKey是一个简单字符串,这决定了它只能按照一个条件 ...

  10. RabbitMQ学习之旅(一)

    RabbitMQ学习总结(一) RabbitMQ简介 RabbitMQ是一个消息代理,其接收并转发消息.类似于现实生活中的邮局:你把信件投入邮箱的过程,相当于往队列中添加信息,因为所有邮箱中的信件最终 ...

随机推荐

  1. 【JMeter】---入门

    JMeter入门 一.概述 JMeter是Apache下一款在国外非常流行和受欢迎的开源性能测试工具,JMeter可用于模拟大量负载来测试一台服务器,网络或者对象的健壮性或者分析不同负载下的整体性能. ...

  2. Dicom C-move 请求QR服务

    个人理解 Dicom C-get 就是在没有设置任何验证情况下请求QR服务,而C-move是有验证的情况下请求QR服务.一般都是C-move,因为机器都需要验证. Dicom C-move 原理:自己 ...

  3. Collection接口与其子接口实现类-----总复习

    数组与集合 1. 集合与数组存储数据概述:集合.数组都是对多个数据进行存储操作的结构,简称Java容器.说明:此时的存储,主要指的是内存层面的存储,不涉及到持久化的存储(.txt,.jpg,.avi, ...

  4. manim边学边做--时针方向变换

    今天介绍的两个动画类ClockwiseTransform和CounterclockwiseTransform, 用于将某一个元素按照时针方向变换为另一个对象. ClockwiseTransform:将 ...

  5. VS2022编译项目出现““csc.exe”已退出,代码为 -1073741819”的错误解决办法

    1.问题描述 编译出错如下图所示: 2.解决办法 在NuGet包中输入Microsoft.Net.Compilers,安装该包,安装完后重新生成就不报错了,如下图所示:

  6. P3306 [SDOI2013] 随机数生成器 题解

    传送门 题解 思路 由题目中可知: \[\large x_i \equiv ax_{i-1}+b\pmod{p} \] 可以得出: \[\large t=x_{n+1} \equiv a^nx_1+b ...

  7. datawhale-leetcode打卡 第013-025题

    搜索旋转排序数组(leetcode-033) 这道题非常简单,基本送分,之前做的代码还能用上 class Solution: def search(self, nums: List[int], tar ...

  8. 前端视角看 HTTPS

    最近用Docusaurus搭了一个个人网站,部署后看到浏览器地址栏上"不安全"三个字感觉特别辣眼,便不由自主的想起了HTTPS.回忆起自己在日常开发中遇到的一些与HTTPS相关的知 ...

  9. c++用正则表达式判断匹配字符串中的数字数值(包括负数,小数,整数)MFC编辑框判断数值

    原文作者:aircraft 原文链接:https://www.cnblogs.com/DOMLX/p/12097381.html 因为今天做那个MFC的编辑框有一些框就是要判断输入的是否是数值,一开始 ...

  10. FolderMove:盘符文件/软件迁移工具,快速给C盘瘦身

    前言 很多朋友安装软件的时候总会直接点击下一步,每次都把软件安装到了C盘.时间长了以后系统C盘就会爆满,只能重做系统处理,有了这个软件就可以随时把C盘文件转移到其他分区 介绍 这款是国外软件,界面介绍 ...