目录
第一章 MQ消息中间件+JMS+AMQP核⼼知识. 2
1.1 什么是MQ消息中间件与其的应用场景. 2
1.2 JMS消息服务和和常⻅核⼼概念介绍. 3
1.3 介绍什么是AMQP⾼级消息队列协议和MQTT科普. 5
1.4 对⽐当下主流的消息队列和选择问题. 7
第二章 什么是RabbitMQ消息队列和核⼼概念介绍. 8
简介:介绍RabbitMQ消息队列. 8
RabbitMq的核⼼概念. 8
第三章 新版RabbitMQ docker安装和Rabbitmq管控台讲解. 11
第四章 RabbitMq的消息模型以及JAVA整合RabbitMq 14
4.1 交换机介绍. 14
4.2 基本消息模型. 16
4.3 Work Queues工作队列模型. 19
4.4 发布订阅模型(fanout). 22
4.5 路由模型Direct 25
4.6 主题模型Topics(根据主题模式接收消息). 28
4.7 rabbitMq多种工作模式的总结. 32
第五章 springboot整合RabbitMq 33
什么是Spring-AMQP 33
简介:介绍SpringBoot2.X整合Rabbitmq实战. 33
创建一个RabbitMQ的配置类-简化RabbitMQ开发. 34
第6章 RabbitMQ消息的可靠性投递+消费者端ACK 35
6.1 ⾼级知识点之RabbitMQ消息可靠性投递讲解. 35
6.2 Rabbitmq的消息可靠性投递confirmCallback实战. 37
6.3 RabbitMQ消息可靠性投递returnCallback实战. 39
6.4 定制化RabbitTemplate 41
6.5 Rabbitmq的消费者端消息确认机制ACK讲解. 43
什么是MQ消息中间件
全称MessageQueue,主要是⽤于程序和程序直接通信,异步+解耦
使⽤场景:(核心应用)
1 应用之间解耦:订单系统-》物流系统
(服务与服务之间,系统与系统之间的耦合性大大减少,比如订单系统要调用物流系统,使用MQ后订统就会监听消费,这样的话,就算以后物流系统处理业务的接口发生了变化(如URL,参数变化)也没有关系,订单系统也不用做出任何改变)
2 异步处理:⽤户注册-》发送邮件,短信,初始化信息
业务异步执行,然后直接返回应答给客户端(要实现异步处理我们也可以使用多线程来完成)
3 流量削峰:秒杀、⽇志处理
(大量请求进来以后,并不实时处理而是进入到消息队列中去,消息队列起一个缓冲作用,我们的微服务并不会实时处理业务,而是根据消费能力去监听消费)
4 解决分布式事务、最终⼀致性(比如补偿机制)
特性
可以跨平台 、多语⾔
可以解决分布式事务、最终⼀致性(比如补偿机制)
RPC调⽤上下游对接,数据源变动->通知下属
什么是JMS: Java消息服务(Java Message Service),Java平台中关于⾯向消息中间件的接⼝
JMS是⼀种与⼚商⽆关的 API(接口),⽤来访问消息收发系统消息,它类似于JDBC(Java Database Connectivity)。这⾥,JDBC 是可以⽤来访问许多不同关系数据库的API。
是由Sun公司早期提出的消息标准,旨在为java应⽤提供统⼀的消息操作,包括create、send、receive
JMS是针对java的,那微软开发了NMS(.NET消息传递服务)
特性
1 仅⾯向Java平台的标准消息传递API(不夸平台和语言)
2 它在Java或JVM语⾔⽐如Scala、Groovy中具有互⽤性
3 ⽆需担⼼底层协议
4 有queues和topics两种消息传递模型
5 ⽀持事务、能够定义消息格式(消息头、属性和内容)
常⻅概念
1 JMS提供者:连接⾯向消息中间件的,JMS接⼝的⼀个实现,RocketMQ,ActiveMQ,Kafka等等
2 JMS⽣产者(Message Producer):⽣产消息的服务
3 JMS消费者(Message Consumer):消费消息的服务
4 JMS消息:数据对象
5 JMS队列:存储待消费消息的区域
6 JMS主题:⼀种⽀持同时发送消息给多个订阅者(消费者)的机制
JMS发送消息通常有两种类型:点对点(Point-to-Point)(一对一),发布/订阅(Publish/Subscribe)(一对对)
MQ基础编程模型
1 MQ中需要⽤的⼀些类
2 ConnectionFactory :连接⼯⼚,JMS ⽤它创建连接
3 Connection :JMS 客户端到JMS Provider 的连接
4 Session: ⼀个发送或接收消息的线程
5 Destination :消息的⽬的地;消息发送给谁. MessageConsumer /
背景
JMS或者NMS都没有标准的底层协议,它们可以在任何底层协议上运⾏,但是API是与编程语⾔绑定的(这样就无法实现跨平台跨语言的效果), AMQP解决了这个问题,它使⽤了⼀套标准的底层协议
什么是AMQP
AMQP(advanced message queuing protocol)在2003年时被提出,最早⽤于解决金融领域不同平台之间的消息传递交互问题,就是是⼀种协议(高级消息队列协议),兼容JMS
更准确说的链接协议 binary- wire-level-protocol 直接定义⽹络交换的数据格式,类似http
具体的产品实现⽐较多,RabbitMQ就是其中⼀种
特性
1独⽴于平台的底层消息传递协议
2 消费者驱动消息传递
3 跨语⾔和平台的互⽤性、属于底层协议
4 有5种交换类型direct,fanout,topic,headers,system
5 ⾯向缓存的、可实现⾼性能、⽀持经典的消息队列,循环,存储和转发
6 ⽀持⻓周期消息传递、⽀持事务(跨消息队列)
AMQP和JMS的主要区别(重点)
1 AMQP不从API层进⾏限定,直接定义⽹络交换的数据格式,这使得实现了AMQP的provider(中间件平台)天然性就是跨平台, ⽐如Java语⾔产⽣的消息,可以⽤其他语⾔⽐如python 的进⾏消费
2AQMP可以⽤http来进⾏类⽐,不关⼼实现接⼝的语⾔,只要都按照相应的数据格式去发送报⽂请求,不同语⾔的client可以和不同语⾔的server进⾏通讯
3JMS消息类型: TextMessage/ObjectMessage/StreamMessage等
4 AMQP消息类型:Byte[](字节数组)
科普:⼤家可能也听过MQTT
MQTT: 消息队列遥测传输(Message Queueing Telemetry Transport )
背景:
我们有⾯向基于Java的企业应⽤的JMS和⾯向所有其他应⽤需求的AMQP,那这个MQTT是做啥的?
原因
计算性能不⾼的设备不能适应AMQP上的复杂操作,MQTT它是专⻔为⼩设备设计的
MQTT主要是是物联⽹(IOT)中⼤量的使⽤
特性
1 内存占⽤低,为⼩型⽆声设备之间通过低带宽发送短消息⽽设计
2 不⽀持⻓周期存储和转发,不允许分段消息(很难发送⻓消息)
3 ⽀持主题发布-订阅、不⽀持事务(仅基本确认) 消息实际上是短暂的(短周期)
4 简单⽤户名和密码、不⽀持安全连接、消息不透明
业界主流的消息队列:Apache ActiveMQ、Kafka、RabbitMQ、RocketMQ
1 ActiveMQ:http://activemq.apache.org/
Apache出品,历史悠久,⽀持多种语⾔的客户端和协议,⽀持多种语⾔Java, .NET, C++ 等
基于JMS Provider的实现
缺点:吞吐量不⾼,多队列的时候性能下降,存在消息丢失的情况,⽐较少⼤规模使⽤
2 Kafka:http://kafka.apache.org/
是由Apache软件基⾦会开发的⼀个开源流处理平 台,由Scala和Java编写。Kafka是⼀种⾼吞吐量的分布式发布订阅消息系统,它可以处理⼤规模的⽹站 中的所有动作流数据(⽹⻚浏览,搜索和其他⽤户的⾏动),副本集机制,实现数据冗余,保障数据尽量不丢失;⽀持多个⽣产者和消费者(主要用于大数据领域)
类似MQ(不是一个纯MQ),功能较为简单,主要⽀持简单的MQ功能
缺点:
不⽀持批量和⼴播消息,运维难度⼤,⽂档 ⽐较少, 需要掌握Scala
RocketMQ:http://rocketmq.apache.org/
阿⾥开源的⼀款的消息中间件, 纯Java开发,具有⾼吞吐量、⾼可⽤性、适合⼤规模分布式系统应⽤的特点, 性能强劲(零拷⻉技术),⽀持海量堆积, ⽀持指定次数和时间间隔的失败消息重发,⽀持consumer 端tag过滤、延迟消息等,在阿⾥内部进⾏⼤规模使⽤,适合在电商,互联⽹⾦融等领域
基于JMS Provider实现
缺点:社区相对不活跃,更新⽐较快,纯java⽀持
RabbitMQ:http://www.rabbitmq.com/
是⼀个开源的AMQP实现,服务器端⽤Erlang语⾔编写,⽀持多种客户端,如:Python、Ruby、.NET、Java、C、⽤于在分布式系统中存储转发消息,在易⽤性、扩展性、⾼可⽤性等⽅⾯表现不错
缺点:使⽤Erlang开发,阅读和修改源码难度⼤
什么我们讲RabbitMQ呢,只要你⽬标是⾼级⼯程师或者架构师,就要多学,才知道解决⽅案+适合场景
因为这个是rabbitmq专题课程下集专⻔介绍rabbitmq
RabbitMQ:http://www.rabbitmq.com/
是⼀个开源的基于AMQP实现的消息中间件,服务器端⽤Erlang语⾔编写,⽀持多种客户端,如:Python、Ruby、.NET、Java、C、⽤于在分布式系统中存储转发消息,在易⽤ 性、扩展性、⾼可⽤性等⽅⾯表现不错,与SpringAMQP完美的整合、API丰富易⽤
⽂档:https://www.rabbitmq.com/getstarted.html
核⼼概念, 了解了这些概念,是使⽤好RabbitMQ的基础
1 Broker(代理)(也就是消息代理就是 我们的中间件)
RabbitMQ的服务端程序,可以认为⼀个mq节点就是⼀个broker
2 Producer⽣产者
创建消息Message,然后发布到RabbitMQ中
3 Consumer消费者:
消费消息队列⾥⾯的消息
4 Message 消息
⽣产消费的内容,有消息头和消息体,也包括多个属性配置,⽐如routingKey路由键
5 Queue 消息队列
是RabbitMQ 的内部对象,⽤于存储消息,消息都只能存储在队列中
6 Channel 信道
一条⽀持多路复⽤的通道,独⽴的双向数据流通道,可以发布、订阅(消费)、接收消息。
信道是建⽴在真实的TCP连接内的虚拟连接,复⽤TCP连接的通道(无需重复建立TCP连接减少开销,一个信道中有多个通道,可以通过多条通道达到数据双向流动的效果)
7 Connection连接
是RabbitMQ的socket链接,它封装了socket协议相关部分逻辑,⼀个连接上可以有多个channel进⾏通信(从而达到一个消费者同时订阅多个消息的效果)
8 Exchange 交换器
对于RabbitMQ ⽣产者将消息首先发送到 Exchange(而不是队列中),交换器再将消息根据RoutingKey和BindingKey路由到⼀个或者多个队列中,Exchange 有多个类型,后续再⼀⼀介绍,队列和交换机是多对多的关系。
9 RoutingKey 路由键
⽣产者将消息发给交换器的时候,⼀般会指定⼀个RoutingKey,从而来指定这个消息的路由规则(即这个消息最终将发送到哪个消息队列中存储),最⼤⻓度255 字节
10 Binding 绑定
通过绑定将交换器与消息队列关联起来,在绑定的时候⼀般会指定⼀个绑定键 ( BindingKey ),这样RabbitMQ中的 Exchange就知道如何正确地将消息路由到队列了
⽣产者将消息发送给交换器时,需要⼀个RoutingKey,当BindingKey和 RoutingKey匹配时,消息会被交换器路由(发送)到对应的消息队列中
11 Virtual host 虚拟主机
⽤于不同业务模块的逻辑(或者不同环境如上线,测试德国)隔离,⼀个Virtual Host⾥⾯可以有若⼲个Exchange和Queue,同⼀个VirtualHost ⾥⾯不能有相同名称的Exchange或Queue
默认名称是 /
/dev
/test
/pro
#拉取镜像
docker pull rabbitmq:management
#启动容器
docker run -d --hostname rabbit_host1 --name rabbit1 -e RABBITMQ_DEFAULT_USER=admin -e RABBITMQ_DEFAULT_PASS=1234 -p 15672:15672 -p 5672:5672 415614b8c071
#介绍
-d 以守护进程⽅式在后台运⾏
-p 15672:15672 management 界⾯管理访问端⼝
-p 5672:5672 amqp 访问端⼝
--name:指定容器名
--hostname:设定容器的主机名,它会被写到容器内的
/etc/hostname 和 /etc/hosts,作为容器主机IP的别名,并且将显示在容器的bash中
-e 参数(设置环境变量的参数)
RABBITMQ_DEFAULT_USER ⽤户名
RABBITMQ_DEFAULT_PASS 密码
主要端⼝介绍
(阿里云部署的话这些端口都要在安全组中放行)
4369 erlang 发现⼝
5672 client 客户端通信⼝(AMQP访问端口)
15672 管理界⾯ ui 端⼝
25672 server 间内部通信⼝(集群时用的)
RabbitMQ控制台介绍
管控台介绍
默认rabbitmq账号密码 guest/guest
当前的账号密码 admin/1234
每个虚拟主机默认就有7个交换机
在rabbitMq中消息模型一共有6种,基本消息模型,工作队列模型,发布订阅模型,路由模型,主题模型(最常用的模型),和RPC远程调用模型
⽣产者将消息发送到 Exchange,交换器将消息根据路由键的路由规则路由到⼀个或者多个队列中,交换机有多个类型,消息队列和交换机 是多对多的关系
交换机只负责转发消息,不具备存储消息的能⼒,如果没有队列和exchange绑定,或者没有符合的路由规 则(消息根据路由规则找不到对应的消息队列),消息会被丢失
RabbitMQ有四种交换机类型,分别是Direct exchange、Fanout exchange、Topic exchange(最重要),Headers exchange,最后的基本不⽤
交换机类型
Direct Exchange 定向交换机(默认交换机)
将⼀个队列绑定到交换机上,要求该消息与⼀个特定的路由键完全匹配
例⼦:如果⼀个队列绑定到该交换机上要求路由键“aabb”,则只有被标记为“aabb”的消息才被转发给该队列,不会转发aabb.cc,也不会转发gg.aabb,只会转发aabb
根据路由建,点对点转发消息
Fanout Exchange ⼴播交换机
只需要简单的将队列绑定到交换机上,发送到该类型交换机上的消息都会被转发到与该交换机绑定的所有队列上。很像⼦⽹⼴播,每台⼦⽹内的主机都获得了⼀份复制的消息
Fanout交换机转发消息是最快的,⽤于发布订阅,
⼴播形式,中⽂是扇形
不处理路由健(与路由键无关)
Topic Exchange 主题交换机(用的最多)
主题交换机是⼀种发布/订阅的模式,结合了直连交换机与扇形交换机的特点
将路由键和某模式进⾏匹配。此时队列需要绑定要⼀个模式上
符号“#”匹配⼀个或多个词,符号“*”匹配不多不少⼀个词(注意是一个词,不是一个字母,每一个词之间用点.号分开)
例⼦:因此“abc.#”能够匹配到“abc.def.ghi”,但是“abc.*” 只会匹配到“abc.def”。
Headers Exchanges(少⽤)
根据发送的消息内容中的headers属性进⾏匹配, 在绑定Queue与Exchange时指定⼀组键值对
当消息发送到RabbitMQ时会取到该消息的headers 与Exchange绑定时指定的键值对进⾏匹配;
如果完全匹配则消息会路由到该队列,否则不会路由到该队列
不处理路由键 (与路右键无关)
注意
rabbitMq中每一个虚拟主机自带7个交换机
生产者将消息发送到队列,消费者从队列中获取消息,队列是存储消息的缓冲区,消费者和生产者时意义对应
代码
//接收消息端(消费者端)
public class receivemessage {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("1111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//消费者回调函数
Consumer consumer = new DefaultConsumer(channel){
//处理交货过来的消息(消费消息)
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//一般是固定的,可以作为会话的名称
System.out.println("consumerTag="+consumerTag);
//可以获取交换机、路由健等信息
System.out.println("envelope="+envelope);
System.out.println("properties="+properties);
//消息内容
System.out.println("body="+new String(body,"utf-8"));
}
};
//监听消费某个队列(这个方法执行后如果我们不关闭connection和channel的话当前线程就不会结束,从而一直保持监听状态)
//利用的是监听器,加线程暂歇的原理
//参数1 队列名,
// 参数2 是否自动确认消息(确认消息被成功消费),(确认消息后,消息将在队列中被移除)
// 消费者回调函数
channel.basicConsume(QUEUE_NAME,true,consumer);
//
// DeliverCallback deliverCallback = (consumerTag, delivery) -> {
// String message = new String(delivery.getBody(), "UTF-8");
// System.out.println(" [x] Received '" + message + "'");
// };
//
// channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
///生成消息端(生产者端)
public class sendmessage {
private final static String QUEUE_NAME = "hello";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
//JDK7语法,自动关闭,创建连接
try (Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel())
{
/**
* 队列名称
* 持久化配置:mq重启后该消息队列还在
* 是否独占:只能有一个消费者监听队列;当connection关闭是否删除队列,一般是false,发布订阅是独占
* 自动删除: 当没有消费者的时候,自动删除掉消息队列,一般是false
* 其他参数
*
* 队列不存在则会自动创建,如果存在则不会覆盖(要把之前的删除),所以此时的时候需要注意属性
*/
//声明一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
String message = "欢迎来小滴课堂xdclass.net";
///第一个参数是交换机,(简单队列模式下可以不使用交换机)
// 第二参数是路右键routingKey(也可以是是消息队列名称)
//第三个参数,而外参数
//第四个参数,消息内容
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
在工人之间分配任务(竞争的消费者模式))
在基本消息模型中,一个生产者对应一个消费者,而实际生产过程中,往往消息生产会发送很多条消息,如果消费者只有一个的话效率就会很低,因此rabbitmq有另外一种消息模型,这种模型下,一个生产发送消息到队列,允许有多个消费者同时接收消费消息,但是一条消息只会被一个消费者获取。
特点
1 消息⽣产能⼒⼤于消费能⼒,增加多⼏个消费者
2 和简单队列类似,增加多个⼏个消费节点,不同消费节点处于竞争关系
3 默认策略:round robin 轮询(多个消息时,会把消息轮询分配给每一个消费者),我们可以选择公平策略
模拟轮询 策略时先让消费者先启动 ,再让生产者启动(要确保手动确认消息)
1.2.1 工作队列的轮询策略实战
先启动两个消费者,再启动⽣产者
缺点:存在部分节点消费过快,部分节点消费慢,导致不能合理处理消息(因为不同的机器,消费能力不同使用轮询有时是不合理的,会严重影响效率
代码(轮询策略)消费者端代码
public class receivemessage1 {
private final static String QUEUE_NAME = "work_mq";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
System.out.println(" [*] Waiting for messages. To exit press CTRL+C");
//消费者回调函数
Consumer consumer = new DefaultConsumer(channel){
//处理交货过来的消息(消费消息)
@SneakyThrows
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
Thread.sleep(2000);
//一般是固定的,可以作为会话的名称
System.out.println("consumerTag="+consumerTag);
//可以获取交换机、路由健等信息,消息标记DeliveryTag
System.out.println("envelope="+envelope);
System.out.println("properties="+properties);
//消息内容
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息(第二个参数,表示是否是多条确认,一般设置为false,即每次仅确认一条,每消费一条确认一条)
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//监听消费某个队列(这个方法执行后如果我们不关闭connection和channel的话当前线程就不会结束,从而一直保持监听状态)
//利用的是监听器,加线程暂歇的原理
//参数1 队列名,
// 参数2 是否自动确认消息(确认消息被成功消费),(确认消息后,消息将在队列中被移除)(一般都是关闭)
// 消费者回调函数
channel.basicConsume(QUEUE_NAME,false,consumer);
//
// DeliverCallback deliverCallback = (consumerTag, delivery) -> {
// String message = new String(delivery.getBody(), "UTF-8");
// System.out.println(" [x] Received '" + message + "'");
// };
//
// channel.basicConsume(QUEUE_NAME, true, deliverCallback, consumerTag -> { });
}
}
代码(轮询策略)生产者端代码
public class sendmessage {
private final static String QUEUE_NAME = "work_mq";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("11111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
//JDK7语法,自动关闭,创建连接
try (Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel())
{
/**
* 队列名称
* 持久化配置:mq重启后该消息队列还在
* 是否独占:只能有一个消费者监听队列;当connection关闭是否删除队列,一般是false,发布订阅是独占
* 自动删除: 当没有消费者的时候,自动删除掉消息队列,一般是false
* 其他参数
*
* 队列不存在则会自动创建,如果存在则不会覆盖(要把之前的删除),所以此时的时候需要注意属性
*/
//声明一个队列
channel.queueDeclare(QUEUE_NAME, false, false, false, null);
for(int i=1;i<=10;i++) {
String message = "欢迎来小滴课堂xdclass.net"+i;
///第一个参数是交换机,(简单队列模式下可以不使用交换机)
// 第二参数是路右键routingKey(也可以是是消息队列名称)
//第三个参数,而外参数
//第四个参数,消息内容
channel.basicPublish("", QUEUE_NAME, null, message.getBytes(StandardCharsets.UTF_8));
System.out.println(" [x] Sent '" + message + "'");
}
}
}
}
1.2.2 工作队列的公平策略实战(要确保手动确认消息)
修改消费者策略
解决消费者能⼒消费不⾜的问题,降低消费时间问题,提高了效率
代码在上面代码的基础上加一行
//限制消费者端每次只能消费一个,消费完才能接受下一个消息(接收消息是异步执行的)这样就把策略改成了公平模式
//限制消费者每次只能处理一条消息,处理完才能处理下一条消息
channel.basicQos(1);
在之前的模型中,一条消息只能被一个消费者获取,而在订阅模式中,可以实现一条消息被多个消费者获取。在这种模型下,消息传递过程中比之前多了一个exchange交换机,生产者不是直接发送消息到队列,而是先发送给交换机,经由交换机分配到不同的队列,而每个消费者都有自己的队列:
1 什么是rabbitmq的fanout发布订阅模式(立即向许多消费者发送消息)
发布-订阅模型中,消息⽣产者不再是直接⾯对 queue(消息队列了),⽽是直⾯exchange,所有消息都需要经过exchange来进⾏消息的转发(转发到具体的消息队列中)
在广播模式下,消息发送的流程如下:
2. fanout发布订阅模型应⽤场景
微信信公众号
新浪微博关注
3. rabbitmq fanout发布订阅模型的特点与注意事项
1通过把消息发送给交换机,交互机转发给对应绑定的队列
2 fanout交换机绑定的队列是排它独占队列,⾃动删除(一旦声明队列的客户端(消费端)下线了,该队列会被自动删除,)
4 实战代码:
先启动两个消费端,在启动一个生产端
Fanout 交换机发布订阅模式 消费者代码
public class Recv1 {
private final static String EXCHANG_NAME = "exchang_fanout";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("11111111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//绑定交换机,fanout扇形,即广播
channel.exchangeDeclare(EXCHANG_NAME,BuiltinExchangeType.FANOUT);
//获取队列(让信道自动声明创建一个队列,但是该消息队列是自动删除的,一旦消费者端停止运行后)
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列, fanout交换机不用routingkey,注意与fanout交换机绑定的消息队列,被fanout交换机独占
channel.queueBind(queueName,EXCHANG_NAME,"");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息消费,不是多条确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//消费,关闭消息消息自动确认,重要
channel.basicConsume(queueName,false,consumer);
}
}
Fanout 交换机发布订阅模式 生产者端代码
public class sendmessage {
private final static String EXCHANG_NAME = "exchang_fanout";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
//JDK7语法,自动关闭,创建连接
try (Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel())
{
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME, BuiltinExchangeType.FANOUT);
String msg="测试fanout交换机的发布订阅模式";
//因为交换机类型是fanout类型所以无需指定路由键,写成""即可不可以写成null
channel.basicPublish(EXCHANG_NAME,"",null,msg.getBytes(StandardCharsets.UTF_8));
System.out.println("发送消息");
}
}
}
在fanout模型中,生产者发布消息,所有消费者都可以获取所有消息。在路由模式(Direct)中,可以实现不同的消息被不同的队列消费,在Direct模式下,交换机不再将消息发送给所有绑定的队列,而是根据Routing Key将消息发送到指定的队列,队列在与交换机绑定时会设定一个Routing Key,而生产者发送的消息时也需要携带一个Routing Key。
1 什么是rabbitmq的路由模式(有选择地接收消息)
⽂档:https://www.rabbitmq.com/tutorials/tutorial- four-java.html
1 交换机类型是Direct
2 队列和交换机绑定,需要指定⼀个路由key( 也叫Bingding Key)
3 消息⽣产者发送消息给交换机,需要指定routingKey 交换机根据消息的路由key和Bingding Key,转发给对应的队列(routingKey必须和Bingding Key完全匹配,direct交换机才会把消息转发到该绑定的消息队列中)
2 路由模式的应用场景
⽇志采集系统 ELK
⼀个队列收集error信息-》告警
⼀个队列收集全部信息-》⽇常使⽤
3 实战代码
实战(direct路由模式)的消费者端1
public class Recv1 {
private final static String EXCHANG_NAME = "exchang_direct";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机,
channel.exchangeDeclare(EXCHANG_NAME,BuiltinExchangeType.DIRECT);
//获取队列(让信道自动声明创建一个队列,但是该消息队列是自动删除的,一旦消费者端停止运行后)
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列, direct交换机与队列绑定要指定bingkey
// (一个队列和directt交换机之间可以绑定多个Bingding Key),
channel.queueBind(queueName,EXCHANG_NAME,"infoRoutingKey");
channel.queueBind(queueName,EXCHANG_NAME,"errorRoutingKey");
channel.queueBind(queueName,EXCHANG_NAME,"debugRoutingKey");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息消费,不是多条确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//消费,关闭消息消息自动确认,重要
channel.basicConsume(queueName,false,consumer);
}
}
实战(direct路由模式)的消费者端2
public class Recv2 {
private final static String EXCHANG_NAME = "exchang_direct";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME,BuiltinExchangeType.DIRECT);
//获取队列(让信道自动声明创建一个队列,但是该消息队列是自动删除的,一旦消费者端停止运行后)
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列, direct交换机与队列绑定要指定bingkey
// (一个队列和directt交换机之间可以绑定多个Bingding Key),
channel.queueBind(queueName,EXCHANG_NAME,"errorRoutingKey");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息消费,不是多条确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//消费,关闭消息消息自动确认,重要
channel.basicConsume(queueName,false,consumer);
}
}
实战(direct路由模式)的生产者端
public class sendmessage {
private final static String EXCHANG_NAME = "exchang_direct";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("1111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
//JDK7语法,自动关闭,创建连接
try (Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel())
{
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME, BuiltinExchangeType.DIRECT);
String msg1="info信息";
String msg2="error信息";
String msg3="debug信息";
channel.basicPublish(EXCHANG_NAME,"infoRoutingKey",null,msg1.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EXCHANG_NAME,"errorRoutingKey",null,msg2.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EXCHANG_NAME,"debugRoutingKey",null,msg3.getBytes(StandardCharsets.UTF_8));
System.out.println("发送消息");
}
}
背景:
如果业务很多路由key,怎么维护??(使用默认direct模型时,当一个项目中出现非常多的bindingkey时,我们可能要写非常多行的交换机与消息队列的绑定,且去维护这些bindingkey是十分困难的,所以产生了topic模式)
topic交换机,⽀持通配符匹配模式(在绑定bindingkey时可以使用通配符),更加强⼤
⼯作基本都是⽤这个topic模式
Topic类型的Exchange与Direct相比,都是可以根据RoutingKey把消息路由到不同的队列。只不过Topic类型Exchange可以让队列在绑定Routing key 的时候使用通配符
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
什么是rabbitmq的主题模式
⽂档 https://www.rabbitmq.com/tutorials/tutorial-fi ve-java.html
交换机是 topic, 可以实现发布订阅模式fanout和路由模式Direct 的功能,更加灵活,⽀持模式匹配,通配符等
交换机同过通配符进⾏转发到对应的队列,* 代表⼀个词,#代表1个或多个词,⼀般⽤#作为通配符居多,⽐如 #.order, 会匹配 info.order 、sys.error.order, ⽽*.order ,只会匹配 info.order, 词之间是使⽤. 点进⾏分割多个词的; 如果是 ., 则info.order、error.order都会匹配
注意事项
1 交换机和队列绑定时可以使用通配符来写bindingkey(路由键)
2 ⽣产者发送消息时需要使⽤具体的路由健
Routingkey 一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#:匹配一个或多个词
*:匹配不多不少恰好1个词
举例:
audit.#:能够匹配audit.irs.corporate 或者 audit.irs
audit.*:只能匹配audit.irs
根据上面的模型图
quick.orange.rabbit 只会匹配 *.orange.* 和
*.*.rabbit ,进到Q1和Q2 lazy.orange.elephant 只会匹配 *.orange.* 和lazy.#,进到Q1和Q2
quick.orange.fox 只会匹配 *.orange.*,进⼊Q1 lazy.brown.fox 只会匹配azy.#,进⼊Q2 lazy.pink.rabbit 只会匹配 lazy.#和*.*.rabbit ,同个队列进⼊Q2(消息只会发⼀次)
quick.brown.fox 没有匹配,默认会被丢弃,可以通过回调监听⼆次处理
lazy.orange.male.rabbit,只会匹配 lazy.#,进⼊Q2
应用场景例⼦:
⽇志采集系统
⼀个队列收集订单系统的全部⽇志信息,order.log.#
⼀个队列收集全部系统的全部⽇志信息, #.log
实战代码
消费者端1
public class Recv1 {
private final static String EXCHANG_NAME = "exchang_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("1111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME,BuiltinExchangeType.TOPIC);
//获取队列(让信道自动声明创建一个队列,但是该消息队列是自动删除的,一旦消费者端停止运行后)
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列, topic交换机与队列绑定要指定bingkey,可以使用通配符
channel.queueBind(queueName,EXCHANG_NAME,"*.log.*");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息消费,不是多条确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//消费,关闭消息消息自动确认,重要
channel.basicConsume(queueName,false,consumer);
}
}
消费者端2
public class Recv2 {
private final static String EXCHANG_NAME = "exchang_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("11111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
Connection connection = factory.newConnection();
Channel channel = connection.createChannel();
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME,BuiltinExchangeType.TOPIC);
//获取队列(让信道自动声明创建一个队列,但是该消息队列是自动删除的,一旦消费者端停止运行后)
String queueName = channel.queueDeclare().getQueue();
//绑定交换机和队列, topic交换机与队列绑定要指定bingkey,可以使用通配符
channel.queueBind(queueName,EXCHANG_NAME,"order.log.error");
Consumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("body="+new String(body,"utf-8"));
//手工确认消息消费,不是多条确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
//消费,关闭消息消息自动确认,重要
channel.basicConsume(queueName,false,consumer);
}
}
生产者端
public class sendmessage {
private final static String EXCHANG_NAME = "exchang_topic";
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("111");
factory.setUsername("admin");
factory.setPassword("1234");
factory.setVirtualHost("/dev");
factory.setPort(5672);
//JDK7语法,自动关闭,创建连接
try (Connection connection = factory.newConnection();
//创建信道
Channel channel = connection.createChannel())
{
//声明一个交换机,如果rabbitMq中有对应的交换机,该行代码没有什么执行效果,如果没有,则会创建一个指定名字类型的交换机
channel.exchangeDeclare(EXCHANG_NAME, BuiltinExchangeType.TOPIC);
String msg1="订单服务的info信息";
String msg2="订单服务的error信息";
String msg3="商品服务debug信息";
//使用topic模式时,生产者发送消息时,还是要写具体的路右键名
channel.basicPublish(EXCHANG_NAME,"order.log.info",null,msg1.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EXCHANG_NAME,"order.log.error",null,msg2.getBytes(StandardCharsets.UTF_8));
channel.basicPublish(EXCHANG_NAME,"goods.log.debug",null,msg3.getBytes(StandardCharsets.UTF_8));
System.out.println("发送消息");
}
}
}
RabbitMq一般只使用以下的5种工作模式
简单模式
⼀个⽣产、⼀个消费,不⽤指定交换机,使⽤默认交换机,发布消息时指定的路由键名就是要接受消息的队列名
⼯作队列模式
⼀个⽣产,多个消费,可以有默认轮训和公平策略(一般用公平策略),不⽤指定交换机,使⽤默认交换机,发布消息时指定的路由键名就是要接受消息的队列名
(要确保手动确认消息)
发布订阅模式
fanout类型交换机,通过交换机和队列绑定,不⽤指定绑定的路由健,⽣产者发送消息到交换机,fanout交换机直接进⾏转发(转发到所有与该fanout交换机有绑定关系的队列中去),注意发布消息时不⽤指定routingkey路由健
路由模式(用的多)direct类型交换机,过交换机和队列绑定,指定绑定的bindingkey,⽣产者发送消息到交换机,交换机根据消息的路由key和bindingkey进⾏转发到对应的队列,发送消息要指定routingkey路由健
主题模式(用的最多)(重点)topic交换机,过交换机和队列绑定,可以通过通配符去设置bindingkey,⽣产者发送消息到交换机,交换机根据消息的路由key和绑定的bingdkey进⾏转发到对应的队列,发送消息要指定routingkey路由健
⽤的抽象,最终⽤户代码将很容易实现更易替换、添加和删除AMQP,因为它可以只针对抽象层来开发(也就是说如果我们要替换其他的实现了AMQP的消息中间件,我们要改的代码就很少)总之就是提⾼我们的框架整合消息队列的效率,SpringBoot为更⽅便开发RabbitMQ推出了starter,
我们使⽤ spring-boot-starter-amqp 进⾏开发
#消息队列spring: rabbitmq: host: 10.211.55.13 port: 5672 virtual-host: /dev password: password username: admin |
使用Springboot整合RabbitMQ时我们会去创建一个RabbitMQ的配置类,我们在这里类上进行统一的声明交换机,消息队列以及绑定它们之间的关系(从而简化RabbitMQ的开发)。
@Configuration
//该配置类可以自动创建 交换机,自动创建队列,自动将以下创建的交换机和队列绑定关系
//注意可以同时自动创建多个交换机,队列以及它们的绑定关系
public class RabbitMQConfig {
public static final String ORDER_EXCHANGE_NAME = "order_exchange";
public static final String GOODS_EXCHANGE_NAME = "goods_exchange";
public static final String ORDER_QUEUE = "order_queue";
public static final String GOODS_QUEUE = "goods_queue";
/**
* 自动创建topic 交换机
* @return
*/
@Bean
//当有多个类型相同的bean时,我们依赖注入时,到底注入的是哪一个呢。通过使用 @Qualifier 注解,我们可以消除需要注入哪个 bean 的问题
//这时我们可以使用@Qualifier给这个bean标注一个名字,在依赖注入时再利用@Qualifier和标注名去决定具体注入哪个bean
@Qualifier(value = "orderExchange")
public Exchange orderExchange(){
//durable()是指是否持久化
return ExchangeBuilder.topicExchange(ORDER_EXCHANGE_NAME).durable(true).build();
}
@Bean
@Qualifier(value = "goodsExchange")
public Exchange goodsExchange(){
//durable()是指是否持久化
return ExchangeBuilder.topicExchange(GOODS_EXCHANGE_NAME).durable(true).build();
}
/**
* 自动创建队列
* @return
*/
@Bean
@Qualifier(value = "orderQueue")
public Queue orderQueue(){
return QueueBuilder.durable(ORDER_QUEUE).build();
}
@Bean
@Qualifier(value = "goodsQueue")
public Queue goodsQueue(){
return QueueBuilder.durable(GOODS_QUEUE).build();
}
/**
* 自动将以上创建的交换机和队列绑定关系
*/
@Bean
public Binding orderBinding(@Qualifier("orderQueue") Queue queue, @Qualifier("orderExchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("order.#").noargs();
}
@Bean
public Binding goodsBinding(@Qualifier("goodsQueue") Queue queue, @Qualifier("goodsExchange")Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("goods.#").noargs();
}
}
什么是消息的可靠性投递
保证消息百分百发送到消息队列中去
详细介绍消息的可靠性投递
保证mq节点成功接受消息
消息发送端需要接受到mq服务端接受到消息的确认应答
完善的消息补偿机制,发送失败的消息可以再感知并⼆次处理
RabbitMQ消息投递路径
⽣产者-->交换机->队列->消费者
通过两个的点控制消息的可靠性投递
⽣产者到交换机,通过confirmCallback
交换机到消息队列,通过returnCallback
建议
开启消息确认机制以后,保证了消息的准确送达,但由于频繁的确认交互, rabbitmq 整体效率变低,吞吐量下降严重,不是⾮常重要的消息真⼼不建议⽤消息确认机制
⽣产者到交换机可靠性投递
通过confirmCallback
⽣产者投递消息后,如果Broker(中间件)收到消息后,会给⽣产者⼀个ACK。⽣产者通过ACK,可以确认这条消息是否正常发送到Broker,这种⽅式是消息可靠性投递的核⼼
开启confirmCallback
(注意无论消息是否成功投递到交换机上只要开启了confirmCallback就会触发该方法)
#旧版,确认消息发送成功,通过实现ConfirmCallBack接 ⼝,消息发送到交换器Exchange后触发回调 spring.rabbitmq.publisher-confirms=true #新版,NONE值是禁⽤发布确认模式,是默认值, CORRELATED值是发布消息成功到交换器后会触发回调⽅法 spring.rabbitmq.publisher-confirm-type: correlated |
开发实战
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testConfirm()
{ //要模拟调用ConfirmCallback,我们就随便向一个没有创建过的交换机发送消息即可
//设置消息投递确认(⽣产者到交换机的确认)的回调
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
/**
* @param correlationData 配置
* @param ack 交换机是否收到消息,true是成功,false是失败
* @param cause 失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
System.out.println("confirm=====>");
System.out.println("confirm====ack="+ack);
System.out.println("confirm==== cause="+cause);
if(ack)
{
System.out.println("交换机已经成功收到消息");
//TODO 向数据库更新一条消息记录,状态为成功
}else{
System.out.println("交换机没有接收到消息");
//TODO 向数据库更新一条消息记录,状态为失败
}
}
});
//TODO 向数据库插入一条消息记录,状态为发送
//要模拟调用ConfirmCallback,我们就随便向一个没有创建过的交换机发送消息即可
rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_EXCHANGE_NAME,"goods.newgoods","新商品");
}
模拟出现投递到交换机异常:
修改投递的交换机名称(我们就随便向一个没有创建过的交换机发送消息即可)
交换机到消息队列的可靠性投递
通过returnCallback
消息从交换器发送到对应的消息队列失败时触发
returnCallback两种模式
交换机到队列不成功,则丢弃消息(默认)
交换机到队列不成功,返回信息(该信息中有很多重要的内容,必须消息的内容)给消息⽣产者,触发returnCallback
开启returnCallback的步骤
第⼀步 开启returnCallback配置
#新版 spring.rabbitmq.publisher-returns=true |
要模拟出现交换机到消息队列的路由失败,我们只需要在发送消息指定一个不会被匹配的路由键即可
开发实战
//模拟出现交换机到消息队列的投递失败
//要模拟出现交换机到消息队列的路由失败,我们只需要在发送消息指定一个不会被匹配的路由键即可
//为了方便所有的消息都做可靠性投递,我们已经在配置类中定制了rabbitTemplate,只要是rabbitTemplate发送消息都会做setReturnCallback和ConfirmCallback
//该方法已经无法使用了
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
void testReturnsBack()
{
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
@Override
public void returnedMessage(Message message, int replyCode, String cause, String exchange, String routingkey) {
System.out.println(message.toString());
System.out.println(message.getBody().toString());
System.out.println(message.getMessageProperties().toString());
System.out.println(replyCode);
System.out.println(cause);
System.out.println(exchange);
System.out.println(routingkey);
//TODO 向数据库更新一条消息记录,状态为不成功
//TODO 重新发送消息
}
});
//TODO 向数据库插入一条消息记录,状态为发送
rabbitTemplate.convertAndSend(RabbitMQConfig.GOODS_EXCHANGE_NAME,"goodss.newgoods"," 新商品");
}
模拟异常
要模拟出现交换机到消息队列的路由失败,我们只需要在发送消息指定一个不会被匹配的路由键即可
为啥要定制化RabbitMQ
为了确保消息的可靠性投递,我们可以使用confirmCallback(生产者与交换机)和returnCallback(交换机与消息队列),但是我们不可能在每一个发布消息的方法中都去写这个两个方法(重复量太大了,且没有意义,所以我们可以定制化RabbitTemplate
具体步骤和原理
我们可以创建一个RabbitTemplate的配置类,并且注入RabbitTemplate,在利用@PostConstruct注解设置一个该配置类的构造方法,在构造方法中调用
confirmCallback和returnCallback,这样这样被Spring框架管理的那个rabbitTemplate对象,就统一的设置了confirmCallback和returnCallback
具体代码
@Configuration
public class RabbitTemplateConfig {
@Autowired
private RabbitTemplate rabbitTemplate;
public RabbitTemplateConfig() {
System.out.println("创建了RabbitTemplateConfig配置类");
}
/*定制RabbitTemplate*/
@PostConstruct
//spring容器启动后会自动初始化所有被它管理的类的一个对象
//@PostConstruct表示该方法是本配置类的一个构造方法,创建RabbitTemplateConfig象时调用这个方法,
// 这样被Spring框架管理的那个rabbitTemplate对象,就可以做一些统一的定制化的设置了
public void initRabbitTemplate()
{
System.out.println("调用initRabbitTemplate方法");
/*设置RabbitTemplate去发送消息的消息确认回调*/
/*只要消息抵达我们的消息代理服务器就会触发这个方法--不管是否会被消费*/
rabbitTemplate.setConfirmCallback(new RabbitTemplate.ConfirmCallback() {
@Override
public void confirm(CorrelationData correlationData, boolean b, String s) {
/*todo 消息代理服务器MQ已经收到消息,把数据库中的当前消息的状态修改一下
* */
System.out.println("当前的消息的唯一ID"+correlationData);
System.out.println("当前消息是否成功被生产者发送到交换机上:"+b);
System.out.println("失败的原因:"+s);
}
});
/*消息如果没有抵达队列才会执行回调*/
rabbitTemplate.setReturnCallback(new RabbitTemplate.ReturnCallback() {
// message 消息抵达(投递)失败的详细信息--在数据库应该修改当前消息的错误状态
//replytext回复的内容
@Override
public void returnedMessage
(Message message, int replyCode, String cause, String exchange, String routingkey) {
System.out.println("消息未被交换机"+exchange+"成功投递到消息队列上"+message);
System.out.println("消息未成功投递到消息队列上的原因:"+cause);
}
});
}
}
注意:
这样写后第2集和第3集写的代码就不能用了,因为RabbitTemplate同时只能设置一个confirmCallback和returnCallback(多了会报错)
背景:
消费者从broker中监听消息,需要确保消息被合理处理
RabbitMQ的ACK介绍
1 消费者从RabbitMQ收到消息并处理完成后,反馈给RabbitMQ,RabbitMQ收到反馈后才将此消息从队列中删除
2 消费者在处理消息出现了⽹络不稳定、服务器异常等现象,那么就不会有ACK反馈,RabbitMQ会认为这个消息没有正常消费,会将消息重新放⼊队列中(所以很可能出现一条消息被重复消费的情况,所以我们一定要做好消息的等幂性处理,同一条消息无论消费多少次都是同样的结果)
3 只有当消费者正确发送ACK反馈,RabbitMQ确认收到后,消息才会从RabbitMQ服务器的数据中删除。 消息的ACK确认机制默认是打开的,消息如未被进⾏ACK的消息确认机制,这条消息被锁定Unacked,之后默认的Unacked的消息会被重新放入到队列中等待消息(这个过程很快,如果不加断点调式我们无法看到Unacked
消费者ACK确认⽅式
⾃动确认(默认)
spring: rabbitmq: #开启⼿动确认消息,如果消息重新⼊队,进⾏重试 listener: simple: acknowledge-mode: manual |
代码实战
@RabbitHandler
@RabbitListener(queues = "goods_queue")
public void handleconsume(String msg, Message message, Channel channel) throws IOException {
//消息的标识
System.out.println(message.getMessageProperties().getDeliveryTag());
System.out.println(message.toString());
System.out.println("接收到消息"+msg);
//消息消费成功,我们向中间件发送ACK确认,第二个参数是指,是否同时针对多条消息进行ACK确认
//DeliveryTag表示投递序号也是消息的唯一标识符
channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
//拒绝确认。表示消息没有消费成功,第二个参数是指,是否同时针对多条消息进行拒绝ACK确认,最后一个参数表示是否重新回到队列
// channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
}
deliveryTag介绍
表示消息投递序号,每次消费消息或者消息重新投递后,deliveryTag都会增加
basicNack和basicReject介绍basicReject⼀次只能拒绝接收⼀个消息,可以设置是否requeue。
basicReject⼀次只能拒绝接收⼀个消息,可以设置是否requeue。
basicNack⽅法可以⽀持⼀次0个或多个消息的拒收,可以设置是否requeue。
⼈⼯审核异常消息(重试消息的处理)
设置重试阈值,超过后确认消费成功,记录消息,⼈⼯处理(一个消息如果被消费者拒绝ack后,一般我们会设置重新进入队列,此时这个消息又会被该消费者消费—但是大概率还是消费失败,这时可能出现一个消费失败的死循环,我们不能让这个消息出现消费失败死循环,一个消息如果重试消费达到一定的次数,我们应该直接确认它消息成功,然后记录消息,⼈⼯处理)