1. 基本概念
RabbitMQ是一个由erlang开发的AMQP(Advanced Message Queue )的开源实现。
1.1 消息模型
-
Broker:简单来说就是消息队列服务器实体。
-
Exchange:消息交换机,它指定消息按什么规则,路由到哪个队列。
-
Queue:消息队列载体,每个消息都会被投入到一个或多个队列。
-
Binding:绑定,它的作用就是把exchange和queue按照路由规则绑定起来。
-
Routing Key:路由关键字,由消息携带,exchange根据这个关键字进行消息投递。
-
vhost:虚拟主机,一个broker里可以开设多个vhost,用作不同用户的权限分离。
-
producer:消息生产者,就是投递消息的程序。
-
consumer:消息消费者,就是接受消息的程序。
-
channel:消息通道,在客户端的每个连接里,可建立多个channel,每个channel代表一个会话任务。
1.2 细节阐述
1. Message acknowledgment
在实际应用中,可能会发生消费者收到Queue中的消息,但没有处理完成就宕机(或出现其他意外)的情况,这种情况下就可能会导致消息丢失。为了避免这种情况发生,我们可以要求消费者在消费完消息后发送一个回执给RabbitMQ,RabbitMQ收到消息回执(Message acknowledgment)后才将该消息从Queue中移除;如果RabbitMQ没有收到回执并检测到消费者的RabbitMQ连接断开,则RabbitMQ会将该消息发送给其他消费者(如果存在多个消费者)进行处理。这里不存在timeout概念,一个消费者处理消息时间再长也不会导致该消息被发送给其他消费者,除非它的RabbitMQ连接断开。
这里会产生另外一个问题,如果我们的开发人员在处理完业务逻辑后,忘记发送回执给RabbitMQ,这将会导致严重的bug——Queue中堆积的消息会越来越多;消费者重启后会重复消费这些消息并重复执行业务逻辑。
2. Message durability
如果我们希望即使在RabbitMQ服务重启的情况下,也不会丢失消息,我们可以将Queue与Message都设置为可持久化的(durable),这样可以保证绝大部分情况下我们的RabbitMQ消息不会丢失 。
3. Prefetch count
前面我们讲到如果有多个消费者同时订阅同一个Queue中的消息,Queue中的消息会被平摊给多个消费者。这时如果每个消息的处理时间不同,就有可能会导致某些消费者一直在忙,而另外一些消费者很快就处理完手头工作并一直空闲的情况。我们可以通过设置prefetchCount来限制Queue每次发送给每个消费者的消息数,比如我们设置prefetchCount=1,则Queue每次给每个消费者发送一条消息;消费者处理完这条消息后Queue会再给该消费者发送一条消息。
4. Exchange Types
RabbitMQ常用的Exchange Type有fanout、direct、topic、headers这四种(AMQP规范里还提到两种Exchange Type,分别为system与自定义,这里不予以描述)
- fanout
fanout类型的Exchange路由规则非常简单,它会把所有发送到该Exchange的消息路由到所有与它绑定的Queue中。
上图中,生产者(P)发送到Exchange(X)的所有消息都会路由到图中的两个Queue,并最终被两个消费者(C1与C2)消费。
- direct
direct类型的Exchange路由规则也很简单,它会把消息路由到那些binding key与routing key完全匹配的Queue中。
以上图的配置为例,我们以routingKey=”error”发送消息到Exchange,则消息会路由到Queue1(amqp.gen-S9b…,这是由RabbitMQ自动生成的Queue名称)和Queue2(amqp.gen-Agl…);如果我们以routingKey=”info”或routingKey=”warning”来发送消息,则消息只会路由到Queue2。如果我们以其他routingKey发送消息,则消息不会路由到这两个Queue中。
- topic
前面讲到direct类型的Exchange路由规则是完全匹配binding key与routing key,但这种严格的匹配方式在很多情况下不能满足实际业务需求。topic类型的Exchange在匹配规则上进行了扩展,它与direct类型的Exchage相似,也是将消息路由到binding key与routing key相匹配的Queue中,但这里的匹配规则有些不同,它约定:
routing key为一个句点号“.”分隔的字符串(我们将被句点号“. ”分隔开的每一段独立的字符串称为一个单词),如“stock.usd.nyse”、“nyse.vmw”、“quick.orange.rabbit” binding key与routing key一样也是句点号“. ”分隔的字符串
binding key中可以存在两种特殊字符“*”与“#”,用于做模糊匹配,其中“*”用于匹配一个单词,“#”用于匹配多个单词(可以是零个)
以上图中的配置为例,routingKey=”quick.orange.rabbit”的消息会同时路由到Q1与Q2,routingKey=”lazy.orange.fox”的消息会路由到Q1,routingKey=”lazy.brown.fox”的消息会路由到Q2,routingKey=”lazy.pink.rabbit”的消息会路由到Q2(只会投递给Q2一次,虽然这个routingKey与Q2的两个bindingKey都匹配);routingKey=”quick.brown.fox”、routingKey=”orange”、routingKey=”quick.orange.male.rabbit”的消息将会被丢弃,因为它们没有匹配任何bindingKey。
- headers
headers类型的Exchange不依赖于routing key与binding key的匹配规则来路由消息,而是根据发送的消息内容中的headers属性进行匹配。 不常用。
5. RPC
MQ本身是基于异步的消息处理,前面的示例中所有的生产者(P)将消息发送到RabbitMQ后不会知道消费者(C)处理成功或者失败(甚至连有没有消费者来处理这条消息都不知道)。
但实际的应用场景中,我们很可能需要一些同步处理,需要同步等待服务端将我的消息处理完成后再进行下一步处理。这相当于RPC(Remote Procedure Call,远程过程调用)。
在RabbitMQ中也支持RPC
6. 谁应该负责创建queue呢?是Consumer,还是Producer?
如果queue不存在,当然Consumer不会得到任何的Message。但是如果queue不存在,那么Producer Publish的Message会被丢弃。所以,还是为了数据不丢失,Consumer和Producer都try to create the queue!反正不管怎么样,这个接口都不会出问题。
2. 安装部署
RabbitMQ 有三种模式:单机模式,普通集群模式,镜像集群模式。
1. 单机模式
即单独运行一个 rabbitmq 实例,而集群模式需要创建多个 rabbitmq实例。
2. 普通集群模式
上图中采用三个节点组成了一个RabbitMQ集群,Exchange A(交换器)的元数据信息在所有的节点上是一致的,而Queue的完整数据则只会存在于它所创建的那个节点上,其它节点只知道这个queue的metadata信息和一个指向queue的owner node的指针。
RabbitMQ集群元数据的同步
RabbitMQ集群会始终同步四种类型的内部元数据(类似索引):
-
队列元数据:队列名称和它的属性;
-
交换器元数据:交换器名称、类型和属性;
-
绑定元数据:一张简单的表格展示了如何将消息路由到队列;
-
vhost元数据:为vhost内的队列、交换器和绑定提供命名空间和安全属性;
因此,当用户访问其中任何一个RabbitMQ节点时,通过rabbitmqctl查询到的queue、user、exchange、vhost等信息都是相同的。
为何RabbitMQ集群仅采用元数据同步的方式
我想肯定会有不少同学会问,要想实现HA方案,那将RabbitMQ集群中的所有Queue的完整数据在所有节点上都保存一份不就可以了么?(可以类似MySQL的主从模式),这样子,任何一个节点出现故障或者跌机不可用时,那么使用者的客户端只要能连接至其他节点能够照常完成消息的发布和订阅。
我想RabbitMQ的作者这么设计主要还是基于集群本身的性能和存储空间上来考虑。
- 第一,存储空间,如果每个集群节点都拥有所有Queue的完全数据拷贝,那么每个节点的存储空间会非常大,集群的消息积压能力会非常弱(无法通过集群节点的扩容提高消息积压能力)
- 第二,性能,消息的发布者需要将消息复制到每一个集群节点,对于持久化消息,网络和磁盘同步复制的开销都会明显的增加。
RabbitMQ集群发送/订阅消息的基本原理
RabbitMQ集群的工作原理如下图:
场景1:客户端直接连接队列所在节点
如果有一个消息生产者或者消息消费者通过amqp-client的客户端连接至节点1进行消息的发布或者订阅,那么此时的集群中的消息收发只与节点1相关,这个没有任何问题;如果客户端相连的是节点2或者节点3(队列1数据不在该节点上),那么情况又会是什么样呢?
场景2:客户端连接的是非队列数据所在节点
如果消息生产者所连接的是节点2或者节点3,此时队列1的完整数据不在该两个节点上,那么在发送消息过程中这两个节点主要起了一个路由转发作用,根据这两个节点上的元数据(也就是上文提到的:指向queue的owner node的指针)转发至节点1上,最终发送的消息还是会存储至节点1的队列上。
同样,如果消息消费者所连接的节点2或者节点3,那这两个节点也会作为路由节点起到转发作用,将会从节点1的队列1中拉取消息进行消费。 所以 consumer 应尽量连接每一个节点。并针对同一个逻辑队列,要在多个节点建立物理 Queue。否则无论 consumer 连接哪个节点,都会从创建 queue 的节点获取消息,会产生瓶颈。
磁盘节点和RAM节点
一个节点可以是一个磁盘(disk)节点或者一个RAM节点(注意:磁盘和光盘可以互换使用)。RAM节点仅将内部数据库表存储在RAM中,这不包括消息,消息存储索引,队列索引和其它节点状态(即,如果消息声明为持久化的话,这些信息还是安全的存在磁盘的)。
在大多数情况下你希望所有的节点都是磁盘(disk)节点,RAM节点是一种特殊情况,可用于提高队列、交换器或绑定交换频率比较高的性能。RAM节点不提供更高的发布/消费消息速率,如有疑问,请仅使用磁盘节点。
由于RAM节点仅将内部数据库表存储在RAM中,因此他们必须在启动时从对等节点同步它们。这意味着一个集群必须至少包含一个磁盘节点。因此,不可能手动删除集群中最后剩余的磁盘节点。
3. 镜像集群模式
把队列做成镜像队列,让各队列存在于多个节点中,属于 RabbitMQ 的高可用性方案。镜像模式和普通模式的不同在于,queue和 message 会在集群各节点之间同步,而不是在 consumer 获取数据时临时拉取。镜像模式可在policy中进行设置。特点:
- 实现了高可用性。部分节点挂掉后,不会影响 rabbitmq 的使用。
- 降低了系统性能。镜像队列数量过多,大量的消息同步也会加大网络带宽开销。
- 适合对可用性要求较高的业务场景。
2.1 单机部署
2.1.1 下载rabbitmq镜像
[root@lgying rabbitmq]# docker pull rabbitmq:3.8-management
注意:使用后缀为“-management”的镜像版本,是包含网页控制台的。
2.1.2 使用docker images命令查看下载的镜像
[root@lgying rabbitmq]# docker images
REPOSITORY TAG IMAGE ID CREATED SIZE
rabbitmq 3.8-management 30e33de9be86 8 days ago 184MB
2.1.3 启动三个相互独立的RabbitMQ服务
docker run -d --hostname localhost --name rabbit1 -p 15672:15672 -p 5672:5672 rabbitmq:3.8-management
docker run -d --hostname localhost --name rabbit2 -p 15673:15672 -p 5673:5672 rabbitmq:3.8-management
docker run -d --hostname localhost --name rabbit3 -p 15674:15672 -p 5674:5672 rabbitmq:3.8-management
这样我们就可以通过:http://ip:15672、http://ip:15673、http://ip:15674来访问各个单例服务;
2.2 集群部署—docker
2.2.1 部署rabbitmq服务
docker run -d --hostname rabbit1 --name rabbitmq1 -p 15672:15672 -p 5672:5672 -v /home/lgying/rabbitmq/mq1/conf:/etc/rabbitmq -v /home/lgying/rabbitmq/mq1/log:/var/log/rabbitmq/log -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --restart=always rabbitmq:3.8-management
docker run -d --hostname rabbit2 --name rabbitmq2 -p 15673:15672 -p 5673:5672 -v /home/lgying/rabbitmq/mq2/conf:/etc/rabbitmq -v /home/lgying/rabbitmq/mq2/log:/var/log/rabbitmq/log --link rabbitmq1:rabbit1 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --restart=always rabbitmq:3.8-management
docker run -d --hostname rabbit3 --name rabbitmq3 -p 15674:15672 -p 5674:5672 -v /home/lgying/rabbitmq/mq3/conf:/etc/rabbitmq -v /home/lgying/rabbitmq/mq3/log:/var/log/rabbitmq/log --link rabbitmq1:rabbit1 --link rabbitmq2:rabbit2 -e RABBITMQ_ERLANG_COOKIE='rabbitcookie' --restart=always rabbitmq:3.8-management
主要参数:
- -p 15672:15672 management 界面管理访问端口
- -p 5672:5672 amqp 访问端口
- --link 容器之间连接
- Erlang Cookie 值必须相同,也就是一个集群内 RABBITMQ_ERLANG_COOKIE 参数的值必须相同。因为 RabbitMQ 是用Erlang实现的,Erlang Cookie 相当于不同节点之间通讯的密钥,Erlang节点通过交换 Erlang Cookie 获得认证。
2.2.2 将RabbitMQ节点加入集群
设置节点1:
[root@lgying rabbitmq]# docker exec -it rabbitmq1 bash
root@rabbit1:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit1 ...
root@rabbit1:/# rabbitmqctl reset
Resetting node rabbit@rabbit1 ...
root@rabbit1:/# rabbitmqctl start_app
Starting node rabbit@rabbit1 ...
completed with 0 plugins.
root@rabbit1:/#
设置节点2,加入到集群:
[root@lgying rabbitmq]# docker exec -it rabbitmq2 bash
root@rabbit2:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit2 ...
root@rabbit2:/# rabbitmqctl reset
Resetting node rabbit@rabbit2 ...
root@rabbit2:/# rabbitmqctl join_cluster --ram rabbit@rabbit1
Clustering node rabbit@rabbit2 with rabbit@rabbit1
root@rabbit2:/# rabbitmqctl start_app
Starting node rabbit@rabbit2 ...
completed with 0 plugins.
参数“–ram”表示设置为内存节点,忽略此参数默认为磁盘节点。
设置节点3,加入集群:
[root@lgying rabbitmq]# docker exec -it rabbitmq3 bash
root@rabbit3:/# rabbitmqctl stop_app
Stopping rabbit application on node rabbit@rabbit3 ...
root@rabbit3:/# rabbitmqctl reset
Resetting node rabbit@rabbit3 ...
root@rabbit3:/# rabbitmqctl join_cluster rabbit@rabbit1
Clustering node rabbit@rabbit3 with rabbit@rabbit1
root@rabbit3:/# rabbitmqctl start_app
Starting node rabbit@rabbit3 ...
completed with 0 plugins.
root@rabbit3:/#
设置好之后,使用http:物理机IP:15672,默认账号密码是:guest/guest
2.2.3 rabbitmq集群其他命令
#查看集群的状态
rabbitmqctl cluster_status
#停止节点
rabbitmqctl stop_app
#启动节点
rabbitmqctl start_app
#重置节点
rabbitmqctl reset
从集群中删除节点
#停止节点
rabbitmqctl stop_app
#重置节点
rabbitmqctl reset
#启动节点
rabbitmqctl start_app
我们可以远程删除节点,例如,在必须处理无响应的节点时,这很有用,例如可以删除rabbit@rabbit1节点从rabbit@rabbit2节点
# on rabbit1
rabbitmqctl stop_app
# on rabbit2
rabbitmqctl forget_cluster_node rabbit@rabbit1
注意:此时rabbit1仍然认为和rabbit2是在同一个集群,并且试图启动它将会报错,我们将会重置之后再重启。
# on rabbit1
rabbitmqctl start_app
# => Starting node rabbit@rabbit1 ...
# => Error: inconsistent_cluster: Node rabbit@rabbit1 thinks it's clustered with node rabbit@rabbit2, but rabbit@rabbit2 disagrees
rabbitmqctl reset
# => Resetting node rabbit@rabbit1 ...done.
rabbitmqctl start_app
# => Starting node rabbit@rabbit1 ...
# => ...done.
集群节点重置(reset)
有时可能需要重置节点(擦除其所有数据),然后使其重新加入集群。一般来说,有两种可能情况:节点正在运行时,以及节点由于诸如 ERL-430之 类的问题而无法启动或无法响应CLI工具命令时。
重置节点将删除其所有数据,集群成员信息,已配置的运行时参数,用户,虚拟主机以及任何其它节点数据。它还将从该群集中永久删除该节点。
要重置一个正在运行的响应节点,请首先使用rabbitmqctl stop_app停止RabbitMQ,然后使用rabbitmqctl reset对其进行重置:
# on rabbit1
rabbitmqctl stop_app
# => Stopping node rabbit@rabbit1 ...done.
rabbitmqctl reset
# => Resetting node rabbit@rabbit1 ...done.
对于无响应的节点,必须先使用任何必要的方法将其停止。对于无法启动的节点,情况也是如此。
已重置并重新加入其原始集群的节点将同步所有虚拟主机,用户,权限和拓扑(队列,交换,绑定),运行时参数和策略。如果选择托管副本,他可能会同步镜像队列的内容。重置节点上的非镜像队列内容将丢失。
更改节点的类型disk|ram
# on rabbit3
rabbitmqctl stop_app
# => Stopping node rabbit@rabbit3 ...done.
rabbitmqctl change_cluster_node_type ram
# => Turning rabbit@rabbit3 into a ram node ...done.
rabbitmqctl start_app
# => Starting node rabbit@rabbit3 ...done.
2.3 集群部署—docker-compose
2.3.1 安装docker-compose
#下载最新的docker-compose文件
curl -L https://github.com/docker/compose/releases/download/1.16.1/docker-compose-`uname -s`-`uname -m` -o /usr/local/bin/docker-compose
#下载完成后需要对/usr/local/bin/docker-compose目录进行赋权
chmod +x /usr/local/bin/docker-compose
#测试结果
docker-compose --version
输出
docker-compose version 1.16.1, build 6d1ac21
#卸载方式
rm /usr/local/bin/docker-compose
2.3.2 docker-compose常用命令
命令 | 描述 |
---|---|
docker-compose up -d nginx | 构建建启动nignx容器 |
docker-compose exec nginx bash | 登录到nginx容器中 |
docker-compose down | 删除所有nginx容器,镜像 |
docker-compose ps | 显示所有容器 |
docker-compose restart nginx | 重新启动nginx容器 |
docker-compose run --no-deps --rm php-fpm php -v | 在php-fpm中不启动关联容器,并容器执行php -v 执行完成后删除容器 |
docker-compose build nginx | 构建镜像 |
docker-compose build --no-cache nginx | 不带缓存的构建 |
docker-compose logs nginx | 查看nginx的日志 |
docker-compose logs -f nginx | 验证(docker-compose.yml)文件配置,当配置正确时,不输出任何内容,当文件配置错误,输出错误信息 |
docker-compose pause nginx | 暂停nignx容器 |
docker-compose unpause nginx | 恢复ningx容器 |
docker-compose rm nginx | 删除容器(删除前必须关闭容器) |
docker-compose stop nginx | 停止nignx容器 |
docker-compose start nginx | 启动nignx容器 |
2.3.3 使用docker-compose部署rabbitmq集群
部署rabbitmq集群(单机版)
rabbit1:
image: rabbitmq:3.8-management
hostname: rabbit1
ports:
- "5672:5672"
- "15672:15672"
environment:
- RABBITMQ_DEFAULT_USER=lgy
- RABBITMQ_DEFAULT_PASS=123456
rabbit2:
image: rabbitmq:3.8-management
hostname: rabbit2
links:
- rabbit1
environment:
- CLUSTERED=true
- CLUSTER_WITH=rabbit1
- RAM_NODE=true
ports:
- "5673:5672"
- "15673:15672"
rabbit3:
image: rabbitmq:3.8-management
hostname: rabbit3
links:
- rabbit1
- rabbit2
environment:
- CLUSTERED=true
- CLUSTER_WITH=rabbit1
ports:
- "5674:5672"
- "15674:15672"
部署rabbitmq集群(多机版)
上面我们使用docker-compose搭建出了一个rabbitmq单机集群,这种模式只能用来测试玩玩,无法再生产环境中使用。在生产环境中,需要把3个节点的集群分布到各个主机上面去。这个时候docker-compose就需要做调整了,主要是对外端口和link方式。
#abbit1(10.106.136.7)
rabbit1:
image: rabbitmq:3.8-management
hostname: rabbit1
ports:
- "5672:5672"
- "4369:4369"
- "1883:1883"
- "15672:15672"
- "25672:25672"
environment:
- RABBITMQ_DEFAULT_USER=lgy
- RABBITMQ_DEFAULT_PASS=123456
#rabbit2(10.106.136.8)
rabbit2:
image: rabbitmq:3.8-management
hostname: rabbit2
extra_hosts:
- "rabbit1:10.106.136.7"
environment:
- CLUSTERED=true
- CLUSTER_WITH=rabbit1
- RAM_NODE=true
ports:
- "5672:5672"
- "4369:4369"
- "1883:1883"
- "15672:15672"
- "25672:25672"
#rabbit3(10.106.136.9)
rabbit3:
image: rabbitmq:3.8-management
hostname: rabbit3
extra_hosts:
- "rabbit1:10.106.136.7"
- "rabbit2:10.106.136.8"
environment:
- CLUSTERED=true
- CLUSTER_WITH=rabbit1
- RAM_NODE=true
ports:
- "5672:5672"
- "4369:4369"
- "1883:1883"
- "15672:15672"
- "25672:25672"
2.4. 参考文章
rabbitmq集群-docker部署
RabbitMQ学习笔记:使用Docker部署RabbitMQ集群_rabbitmq docker-CSDN博客 https://www.cnblogs.com/alan6/p/11691229.html https://segmentfault.com/q/1010000021555885/ rabbitmq 集群功能讲解_rabbitmq集群-CSDN博客 rabbitmq 集群数据存储与单点故障-CSDN博客
rabbitmq集群-docker-compose部署
http://michael728.github.io/2019/06/07/docker-rabbitmq-env/ 使用Docker-Compose搭建Rabbitmq集群-CSDN博客 https://github.com/bijukunjummen/docker-rabbitmq-cluster https://www.cnblogs.com/cheyunhua/p/8362200.html docker-compose配置rabbitmq集群服务器_docker-compose rabbitmq3.6-CSDN博客 关于docker-Compose基本使用 - 简书 https://www.cnblogs.com/linjiqin/p/8849432.html
3. 使用
3.1 pom.xml引入依赖
<!--mq-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
3.2 application.properties配置文件
# rabbitmq
spring.rabbitmq.host=192.168.0.103
spring.rabbitmq.port=5676
spring.rabbitmq.username=guest
spring.rabbitmq.password=guest
# 设置手动确认(ack) Consumer -> Queue
spring.rabbitmq.listener.simple.acknowledge-mode=manual
# 并发量配置
# 并发消费者的初始化值
spring.rabbitmq.listener.simple.concurrency=3
# 并发消费者的最大值
spring.rabbitmq.listener.simple.max-concurrency=5
# 每个消费者每次监听时可拉取处理的消息数量,即Consumer每次抓取消息至队列缓存的数量
spring.rabbitmq.listener.simple.prefetch=2
#消息模型
lgy.test.queue=lgy-queue
lgy.test.exchange=lgy-exchange
lgy.test.binding=lgy.direct.binding
3.3. Rabbitmq相关配置类
package com.lgy.frame.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.*;
import org.springframework.amqp.rabbit.config.SimpleRabbitListenerContainerFactory;
import org.springframework.amqp.rabbit.connection.CachingConnectionFactory;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.amqp.SimpleRabbitListenerContainerFactoryConfigurer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.env.Environment;
@Configuration
@Slf4j
public class RabbitConfig {
@Autowired
private Environment env;
@Autowired
private Jackson2JsonMessageConverter jackson2JsonMessageConverter;
@Autowired
private CachingConnectionFactory connectionFactory;
@Autowired
private SimpleRabbitListenerContainerFactoryConfigurer factoryConfigurer;
/*
* 消息模型配置(queue、exchange、binding)
* 说明:可以在生产者中配置,也可以在消费者中配置,重复创建属性相同无影响,若属性不同则会报错
*/
@Bean
public Queue lgyQueue() {
// 第一个参数是创建的queue的名字,第二个参数是是否支持持久化
return new Queue(env.getProperty("lgy.test.queue"), true);
}
@Bean
public DirectExchange lgyExchange() {
// 一共有三种构造方法,
// 第一种,可以只传exchange的名字;
// 第二种,可以传exchange名字,是否支持持久化,是否可以自动删除;
// 第三种在第二种参数上可以增加Map,Map中可以存放自定义exchange中的参数
return new DirectExchange(env.getProperty("lgy.test.exchange"), true, false);
}
@Bean
public Binding lgyBinding() {
return BindingBuilder.bind(lgyQueue()).to(lgyExchange()).with(env.getProperty("lgy.test.binding"));
}
/*消费者配置
* SimpleRabbitListenerContainerFactory
* 作用:用于管理 RabbitMQ监听器listener 的容器工厂
* 配置:springboot提供了SimpleRabbitListenerContainerFactory的默认实现,参数可在配置文件中定义
* 也可按照如下方式进行自定义
* 使用: @RabbitListener(queues = "", containerFactory="singleListenerContainer")
* 如此方式给消费者指定 容器工厂
*/
/*
* 1、单一消费者
* @return
*/
@Bean(name = "singleListenerContainer")
public SimpleRabbitListenerContainerFactory listenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factory.setConnectionFactory(connectionFactory);
factory.setMessageConverter(jackson2JsonMessageConverter);
factory.setConcurrentConsumers(1);
factory.setMaxConcurrentConsumers(1);
factory.setPrefetchCount(1);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
return factory;
}
/*
* 2、多个消费者
* @return
*/
@Bean(name = "multiListenerContainer")
public SimpleRabbitListenerContainerFactory multiListenerContainer(){
SimpleRabbitListenerContainerFactory factory = new SimpleRabbitListenerContainerFactory();
factoryConfigurer.configure(factory,connectionFactory);
factory.setMessageConverter(jackson2JsonMessageConverter);
factory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
factory.setConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.concurrency", int.class));
factory.setMaxConcurrentConsumers(env.getProperty("spring.rabbitmq.listener.simple.max-concurrency", int.class));
factory.setPrefetchCount(env.getProperty("spring.rabbitmq.listener.simple.prefetch", int.class));
return factory;
}
/*生产者配置
* RabbitTemplate
* 作用:为生产者发送消息所用的消息发送组件
* 配置:可进行以下配置,
* 1、confirmCallback 消息发送 生产者 -> exchange 是否成功,可根据ack的发送结果进行相应的业务处理
* 注意:confirm失败的原因可能为mq内部错误,导致消息没有存入mq存储,此时不会return
* 2、returnCallback 消息发送 生产者 -> exchange 是否被丢弃,不设置此方法,消息找不到queue会直接丢 * 弃,故通过此方法可以接收丢弃的消息,进行相应的业务处理
* 注意:return回来的消息,为exchange没有找到匹配的queue,此时消息confirmack=true
* 使用:生产者发送消息的时候,使用这个类
* 注意:如果有多个生产者,confirmCallback和returnCallback的逻辑可能不一样
* 这时候,可以使用 @Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE) 注解,非单例模式
* 然后每个生产者再去自定义confirmCallback和returnCallback方法
*/
@Bean
public RabbitTemplate rabbitTemplate() {
RabbitTemplate rabbitTemplate = new RabbitTemplate(connectionFactory);
rabbitTemplate.setMessageConverter(jackson2JsonMessageConverter);
// 消息是否成功发送到Exchange
rabbitTemplate.setConfirmCallback((correlationData, ack, cause) -> {
if (ack) {
String msgId = correlationData.getId();
log.info("消息成功发送到Exchange, msgId = " + msgId);
//实际项目中,为保证消息的可靠传输,这里可以记录消息的状态为已发送成功
} else {
log.info("消息发送到Exchange失败, {}, cause: {}", correlationData, cause);
//实际项目中,为保证不丢失消息,这里可以将未成功发送的消息记录下来,起异步任务重新发送
}
});
// 触发setReturnCallback回调必须设置mandatory=true, 否则Exchange没有找到Queue就会丢弃掉消息, 而不会触发回调
rabbitTemplate.setMandatory(true);
// 消息是否从Exchange路由到Queue, 注意: 这是一个失败回调, 只有消息从Exchange路由到Queue失败才会回调这个方法
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey) -> {
log.info("消息从Exchange路由到Queue失败: exchange: {}, route: {}, replyCode: {}, replyText: {}, message: {}", exchange, routingKey, replyCode, replyText, message);
//实际项目中,为保证不丢失消息,这里可以将未成功发送的消息记录下来,查看消息被丢弃的原因
});
return rabbitTemplate;
}
}
package com.lgy.frame.config;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.support.converter.Jackson2JsonMessageConverter;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
@Slf4j
public class MessageConverterConfig {
@Bean
public Jackson2JsonMessageConverter jackson2JsonMessageConverter() {
return new Jackson2JsonMessageConverter();
}
}
3.4 生产者代码
package com.lgy.frame.mq.producer;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.stereotype.Component;
import java.util.UUID;
@Slf4j
@Component
public class MqProducer {
@Autowired
private Environment env;
@Autowired
private RabbitTemplate rabbitTemplate;
public String send(String message) {
String msgId = UUID.randomUUID().toString().replaceAll("-","");
CorrelationData correlationData = new CorrelationData(msgId);
// 发送消息
rabbitTemplate.convertAndSend(env.getProperty("lgy.test.exchange"), env.getProperty("lgy.test.binding"), message, correlationData);
log.info("生产者发送消息, {}:{}", msgId, message);
return "success";
}
}
3.5. 消费者代码
package com.lgy.frame.mq.consumer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.core.MessageProperties;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.io.IOException;
@Slf4j
@Component
@RabbitListener(queues = "${lgy.test.queue}",
//手动指明消费者的监听容器,默认Spring为自动生成一个SimpleMessageListenerContainer
containerFactory = "singleListenerContainer")
public class MqConsumer {
@RabbitHandler
public void consume(Message message, Channel channel) throws IOException {
log.info("消费者收到消息: {}", new String(message.getBody()));
MessageProperties properties = message.getMessageProperties();
long tag = properties.getDeliveryTag();
//执行业务逻辑的结果
boolean success = true;
if (success) {
// 实际项目中,可更新消息的状态为消费成功了
// 消费确认
channel.basicAck(tag, false);
} else {
//消息消费异常,可入queue重新消费,或者不入queue直接丢弃消息
channel.basicNack(tag, false, true);
}
}
}
3.6 发送失败的消息定时重发调度任务
package com.lgy.frame.mq.schedule;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.connection.CorrelationData;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.env.Environment;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.List;
import java.util.UUID;
@Component
@Slf4j
public class MqResend {
@Autowired
private Environment env;
@Autowired
private RabbitTemplate rabbitTemplate;
// 最大投递次数
private static final int MAX_TRY_COUNT = 3;
/*
* 每30s拉取投递失败的消息, 重新投递
*/
@Scheduled(cron = "0/30 * * * * ?")
public void resend() {
log.info("开始执行定时任务(重新投递消息)");
//获取之前投递失败的消息
List<String> msgs = new ArrayList<String>();
msgs.add("to resend msg");
msgs.forEach(msg -> {
//实际应用的时候,msgId是第一次发送的时候生成存在数据库中的
String msgId = UUID.randomUUID().toString().replaceAll("-","");
//实际应用的时候,tryCount也是消息记录的一个字段
int tryCount = 1;
if (tryCount >= MAX_TRY_COUNT) {
log.info("超过最大重试次数, 消息投递失败, msgId: {}", msgId);
} else {
//更新消息的重试次数+1
CorrelationData correlationData = new CorrelationData(msgId);
rabbitTemplate.convertAndSend(env.getProperty("lgy.test.exchange"), env.getProperty("lgy.test.binding"), msg, correlationData);
log.info("第 " + (tryCount + 1) + " 次重新投递消息");
}
});
log.info("定时任务执行结束(重新投递消息)");
}
}
3.7 其他要注意的细节
3.7.1 同一个队列多消费类型
如果是同一个队列多个消费类型那么就需要针对每种类型提供一个消费方法,否则找不到匹配的方法会报错,如下:
@Component
@Slf4j
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.MULTIPART_HANDLE_EXCHANGE, type = ExchangeTypes.TOPIC, durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.MULTIPART_HANDLE_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
key = RabbitMQConstant.MULTIPART_HANDLE_KEY
)
)
@Profile(SpringConstant.MULTIPART_PROFILE)
public class MultipartConsumer {
/**
* RabbitHandler用于有多个方法时但是参数类型不能一样,否则会报错
* @param msg
*/
@RabbitHandler
public void process(ExampleEvent msg) {
log.info("param:{msg = [" + msg + "]} info:");
}
@RabbitHandler
public void processMessage2(ExampleEvent2 msg) {
log.info("param:{msg2 = [" + msg + "]} info:");
}
/**
* 下面的多个消费者,消费的类型不一样没事,不会被调用,但是如果缺了相应消息的处理Handler则会报错
* @param msg
*/
@RabbitHandler
public void processMessage3(@Payload ExampleEvent3 msg) {
log.info("param:{msg3 = [" + msg + "]} info:");
}
}
3.7.2 注解将消息和消息头注入消费者方法
在上面也看到了@Payload
等注解用于注入消息。这些注解有:
-
@Header 注入消息头的单个属性
-
@Payload 注入消息体到一个JavaBean中
-
@Headers 注入所有消息头到一个Map中
注意:
-
如果是
com.rabbitmq.client.Channel
,org.springframework.amqp.core.Message
和org.springframework.messaging.Message
这些类型,可以不加注解,直接可以注入; -
如果不是这些类型,那么不加注解的参数将会被当做消息体。不能多于一个消息体。如下方法ExampleEvent就是默认的消息体:
public void process2(@Headers Map<String, Object> headers,ExampleEvent msg);
3.7.3 关于消费者确认
RabbitMq消费者可以选择手动和自动确认两种模式,如果是自动,消息已到达队列,RabbitMq会无脑的将消息抛给消费者,一旦发送成功,他会认为消费者已经成功接收,在RabbitMq内部就把消息给删除了。另外一种就是手动模式,手动模式需要消费者对每条消息进行确认(也可以批量确认),RabbitMq发送完消息之后,会进入到一个待确认(unacked)的队列 。
如果消费者发送了ack,RabbitMq将会把这条消息从待确认中删除。如果是nack并且指明不要重新入队列,那么该消息也会删除。但是如果是nack且指明了重新入队列那么这条消息将会入队列,然后重新发送给消费者,被重新投递的消息消息头amqp_redelivered属性会被设置成true,客户端可以依靠这点来判断消息是否被确认,可以好好利用这一点,如果每次都重新回队列会导致同一消息不停的被发送和拒绝。消费者在确认消息之前和RabbitMq失去了连接那么消息也会被重新投递。所以手动确认模式很大程度上提高可靠性。自动模式的消息可以提高吞吐量。
spring手动确认消息需要将SimpleRabbitListenerContainerFactory
设置为手动模式:
simpleRabbitListenerContainerFactory.setAcknowledgeMode(AcknowledgeMode.MANUAL);
手动确认的消费者代码如下:
@SneakyThrows
@RabbitListener(bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.CONFIRM_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT,autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.CONFIRM_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
key = RabbitMQConstant.CONFIRM_KEY),
containerFactory = "containerWithConfirm")
public void process(ExampleEvent msg, Channel channel, @Header(name = "amqp_deliveryTag") long deliveryTag, @Header("amqp_redelivered") boolean redelivered, @Headers Map<String, String> head) {
try {
log.info("ConsumerWithConfirm receive message:{},header:{}", msg, head);
channel.basicAck(deliveryTag, false);
} catch (Exception e) {
log.error("consume confirm error!", e);
//这一步千万不要忘记,不会会导致消息未确认,消息到达连接的qos之后便不能再接收新消息
//一般重试肯定的有次数,这里简单的根据是否已经重发过来来决定重发。第二个参数表示是否重新分发
channel.basicReject(deliveryTag, !redelivered);
//这个方法我知道的是比上面多一个批量确认的参数
// channel.basicNack(deliveryTag, false,!redelivered);
}
}
关于spring的AcknowledgeMode需要说明,他一共有三种模式:NONE,MANUAL,AUTO,默认是AUTO模式。这比RabbitMq原生多了一种。这一点很容易混淆,这里的NONE对应其实就是RabbitMq的自动确认,MANUAL是手动。而AUTO其实也是手动模式,只不过是Spring的一层封装,他根据你方法执行的结果自动帮你发送ack和nack。如果方法未抛出异常,则发送ack。如果方法抛出异常,并且不是AmqpRejectAndDontRequeueException
则发送nack,并且重新入队列。如果抛出异常时AmqpRejectAndDontRequeueException
则发送nack不会重新入队列。我有一个例子专门测试NONE,见CunsumerWithNoneTest
。
还有一点需要注意的是消费者有一个参数prefetch,它表示的是一个Channel(也就是SimpleMessageListenerContainer的一个线程)预取的消息数量,这个参数只会在手动确认的消费者才生效。可以客户端利用这个参数来提高性能和做流量控制。如果prefetch设置的是10,当这个Channel上unacked的消息数量到达10条时,RabbitMq便不会在向你发送消息,客户端如果处理的慢,便可以延迟确认在方法消息的接收。至于提高性能就非常容易理解,因为这个是批量获取消息,如果客户端处理的很快便不用一个一个去等着去新的消息。SpringAMQP2.0开始默认是250,这个参数应该已经足够了。注意之前的版本默认值是1所以有必要重新设置一下值。当然这个值也不能设置的太大,RabbitMq是通过round robin这个策略来做负载均衡的,如果设置的太大会导致消息不多时一下子积压到一台消费者,不能很好的均衡负载。另外如果消息数据量很大也应该适当减小这个值,这个值过大会导致客户端内存占用问题。如果你用到了事务的话也需要考虑这个值的影响,因为事务的用处不大,所以我也没做过多的深究。
3.7.4 关于发送者确认模式
考虑这样一个场景:你发送了一个消息给RabbitMq,RabbitMq接收了但是存入磁盘之前服务器就挂了,消息也就丢了。为了保证消息的投递有两种解决方案,最保险的就是事务(和DB的事务没有太大的可比性), 但是因为事务会极大的降低性能,会导致生产者和RabbitMq之间产生同步(等待确认),这也违背了我们使用RabbitMq的初衷。所以一般很少采用,这就引入第二种方案:发送者确认模式。
发送者确认模式是指发送方发送的消息都带有一个id,RabbitMq会将消息持久化到磁盘之后通知生产者消息已经成功投递,如果因为RabbitMq内部的错误会发送nack。注意这里的发送者和RabbitMq之间是异步的,所以相较于事务机制性能大大提高。其实很多操作都是不能保证绝对的百分之一百的成功,哪怕采用了事务也是如此,可靠性和性能很多时候需要做一些取舍,想很多互联网公司吹嘘的5个9,6个9也是一样的道理。如果不是重要的消息如:性能计数器,完全可以不采用发送者确认模式。
这里有一点我当时纠结了很久,我一直以为发送者确认模式的回调是客户端的ack触发的,这里是大大的误解!发送者确认模式和消费者没有一点关系,消费者确认也和发送者没有一点关系,两者都是在和RabbitMq打交道,发送者不会管消费者有没有收到,只要消息到了RabbitMq并且已经持久化便会通知生产者,这个ack是RabbitMq本身发出的,和消费者无关
发送者确认模式需要将Channel设置成Confirm模式,这样才会收到通知。Spring中需要将连接设置成Confirm模式:
connectionFactory.setPublisherConfirms(isConfirm);
然后在RabbitTemplate中设置确认的回调,correlationData是消息的id,如下(只是简单打印下):
// 设置RabbitTemplate每次发送消息都会回调这个方法
rabbitTemplate.setConfirmCallback((correlationData, ack, cause)
-> log.info("confirm callback id:{},ack:{},cause:{}", correlationData, ack, cause));
发送时需要给出唯一的标识(CorrelationData
):
rabbitTemplateWithConfirm.convertAndSend(RabbitMQConstant.DEFAULT_EXCHANGE, RabbitMQConstant.DEFAULT_KEY,
new ExampleEvent(i, "confirm message id:" + i),
new CorrelationData(Integer.toString(i)));
还有一个参数需要说下:mandatory。这个参数为true表示如果发送消息到了RabbitMq,没有对应该消息的队列。那么会将消息返回给生产者,此时仍然会发送ack确认消息。
设置RabbitTemplate的回调如下:
rabbitTemplate.setReturnCallback((message, replyCode, replyText, exchange, routingKey)
-> log.info("return callback message:{},code:{},text:{}", message, replyCode, replyText));
另外如果是RabbitMq内部的错误,不会调用该方法。所以如果消息特别重要,对于未确认的消息,生产者应该在内存用保存着,在确认时候根据返回的id删除该消息。如果是nack可以将该消息记录专门的日志或者转发到相应处理的逻辑进行后续补偿。RabbitTemplate也可以配置RetryTemplate,发送失败时直接进行重试,具体还是要结合业务。
最后关于发送者确认需要提的是spring,因为spring默认的Bean是单例的,所以针对不同的确认方案(其实有不同的确认方案是比较合理的,很多消息不需要确认,有些需要确认)需要配置不同的bean.
3.7.5 消费消息、死信队列和RetryTemplate
上面也提到了如果消费者抛出异常时默认的处理逻辑。另外我们还可以给消费者配置RetryTemplate,如果是采用SpringBoot的话,可以在application.yml配置中配置如下:
spring:
rabbitmq:
listener:
retry:
# 重试次数
max-attempts: 3
# 开启重试机制
enabled: true
如上,如果消费者失败的话会进行重试,默认是3次。注意这里的重试机制RabbitMq是为感知的!到达3次之后会抛出异常调用MessageRecoverer
。默认的实现为RejectAndDontRequeueRecoverer,也就是打印异常,发送nack,不会重新入队列。 我想既然配置了重试机制消息肯定是很重要的,消息肯定不能丢,仅仅是日志可能会因为日志滚动丢失而且信息不明显,所以我们要讲消息保存下来。可以有如下这些方案:
-
使用RepublishMessageRecoverer这个MessageRecoverer会发送发送消息到指定队列
-
给队列绑定死信队列,因为默认的RepublishMessageRecoverer会发送nack并且requeue为false。这样抛出一场是这种方式和上面的结果一样都是转发到了另外一个队列。详见DeadLetterConsumer
-
注册自己实现的MessageRecoverer
-
给MessageListenerContainer设置RecoveryCallback
-
对于方法手动捕获异常,进行处理
我比较推荐前两种。这里说下死信队列,死信队列其实就是普通的队列,只不过一个队列声明的时候指定的属性,会将死信转发到该交换器中。声明死信队列方法如下:
@RabbitListener(
bindings = @QueueBinding(
exchange = @Exchange(value = RabbitMQConstant.DEFAULT_EXCHANGE, type = ExchangeTypes.TOPIC,
durable = RabbitMQConstant.FALSE_CONSTANT, autoDelete = RabbitMQConstant.true_CONSTANT),
value = @Queue(value = RabbitMQConstant.DEFAULT_QUEUE, durable = RabbitMQConstant.FALSE_CONSTANT,
autoDelete = RabbitMQConstant.true_CONSTANT, arguments = {
@Argument(name = RabbitMQConstant.DEAD_LETTER_EXCHANGE, value = RabbitMQConstant.DEAD_EXCHANGE),
@Argument(name = RabbitMQConstant.DEAD_LETTER_KEY, value = RabbitMQConstant.DEAD_KEY)
}),
key = RabbitMQConstant.DEFAULT_KEY
))
其实也就只是在声明的时候多加了两个参数x-dead-letter-exchange和x-dead-letter-routing-key。这里一开始踩了一个坑,因为@QueueBinding
注解中也有arguments属性,我一开始将参数声明到@QueueBinding
中,导致一直没绑定成功。如果绑定成功可以在控制台看到queue的Featrues有DLX(死信队列交换器)和DLK(死信队列绑定)。
-
消息被拒绝(basic.reject/basic.nack)并且requeue=false
-
消息TTL过期
-
队列达到最大长度
我们用到的就是第一种。
3.8 参考文章
SpringBoot整合RabbitMQ之 典型应用场景实战一_rabbitmq实战-CSDN博客 https://www.cnblogs.com/chenfangzhi/p/9710698.html Springboot+rabbitmq的简单使用_rabbitmq 和springboot 实现简单功能-CSDN博客 Spring Boot整合RabbitMQ详细教程_setexposelistenerchannel-CSDN博客 https://www.cnblogs.com/skychenjiajun/p/9037324.html SpringBoot+RabbitMQ (保证消息100%投递成功并被消费)
4. 其他疑问
rabbitmq集群的使用:
-
配置多个ip是否会自动负载均衡?还是另外实现负载均衡?
程序只能连接单个mq节点,负载均衡需要另外实现
-
生产者和消费者配置同一个集群的不同ip,是否可以进行消费?
既然是集群,这个是必须的吧,未测试
-
consumer 应尽量连接每一个节点。并针对同一个逻辑队列,要在多个节点建立物理 Queue。否则无论 consumer连接哪个节点,都会从创建 queue 的节点获取消息,会产生瓶颈。???