RabbitMQ 实战教程

1、MQ引言

1.1.解释MQ(Message Quene) : 消息队列。

- 消息:指在应用间传递的数据。消息可以是简单的文本字符串、json等,也可以是复杂的对象。
- 队列:FIFO先入先出的数据结构,存放的内容是消息。
- 消息队列:用来存储数据,且严格遵循FIFO规则的一种数据集合

1.2 MQ的应用场景

a.异步处理
# 场景说明:用户注册
# 核心功能:添加用户到表中
# 辅助功能:通知注册成功(短信,邮件)

传统的做法:
 1.串行的方式:
 	表的添加===》发送邮件===》发送短信
 	将注册信息写入数据库后,发送注册邮件,再发送注册短信,以上三个任务全部完成后才返回给客户端。
 	缺点:通知需要客户端等待,浪费时间
 	
 2.并行的方式(节约了时间)
	表的添加===》发送邮件+发送短信
 	将注册信息写入数据库后,发送邮件的同时,发送短信,以上三个任务完成后,返回给客户端。
 	缺点:代码逻辑复杂
 	
消息队列的做法
	表的添加===》加入消息队列中===》显示注册成功(不需要管辅助功能,对正常业务几乎无影响)
	优点:
		用户的响应时间等于写入数据库的时间+写入消息队列的时间(忽略不计),响应时间大大减少。
b.应用解耦
# 场景说明:双11是购物狂节,用户下单后,订单系统需要通知库存系统

传统的做法: 
	订单系统调用库存系统的接口.
	缺点:当库存系统出现故障时,订单就会失败。 订单系统和库存系统高耦合.  
	 
消息队列的做法:
	订单系统: 用户下单后,订单系统完成持久化处理,将消息写入消息队列,返回用户订单下单成功。
	库存系统: 订阅下单的消息,获取下单消息,进行减库存操作。 
	优点:库存系统出现故障,消息队列也能保证消息的可靠投递,不会导致消息丢失.
c.流量削峰
 # 场景说明: 秒杀活动
 # 问题:流量过大可能导致应用挂掉
 
 # 解决:使用消息队列
 		1. 用户的请求,服务器收到之后,首先写入消息队列,加入消息队列长度超过最大值,则直接抛弃用户请求或跳转到错误页面.  
		2. 秒杀业务根据消息队列中的请求信息,再做后续处理.
 # 作用:	
 		1. 可以控制活动人数(超过此一定阀值的订单直接丢弃)
 		2. 可以缓解短时间的高流量压垮应用(应用程序按自己的最大处理能力获取订单) 

1.3 常见的MQ

当今市面上有很多主流的消息中间件,如老牌的ActiveMQRabbitMQ,炙手可热的Kafka,阿里巴巴自主开发RocketMQ等。

特性 ActiveMq RabbitMq RocketMQ Kafka
成熟度 成熟 成熟 比较成熟 成熟的日志领域
时效性 微秒级 毫秒级 毫秒级
社区活跃度
单机吞吐量 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 万级,吞吐量比 RocketMQ 和 Kafka 要低了一个数量级 10 万级,RocketMQ 也是可以支撑高吞吐的一种 MQ 10 万级别,这是 kafka 最大的优点,就是吞吐量高。一般配合大数据类的系统来进行实时数据计算、日志采集等场景
topic数量对吞吐量的影响 topic 可以达到几百,几千个的级别,吞吐量会有较小幅度的下降这是 RocketMQ 的一大优势,在同等机器下,可以支撑大量的 topic topic 从几十个到几百个的时候,吞吐量会大幅度下降所以在同等机器下,kafka 尽量保证 topic 数量不要过多。如果要支撑大规模 topic,需要增加更多的机器资源
可用性 高,基于主从架构实现高可用性 高,基于主从架构实现高可用性 非常高,分布式架构 非常高,kafka 是分布式的,一个数据多个副本,少数机器宕机,不会丢失数据,不会导致不可用
消息可靠性 有较低的概率丢失数据 经过参数优化配置,可以做到 0 丢失 经过参数优化配置,消息可以做到 0 丢失
功能支持 MQ 领域的功能极其完备 基于 erlang 开发,所以并发能力很强,性能极其好,延时很低 MQ 功能较为完善,还是分布式的,扩展性好 功能较为简单,主要支持简单的 MQ 功能,在大数据领域的实时计算以及日志采集被大规模使用,是事实上的标准

总结

1.ActiveMQ 的性能较差,社区活跃度低,有问题无法及时解决,且版本迭代很慢,不推荐使用。

2.RabbitMQ 在吞吐量方面虽稍逊于 Kafka 和 RocketMQ ,但由于它基于 erlang 开发,所以并发能力强,性能极好,延时很低,达到微秒级。但也因为 RabbitMQ 基于 erlang 开发,所以国内很少有公司有实力做 erlang 源码级别的研究和定制。如果业务场景对并发量要求不是太高(十万级、百万级),那这四种消息队列中,RabbitMQ 一定是你的首选。

3.业内标准的Kafka,用在大数据领域的实时计算、日志采集等场景 ,绝对没问题,社区活跃度很高,绝对不会黄,何况几乎是全世界这个领域的事实性规范。

4.RocketMQ 阿里出品,Java 系开源项目,可以定制自己公司的 MQ,并且 RocketMQ 有阿里巴巴的实际业务场景的实战考验。RocketMQ 社区活跃度相对较为一般,文档相对来说简单一些,但接口这块不是按照标准 JMS 规范走的有些系统要迁移需要修改大量代码。还有就是阿里出台的技术,你得做好这个技术万一被抛弃,社区黄掉的风险,那如果你们公司有技术实力我觉得用 RocketMQ 挺好的。


2、RabbitMQ 引言

2.1 RabbitMQ 简介

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

RibbitMQ基于AMQP协议,erlang语言开发,是部署最广泛的开源消息中间件,是最受欢迎的开源消息中间件之一。

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

官方教程: https://www.rabbitmq.com/#getstarted

JMS

在RabbitMQ诞生之前,有一些消息中间件的商业实现,比如微软的MSMQ(MicroSofg Message Queue)、IBM的WebSphere等。由于高昂的价格,一般只应用于大型组织机构,它们需要可靠性、解耦以及实时消息通信的功能。由于商业壁垒,商业MQ供应商想要解决应用胡同的问题,而不是去创建标准来实现不同的MQ产品间的互通,或者允许应用程序更改MQ平台。
为了打破这个壁垒,同时为了能够让消息在各个消息队列平台间互融互通,JMS试图通过提供公共Java API的方式,隐蔽MQ产品供应商提供的实际接口,从而跨越壁垒解决互通问题。从技术上讲,Java应用程序只需要针对JMS API编程,选择合适的MQ驱动即可。

总结:JMS是定义了统一的接口,规范了对消息操作的API。

AMQP

由于依赖JMS标准化接口只从API层面来胶合众多不同的MQ实现,使得应用程序变得更加脆弱,因此需要一个数据传输时的消息通信标准化方案。在2006年6月,由Cisco、Redhat、IMatix等联合制定了AMQP的公开标准。它为面向消息的中间件设计,基于此协议的客户端和消息中间件可以传递消息,并不受产品、开发语言等条件的限制。

AMQP(advanced message queuing protocol),最早用于解决金融领域不同平台之间的消息传递交互问题。顾名思义,AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。这使得实现了AMQP的provider天然性就是跨平台的。

2.2 RabbitMQ的架构

RabbitMQ 实战教程_第1张图片

  • Queue:队列,是RabbitMQ的内部对象,用于暂存生产者发布的消息,为消费者提供消费的消息。
  • Exchange:交换器,用来将生产者发布的消息按照特定的规则转发(路由)到一个或多个队列中。
  • Producer:生产者,就是发布消息的一方。
  • Consumer:消费者,就是接收消息的一方,多个消费者可以订阅同一个队列,这时队列中的消息会被平均分摊,提高了消费速率。
  • Broker:消息中间件的服务节点。大多数情况Broker就是一台安装了RabbitMQ的服务器。
  • Virtual Host:虚拟主机,在一个Broker中可以划分出多个vhost,每个vhost是虚拟的独立的小型RabbitMQ服务器。vhost提供了逻辑上的独立空间,可以避免队列和交换器的命名冲突无法将不同vhost中的交换器和队列绑定。主要作用是为了支持多用户

3、RabbitMQ 的安装(docker)

1. 下载镜像
   docker pull rabbitmq:3.7.28-management

2. 启动容器
   docker run -d  --name rabbitmq \
   -p 15672:15672 -p 5672:5672 \
   -e RABBITMQ_DEFAULT_USER=guest -e RABBITMQ_DEFAULT_PASS=guest \
   rabbitmq:3.7.28-management

3. 解释:   
	-p暴露端口:15672: web访问		5672: java操作,TCP监听端口	25672:集群访问
	-e初始化web访问的用户名和密码

4、RabbitMQ 的管理

4.1 web端管理

RabbitMQ management插件可以提供Web管理界面用来管理虚拟主机、用户等,也可以用来管理队列、交换器、绑定关系、策略、参数等,还可以监控RabbitMQ服务的状态及一些数据统计类信息,功能强大,基本上能够涵盖所有RabbitMQ管理的功能。

注:在使用Web管理界面之前需要先开启RabbitMQ management插件(docker安装不需要)

a.Overview概览页面

进入Web管理界面后,首页一共六个选项卡
在这里插入图片描述

  • Overview: 这里可以概览 RabbitMQ 的整体情况,如果是集群,也可以查看集群中各个节点的情况。包括 RabbitMQ 的端口映射信息等,都可以在这个选项卡中查看。
  • Connections:这个选项卡中是连接上 RabbitMQ 的生产者和消费者的情况。
  • Channels:通道,建立连接后会形成通道,消息的投递获取依赖通道。这里展示的是“通道”信息,关于“通道”和“连接”的关系。
  • Exchanges:交换机,这里展示所有的交换机信息。
  • Queues: 这里展示所有队列的信息。
  • Admin: 这里展示所有的用户信息。

    右上角是页面刷新和展示的Virtual host的配置,默认是 5 秒刷新一次,展示的是所有的 Virtual host。
b.Exchange和Queue

Exchange面板展示交换器信息,并且可以在本页面手动添加交换器。
RabbitMQ 实战教程_第2张图片
Queue选项卡显示RabbitMQ中的消息队列,并且可以在本页面添加队列。
RabbitMQ 实战教程_第3张图片

c.Admin管理(用户、虚拟主机的管理操作)

1.新建用户
RabbitMQ 实战教程_第4张图片
上面的Tags选项,其实是指定用户的角色,可选的有以下几个:

  • 超级管理员(administartor):可登陆管理控制台,查看所有信息,且可以对用户,策略(policy)进行操作。
  • 监控者(monitoring):可登陆管理控制台,同s时可以查看rabbitmq节点的相关信息(进程数,内存使用情况,磁盘使用情况等)
  • 策略制定者(policymaker):可登陆管理控制台, 同时可以对policy进行管理。但无法查看节点的相关信息
  • 普通管理者(management):仅可登陆管理控制台,无法看到节点信息,也无法对策略进行管理。
  • 其他:无法登陆管理控制台,通常就是普通的生产者和消费者。


2.新建虚拟主机
RabbitMQ 实战教程_第5张图片
绑定虚拟主机和用户
RabbitMQ 实战教程_第6张图片
创建好虚拟主机,我们还要给用户添加访问权限。点击添加好的虚拟主机,进入虚拟机设置界面:
RabbitMQ 实战教程_第7张图片
需要注意一点,之前我们新建的交换器和队列是在默认的虚拟主机中的,并不在新建的虚拟主机中。如果需要使用新建的虚拟主机,则需要在新主机下再新建交换器和队列。

4.2 命令管理(了解)

注:先进入容器:docker exec -it rabbitmq bash才能执行命令

虚拟主机管理

# 新建虚拟主机
rabbitmqctl add_vhost 虚拟主机名

#罗列虚拟主机
rabbitmqctl list_vhosts

#删除虚拟主机,删除一个vhost的同事也会删除其下所有的队列、交换器、绑定关系、用户权限、参数和策略信息
rabbitmqctl delete_vhost 虚拟主机名

用户管理

#添加用户
rabbitmqctl add_user 用户名 密码

#修改密码
rabbitmqctl change_password 用户名 密码

#罗列所有用户
rabbitmqctl list_users

#删除用户
rabbitmqctl delete_user 用户名

#设置用户身份 administartor management policymaker monitoring none
rabbitmqctl set_user_tags 用户名 角色 

权限管理

#授予权限
rabbitmqctl set_permissions [-p vhosts] {user} {conf} {write} {read}
#示例
rabbitmqctl set_permissions -p /es root ".*" ".*" ".*"

- [-p vhost]:授予用户访问权限的 vhost 名称,如果不写默认为 `/`。
- user:用户名。
- conf:用户在哪些资源上拥有可配置权限(支持正则表达式)。
- write:用户在哪些资源上拥有写权限(支持正则表达式)。
- read:用户在哪些资源上拥有读权限(支持正则表达式)。

5、RabbitMQ 的基础使用

5.1 RabbitMQ使用初体验

RabbitMQ作为队列的基本功能是暂存生产者发布的消息,并供消费者消费消息。使用RabbitMQ核心就是围绕消息的发布和消费展开的,为加深大家对这个流程的理解,我们借助Web管理端先体验一下。

a.绑定Exchange和Queue

RabbitMQ 实战教程_第8张图片

b.发布消息

RabbitMQ 实战教程_第9张图片

c.消费消息

RabbitMQ 实战教程_第10张图片

5.2 RabbitMQ编码步骤

RabbitMQ 实战教程_第11张图片

  • RoutingKey:路由键。生产者将消息发给交换器时,一般会指定一个RoutingKey。路由键决定了交换器将消息发送到哪个消息队列。
  • BindingKey:绑定键。RabbitMQ中通过绑定键将交换器与队列关联起来,在绑定的时候一般会指定一个绑定键,这样Exchange就会根据消息的路由键和绑定键进行匹配,从而决定消息路由到哪个队列。
  • Connection:连接,生产者和消费者都需要和RabbitMQ建立TCP连接从而通信。
  • Channel:信道,是建立在Connection之上的虚拟连接。多个Channel可以共用一个Connection,从而减少性能开销。
a.引入依赖
 <dependency>
   <groupId>com.rabbitmqgroupId>
   <artifactId>amqp-clientartifactId>
   <version>5.9.0version>
 dependency>
b.生产者开发步骤
public class RabbitProducerTest {
    public static void main(String[] args) throws IOException, TimeoutException {
//        1. 创建ConnectionFactory,并配置连接参数
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.11.9");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");

//        2. 使用ConnectionFactory创建Connection
        Connection connection = connectionFactory.newConnection();
//        3. 使用Connection创建Channel
        Channel channel = connection.createChannel();
 

//        4.发布消息
        for (int i = 0; i < 10; i++) {
            channel.basicPublish("my_exchange", "es", MessageProperties.PERSISTENT_TEXT_PLAIN, ("hello world" + i).getBytes());
        }
//       5.释放资源
        channel.close();
        connection.close();

    }
}
c.消费者开发步骤
public class RabbitConsumerTest {
    public static void main(String[] args) throws IOException, TimeoutException {
//        1. 创建ConnectionFactory,并配置参数
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");
//      2. 使用ConnectionFactory创建Connection
        Connection connection = connectionFactory.newConnection(new Address[]{
                new Address("192.168.11.9", 5672)
        });
//      3. 使用Connection创建Channel
        Channel channel = connection.createChannel();

//      4. 消费指定队列的消息  autoAck:是否主动响应确认消息,如果为false,则需要通过channel.basicAck确认
        channel.basicConsume("q1",true, new DefaultConsumer(channel) {
            @Override
            public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                System.out.println("接收到消息:" + new String(body));
            }
        });
//      5. 释放资源(如果需要一直消费,则不要释放)
//        channel.close();
//        connection.close();
    }
}
d.Exchange和Queue的创建
public class BindingsTest {
    public static void main(String[] args) throws IOException, TimeoutException {
//        1. 创建ConnectionFactory,并配置连接参数
        ConnectionFactory connectionFactory = new ConnectionFactory();
        connectionFactory.setHost("192.168.136.137");
        connectionFactory.setPort(5672);
        connectionFactory.setUsername("guest");
        connectionFactory.setPassword("guest");

//        2. 使用ConnectionFactory创建Connection
        Connection connection = connectionFactory.newConnection();
//        3. 使用Connection创建Channel
        Channel channel = connection.createChannel();
//        4.通过通道channel声明交换器
        /*
        * exchange:     交换器的名称
        * type:         交换器的类型(direct,topic,fanout,headers)
        * durable:      是否是持久交换器(true:持久,在rabbitmq重启前后,信息不丢失)
        * autoDelete:   是否是自动删除的(true:意味着与之关联的所有队列断开了关联,该交换器自动删除)
        * internal:     是否是内部的交换器(true:内部交换器不能被生产者直接使用)
        * arguments:    设置交换器的额外描述信息
        * */
        channel.exchangeDeclare("e1", BuiltinExchangeType.DIRECT,true,false,false,null);
//        5.通过通道channel声明队列
        /*
        * queue:        队列名
        * durable:      是否是持久(true:rabbitmq重启前后队列不丢失)
        * exclusive:    是否是排他的(true:只有声明创建它的连接才可以使用这个队列)
        * autoDelete:   是否自动删除的(true:当与之关联的消费者断开与队列的连接后,将自动删除)
        * arguments:    设置队列的额外描述信息(如队列长度,消息时效等高级功能)
        * */
        channel.queueDeclare("q1",true,false,false,null);
//         6.绑定交换器和队列
        /*
        * queue:        队列名
        * exchange:     交换器名
        * bingdingKey:  绑定键
        * */
        channel.queueBind("q1","e1","bk1");
//        7.关闭资源
        channel.close();
        connection.close();
    }
}

注:
和我们想象的不同,上述代码并不需要提前运行,完全可以嵌入到生产者或者消费者代码中,且上述代码是幂等的,即多次执行不会出错(只要中间不修改参数)。

那么何时创建交换器和队列?

RabbitMQ 的消息存储在队列中,交换器的使用并不真正耗费服务器的性能,而队列会。如果要衡量 RabbitMQ 当前的 OPS只需看队列的即可。在实际业务应用中,需要对所创建的队列的流量、内存占用及网卡占用有一个清晰的认知,预估其平均值和峰值,以便在固定硬件资源的情况下能够进行合理有效的分配。

按照 RabbitMQ 官方建议,生产者和消费者都应该尝试创建(这里指声明操作)队列。这是一个很好的建议,但不适用于所有的情况。如果业务本身在架构设计之初已经充分地预估了队列的使用情况,完全可以在业务程序上线之前在服务器上创建好 (比如通过页面管理、RabbitMQ命令或者更好的是从配置中心下发),这样业务程序也可以免去声明的过程,直接使用即可。
预先创建好资源有2个好处:

—— 提高代码的健壮性:可以确保交换器和队列之间正确地绑定匹配。由于人为因素、代码缺陷等,发送消息的交换器没有绑定队列或交换器绑定了某个队列,但发送消息时的路由键无法与现存的队列匹配,那么消息会丢失。

——便于管理和迁移:在后期运行过程中超过预定的阙值,可根据实际情况对当前集群进行扩容或者将相应的队列迁移到其他集群。此种方法也更有利于开发和运维分工,便于相应资源的管理。

5.3 Exchange的类型

Exchange负责将生产者发布的消息路由到特定的队列中。
RabbitMQ中常用的交换器有4种类型:direct、topic、fanout和header,不同类型的交换器遵循不同的路由规则。

direct : BindingKey和RoutingKey完全匹配
topic :模糊匹配,与 direct 类型相似

匹配规则约定:
——RoutingKey 和 RoutingKey为一个点号“.”分隔的字符串(被点号“.”分隔开的每一段独立的字符串称为一个单词),如“com.rabbitmq.client”、“java.util.concurrent”;
——BindingKey 中可以存在两种特殊字符串“*”和“#”,用于做模糊匹配,其中“ * ”用来匹配一个单词,“#”用来匹配任意个单词。

fanout :会将消息路由到所有与该交换器绑定的队列中
headers : 根据发送的消息内容中的 headers 属性进行匹配(性能差,不采用)

5.4 生产端的细节

a.消息属性

BasicPropeties是 channel.basicPublish 方法中的参数,该类包含14个属性成员,每个属性有不同的作用,发送消息时,我们可以通过BasicProperties参数对消息本身进行一个额外的设置,但是其构造方法比较繁琐。

我们可以通过BasicProperties.Builder构建特定的消息属性。

new AMQP.BasicProperties.Builder()
   .contentType("application/json")//内容类型
   .deliveryMode(2)//投递模式
   .priority(1)//优先级
   .expiration("5000")//过期时间
   .build();

也可以使用MessageProperties中内置的公开静态常量

例如:MessageProperties.PERSISTENT_TEXT_PLAIN
b.mandatory参数

mandatory是 channel.basicPublish 方法中的参数,决定交换器无法路由到达队列,如何处理消息。

  • 为false时,直接丢弃消息。
  • 为true时,会调用Basic.Return命令将消息返还给生产者可以监听消息的返回


mandatory生效的场景:

  • 当交换器没有绑定任何队列
  • 当交换器绑定队列,但消息的路由键和所有队列的绑定键不匹配
//监听交换器无法路由到达队列
channel.addReturnListener(new ReturnListener() {
	@Override
	public void handleReturn(int replyCode, String replyText, String exchange, String routingKey, AMQP.BasicProperties properties, byte[] body) throws IOException {
		System.out.println("发送失败:"+new String(body));
	}
});
//发布信息(消息的路由键和所有队列的绑定键不匹配,mandatory生效)
channel.basicPublish("交换器", "绑定键",true, MessageProperties.PERSISTENT_TEXT_PLAIN, ("hello world").getBytes());
c.备份交换器

备份交换器,英文名称为Alternate Exchange,简称AE。
使用场景:

——生产者在发送消息时不设置 mandatory 参数,则消息未被路由将会丢失
——设置了mandatory 参数,需要添加 ReturnListener 的编程逻辑,生产者代码将变得复杂。
——如果既不想复杂化生产者的编程逻辑,又不想消息丢失,可以使用备份交换器
这样可以将未被路由的消息存储在 RabbitMQ 中,再在需要的时候去处理这些消息。

代码实现:

	   //声明备份交换器和备份队列,并绑定
       channel.exchangeDeclare("ae_exchange",BuiltinExchangeType.FANOUT);
       channel.queueDeclare("ae_queue",true,false,false,null);
       channel.queueBind("ae_queue","ae_exchange","");

       //声明常规的交换器和队列,绑定并设置备份交换器
       Map<String, Object> arguments = new HashMap<>();
       arguments.put("alternate-exchange","ae_exchange");

       channel.exchangeDeclare("normal_exchange", BuiltinExchangeType.DIRECT,true,false,arguments);
       channel.queueDeclare("normal_queue",true,false,false,null);
       channel.queueBind("normal_queue","normal_exchange","normal_key");

       //发布信息(消息的路由键和所有队列的绑定键不匹配,备份交换器生效)
       channel.basicPublish("normal_exchange", "random",true, MessageProperties.PERSISTENT_TEXT_PLAIN, ("hello world").getBytes());

注:
——还可以通过策略(Policy)的方式实现如果两者同时使用,则声明交换器时设置的优先级更高,会覆盖掉 Policy 的设置。
——备份交换器和普通的交换器没什么区别,为方便使用,建议设置为 fanout 类型。如否则消息发送到备份交换器时,再次进行路由匹配有可能失败。

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

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

5.5 消费端的细节

a.并发消费

一个RabbitMQ队列可以被多个消费者同时消费,队列将以轮询(round-robin)的分发方式发送给消费者。每个消息只会被一个消费者消费。


场景如下:

某些消费者任务大,机器性能低等,而有些消费者业务逻辑简单、机器性能卓越等,分配到的消息不均,造成整体应用吞吐量的下降。

解决方法:

用 channel.basicQos(int prefetchCount)方法。可以限制消费者所能保持的最大未确认消息的数量。

b.消费的确认与拒绝

为保证消息从队列可靠地达到消费者,RabbitMQ 提供了消息确认(messageacknowledgement)。消费者在订阅队列时,可以指定 autoAck 参数

  • false : 手动确认消息
  • true : 自动确认消息

自动确认的问题 :处理消息过程中消费者进程挂掉后消息丢失(因此使用手动确认消息更可靠)


手动确认代码示例 :
注:业务逻辑发生异常,从而需要拒绝消息,可能返回队列,也可能移除从而成为死信

public class No_AutoAck {
   public static void main(String[] args) throws IOException, TimeoutException {
       //创建ConnectionFactory
       ConnectionFactory connectionFactory = new ConnectionFactory();
       connectionFactory.setHost("192.168.136.137");
       connectionFactory.setPort(5672);
       connectionFactory.setUsername("guest");
       connectionFactory.setPassword("guest");
       //创建Connection
       Connection connection = connectionFactory.newConnection();
       //创建Channel
       Channel channel = connection.createChannel();
       //消费指定队列的消息  autoAck:是否主动响应确认消息(确认则会从内存或磁盘中移除)
       channel.basicConsume("my_queue", false, new DefaultConsumer(channel) {
                   @Override
                   public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
                       System.out.println("接收到消息:" + new String(body));
                       try{
                           //业务逻辑(处理消息)...
                           /*
                            * 手动确认
                            * deliveryTag   : 相当于消息的编号,它是一个64位的long值
                            * multiple      :true表示将本条及之前都确认,false仅本条
                            * */
                           channel.basicAck(envelope.getDeliveryTag(),false);
                       }catch (Exception e){
                           /*
                           *拒绝
                           * deliveryTag   : 相当于消息的编号,它是一个64位的long值
                           * multiple      :true表示将本条及之前都确认,false仅本条
                           * requeue       :true表示拒绝重新进入队列,false表示移除
                           * */
                           channel.basicNack(envelope.getDeliveryTag(),false,true);
                       }
                   }
               });
       //释放资源(如果需要一直消费,则不要释放)
   }
}

RabbitMQ在2.0.0版本引入了Basic.Reject命令,消费者客户端可以调用channel.basicReject或者basicNack方法拒绝消息

//requeue 为false 被拒绝消息从队列中移除  为true被拒绝消息会被重新入队,发给下个消费者
channel.basicReject(long deliveryTag, boolean requeue);

5.7 持久化

持久化可提高 RabbitMQ 的可靠性,以防在异常情况(重启、关闭、宕机等)下的数据丢失。

RabbitMQ 的持久化分为三个部分:交换器的持久化、队列的持久化和消息的持久化。

  • 交换器和队列的持久化是通过在声明时将 durable 参数置为true 实现。若不设置持久化,在 RabbitMQ 服务重启之后,则交换器消失(消息无法进入该交换器)或队列消失(队列中的消息丢失)
  • 消息持久化是通过发布消息时,设置属性deliveryMode2实现的。这样保存到磁盘中,在 RabbitMQ 服务重启之后不会丢失

注意:

  • 设置队列和消息的持久化,RabbitMQ 服务重启之后,消息存在。
  • 只设置队列持久化,重启之后消息会丢失;
  • 只设置消息的持久化,重启之后队列消失,继而消息也丢失;
  • 可以将所有的消息都设置为持久化,但是这样会严重影响 RabbitMQ 的性能 (随机)
    所以在选择是否要将消息持久化时做一个权衡

6、RabbitMQ进阶

6.1 过期时间(TTL)

a.设置消息的TTL(两种方式,两种同时存在取小值,消息超时则会变成死信

第一种 :通过队列属性设置,队列中所有消息都有相同的TTL。

//声明 交换器、队列,绑定并设置队列消息统一的ttl
channel.exchangeDeclare("ttl_exchange",BuiltinExchangeType.DIRECT);
Map<String, Object> arguments = new HashMap<>();
arguments.put("x-message-ttl",10000);
channel.queueDeclare("ttl_queue",true,false,false,arguments);
channel.queueBind("ttl_queue","ttl_exchange","ttl");
//发布消息
channel.basicPublish("ttl_exchange","ttl",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello world".getBytes());

第二种 :对消息本身进行单独设置,每条消息的 TTL 可以不同。

//设置消息属性
channel.basicPublish("ttl_exchange","ttl", new AMQP.BasicProperties.Builder()
                    .expiration("5000")
                    .build(),"ttlTestMessage".getBytes());
b.设置队列的TTL
  • 通过 channel.queueDeclare方法中的x-expires 参数可以控制队列被自动删除前处于未使用状态的时间。
  • 未使用状态是消费者未正在使用该队列,队列也没有被重新声明,且在过期时间段内未调用过 Basic.Get 命令。
  • RabbitMQ 将队列删除的动作可能不及时。在RabbitMQ 重启后,持久化的队列的过期时间会被重新计算
	   //声明队列,并设置队列的ttl
       Map<String, Object> props = new HashMap<>();
       props.put("x-expires",10000);
       channel.queueDeclare("expire_queue",true,false,false,props);
       //消费消息(一直在使用状态,队列将不会计算过期时间,该消费者一旦关闭,则开启计算)
       channel.basicConsume("expire_queue",true,new DefaultConsumer(channel){
           @Override
           public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
               System.out.println("接收到消息:" + new String(body));
           }
       });

6.2 死信队列

DLX,全称为 Dead-Letter-Exchange,可称为死信交换器。当消息在队列中变成死信 (dead message)后,能被重新被发送到死信交换器中,绑定在该死信交换器的队列就称为死信队列。

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

  • 消息被拒绝 (Basic.Reject/Basic.Nack),并且没有将其重新入队(设置 requeue 参数为 false);
  • 消息过期被移除;
  • 队列达到最大长度;


代码演示:

	   //先声明死信交换器和死信队列,并绑定
       channel.exchangeDeclare("dlx_exchange", BuiltinExchangeType.DIRECT,true);
       channel.queueDeclare("dlx_queue",true,false,false,null);
       channel.queueBind("dlx_queue","dlx_exchange","dlx");
       //再声明ttl的交换器和队列,并绑定
       channel.exchangeDeclare("ttl2_exchange",BuiltinExchangeType.DIRECT);
       Map<String, Object> arguments = new HashMap<>();
       arguments.put("x-message-ttl",5000);
       //绑定死信交换器,并更换绑定键
       arguments.put("x-dead-letter-exchange","dlx_exchange");
       arguments.put("x-dead-letter-routing-key","dlx");
       channel.queueDeclare("ttl2_queue",true,false,false,arguments);
       channel.queueBind("ttl2_queue","ttl2_exchange","ttl");
       //发送消息
       channel.basicPublish("ttl2_exchange","ttl",MessageProperties.TEXT_PLAIN,"hello world".getBytes());

6.3 延迟队列

延迟队列存储的对象是延迟消息,指当消息被发送后,等待特定时间后消费者才能拿到消息进行消费。

使用场景 :

  • 在订单系统中,使用延迟队列进行延迟支付的功能
  • 使用延迟队列发送延迟消息(指令),智能设备在后续时间工作。


RabbitMQ 本身没有直接支持延迟队列的功能,但可以通过DLX和TTL 模拟出该功能。
实现:

  • 利用绑定了TTL的队列或消息,在该队列上绑定死信交换器和死信队列
  • 即设置信息超时时间,超时后,进入死信交换器匹配死信队列,让消费者消费死信队列即可

6.4 生产者确认

使用 RabbitMQ,可通过消息持久化操作来解决因服务器的异常导致的消息丢失,但在生产者发送消息时,无法判断消息是否正常到达服务器,略有丢失的风险。

两种解决方式(不能同时使用:互斥):

  • 事务机制(严重影响性能);
//开启事务
channel.txSelect();
try {
	channel.basicPublish("my_exchange","es",MessageProperties.TEXT_PLAIN,"hello".getBytes());
	if(true) throw new IOException("出错了!!!===》消息回滚===》消息未到达服务器");
	//事务提交
	channel.txCommit();
}catch (Exception e){
	//事务回滚
	e.printStackTrace();
	channel.txRollback();
}
  • 发送方确认 (publisher confirm)机制。

同步模式(效率低:发送一条,下一条需要等待上一条的结果)

//开启发布确认模式
channel.confirmSelect();
//获取刚才发布消息的确认结果
channel.basicPublish("e1","bk1",MessageProperties.PERSISTENT_TEXT_PLAIN,"hello".getBytes());
if (channel.waitForConfirms()){
	System.out.println("消息成功发布到broker");
}else {
	System.out.println("消息发布到交换器失败了");
}

异步模式

//开启发布确认模式( 每一个通道会给自己发送出的消息设置一个从1开始自增的编号 )
channel.confirmSelect();
//设置确认监听器( deliveryTag:编号   multiple:当前及以上消息是否成功)
channel.addConfirmListener(new ConfirmListener() {
	@Override
	//消息返回成功回调
	public void handleAck(long deliveryTag, boolean multiple) throws IOException {
		System.out.println("消息成功发布到交换器");
		System.out.println("编号:"+deliveryTag);
		System.out.println(multiple);
	}
	@Override
	//消息返回失败回调
	public void handleNack(long deliveryTag, boolean multiple) throws IOException {
		System.out.println("消息发布到交换器失败");
		System.out.println("deliveryTag = " + deliveryTag + ", multiple = " + multiple);
	}
});

6.6 消息传输保障

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

  • At most once: 最多一次。消息可能会丢失,但绝不会重复传输

  • Atleast once: 最少一次。消息绝不会丢失,但可能会重复传输

实现:
——生产端:开启发布确认机制(确保消息传送到RabbitMQ)
——交换器:开启备份交换器和队列(确保消息能路由到队列)
——队列和消息:开启持久化(确保不会因服务器异常导致消息丢失)
——消费端:手动确认消费(确保消息被正常消费)
场景:
——生产者发送完消息,但因网络波动等原因,造成RabbitMQ未收到ack确认,此时因开启消息确认机制,导致消息需重新发送,导致重复发送
——消费者消费完消息,但因网络波动等原因,造成RabbitMQ未收到ack确认,此时因开启消息确认机制,导致消息重新返回队列再次消费,导致重复消费

  • Exactly once:恰好一次。每条消息肯定会被传输一次且仅传输一次

RabbitMQ无法保障,可再通过业务端进行去重处理。

7、SpringBoot中使用RabbitMQ

7.1 开发第一个项目

a. 引入依赖
<dependency>
 <groupId>org.springframework.bootgroupId>
 <artifactId>spring-boot-starter-amqpartifactId>
 <version>2.3.12.RELEASEversion>
dependency>
b. 配置配置文件
spring:
 rabbitmq:
   host: 192.168.136.137
   port: 5672
   username: guest
   password: guest
c. 生产者(使用SpringBoot Amqp模块提供的RabbitTemplate)
@RestController
public class ProducerController {
   @Autowired
   private RabbitTemplate rabbitTemplate;
   @RequestMapping("producer")
   public String producer(String message){
       rabbitTemplate.convertAndSend("e1","bk1","hello world".getBytes());
       return "success";
   }
}
d.消费者
@Component
public class RabbitConsumer {
   //concurrency:并发消费的线程数(轮询)
   @RabbitListener(queues = "q1",concurrency = "3")
   public void consumer1(Message message, Channel channel) throws IOException {
       String msg = new String(message.getBody());
       System.out.println("thread = "+Thread.currentThread()+"msg = " + msg);
   }
}

7.2 声明Exchange、Queue并绑定

a.配置类提前声明
@Configuration
public class RabbitMQConfig {
   //声明交换器和队列及关系
   @Bean("bootExchange")
   public DirectExchange directExchange(){
       return new DirectExchange("boot_exchange",true,false,null);
   }
   @Bean("bootQueue")
   public Queue bootQueue(){
       return new Queue("boot_queue",true,false,false,null);
   }
   //第一种绑定方式
   /*@Bean
   public Binding binding(){
       return new Binding("boot_queue", Binding.DestinationType.QUEUE,"boot_exchange","bebq",null);
   }*/
   //第二种绑定方式
   @Bean
   public Binding binding(@Qualifier("bootExchange") DirectExchange exchange,@Qualifier("bootQueue") Queue queue){
       return BindingBuilder.bind(queue).to(exchange).with("bebq");
   }
}
b.消费端声明配置
   //消费端声明交换器和队列及关系,监听并消费
   @RabbitListener(
           bindings={
                   @QueueBinding(
                           value=@Queue(name="bootQueue"), exchange=@Exchange(name="bootExchange"),key="boot"
                   )
           }
   )
   public void consumer2(Message message, Channel channel) throws IOException {
       System.out.println("msg = " + new String(message.getBody()));
   }

7.3 生产端的确认机制

a.yaml配置
spring:
 rabbitmq:
   ...
   #发布确认模式类型(默认none:关闭发布确认模式;correlated:异步模式;simple:同步模式(单个确认))
   publisher-confirm-type: correlated
   #消息到达队列的确认回调
   publisher-returns: true
   template:
     mandatory: true
b.监听消息发布到交换器的确认
public class SendConfirmCallback implements RabbitTemplate.ConfirmCallback {
   @Override
   //CorrelationData:发送消息时附带的关联消息  ack:响应结果    cause:失败原因
   public void confirm(CorrelationData correlationData, boolean ack, String cause) {
       if(ack){
           System.out.println("消息发送到交换器成功, correlationData = "+correlationData);
       }else {
           System.out.println("消息发送到交换器失败, correlationData = "+correlationData);
       }
   }
}
c.监听消息没有被正确发布到队列返回的响应
public class SendReturnCallback implements RabbitTemplate.ReturnCallback {
   @Override
   public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) {
       System.out.println("从交换器"+exchange+"以路由键"+routingKey+"发送消息"+new String(message.getBody())+"未找到匹配队列。replyCode = "+replyCode+" replyText = "+replyText);
   }
}
d.配置类中配置两个监听器到RebbitTemplate(自定义)
@Configuration
public class RabbitMQConfig {
	@Bean
	public RabbitTemplate rabbitTemplate(RabbitTemplateConfigurer >configurer, ConnectionFactory connectionFactory) {
   		RabbitTemplate template = new RabbitTemplate();
   		configurer.configure(template, connectionFactory);
   		template.setConfirmCallback(new SendConfirmCallback());
   		template.setReturnCallback(new SendReturnCallback());
   		return template;
	}
}

7.4 消费端的手动确认

a.yaml配置开启手动确认
spring:
 rabbitmq:
   ...
   #开启手动确认
   listener:
     type: direct
     direct:
       acknowledge-mode: manual
b.消费时手动确认
@Component
public class RabbitConsumer {
   //concurrency:并发消费的线程数(轮询)
   @RabbitListener(queues = "q1",concurrency = "3")
   public void consumer1(Message message, Channel channel) throws IOException {
       try{
           //业务逻辑(处理消息)...
           /*
            * 手动确认
            * deliveryTag   : 相当于消息的编号,它是一个64位的long值
            * multiple      :true表示将本条及之前都确认,false仅本条
            * */
           channel.basicAck(message.getMessageProperties().getDeliveryTag(),false);
       }catch (Exception e){
           /*
            *拒绝
            * deliveryTag   : 相当于消息的编号,它是一个64位的long值
            * multiple      :true表示将本条及之前都确认,false仅本条
            * requeue       :true表示拒绝重新进入队列,false表示移除
            * */
           channel.basicNack(message.getMessageProperties().getDeliveryTag(),false,true);
       }
   }

你可能感兴趣的:(java-rabbitmq,rabbitmq,java)