《消息中间件之RabbitMQ入门到高级使用》
文章中涉及到的代码参考:https://github.com/luguangdong/rabbitmq-demo.git
消息中间件概述
什么是消息中间件
MQ全称为Message Queue,消息队列是应用程序和应用程序之间的通信方法。
-
为什么使用MQ
在项目中,可将一些无需即时返回且耗时的操作提取出来,进行异步处理,而这种异步处理的方式大大的节省了服务器的请求响应时间,从而提高了系统的吞吐量。
-
开发中消息队列通常有如下应用场景:
-
任务异步处理
将不需要同步处理的并且耗时长的操作由消息队列通知消息接收方进行异步处理。提高了应用程序的响应时间。
-
应用程序解耦合
MQ相当于一个中介,生产方通过MQ与消费方交互,它将应用程序进行解耦合。
-
削峰填谷
如订单系统,在下单的时候就会往数据库写数据。但是数据库只能支撑每秒1000左右的并发写入,并发量再高就容易宕机。低峰期的时候并发也就100多个,但是在高峰期时候,并发量会突然激增到5000以上,这个时候数据库肯定卡死了。
-
消息被MQ保存起来了,然后系统就可以按照自己的消费能力来消费,比如每秒1000个数据,这样慢慢写入数据库,这样就不会卡死数据库了。
但是使用了MQ之后,限制消费消息的速度为1000,但是这样一来,高峰期产生的数据势必会被积压在MQ中,高峰就被“削”掉了。但是因为消息积压,在高峰期过后的一段时间内,消费消息的速度还是会维持在1000QPS,直到消费完积压的消息,这就叫做“填谷”
AMQP 和 JMS
MQ是消息通信的模型;实现MQ的大致有两种主流方式:AMQP、JMS。
AMQP
AMQP是一种协议,更准确的说是一种binary wire-level protocol(链接协议)。这是其和JMS的本质差别,AMQP不从API层进行限定,而是直接定义网络交换的数据格式。
JMS
JMS即Java消息服务(JavaMessage Service)应用程序接口,是一个Java平台中关于面向消息中间件(MOM)的API,用于在两个应用程序之间,或分布式系统中发送消息,进行异步通信。
AMQP 与 JMS 区别
-
JMS是定义了统一的接口,来对消息操作进行统一;AMQP是通过规定协议来统一数据交互的格式
-
JMS限定了必须使用Java语言;AMQP只是协议,不规定实现方式,因此是跨语言的。
-
JMS规定了两种消息模式;而AMQP的消息模式更加丰富
消息队列产品
市场上常见的消息队列有如下:
- ActiveMQ:基于JMS
- ZeroMQ:基于C语言开发
- RabbitMQ:基于AMQP协议,erlang语言开发,稳定性好
- RocketMQ:基于JMS,阿里巴巴产品
- Kafka:类似MQ的产品;分布式消息系统,高吞吐量
RabbitMQ
RabbitMQ简介
RabbitMQ是由erlang语言开发,基于AMQP(Advanced Message Queue 高级消息队列协议)协议实现的消息队列,它是一种应用程序之间的通信方法,消息队列在分布式系统开发中应用非常广泛。
RabbitMQ官方地址:http://www.rabbitmq.com/
RabbitMQ提供了6种模式:简单模式,work模式,Publish/Subscribe发布与订阅模式,Routing路由模式,Topics主题模式,RPC远程调用模式(远程调用,不太算MQ;暂不作介绍);
官网对应模式介绍:https://www.rabbitmq.com/getstarted.html
具体特点包括:
- 可靠性(Reliability)RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
- 灵活的路由(Flexible Routing)在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
- 消息集群(Clustering)多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
- 高可用(Highly Available Queues)队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
- 多种协议(Multi-protocol)RabbitMQ 支持多种消息队列协议,比如 STOMP、MQTT 等等。
- 多语言客户端(Many Clients)RabbitMQ 几乎支持所有常用语言,比如 Java、.NET、Ruby 等等。
- 管理界面(Management UI)RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
- 跟踪机制(Tracing)如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
- 插件机制(Plugin System)RabbitMQ 提供了许多插件,来从多方面进行扩展,也可以编写自己的插件。
架构图与主要概念
架构图
主要概念
RabbitMQ Server: 也叫broker server,它是一种传输服务。 他的角色就是维护一条从Producer到Consumer的路线,保证数据能够按照指定的方式进行传输。
Producer: 消息生产者,如图A、B、C,数据的发送方。消息生产者连接RabbitMQ服务器然后将消息投递到Exchange。
Consumer:消息消费者,如图1、2、3,数据的接收方。消息消费者订阅队列,RabbitMQ将Queue中的消息发送到消息消费者。
Exchange:生产者将消息发送到Exchange(交换器),由Exchange将消息路由到一个或多个Queue中(或者丢弃)。Exchange并不存储消息。RabbitMQ中的Exchange有direct、fanout、topic、headers四种类型,每种类型对应不同的路由规则。
Queue:(队列)是RabbitMQ的内部对象,用于存储消息。消息消费者就是通过订阅队列来获取消息的,RabbitMQ中的消息都只能存储在Queue中,生产者生产消息并最终投递到Queue中,消费者可以从Queue中获取消息并消费。多个消费者可以订阅同一个Queue,这时Queue中的消息会被平均分摊给多个消费者进行处理,而不是每个消费者都收到所有的消息并处理。
RoutingKey:生产者在将消息发送给Exchange的时候,一般会指定一个routing key,来指定这个消息的路由规则,而这个routing key需要与Exchange Type及binding key联合使用才能最终生效。在Exchange Type与binding key固定的情况下(在正常使用时一般这些内容都是固定配置好的),我们的生产者就可以在发送消息给Exchange时,通过指定routing key来决定消息流向哪里。RabbitMQ为routing key设定的长度限制为255bytes。
Connection: (连接):Producer和Consumer都是通过TCP连接到RabbitMQ Server的。以后我们可以看到,程序的起始处就是建立这个TCP连接。
Channels: (信道):它建立在上述的TCP连接中。数据流动都是在Channel中进行的。也就是说,一般情况是程序起始建立TCP连接,第二步就是建立这个Channel。
VirtualHost:权限控制的基本单位,一个VirtualHost里面有若干Exchange和MessageQueue,以及指定被哪些user使用
RabbitMQ安装与启动
本文采用docker-compose形式安装
安装
docker-compose.yml
version: '3.1'
services:
rabbitmq:
restart: always
image: rabbitmq:management
container_name: rabbitmq
ports:
- 5672:5672
- 15672:15672
environment:
TZ: Asia/Shanghai
RABBITMQ_DEFAULT_USER: rabbit
RABBITMQ_DEFAULT_PASS: 123456
volumes:
- ./data:/var/lib/rabbitmq
启动
进入docker-compose.yml
文件的目录,执行 docker up -d
RabbitMQ WebUI
- 访问地址 http://ip:15672
RabbitMQ入门
搭建示例工程
本文采用springboot集成rabbitmq来实现
创建工程
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.3.0.RELEASE</version>
</parent>
<groupId>com.beyond.rabbitmq</groupId>
<artifactId>rabbitmq-demo</artifactId>
<version>1.0.0-SNAPSHOT</version>
<packaging>jar</packaging>
<url>http://www.beyond.com</url>
<inceptionYear>2020-Now</inceptionYear>
<description></description>
<licenses>
<license>
<name>Apache 2.0</name>
<url>https://www.apache.org/licenses/LICENSE-2.0.txt</url>
</license>
</licenses>
<developers>
<developer>
<id>luguangdong</id>
<name>Xiu Lu</name>
<email>lgd15095370993@gmail.com</email>
</developer>
</developers>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<!-- 处理MQ队列消息绑定依赖 -->
<dependency>
<groupId>com.fasterxml.jackson.core</groupId>
<artifactId>jackson-databind</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<scope>provided</scope>
</dependency>
</dependencies>
</project>
编写生产者
package com.beyond.rabbitmq.test.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通过原生rabbitmq.client来实现消息发送
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/4 15:59
* @company https://www.beyond.com/
*/
public class ProducerTest {
static final String QUEUE_NAME = "simple_queue";
/**
* 通过原生rabbitmq.client来实现消息发送
* @throws IOException
*/
@Test
public void testProducer() throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 要发送的信息
String message = "你好;小兔子!";
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
在执行上述的消息发送之后;可以登录rabbitMQ的管理控制台,可以发现队列和其消息:
编写消费者
package com.beyond.rabbitmq.test.simple;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通过原生rabbitmq.client来实现消息接受
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest
* @date 2020/7/4 16:20
* @company https://www.beyond.com/
*/
public class ConsumerTest {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.QUEUE_NAME, true, false, false, null);
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.QUEUE_NAME, true, consumer);
//不关闭资源,应该一直监听消息
//channel.close();
//connection.close();
}
}
小结
上述的入门案例中中其实使用的是如下的简单模式:
在上图的模型中,有以下概念:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来。
- queue:消息队列,图中红色部分。类似一个邮箱,可以缓存消息;生产者向其中投递消息,消费者从其中取出消息。
在rabbitMQ中消费者是一定要到某个消息队列中去获取消息的
AMQP
相关概念介绍
AMQP 一个提供统一消息服务的应用层标准高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
AMQP是一个二进制协议,拥有一些现代化特点:多信道、协商式,异步,安全,扩平台,中立,高效。
RabbitMQ是AMQP协议的Erlang的实现。
概念 | 说明 |
---|---|
连接Connection | 一个网络连接,比如TCP/IP套接字连接。 |
会话Session | 端点之间的命名对话。在一个会话上下文中,保证“恰好传递一次”。 |
信道Channel | 多路复用连接中的一条独立的双向数据流通道。为会话提供物理传输介质。 |
客户端Client | AMQP连接或者会话的发起者。AMQP是非对称的,客户端生产和消费消息,服务器存储和路由这些消息。 |
服务节点Broker | 消息中间件的服务节点;一般情况下可以将一个RabbitMQ Broker看作一台RabbitMQ 服务器。 |
端点 | AMQP对话的任意一方。一个AMQP连接包括两个端点(一个是客户端,一个是服务器)。 |
消费者Consumer | 一个从消息队列里请求消息的客户端程序。 |
生产者Producer | 一个向交换机发布消息的客户端应用程序。 |
RabbitMQ运转流程
在入门案例中:
-
生产者发送消息
- 生产者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker;
- 声明队列并设置属性;如是否排它,是否持久化,是否自动删除;
- 将路由键(空字符串)与队列绑定起来;
- 发送消息至RabbitMQ Broker;
- 关闭信道;
- 关闭连接;
-
消费者接收消息
- 消费者创建连接(Connection),开启一个信道(Channel),连接到RabbitMQ Broker
- 向Broker 请求消费相应队列中的消息,设置相应的回调函数;
- 等待Broker回应闭关投递响应队列中的消息,消费者接收消息;
- 确认(ack,自动确认)接收到的消息;
- RabbitMQ从队列中删除相应已经被确认的消息;
生产者流转过程说明
-
客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
-
客户端调用connection.createChannel方法。此方法开启信道,其包装的channel.open命令发送给Broker,等待channel.basicPublish方法,对应的AMQP命令为Basic.Publish,这个命令包含了content Header 和content Body()。content Header 包含了消息体的属性,例如:投递模式,优先级等,content Body 包含了消息体本身。
-
客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。
消费者流转过程说明
-
消费者客户端与代理服务器Broker建立连接。会调用newConnection() 方法,这个方法会进一步封装Protocol Header 0-9-1 的报文头发送给Broker ,以此通知Broker 本次交互采用的是AMQPO-9-1 协议,紧接着Broker 返回Connection.Start 来建立连接,在连接的过程中涉及Connection.Start/.Start-OK 、Connection.Tune/.Tune-Ok ,Connection.Open/ .Open-Ok 这6 个命令的交互。
-
消费者客户端调用connection.createChannel方法。和生产者客户端一样,协议涉及Channel . Open/Open-Ok命令。
-
在真正消费之前,消费者客户端需要向Broker 发送Basic.Consume 命令(即调用channel.basicConsume 方法〉将Channel 置为接收模式,之后Broker 回执Basic . Consume - Ok 以告诉消费者客户端准备好消费消息。
- 代码层面:channel.basicConsume(ProducerTest.QUEUE_NAME, false, consumer);
- autoAck:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
- 设置为true,则在handleDelivery()消息处理方法中不必处理,设置为false则需要手动确认,在handleDelivery()方法中处理完业务逻辑后调用具体需要的回执方法,例如:channel.basicAck(envelope.getDeliveryTag(), false);
-
Broker 向消费者客户端推送(Push) 消息,即Basic.Deliver 命令,这个命令和Basic.Publish 命令一样会携带Content Header 和Content Body。
-
消费者接收到消息并正确消费之后,向Broker 发送确认,即Basic.Ack 命令。
-
客户端发送完消息需要关闭资源时,涉及到Channel.Close和Channl.Close-Ok 与Connetion.Close和Connection.Close-Ok的命令交互。
RabbitMQ工作模式
Work queues工作队列模式
模式说明
Work Queues
与入门程序的简单模式
相比,多了一个或一些消费端,多个消费端共同消费同一个队列中的消息。
应用场景:对于 任务过重或任务较多情况使用工作队列可以提高任务处理的速度。
代码
Work Queues
与入门程序的简单模式
的代码是几乎一样的;可以完全复制,并复制多一个消费者进行多个消费者同时消费消息的测试。
生产者
package com.beyond.rabbitmq.test.work;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.After;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通过原生rabbitmq.client来实现消息发送
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/4 15:59
* @company https://www.beyond.com/
*/
public class ProducerTest {
public static final String QUEUE_NAME = "simple_queue";
private ConnectionFactory connectionFactory = null;
private Connection connection = null;
private Channel channel = null;
@Before
public void before() throws IOException, TimeoutException {
//创建连接工厂
connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
connection = connectionFactory.newConnection();
// 创建频道
channel = connection.createChannel();
}
/**
* 通过原生rabbitmq.client来实现消息发送
*
* @throws IOException
*/
@Test
public void testProducer() throws IOException, TimeoutException {
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(QUEUE_NAME, true, false, false, null);
// 要发送的信息
for (int i = 1; i <= 30; i++) {
// 发送信息
String message = "你好;小兔子!work模式--" + i;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish("", QUEUE_NAME, null, message.getBytes());
System.out.println("已发送消息:" + message);
}
}
@After
public void after() throws IOException, TimeoutException {
// 关闭资源
channel.close();
connection.close();
}
}
消费者1
package com.beyond.rabbitmq.test.work;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通过原生rabbitmq.client来实现消息接受
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest1
* @date 2020/7/4 16:20
* @company https://www.beyond.com/
*/
public class ConsumerTest1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.QUEUE_NAME, true, false, false, null);
//一次只能接收并处理一个消息
channel.basicQos(1);
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
System.out.println("===============================消费者1开始===========================================");
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
System.out.println("===============================消费者1结束==========================================");
Thread.sleep(1000);
//确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.QUEUE_NAME, false, consumer);
//不关闭资源,应该一直监听消息
//channel.close();
//connection.close();
}
}
消费者2
package com.beyond.rabbitmq.test.work;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通过原生rabbitmq.client来实现消息接受
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest2
* @date 2020/7/4 16:20
* @company https://www.beyond.com/
*/
public class ConsumerTest2 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.QUEUE_NAME, true, false, false, null);
//一次只能接收并处理一个消息
channel.basicQos(1);
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
try {
System.out.println("===============================消费者2开始===========================================");
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
System.out.println("===============================消费者2开始===========================================");
Thread.sleep(1000);
//确认消息
channel.basicAck(envelope.getDeliveryTag(), false);
} catch (InterruptedException e) {
e.printStackTrace();
}
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.QUEUE_NAME, false, consumer);
//不关闭资源,应该一直监听消息
//channel.close();
//connection.close();
}
}
测试结果
小结
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系。
订阅模式类型
订阅模式示例图:
前面2个案例中,只有3个角色:
- P:生产者,也就是要发送消息的程序
- C:消费者:消息的接受者,会一直等待消息到来。
- queue:消息队列,图中红色部分
而在订阅模型中,多了一个exchange角色,而且过程略有变化:
-
P:生产者,也就是要发送消息的程序,但是不再发送到队列中,而是发给X(交换机)
-
C:消费者,消息的接受者,会一直等待消息到来。
-
Queue:消息队列,接收消息、缓存消息。
-
Exchange:交换机,图中的X。一方面,接收生产者发送的消息。另一方面,知道如何处理消息,例如递交给某个特别队列、递交给所有队列、或是将消息丢弃。到底如何操作,取决于Exchange的类型。Exchange有常见以下3种类型:
- Direct:定向,把消息交给符合指定routing key 的队列
- Fanout:广播,将消息交给所有绑定到交换机的队列
- Topic:通配符,把消息交给符合routing pattern(路由模式) 的队列
Exchange(交换机)只负责转发消息,不具备存储消息的能力,因此如果没有任何队列与Exchange绑定,或者没有符合路由规则的队列,那么消息会丢失!
Publish/Subscribe发布与订阅模式
模式说明
发布订阅模式:
- 每个消费者监听自己的队列。
- 生产者将消息发给broker,由交换机将消息转发到绑定此交换机的每个队列,每个绑定交换机的队列都将接收
到消息
代码
生产者
package com.beyond.rabbitmq.test.ps;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 发布与订阅使用的交换机类型为:fanout
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/4 21:43
* @company https://www.beyond.com/
*/
public class ProducerTest {
//交换机名称
static final String FANOUT_EXCHAGE = "fanout_exchange";
//队列名称
static final String FANOUT_QUEUE_1 = "fanout_queue_1";
//队列名称
static final String FANOUT_QUEUE_2 = "fanout_queue_2";
private ConnectionFactory connectionFactory = null;
private Connection connection = null;
private Channel channel = null;
@Before
public void before() throws IOException, TimeoutException, TimeoutException {
//创建连接工厂
connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
connection = connectionFactory.newConnection();
// 创建频道
channel = connection.createChannel();
}
@Test
public void testProducer() throws IOException, TimeoutException {
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(FANOUT_QUEUE_1, true, false, false, null);
channel.queueDeclare(FANOUT_QUEUE_2, true, false, false, null);
//队列绑定交换机
channel.queueBind(FANOUT_QUEUE_1, FANOUT_EXCHAGE, "");
channel.queueBind(FANOUT_QUEUE_2, FANOUT_EXCHAGE, "");
for (int i = 1; i <= 10; i++) {
// 发送信息
String message = "你好;小兔子!发布订阅模式--" + i;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(FANOUT_EXCHAGE, "", null, message.getBytes());
System.out.println("已发送消息:" + message);
}
// 关闭资源
channel.close();
connection.close();
}
}
消费者1
package com.beyond.rabbitmq.test.ps;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest1
* @date 2020/7/4 21:50
* @company https://www.beyond.com/
*/
public class ConsumerTest1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.FANOUT_QUEUE_1, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.FANOUT_QUEUE_1, ProducerTest.FANOUT_EXCHAGE, "");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("===============================消费者1开始===========================================");
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
System.out.println("===============================消费者1结束===========================================");
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.FANOUT_QUEUE_1, true, consumer);
}
}
消费者2
package com.beyond.rabbitmq.test.ps;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest2
* @date 2020/7/4 21:50
* @company https://www.beyond.com/
*/
public class ConsumerTest2 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.FANOUT_EXCHAGE, BuiltinExchangeType.FANOUT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.FANOUT_QUEUE_2, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.FANOUT_QUEUE_2, ProducerTest.FANOUT_EXCHAGE, "");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("===============================消费者2开始===========================================");
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
System.out.println("===============================消费者2结束===========================================");
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.FANOUT_QUEUE_2, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在每个消费者对应的控制台可以查看到生产者发送的所有消息;到达广播的效果。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges
选项卡,点击 fanout_exchange
的交换机,可以查看到如下的绑定:
发现所有的消费者是可以消费所有发送到交换机中的消息:
小结
交换机需要与队列进行绑定,绑定之后;一个消息可以被多个消费者都收到。
发布订阅模式与工作队列模式的区别
1、工作队列模式不用定义交换机,而发布/订阅模式需要定义交换机。
2、发布/订阅模式的生产方是面向交换机发送消息,工作队列模式的生产方是面向队列发送消息(底层使用默认交换机)。
3、发布/订阅模式需要设置队列和交换机的绑定,工作队列模式不需要设置,实际上工作队列模式会将队列绑 定到默认的交换机 。
Routing路由模式
模式说明
路由模式特点:
- 队列与交换机的绑定,不能是任意绑定了,而是要指定一个
RoutingKey
(路由key) - 消息的发送方在 向 Exchange发送消息时,也必须指定消息的
RoutingKey
。 - Exchange不再把消息交给每一个绑定的队列,而是根据消息的
RoutingKey
进行判断,只有队列的Routingkey
与消息的Routing key
完全一致,才会接收到消息
图解:
- P:生产者,向Exchange发送消息,发送消息时,会指定一个routing key。
- X:Exchange(交换机),接收生产者的消息,然后把消息递交给 与routing key完全匹配的队列
- C1:消费者,其所在队列指定了需要routing key 为 error 的消息
- C2:消费者,其所在队列指定了需要routing key 为 info、error、warning 的消息
代码
生产者
package com.beyond.rabbitmq.test.routing;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 路由模式的交换机类型为:direct
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/4 22:25
* @company https://www.beyond.com/
*/
public class ProducerTest {
//交换机名称
static final String DIRECT_EXCHAGE = "direct_exchange";
//队列名称
static final String DIRECT_QUEUE_INSERT = "direct_queue_insert";
//队列名称
static final String DIRECT_QUEUE_UPDATE = "direct_queue_update";
private ConnectionFactory connectionFactory = null;
private Connection connection = null;
private Channel channel = null;
@Before
public void before() throws IOException, TimeoutException, TimeoutException, TimeoutException {
//创建连接工厂
connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
connection = connectionFactory.newConnection();
// 创建频道
channel = connection.createChannel();
}
@Test
public void testProducer() throws IOException, TimeoutException {
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、direct、headers
*/
channel.exchangeDeclare(DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(DIRECT_QUEUE_INSERT, true, false, false, null);
channel.queueDeclare(DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(DIRECT_QUEUE_INSERT, DIRECT_EXCHAGE, "insert");
channel.queueBind(DIRECT_QUEUE_UPDATE, DIRECT_EXCHAGE, "update");
// 发送信息
String message = "新增了商品。路由模式;routing key 为 insert " ;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHAGE, "insert", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "修改了商品。路由模式;routing key 为 update" ;
/**
* 参数1:交换机名称,如果没有指定则使用默认Default Exchage
* 参数2:路由key,简单模式可以传递队列名称
* 参数3:消息其它属性
* 参数4:消息内容
*/
channel.basicPublish(DIRECT_EXCHAGE, "update", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
消费者1
package com.beyond.rabbitmq.test.routing;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest1
* @date 2020/7/4 22:31
* @company https://www.beyond.com/
*/
public class ConsumerTest1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.DIRECT_QUEUE_INSERT, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.DIRECT_QUEUE_INSERT, ProducerTest.DIRECT_EXCHAGE, "insert");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.DIRECT_QUEUE_INSERT, true, consumer);
}
}
消费者2
package com.beyond.rabbitmq.test.routing;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest2
* @date 2020/7/4 22:32
* @company https://www.beyond.com/
*/
public class ConsumerTest2 {
public static void main(String[] args) throws IOException, TimeoutException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.DIRECT_QUEUE_UPDATE, ProducerTest.DIRECT_EXCHAGE, "update");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.DIRECT_QUEUE_UPDATE, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges
选项卡,点击 direct_exchange
的交换机,可以查看到如下的绑定:
小结
Routing模式要求队列在绑定交换机时要指定routing key,消息会转发到符合routing key的队列。
Topics通配符模式
模式说明
Topic
类型与Direct
相比,都是可以根据RoutingKey
把消息路由到不同的队列。只不过Topic
类型Exchange
可以让队列在绑定Routingkey
的时候使用通配符!Routingkey
一般都是有一个或多个单词组成,多个单词之间以”.”分割,例如: item.insert
通配符规则:
#
:匹配一个或多个词
*
:匹配不多不少恰好1个词
举例:
item.#`:能够匹配`item.insert.abc` 或者 `item.insert
item.*`:只能匹配`item.insert
图解:
- 红色Queue:绑定的是
usa.#
,因此凡是以usa.
开头的routingkey
都会被匹配到 - 黄色Queue:绑定的是
#.news
,因此凡是以.news
结尾的routingkey
都会被匹配
代码
生产者
使用topic类型的Exchange,发送消息的routing key有3种: item.insert
、item.update
、item.delete
:
package com.beyond.rabbitmq.test.topic;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import org.junit.Before;
import org.junit.Test;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description: 通配符Topic的交换机类型为:topic
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/4 22:56
* @company https://www.beyond.com/
*/
public class ProducerTest {
//交换机名称
static final String TOPIC_EXCHAGE = "topic_exchange";
//队列名称
static final String TOPIC_QUEUE_1 = "topic_queue_1";
//队列名称
static final String TOPIC_QUEUE_2 = "topic_queue_2";
private ConnectionFactory connectionFactory = null;
private Connection connection = null;
private Channel channel = null;
@Before
public void before() throws IOException, TimeoutException, TimeoutException, TimeoutException, TimeoutException {
//创建连接工厂
connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(5672);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
connection = connectionFactory.newConnection();
// 创建频道
channel = connection.createChannel();
}
@Test
public void testProducer() throws IOException, TimeoutException {
/**
* 声明交换机
* 参数1:交换机名称
* 参数2:交换机类型,fanout、topic、topic、headers
*/
channel.exchangeDeclare(TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
// 发送信息
String message = "新增了商品。Topic模式;routing key 为 item.insert " ;
channel.basicPublish(TOPIC_EXCHAGE, "item.insert", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "修改了商品。Topic模式;routing key 为 item.update" ;
channel.basicPublish(TOPIC_EXCHAGE, "item.update", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 发送信息
message = "删除了商品。Topic模式;routing key 为 item.delete" ;
channel.basicPublish(TOPIC_EXCHAGE, "item.delete", null, message.getBytes());
System.out.println("已发送消息:" + message);
// 关闭资源
channel.close();
connection.close();
}
}
消费者1
package com.beyond.rabbitmq.test.topic;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest1
* @date 2020/7/4 23:04
* @company https://www.beyond.com/
*/
public class ConsumerTest1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.TOPIC_EXCHAGE, BuiltinExchangeType.TOPIC);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.TOPIC_QUEUE_1, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.TOPIC_QUEUE_1, ProducerTest.TOPIC_EXCHAGE, "item.update");
channel.queueBind(ProducerTest.TOPIC_QUEUE_1, ProducerTest.TOPIC_EXCHAGE, "item.delete");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.TOPIC_QUEUE_1, true, consumer);
}
}
消费者2
package com.beyond.rabbitmq.test.routing;
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DefaultConsumer;
import com.rabbitmq.client.Envelope;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerTest2
* @date 2020/7/4 22:32
* @company https://www.beyond.com/
*/
public class ConsumerTest2 {
public static void main(String[] args) throws IOException, TimeoutException, TimeoutException {
//创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
//主机地址;默认为 localhost
connectionFactory.setHost("192.168.137.109");
//连接端口;默认为 5672
connectionFactory.setPort(AMQP.PROTOCOL.PORT);
//虚拟主机名称;默认为 /
connectionFactory.setVirtualHost("/beyond");
//连接用户名;默认为guest
connectionFactory.setUsername("rabbit");
//连接密码;默认为guest
connectionFactory.setPassword("123456");
//创建连接
Connection connection = connectionFactory.newConnection();
// 创建频道
Channel channel = connection.createChannel();
//声明交换机
channel.exchangeDeclare(ProducerTest.DIRECT_EXCHAGE, BuiltinExchangeType.DIRECT);
// 声明(创建)队列
/**
* 参数1:队列名称
* 参数2:是否定义持久化队列
* 参数3:是否独占本次连接
* 参数4:是否在不使用的时候自动删除队列
* 参数5:队列其它参数
*/
channel.queueDeclare(ProducerTest.DIRECT_QUEUE_UPDATE, true, false, false, null);
//队列绑定交换机
channel.queueBind(ProducerTest.DIRECT_QUEUE_UPDATE, ProducerTest.DIRECT_EXCHAGE, "update");
//创建消费者;并设置消息处理
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
/**
* consumerTag 消息者标签,在channel.basicConsume时候可以指定
* envelope 消息包的内容,可从中获取消息id,消息routingkey,交换机,消息和重传标志(收到消息失败后是否需要重新发送)
* properties 属性信息
* body 消息
*/
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//路由key
System.out.println("路由key为:" + envelope.getRoutingKey());
//交换机
System.out.println("交换机为:" + envelope.getExchange());
//消息id
System.out.println("消息id为:" + envelope.getDeliveryTag());
//收到的消息
System.out.println("消费者1-接收到的消息为:" + new String(body, "utf-8"));
}
};
//监听消息
/**
* 参数1:队列名称
* 参数2:是否自动确认,设置为true为表示消息接收到自动向mq回复接收到了,mq接收到回复会删除消息,设置为false则需要手动确认
* 参数3:消息接收到后回调
*/
channel.basicConsume(ProducerTest.DIRECT_QUEUE_UPDATE, true, consumer);
}
}
测试
启动所有消费者,然后使用生产者发送消息;在消费者对应的控制台可以查看到生产者发送对应routing key对应队列的消息;到达按照需要接收的效果;并且这些routing key可以使用通配符。
在执行完测试代码后,其实到RabbitMQ的管理后台找到Exchanges
选项卡,点击 topic_exchange
的交换机,可以查看到如下的绑定:
小结
Topic主题模式可以实现 Publish/Subscribe发布与订阅模式
和 Routing路由模式
的功能;只是Topic在配置routing key 的时候可以使用通配符,显得更加灵活。
模式总结
RabbitMQ工作模式:
1、简单模式 HelloWorld 一个生产者、一个消费者,不需要设置交换机(使用默认的交换机)
2、工作队列模式 Work Queue 一个生产者、多个消费者(竞争关系),不需要设置交换机(使用默认的交换机)
3、发布订阅模式 Publish/subscribe 需要设置类型为fanout的交换机,并且交换机和队列进行绑定,当发送消息到交换机后,交换机会将消息发送到绑定的队列
4、路由模式 Routing 需要设置类型为direct的交换机,交换机和队列进行绑定,并且指定routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
5、通配符模式 Topic 需要设置类型为topic的交换机,交换机和队列进行绑定,并且指定通配符方式的routing key,当发送消息到交换机后,交换机会根据routing key将消息发送到对应的队列
Spring Boot整合RabbitMQ初级应用
简介
在Spring项目中,可以使用Spring-Rabbit去操作RabbitMQ
https://github.com/spring-projects/spring-amqp
尤其是在spring boot项目中只需要引入对应的amqp启动器依赖即可,方便的使用RabbitTemplate发送消息,使用注解接收消息。
一般在开发过程中*:
生产者工程:
- application.yml文件配置RabbitMQ相关信息;
- 在生产者工程中编写配置类,用于创建交换机和队列,并进行绑定
- 注入RabbitTemplate对象,通过RabbitTemplate对象发送消息到交换机
消费者工程:
- application.yml文件配置RabbitMQ相关信息
- 创建消息处理类,用于接收队列中的消息并进行处理
搭建生产者工程
启动类
package com.beyond.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName Application
* @date 2020/6/15 21:12
* @company https://www.beyond.com/
*/
@SpringBootApplication
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
配置RabbitMQ
配置文件
创建application.yml,内容如下:
spring:
application:
name: spring-boot-amqp
rabbitmq:
host: 192.168.137.109
port: 5672
virtual-host: /beyond
username: rabbit
password: 123456
# 开启发送消息确认 对应RabbitTemplate.ConfirmCallback接口
publisher-confirm-type: correlated
# 开启发送消息失败返回 对应RabbitTemplate.ReturnCallback接口
publisher-returns: true
# 这个配置是针对消息消费端的配置
listener:
simple:
# 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
acknowledge-mode: manual
concurrency: 1
max-concurrency: 1
retry:
enabled: true
logging:
config: classpath:logback-spring.xml
绑定交换机和队列
创建RabbitMQ队列与交换机绑定的配置类com.beyond.rabbitmq.config.RabbitMQConfig
package com.beyond.rabbitmq.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.ExchangeBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.QueueBuilder;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.util.HashMap;
import java.util.Map;
/**
* <p>
* Description: 队列模版配置
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName RabbitMQConfig
* @date 2020/6/15 21:17
* @company https://www.beyond.com/
*/
@Configuration
public class RabbitMQConfig {
//交换机名称
public static final String ITEM_TOPIC_EXCHANGE = "item_topic_exchange";
//队列名称
public static final String ITEM_QUEUE = "item_queue";
//声明交换机
@Bean("itemTopicExchange")
public Exchange topicExchange(){
return ExchangeBuilder.topicExchange(ITEM_TOPIC_EXCHANGE).durable(true).build();
}
//声明队列
@Bean("itemQueue")
public Queue itemQueue(){
return QueueBuilder.durable(ITEM_QUEUE).build();
}
//绑定队列和交换机
@Bean
public Binding itemQueueExchange(@Qualifier("itemQueue") Queue queue,
@Qualifier("itemTopicExchange") Exchange exchange){
return BindingBuilder.bind(queue).to(exchange).with("item.#").noargs();
}
}
消息发送者
package com.beyond.rabbitmq.test.spring;
import com.beyond.rabbitmq.config.RabbitMQConfig;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName SendMsgTest
* @date 2020/7/5 0:03
* @company https://www.beyond.com/
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class SendMsgTest {
@Autowired
private RabbitTemplate rabbitTemplate;
@Test
public void testSendMsg(){
/**
* 发送消息
* 参数一:交换机名称
* 参数二:路由key
* 参数三:发送的消息
*/
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE, "item.insert", "商品新增,routing key 为item.insert");
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE, "item.update", "商品修改,routing key 为item.update");
rabbitTemplate.convertAndSend(RabbitMQConfig.ITEM_TOPIC_EXCHANGE, "item.delete", "商品删除,routing key 为item.delete");
}
}
消息监听处理类
package com.beyond.rabbitmq.consumer.byspring;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName MyListener
* @date 2020/7/5 11:45
* @company https://www.beyond.com/
*/
@Component
public class MyListener {
/**
* 监听某个队列的消息
* @param message 接收到的消息
*/
@RabbitListener(queues = "item_queue")
public void myListener1(String message){
System.out.println("消费者接收到的消息为:" + message);
}
}
RabbitMQ 高级
过期时间TTL
过期时间TTL表示可以对消息设置预期的时间,在这个时间内都可以被消费者接收获取;过了之后消息将自动被删除。RabbitMQ可以对消息和队列设置TTL。目前有两种方法可以设置。
- 第一种方法是通过队列属性设置,队列中所有消息都有相同的过期时间。
- 第二种方法是对消息进行单独设置,每条消息TTL可以不同。
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就称为dead message被投递到死信队列, 消费者将无法再收到该消息。
设置队列TTL
在 \src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:rabbit="http://www.springframework.org/schema/rabbit"
xsi:schemaLocation=
"http://www.springframework.org/schema/context
http://www.springframework.org/schema/context/spring-context.xsd
http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/rabbit
http://www.springframework.org/schema/rabbit/spring-rabbit.xsd">
<!--定义过期队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_ttl_queue" name="my_ttl_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息如果没有消费都将在6秒之后被删除-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
</rabbit:queue-arguments>
</rabbit:queue>
</beans>
当使用配置文件时,需要在启动类中添加读取配置文件的信息注解
package com.beyond.rabbitmq;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.context.annotation.ImportResource;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName Application
* @date 2020/6/15 21:12
* @company https://www.beyond.com/
*/
@SpringBootApplication
@ImportResource("classpath:spring/spring-rabbitmq.xml")
public class Application{
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
此处也可以是使用配置类来进行配置,可以不用修改启动类
/**
* 声明一个死信队列.
* x-message-ttl 声明 过期时间
*
* @return the queue
*/
@Bean("my_ttl_queue")
public Queue myTtlQueue() {
Map<String, Object> args = new HashMap<>(1);
//x-message-ttl 声明 过期时间
args.put("x-message-ttl", 6000);
return QueueBuilder.durable("my_ttl_queue").withArguments(args).build();
}
然后在测试类,中编写如下方法发送消息到上述定义的队列:
package com.beyond.rabbitmq.test.ttl;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit4.SpringRunner;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ProducerTest
* @date 2020/7/5 14:05
* @company https://www.beyond.com/
*/
@RunWith(SpringRunner.class)
@SpringBootTest
public class ProducerTest {
@Autowired
private RabbitTemplate rabbitTemplate;
/**
* 过期队列消息
* 投递到该队列的消息如果没有消费都将在6秒之后被删除
*/
@Test
public void ttlQueueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", "发送到过期队列my_ttl_queue,6秒内不消费则不能再被消费。");
}
}
参数 x-message-ttl 的值 必须是非负 32 位整数 (0 <= n <= 2^32-1) ,以毫秒为单位表示 TTL 的值。这样,值 6000 表示存在于 队列 中的当前 消息 将最多只存活 6 秒钟。
如果不设置TTL,则表示此消息不会过期。如果将TTL设置为0,则表示除非此时可以直接将消息投递到消费者,否则该消息会被立即丢弃。
设置消息TTL
消息的过期时间;只需要在发送消息(可以发送到任何队列,不管该队列是否属于某个交换机)的时候设置过期时间即可。在测试类中编写如下方法发送消息并设置过期时间到队列:
/**
* 过期消息
* 该消息投递任何交换机或队列中的时候;如果到了过期时间则将从该队列中删除
*/
@Test
public void ttlMessageTest(){
MessageProperties messageProperties = new MessageProperties();
//设置消息的过期时间,5秒
messageProperties.setExpiration("5000");
Message message = new Message("测试过期消息,5秒钟过期".getBytes(), messageProperties);
//路由键与队列同名
rabbitTemplate.convertAndSend("my_ttl_queue", message);
}
expiration 字段以微秒为单位表示 TTL 值。且与 x-message-ttl 具有相同的约束条件。因为 expiration 字段必须为字符串类型,broker 将只会接受以字符串形式表达的数字。
当同时指定了 queue 和 message 的 TTL 值,则两者中较小的那个才会起作用。
死信队列
DLX,全称为Dead-Letter-Exchange , 可以称之为死信交换机,也有人称之为死信邮箱。当消息在一个队列中变成死信(dead message)之后,它能被重新发送到另一个交换机中,这个交换机就是DLX ,绑定DLX的队列就称之为死信队列。
消息变成死信,可能是由于以下的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
DLX也是一个正常的交换机,和一般的交换机没有区别,它能在任何的队列上被指定,实际上就是设置某一个队列的属性。当这个队列中存在死信时,Rabbitmq就会自动地将这个消息重新发布到设置的DLX上去,进而被路由到另一个队列,即死信队列。
要想使用死信队列,只需要在定义队列的时候设置队列参数 x-dead-letter-exchange
指定交换机即可。
定义死信队列的手法:
- 首先定义一个死信交换机(和正常交换机定义方式一样),然后定义死信队列(和正常队列定义方式一样)绑定到死信交换机上
- 定义一个正常的工作队列,给它设置
x-dead-letter-exchange
属性是第一步定义的死信交换机,当消息变成死信后投递到对应的死信交换机 - 再定义一个正常的定向交换机,根据不同的路由key投递消息给正常的工作队列
定义死信交换机
在 \src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<!--定义定向交换机中的持久化死信队列,不存在则自动创建-->
<rabbit:queue id="my_dlx_queue" name="my_dlx_queue" auto-declare="true"/>
<!--定义广播类型交换机;并绑定上述两个队列-->
<rabbit:direct-exchange id="my_dlx_exchange" name="my_dlx_exchange" auto-declare="true">
<rabbit:bindings>
<!--绑定路由键my_ttl_dlx、my_max_dlx,可以将过期的消息转移到my_dlx_queue队列-->
<rabbit:binding key="my_ttl_dlx" queue="my_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
队列设置死信交换机属性
为了测试消息在过期、队列达到最大长度后都将被投递死信交换机上;所以添加配置如下:
在 \src\main\resources\spring\spring-rabbitmq.xml
文件中添加如下内容:
<!--定义过期队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_ttl_dlx_queue" name="my_ttl_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息如果没有消费都将在6秒之后被投递到死信交换机-->
<entry key="x-message-ttl" value-type="long" value="6000"/>
<!--设置当消息过期后投递到对应的死信交换机-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定义限制长度的队列及其属性,不存在则自动创建-->
<rabbit:queue id="my_max_dlx_queue" name="my_max_dlx_queue" auto-declare="true">
<rabbit:queue-arguments>
<!--投递到该队列的消息最多2个消息,如果超过则最早的消息被删除投递到死信交换机-->
<entry key="x-max-length" value-type="long" value="2"/>
<!--设置当消息过期后投递到对应的死信交换机-->
<entry key="x-dead-letter-exchange" value="my_dlx_exchange"/>
</rabbit:queue-arguments>
</rabbit:queue>
<!--定义定向交换机 根据不同的路由key投递消息-->
<rabbit:direct-exchange id="my_normal_exchange" name="my_normal_exchange" auto-declare="true">
<rabbit:bindings>
<rabbit:binding key="my_ttl_dlx" queue="my_ttl_dlx_queue"/>
<rabbit:binding key="my_max_dlx" queue="my_max_dlx_queue"/>
</rabbit:bindings>
</rabbit:direct-exchange>
消息过期的死信队列测试
发送消息代码
/**
* 过期消息投递到死信队列
* 投递到一个正常的队列,但是该队列有设置过期时间,到过期时间之后消息会被投递到死信交换机(队列)
*/
@Test
public void dlxTTLMessageTest(){
rabbitTemplate.convertAndSend("my_normal_exchange", "my_ttl_dlx", "测试过期消息;6秒过期后会被投递到死信交换机");
}
在rabbitMQ管理界面中结果
过5秒后该消息会自动进入死信队列
流程
具体因为队列消息过期而被投递到死信队列的流程:
消息过长的死信队列测试
发送消息代码
/**
* 超过队列长度消息投递到死信队列
* 投递到一个正常的队列,但是该队列有设置最大消息数,到最大消息数之后队列中最早的消息会被投递到死信交换机(队列)
*/
@Test
public void dlxMaxMessageTest(){
rabbitTemplate.convertAndSend("my_normal_exchange", "my_max_dlx",
"队列my_max_dlx_queue的最大长度为2;消息超过后会被投递到死信交换机;这是第1个消息");
rabbitTemplate.convertAndSend("my_normal_exchange", "my_max_dlx",
"队列my_max_dlx_queue的最大长度为2;消息超过后会被投递到死信交换机;这是第2个消息");
rabbitTemplate.convertAndSend("my_normal_exchange", "my_max_dlx",
"队列my_max_dlx_queue的最大长度为2;消息超过后会被投递到死信交换机;这是第3个消息");
}
在rabbitMQ管理界面中结果
上面发送的3条消息中的第1条消息会被投递到死信队列中(如果启动了消费者,那么队列消息很快会被取走消费掉),其他两条消息还在工作队列中。
消费者接收死信队列消息
与过期消息投递到死信队列的代码和配置是共用的,并不需要重新编写。
流程
消息超过队列最大消息长度而被投递到死信队列的流程在前面的图中已包含。
延迟队列
延迟队列存储的对象是对应的延迟消息;所谓“延迟消息” 是指当消息被发送以后,并不想让消费者立刻拿到消息,而是等待特定时间后,消费者才能拿到这个消息进行消费。
在RabbitMQ中延迟队列可以通过 过期时间
+ 死信队列
来实现;具体如下流程图所示:
在上图中;分别设置了两个5秒、10秒的过期队列,然后等到时间到了则会自动将这些消息转移投递到对应的死信队列中,然后消费者再从这些死信队列接收消息就可以实现消息的延迟接收。
延迟队列的应用场景;如:
- 在电商项目中的支付场景;如果在用户下单之后的几十分钟内没有支付成功;那么这个支付的订单算是支付失败,要进行支付失败的异常处理(将库存加回去),这时候可以通过使用延迟队列来处理
- 在系统中如有需要在指定的某个时间之后执行的任务都可以通过延迟队列处理
消息确认机制
确认并且保证消息被送达,提供了两种方式:发布确认和事务。(两者不可同时使用)在channel为事务时,不可引入确认模式;同样channel为确认模式下,不可使用事务。
发布确认
有两种方式:消息发送成功确认和消息发送失败回调。
在发布确认前需要配置
spring:
application:
name: spring-boot-amqp
rabbitmq:
host: 192.168.137.109
port: 5672
virtual-host: /beyond
username: rabbit
password: 123456
# 开启发送消息确认 对应RabbitTemplate.ConfirmCallback接口
publisher-confirm-type: correlated
# 开启发送消息失败返回 对应RabbitTemplate.ReturnCallback接口
publisher-returns: true
# 这个配置是针对消息消费端的配置
listener:
simple:
# 默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
acknowledge-mode: manual
concurrency: 1
max-concurrency: 1
retry:
enabled: true
- 消息发送成功确认
package com.beyond.rabbitmq.callback;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.AmqpException;
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;
import javax.annotation.PostConstruct;
/**
* <p>
* Description: 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中
*
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConfirmCallbackListener
* @date 2020/6/23 17:34
* @company https://www.beyond.com/
*/
@Component("confirmCallbackListener")
public class ConfirmCallbackListener implements RabbitTemplate.ConfirmCallback {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RabbitTemplate rabbitTemplate;
//初始化spring容器时,给rabbitTemplate加载ConfirmCallback配置
@PostConstruct
public void init() {
rabbitTemplate.setConfirmCallback(this);
}
/**
* 当消息发送到交换机(exchange)时,该方法被调用.
* 1.如果消息没有到exchange,则 ack=false
* 2.如果消息到达exchange,则 ack=true
* <p>
* 注意:在confirmCallback中是没有原message的,所以无法在这个函数中调用重发,confirmCallback只有一个通知的作用
*
* @param correlationData
* @param ack
* @param cause
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if (ack) {
System.out.println("消息确认成功....");
} else {
//处理丢失的消息
System.out.println("消息确认失败," + cause);
}
}
}
功能测试如下:
发送消息
com.beyond.rabbitmq.test.ack.ProducerTest#queueTest
@Test
public void queueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("simple_queue", "只发队列simple_queue的消息。");
}
管理界面确认消息发送成功
消息确认回调
-
消息发送失败回调
失败回调类
package com.beyond.rabbitmq.callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.core.Message; import org.springframework.amqp.rabbit.core.RabbitTemplate; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.PostConstruct; /** * <p> * Description: 通过实现 ReturnCallback 接口,启动消息失败返回 * 用于实现消息发送到RabbitMQ交换器后,但无相应队列与交换器绑定时的回调。 * 在脑裂的情况下会出现这种情况 * </p> * * @author luguangdong * @version 1.0.0 * @ClassName ReturnCallbackListener * @date 2020/6/23 19:54 * @company https://www.beyond.com/ */ @Component("returnCallbackListener") public class ReturnCallbackListener implements RabbitTemplate.ReturnCallback{ Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RabbitTemplate rabbitTemplate; //初始化spring容器时,给rabbitTemplate加载ReturnCallback配置 @PostConstruct public void init(){ rabbitTemplate.setReturnCallback(this); //注意:同时需配置mandatory="true",否则消息则丢失 rabbitTemplate.setMandatory(true); } /** * 当消息从交换机到队列失败时,该方法被调用。(若成功,则不调用) * 需要注意的是:该方法调用后,MsgSendConfirmCallBack中的confirm方法也会被调用,且ack = true * @param message * @param replyCode * @param replyText * @param exchange * @param routingKey */ @Override public void returnedMessage(Message message, int replyCode, String replyText, String exchange, String routingKey) { String correlationId = message.getMessageProperties().getCorrelationId(); logger.debug("消息:{} 发送失败, 应答码:{} 原因:{} 交换机: {} 路由键: {}", correlationId, replyCode, replyText, exchange, routingKey); // TODO 保存消息到数据库 } }
功能测试如下:
模拟消息发送失败
com.beyond.rabbitmq.test.ack
@Test
public void testFailQueueTest() throws InterruptedException {
//exchange 正确,queue 错误 ,confirm被回调, ack=true; return被回调 replyText:NO_ROUTE
rabbitTemplate.convertAndSend("test-direct", "3333", "测试消息发送失败进行确认应答。");
}
失败回调结果如下:
说明:该案例中testFailQueueTest()方法总消息是可以正常发送到交换机中的,所以ConfirmCallbackListener是可以监听到这个消息的,所以首先打印了 [nectionFactory2] c.b.r.callback.ConfirmCallbackListener : 消息发送到exchange成功
,发送到交换机后,因为testFailQueueTest()方法中指定的routingkey是不存在的,所以消息发送失败,此时ReturnCallbackListener是可以监听到这条消息的,打印出了[nectionFactory1] c.b.r.callback.ReturnCallbackListener : 消息:null 发送失败, 应答码:312 原因:NO_ROUTE 交换机: test-direct 路由键: 3333
事务支持
场景:业务处理伴随消息的发送,业务处理失败(事务回滚)后要求消息不发送。rabbitmq 使用调用者的外部事务,通常是首选,因为它是非侵入性的(低耦合)。
-
事务的配置
-
application.yml
注释 publisher-confirm-type: correlated ,因为发布确认和事物不能共存# publisher-confirm-type: correlated
-
MQ配置类中RabbitMQConfig添加rabbitmq事物管理器
@Bean("rabbitTransactionManager") public RabbitTransactionManager rabbitTransactionManager(CachingConnectionFactory connectionFactory) { return new RabbitTransactionManager(connectionFactory); }
-
同理需要剔除发布确认监听配置类ConfirmCallbackListener,因为发布确认和事物不能共存
//@Component("confirmCallbackListener") public class ConfirmCallbackListener implements RabbitTemplate.ConfirmCallback {}
-
需要在失败回调监听配置类ReturnCallbackListener中设置channel事物属性
//初始化spring容器时,给rabbitTemplate加载ReturnCallback配置 @PostConstruct public void init(){ rabbitTemplate.setReturnCallback(this); //注意:同时需配置mandatory="true",否则消息则丢失 rabbitTemplate.setMandatory(true); //给Channel设置为事物 rabbitTemplate.setChannelTransacted(true); }
-
-
模拟业务处理失败的场景:
- 在测试类或者测试方法上加入@Transactional注解
@Test @Transactional public void queueTest2() throws InterruptedException { //路由键与队列同名 rabbitTemplate.convertAndSend("simple_queue", "只发队列simple_queue的消息--01。"); System.out.println("----------------dosoming:可以是数据库的操作,也可以是其他业务类型的操作---------------"); //模拟业务处理失败 System.out.println(1/0); rabbitTemplate.convertAndSend("simple_queue", "只发队列simple_queue的消息--02。"); }
- 测试结果:
`此时队列中是没有消息的,因为已经回滚了。`
消息追踪
消息中心的消息追踪需要使用Trace实现,Trace是Rabbitmq用于记录每一次发送的消息,方便使用Rabbitmq的开发者调试、排错。可通过插件形式提供可视化界面。Trace启动后会自动创建系统Exchange:amq.rabbitmq.trace ,每个队列会自动绑定该Exchange,绑定后发送到队列的消息都会记录到Trace日志。
消息追踪启用与查看
以下是trace的相关命令和使用(要使用需要先rabbitmq启用插件,再打开开关才能使用):
使用docker安装rabbitmq需要进入docker容器后执行下面命令集
# 获取运行中的rabbitmq的docker容器id
docker ps
CONTAINER ID 938139e28c4c
# 进入该容器中
docker exec -it 938139e28c4c bin/bash
命令集 | 描述 |
---|---|
rabbitmq-plugins list | 查看插件列表 |
rabbitmq-plugins enable rabbitmq_tracing | rabbitmq启用trace插件 |
rabbitmqctl trace_on | 打开trace的开关 |
rabbitmqctl trace_on -p /beyond | 打开trace的开关(itcast为需要日志追踪的vhost) |
rabbitmqctl trace_off | 关闭trace的开关 |
rabbitmq-plugins disable rabbitmq_tracing | rabbitmq关闭Trace插件 |
rabbitmqctl set_user_tags rabbit administrator | 只有administrator的角色才能查看日志界面 |
安装插件并开启 trace_on 之后,会发现多个 exchange:amq.rabbitmq.trace ,类型为:topic。
安装trace插件后,在Admin下多了一个Tracing插件,此时需要创建一个trace来记录操作日志
创建成功后
日志追踪
- 发送消息
@Test
public void queueTest(){
//路由键与队列同名
rabbitTemplate.convertAndSend("simple_queue", "只发队列simple_queue的消息。");
}
- 在管理页面点击Trace log files文件
- 浏览操作日志内容
- JSON格式的payload(消息体)默认会采用Base64进行编码,我们可以解码payload的内容
Spring Boot整合RabbitMQ高级应用
消息丢失
场景介绍
在实际的生产环境中有可能出现一条消息因为一些原因丢失,导致消息没有消费成功,从而造成数据不一致等问题,造成严重的影响,比如:在一个商城的下单业务中,需要生成订单信息和扣减库存两个动作,如果使用RabbitMQ来实现该业务,那么在订单服务下单成功后需要发送一条消息到库存服务进行扣减库存,如果在此过程中,一条消息因为某些原因丢失,那么就会出现下单成功但是库存没有扣减,从而导致超卖的情况,也就是库存已经没有了,但是用户还能下单,这个问题对于商城系统来说是致命的。
消息丢失的场景主要分为:消息在生产者丢失,消息在RabbitMQ丢失,消息在消费者丢失。
消息在生产者丢失
场景介绍
消息生产者发送消息成功,但是MQ没有收到该消息,消息在从生产者传输到MQ的过程中丢失,一般是由于网络不稳定的原因。
普通解决方案
采用RabbitMQ 发送方消息确认机制,当消息成功被MQ接收到时,会给生产者发送一个确认消息,表示接收成功。RabbitMQ 发送方消息确认模式有以下三种:普通确认模式,批量确认模式,异步监听确认模式。spring整合RabbitMQ后只使用了异步监听确认模式。
说明
异步监听模式,可以实现边发送消息边进行确认,不影响主线程任务执行。
高可用解决方案
- 在发消息的时候如果出现异常,直接将消息记录到异常消息表,等待后台跑批,进行补偿发放。
- 在发消息的时候,如果发送消息的ack回调没有发送成功,在
ConfirmCallbackListener
类中进行消息重发,如果重发3次还是失败,该消息就记录到异常消息表,等待后台跑批,进行补偿发放。消息的重复发送可以使用RabbitMQ的ConfirmCallback、ReturnCallback机制来实现。
思考
我们可以看到,在ReturnCallback
中,返回的参数是Message
对象,我们可以获取消息内容
,exchange
,routingKey
这些信息的。
但是在ConfirmCallback
中,确是没有消息信息,只有一个correlationData相关性的,并且我们看到他的日志,打印出来还是null,所以在ConfirmCallback
是拿不到重试次数的,。
于是我们打开correlationData的源码可以看到里面的属性有一个id和returnedMessage,但是returnedMessage不是我们发送的消息。
/*
* Copyright 2002-2019 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* https://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.springframework.amqp.rabbit.connection;
import org.springframework.amqp.core.Correlation;
import org.springframework.amqp.core.Message;
import org.springframework.lang.Nullable;
import org.springframework.util.concurrent.SettableListenableFuture;
/**
* Base class for correlating publisher confirms to sent messages.
* Use the {@link org.springframework.amqp.rabbit.core.RabbitTemplate}
* methods that include one of
* these as a parameter; when the publisher confirm is received,
* the CorrelationData is returned with the ack/nack.
* @author Gary Russell
* @since 1.0.1
*
*/
public class CorrelationData implements Correlation {
private final SettableListenableFuture<Confirm> future = new SettableListenableFuture<>();
@Nullable
private volatile String id;
private volatile Message returnedMessage;
}
关于重试次数的解决方案
-
于是我们扩展
correlationData
类,将id和消息属性绑定起来。我们在发送消息的时候,可以发送
correlationData
扩展对象,在confirm
的ack=false
的情况下,于是我们就可以拿到消息主体信息和重试次数了。package com.beyond.rabbitmq.callback; import lombok.Data; /** * <p> * Description: * </p> * * @author luguangdong * @version 1.0.0 * @ClassName CorrelationData * @date 2020/6/27 15:53 * @company https://www.beyond.com/ */ @Data public class CorrelationData extends org.springframework.amqp.rabbit.connection.CorrelationData { //消息体 private volatile Object message; //交换机 private String exchange; //队列名称 private String queue; //路由键 private String routingKey; //重试次数 private int retryCount = 0; public CorrelationData() { super(); } public CorrelationData(String id) { super(id); } public CorrelationData(String id, Object data) { this(id); this.message = data; } }
-
重写
ConfirmCallback
类实现消息发送丢失补偿package com.beyond.rabbitmq.callback; import org.slf4j.Logger; import org.slf4j.LoggerFactory; import org.springframework.amqp.AmqpException; 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; import javax.annotation.PostConstruct; /** * <p> * Description: 通过实现 ConfirmCallback 接口,消息发送到 Broker 后触发回调,确认消息是否到达 Broker 服务器,也就是只确认是否正确到达 Exchange 中 * * </p> * * @author luguangdong * @version 1.0.0 * @ClassName ConfirmCallbackListener * @date 2020/6/23 17:34 * @company https://www.beyond.com/ */ @Component("confirmCallbackListener") public class ConfirmCallbackListener implements RabbitTemplate.ConfirmCallback { Logger logger = LoggerFactory.getLogger(this.getClass()); @Autowired private RabbitTemplate rabbitTemplate; //初始化spring容器时,给rabbitTemplate加载ConfirmCallback配置 @PostConstruct public void init() { rabbitTemplate.setConfirmCallback(this); } /** * 当消息发送到交换机(exchange)时,该方法被调用. * 1.如果消息没有到exchange,则 ack=false * 2.如果消息到达exchange,则 ack=true * <p> * 注意:在confirmCallback中是没有原message的,所以无法在这个函数中调用重发,confirmCallback只有一个通知的作用 * * @param correlationData * @param ack * @param cause */ @Override public void confirm(CorrelationData correlationData, boolean ack, String cause) { if (ack) { if (correlationData instanceof com.beyond.rabbitmq.callback.CorrelationData) { logger.debug("消息发送到exchange成功,消息ID:{},correlationData: {}", correlationData.getId(), correlationData); } else { //适配rabbitmq原生的CorrelationData对象 logger.debug("消息发送到exchange成功"); } } else { // 根据业务逻辑实现消息补偿机制 if (correlationData instanceof com.beyond.rabbitmq.callback.CorrelationData) { com.beyond.rabbitmq.callback.CorrelationData messageCorrelationData = (com.beyond.rabbitmq.callback.CorrelationData) correlationData; String exchange = messageCorrelationData.getExchange(); String routingKey = messageCorrelationData.getRoutingKey(); Object message = messageCorrelationData.getMessage(); int retryCount = messageCorrelationData.getRetryCount(); //当重试次数大于3次后,将不再发送 if (((com.beyond.rabbitmq.callback.CorrelationData) correlationData).getRetryCount() <= 3) { //重试次数+1 ((com.beyond.rabbitmq.callback.CorrelationData) correlationData).setRetryCount(retryCount + 1); convertAndSend(exchange, routingKey, message, correlationData); } else { //消息重试发送失败,将消息放到数据库等待补发 logger.warn("MQ消息重发失败,消息入库,消息ID:{},消息体:{}", correlationData.getId(), ((com.beyond.rabbitmq.callback.CorrelationData) correlationData).getMessage()); // TODO 保存消息到数据库 } } else { //适配rabbitmq原生的CorrelationData对象 logger.debug("消息发送到exchange失败,原因: {}", cause); } } } /** * 发送消息 * * @param exchange 交换机名称 * @param routingKey 路由key * @param message 消息内容 * @param correlationData 消息相关数据(消息ID) * @throws AmqpException */ private void convertAndSend(String exchange, String routingKey, final Object message, CorrelationData correlationData) throws AmqpException { try { rabbitTemplate.convertAndSend(exchange, routingKey, message, correlationData); } catch (Exception e) { logger.error("MQ消息发送异常,消息ID:{},消息体:{}, exchangeName:{}, routingKey:{}", correlationData.getId(), message, exchange, routingKey, e); // TODO 保存消息到数据库 } } }
消息在RabbitMQ丢失
场景介绍
消息成功发送到MQ,消息还没被消费却在MQ中丢失,比如MQ服务器宕机或者重启会出现这种情况
解决方案
持久化交换机,队列,消息,确保MQ服务器重启时依然能从磁盘恢复对应的交换机,队列和消息。
我们需要在创建 交换机,队列,消息的时候,需要设置 Durability属性为 Durable
spring整合后默认开启了交换机,队列,消息的持久化,所以不修改任何设置就可以保证消息不在RabbitMQ丢失。
消息在消费者丢失
场景介绍
消费者在消费消息时,如果设置为自动回复MQ,消费者端刚收到消息后会立马自动回复MQ服务器,MQ服务器则会删除该条消息,如果消息已经在MQ被删除但是消费者的业务处理出现异常或者消费者服务宕机,那么就会导致该消息没有处理成功从而导致该条消息丢失。
普通解决方案
消费者端采用手动ACK确认模式
消息确认模式有:
- AcknowledgeMode.NONE:自动确认
- AcknowledgeMode.AUTO:根据情况确认
- AcknowledgeMode.MANUAL:手动确认
默认情况下消息消费者是自动 ack (确认)消息的,如果要手动 ack(确认)则需要修改确认模式为 manual
spring:
rabbitmq:
listener:
simple:
acknowledge-mode: manual
channel.basicAck()
手动应答,当消费者收到消息在合适的时候来显示的进行确认,说我已经接收到了该消息了,RabbitMQ可以从队列中删除该消息了,可以通过显示调用channel.basicAck(deliveryTag, false);来告诉消息服务器来删除消息。
参数:
deliveryTag(唯一标识 ID):当一个消费者向 RabbitMQ 注册后,会建立起一个 Channel ,RabbitMQ 会用 basic.deliver 方法向消费者推送消息,这个方法携带了一个 delivery tag, 它代表了 RabbitMQ 向该 Channel 投递的这条消息的唯一标识 ID,是一个单调递增的正整数,delivery tag 的范围仅限与当前 Channel。
**multiple:**为了减少网络流量,手动确认可以被批处理,当该参数为 false时, 是回执给MQ服务器删除当前处理的一条消息 ;当该参数为 true 时,则可以一次性确认删除 delivery_tag 小于等于传入值的所有消息
void basicAck(long deliveryTag, boolean multiple) throws IOException;
channel.basicNack()
参数:
deliveryTag: 该消息的index
multiple:是否批量. true:将一次性拒绝所有小于deliveryTag的消息
requeue:是否重新入队列 注意:如果设置为true ,则会添加在队列的末端
void basicNack(long deliveryTag, boolean multiple, boolean requeue) throws IOException;
channel.basicReject()
手动应答,当消费者收到消息在合适的时候来显示的进行确认,**requeue=true,表示将消息重新放入到队列中(例如在消费者出现异常的时候调用),**false:表示直接从队列中删除,此时和basicAck(long deliveryTag, false)的效果一样。
参数:
deliveryTag:该消息的index
requeue:是否重新入队列
void basicReject(long deliveryTag, boolean requeue) throws IOException;
channel.basicNack 与 channel.basicReject 的区别在于basicNack可以拒绝多条消息,而basicReject一次只能拒绝一条消息
高可用解决方案
在消费端处理消息的时候,如果出现异常也将消息放到异常消息表中,等待后台跑批,进行补偿发放。如果将异常消息保存到数据库时发生了异常,则将消息放到死信队列,等待后台跑批,进行补偿发放。
定义死信队列
在RabbitMQConfig
配置类中添加一下内容
/**
* 定义死信交换机
* @return
*/
@Bean("myDlxExchange")
public Exchange myDlxExchange() {
return ExchangeBuilder.directExchange("my_dlx_exchange").durable(true).build();
}
/**
* 定义死信队列
* @return
*/
@Bean("myDlxQueue")
public Queue myDlxQueue() {
return QueueBuilder.durable("my_dlx_queue").build();
}
/**
* 将死信队列根据指定的路由key绑定到死信交换机上
* @return
*/
@Bean
public Binding myDlxQueueBinding() {
return new Binding("my_dlx_queue", Binding.DestinationType.QUEUE, "my_dlx_exchange", "EX", null);
}
/**
* 定义正常队列,设置死信交换机属性
* @return
*/
@Bean("myNormalQueue")
public Queue myNormalQueue() {
Map<String, Object> args = new HashMap<>(1);
//x-dead-letter-exchange 声明 死信交换机
args.put("x-dead-letter-exchange", "my_dlx_exchange");
//x-dead-letter-routing-key 声明 死信路由键
//args.put("x-dead-letter-routing-key", "EX");
return QueueBuilder.durable("my_normal_queue").withArguments(args).build();
}
/**
* 定义正常交换机
* @return
*/
@Bean("myNormalExchange")
public Exchange myNormalExchange() {
return ExchangeBuilder.directExchange("my_normal_exchange").durable(true).build();
}
/**
* 将正常队列根据指定的路由key绑定到正常交换机上
* @return
*/
@Bean
public Binding myNormalQueueBinding() {
return new Binding("my_normal_queue", Binding.DestinationType.QUEUE, "my_normal_exchange", "EX", null);
}
消费者补偿方式
package com.beyond.rabbitmq.consumer;
import com.beyond.rabbitmq.callback.CorrelationData;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerConfirm
* @date 2020/6/27 19:31
* @company https://www.beyond.com/
*/
@Component
@RabbitListener(queues = "my_normal_queue")
public class ConsumerConfirm {
Logger logger = LoggerFactory.getLogger(this.getClass());
@RabbitListener(queues = "my_normal_queue")
public void onMessage(Message message, Channel channel, CorrelationData correlationData) throws Exception {
try {
//接受消息后处理业务开始
System.out.println("consumer--:" + message.getMessageProperties() + ":" + new String(message.getBody()));
//接受消息后处理业务结束
//模拟异常
int i = 1 / 0;
/**
* 处理业务结束后,ACK回执给MQ服务器删除该消息。
* multiple:false 是指ACK回执给MQ服务器删除当前处理的一条消息
* multiple:true 是指ACK回执给MQ服务器删除deliveryTag小于等于传入值的所有消息
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
/**
* 出现异常后,ACK回执给MQ服务器是否删除该消息。
* multiple:false 是指ACK回执给MQ服务器删除当前处理的一条消息
* multiple:true 是指ACK回执给MQ服务器删除deliveryTag小于等于传入值的所有消息
* requeue:false 不会重新入队列,ACK回执给MQ服务器删除消息
* requeue:true 会重新入队列,会添加在队列的末端
*/
logger.error("MQ消息处理异常,消息ID:{},消息体:{}", message.getMessageProperties().getCorrelationId(), message, e);
try {
// TODO 保存消息到数据库
// 确认消息已经消费成功
//模拟异常
int i = 1 / 0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e1) {
logger.error("保存异常MQ消息到数据库异常,放到死性队列,消息ID:{}", message.getMessageProperties().getCorrelationId());
/**
* 此处 requeue属性 可以为 false 也可以为true
* 当requeue: true时,消费端如果出现异常后则会将这条消息 一直 给MQ服务器发送,消息会重新入队列,会添加在队列的末端。
* (使用true时,此处出现的异常次数要少,比如网络波动,如果这个异常每次都出现的话,那么就会造成 死循环 )。
*
* 当requeue: false时,消费端如果出现异常后则会将这条消息会发送到死信队列中
*/
//确认消息将消息放到死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
}
}
测试发送
@Test
public void dlxMessageTest() throws InterruptedException {
//路由键与队列同名
rabbitTemplate.convertAndSend("my_normal_exchange", "EX","只发队列simple_queue的消息--01。");
}
效果
当我们在保存消息到数据库时, 模拟异常 int i = 1 / 0;
,此时我们查看mq管理页面发现,正常队列my_normal_queue
中没有消息,死信队列my_dlx_queue
会有一条消息。
重复消费
场景介绍
为了防止消息在消费者端丢失,会采用手动回复MQ的方式来解决,同时也引出了一个问题,消费者处理消息成功,手动回复MQ时由于网络不稳定,连接断开,导致MQ没有收到消费者回复的消息,那么该条消息还会保存在MQ的消息队列,由于MQ的消息重发机制,会重新把该条消息发给和该队列绑定的消息者处理,这样就会导致消息重复消费。而有些操作是不允许重复消费的,比如下单,减库存,扣款等操作。
MQ重发消息场景:
-
消费者未响应ACK,主动关闭频道或者连接
-
消费者未响应ACK,消费者服务挂掉
解决方案
如果消费消息的业务是幂等性操作(同一个操作执行多次,结果不变)就算重复消费也没问题,可以不做处理,如果不支持幂等性操作,如:下单,减库存,扣款等,那么可以在消费者端每次消费成功后将该条消息id保存到数据库,每次消费前查询该消息id,如果该条消息id已经存在那么表示已经消费过就不再消费否则就消费。本方案采用redis存储消息id,因为redis是单线程的,并且性能也非常好,提供了很多原子性的命令,本方案使用setnx命令存储消息id。
setnx(key,value):如果key不存在则插入成功且返回1,如果key存在,则不进行任何操作,返回0
发送消息时需要发送全局唯一的业务id
@Test
public void repeatMessageTest() throws InterruptedException, UnsupportedEncodingException {
byte[] body = "使用redis解决rabbbitmq重复消费问题".getBytes("UTF-8");
MessageProperties messageProperties = new MessageProperties();
//全局唯一id,比如通过消息队列来生成订单,那订单号就是唯一的
messageProperties.setMessageId("123");
Message message = new Message(body,messageProperties);
rabbitTemplate.convertAndSend("my_normal_exchange", "EX",message);
}
消费端判断消息id是否重复来确认消费消息
消费者是集成redis来实现判断消息id是否重复,如果业务量大的时候那么往redis中写入的数据量就有些大,那么这时我们可以设置redis中key的过期时间来降低redis服务的存储压力,具体的时间范围根据业务情况来定
例如设置过期时间为24小时
redisTemplate.opsForValue().set(messageId, "消息正常消费成功",24, TimeUnit.HOURS);
package com.beyond.rabbitmq.consumer;
import com.rabbitmq.client.Channel;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.RedisCallback;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Component;
/**
* <p>
* Description:
* </p>
*
* @author luguangdong
* @version 1.0.0
* @ClassName ConsumerConfirm
* @date 2020/6/27 19:31
* @company https://www.beyond.com/
*/
@Component
public class ConsumerConfirm {
Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
private RedisTemplate redisTemplate;
@RabbitListener(queues = "my_normal_queue")
public void onMessage(Message message, Channel channel) throws Exception {
//获取全局唯一id,比如通过消息队列来生成订单,那订单号就是唯一的
String messageId = message.getMessageProperties().getMessageId();
/**
* 采用redis中的 setNXl 来实现重复消费问题
*/
Boolean exists = (boolean) redisTemplate.execute((RedisCallback) action -> {
return action.setNX(messageId.getBytes(), messageId.getBytes());
});
if (exists) {
//业务处理
} else {
//该条消息已经消费过了,不能重复消费
}
/**
* 采用redisTemplate中的 hasKey() 方法来实现重复消费问题
*/
if (messageId != null && !redisTemplate.hasKey(messageId)) {
try {
//接受消息后处理业务开始
System.out.println("consumer--:" + message.getMessageProperties() + ":" + new String(message.getBody()));
//接受消息后处理业务结束
//将这条消息的id放入redis中
redisTemplate.opsForValue().set(messageId, "消息正常消费成功");
//模拟异常
int i = 1 / 0;
/**
* 处理业务结束后,ACK回执给MQ服务器删除该消息。
* multiple:false 是指ACK回执给MQ服务器删除当前处理的一条消息
* multiple:true 是指ACK回执给MQ服务器删除deliveryTag小于等于传入值的所有消息
*/
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
/**
* 出现异常后,ACK回执给MQ服务器是否删除该消息。
* multiple:false 是指ACK回执给MQ服务器删除当前处理的一条消息
* multiple:true 是指ACK回执给MQ服务器删除deliveryTag小于等于传入值的所有消息
* requeue:false 不会重新入队列,ACK回执给MQ服务器删除消息
* requeue:true 会重新入队列,会添加在队列的末端
*/
logger.error("MQ消息处理异常,消息ID:{},消息体:{}", message.getMessageProperties().getMessageId(), message.getBody(), e);
try {
// TODO 保存消息到数据库
// 确认消息已经消费成功
//将这条消息的id放入redis中
redisTemplate.opsForValue().set(messageId, "消息正常消费成功,是出现异常后将该消息保存到数据库中");
//模拟异常
int i = 1 / 0;
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e1) {
message.getMessageProperties().getMessageId();
logger.error("保存异常MQ消息到数据库异常,放到死性队列,消息ID:{}", message.getMessageProperties().getMessageId());
//将这条消息的id放入redis中
redisTemplate.opsForValue().set(messageId, "消息正常消费成功,是出现异常后保存数据库时出现异常后将该消息发送到死信队列");
/**
* 此处 requeue属性 可以为 false 也可以为true
* 当requeue: true时,消费端如果出现异常后则会将这条消息 一直 给MQ服务器发送,消息会重新入队列,会添加在队列的末端。
* (使用true时,此处出现的异常次数少,比如网络波动,如果这个异常每次都出现的话,那么就会造成 死循环 )。
*
* 当requeue: false时,消费端如果出现异常后则会将这条消息会发送到死信队列中
*/
//确认消息将消息放到死信队列
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, false);
}
}
} else {
logger.error("消息ID={}已经消费过了", message.getMessageProperties().getMessageId());
//该条消息已经消费过了,ACK回执给MQ服务器删除该消息
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
}
}
}