有关于RabbitMQ的一点小笔记
是什么
RabbitMQ是一种消息中间件,是一种处于操作系统和应用程序之间的软件。它可以利用可靠的消息传递机制进行系统和系统间的通讯,并提供了消息传递和排队机制,可以在分布式系统环境下扩展进程间的通讯。
它实现了AMQP协议。
AMQP,即Advanced Message Queuing Protocol,高级消息队列协议,是应用层协议的一个开放标准,为面向消息的中间件设计。
安装步骤
因为RabbitMQ是采用Erlang语言开发的,所以必须先安装Erlang。
这里是在centos7.x的系统安装的。
Erlang语言和RabbitMQ有版本对应的关系,可以在这个网站查看他们的按照比较。
安装步骤:
- 下载并解压Erlang语言安装包
wget https://packages.erlang-solutions.com/erlang-solutions-2.0-1.noarch.rpm
rpm -Uvh erlang-solutions-2.0-1.noarch.rpm
- 安装Erlang语言
yum install -y erlang
- 通过查看Erlang的版本号查看是否安装成功
erl -v
- 安装socat
yum install -y socat
- 下载并解压RabbitMQ安装包
wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.13/rabbitmq-server-3.8.13-1.el8.noarch.rpm
rpm -Uvh rabbitmq-server-3.8.13-1.el8.noarch.rpm
6.安装RabbitMQ
yum install rabbitmq-server -y
7.启动RabbitMQ服务
systemctl start rabbitmq-server
8.查看RabbitMQ服务状态
systemctl status rabbitmq-server
当状态为Active时,表示启动成功。
其他有关命令
# 停止服务
systemctl stop rabbitmq-server
# 开机启动服务
systemctl enable rabbitmq-server
有关端口
- 5672:RabbitMQ的通讯端口
- 25672:RabbitMQ的节点间的CLI通讯端口
- 15672:RabbitMQ HTTP_API的端口,管理员用户才能访问,用于管理RabbitMQ,需要启动Management插件。
- 1883,8883:MQTT插件启动时的端口。
- 61613、61614:STOMP客户端插件启用的时候的端口。
- 15674、15675:基于webscoket的STOMP端口和MOTT端口
安装Web界面
# 安装web界面的插件
rabbitmq-plugins enable rabbitmq_management
# 重启服务后生效
systemctl restart rabbitmq-server
重启之后访问http://ip:15672
之后出现下图界面
RabbitMQ提供了一个默认账号:用户名和密码均为guest
,只能在localhost
下使用。如果需要远程访问则要新增一个账号,使用下列命令:
# 新增用户
rabbitmqctl add_user admin admin
# 设置用户分配操作权限
rabbitmqctl set_user_tags admin administrator
# 为用户添加资源权限
rabbitmqctl set_permissions -p / admin ".*" ".*" ".*"
# 查看用户列表
rabbitmqctl list_users
简单使用
角色分类
none
:不能访问management pluginmanagement
:查看自己相关节点信息
- 列出自己可以通过AMQP登入的虚拟机
- 查看自己的虚拟机节点 virtual hosts的queues,exchanges和bindings信息
- 查看和关闭自己的channels和connections
- 查看有关自己的虚拟机节点virtual hosts的统计信息。包括其他用户在这个节点virtual hosts中的活动信息。
Policymaker
:包含management
的所有权限
- 查看和创建和删除自己的virtual hosts所属的policies和parameters信息。
Monitoring
:包含management
的所有权限
- 罗列出所有的virtual hosts,包括不能登录的virtual hosts。
- 查看其他用户的connections和channels信息
- 查看节点级别的数据如clustering和memory使用情况
- 查看所有的virtual hosts的全局统计信息。
Administrator
:最高权限
- 可以创建和删除virtual hosts
- 可以查看,创建和删除users
- 查看创建permisssions
- 关闭所有用户的connections
组成部分
- Server:又称Broker,接收客户端的连接,实现AMQP实体服务
- Connection:管理每个到RabbitMQ的TCP网络连接
- Channel:网络信道,几乎所有的操作都在Channel中进行,Channel是进行消息读写的通道,客户端可以建立多个Channel,每个Channel代表一个会话任务
- Message:消息,服务与应用程序之间传送的数据,由Properties和body组成,Properties可以对消息进行修饰,比如消息的优先级,延迟等高级特性,Body则是消息体的内容.
- Virual Host:虚拟地址,用于进行逻辑隔离,最上层的消息路由,一个虚拟主机里可以有若干个Exchange和Queue,同一个虚拟主机里面不能有相同名字的Exchange
- Exchange:交换机,接收消息,根据路由键发送消息到绑定的队列,不具备消息存储的能力。生产者将消息发送到交换机,由交换机将消息路由到一个或多个队列中。
- Bindings:Exchange和Queue之间的虚拟连接,binding中可以保护多个routing key
- Routing key:是一个路由规则,虚拟机可以用它来确定如何路由一个特定消息
- Queue:队列,也称为Message Queue消息队列,保存消息并将它们转发给消费者
- Producer:连到RabbitMQ服务器,将消息发送到RabbitMQ服务器的队列,是消息的发送方
- Consumer:连接到RabbitMQ则是为了消费队列中的消息,是消息的接收方
工作流程
生产者发送消息
- 生产者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
- 生产者声明一个交换机,并设置相关属性,比如交换机类型、是否持久化等
- 生产者声明一个队列并设置相关属性,比如是否排他、是否持久化、是否自动删除等
- 生产者通过路由键将交换机和队列进行绑定
- 生产者发送消息至RabbitMQ Broker,其中包含路由键、交换机等信息
- 相应的交换机根据接收到的路由键查找相匹配的队列
- 如果找到,则将从生产者发送过来的消息存入相应的队列中
- 如果没有找到,则根据生产者配置的属性选择丢弃还是回退给生产者
- 关闭信道
- 关闭连接
消费者接收消息
- 消费者连接到RabbitMQ Broker,建立一个连接(Connection),开启一个信道(Channel)
- 消费者向RabbitMQ Broker请求消费相应队列中的消息,可能会设置相应的回调函数,以及做一些准备工作
- 等待RabbitMQ Broker 回应并投递相应队列中的消息,消费者接收消息。
- 消费者确认接收到的消息
- RabbitMQ 从队列中删除相应已经被确认的消息
- 关闭信道
- 关闭连接
工作模式
RabbitMQ一共有六种工作模式,分别为简单模式、工作队列模式、发布/订阅模式、路由模式、主题模式和RPC模式,RPC模式并不常用,因此我们下面只讲前面五种工作模式。
简单模式
简单模式的结构如下图所示:
这个模式有几个显著的特征:
- 只有一个生产者,一个消费者和一个队列
- 生产者和消费者在发送消息时,只需要指定队列名,RabbitMQ使用默认的Exchange来操作,默认的type为direct
SpringBoot导入RabbitMQ依赖:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
eg:
package com.alone.rabbitmqdemo.simple;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.simple
* @Author: Alone
* @CreateTime: 2021-03-22 14:01
* @Description: 简单模式生产者
*/
public class Producer {
public static void main(String[] args) {
//所有的中间件技术都是基于TCP/IP协议基础之上构建新型的协议规范,只不过rabbitmq遵循的是amqp
//ip port
//1.创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//2.创建连接connection Rabbitmq为什么是基于channel去处理而不是连接?
connection = connectionFactory.newConnection("生产者");
//3.通过连接获取通道channel
channel = connection.createChannel();
//4.通过创建交换机,声明队列,绑定关系,路由key,发送消息和接收消息
String queueName = "queue1";
/**
* @params1 队列名称
* @params2 是否要持久化 durable=false 所谓持久化消息是否存盘 如果false 非持久化 true是持久化? 非持久化会存盘? 会存盘,但是会随重启服务器而消失
* @params3 排他性,是否是独占队列
* @params4 是否自动删除, 随着最后一个消费者消息消费完毕后是否删除队列
* @params5 携带附加参数
*/
channel.queueDeclare(queueName, false, false, false, null);
//5.准备消息内容
String message = "Hello world!";
//6.发送消息给队列queue
/**
* @Param1 交换机
* @Param2 队列,路由key
* @Param3 消息的状态控制
* @Param4 消息主题
*/
channel.basicPublish("", queueName, null, message.getBytes());
System.out.println("消息发送成功");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
//7.关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
//8.关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
package com.alone.rabbitmqdemo.simple;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.simple
* @Author: Alone
* @CreateTime: 2021-03-22 14:01
* @Description: 简单模式消费者
*/
public class Consumer {
public static void main(String[] args) {
//1.创建连接工程
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
connectionFactory.setVirtualHost("/");
Connection connection = null;
Channel channel = null;
try {
//2.创建连接connection
connection = connectionFactory.newConnection("生产者");
//3.通过连接获取通道channel
channel = connection.createChannel();
//4.接收消息
String queueName = "queue1";
channel.basicConsume(queueName, true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
System.out.println("收到消息是:" + new String(delivery.getBody(), "UTF-8"));
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
System.out.println("发送失败了");
}
});
System.out.println("开始接收消息");
System.in.read();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
} finally {
//7.关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
//8.关闭连接
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
}
工作模式
当生产者生产消息的速度大于消费者的消费速度时,我们可以添加一个或多个消费者来加快消费速度,这种在simple模式下增加消费者的模式,称为work模式,结构图如下图:
主要有两种模式:
- 轮询模式:一个消费者一条,按均分配
- 公平分发:根据消费者的消费能力进行公平分发,处理快的处理的多,处理慢的处理的少,按劳分配。
eg:
轮询模式
生产者
package com.alone.rabbitmqdemo.work.poll;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:49
* @Description:
*/
public class Producer {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 6: 准备发送消息的内容
//===============================end topic模式==================================
for (int i = 1; i <= 20; i++) {
//消息的内容
String msg = "学相伴:" + i;
// 7: 发送消息给中间件rabbitmq-server
// @params1: 交换机exchange
// @params2: 队列名称/routingkey
// @params3: 属性配置
// @params4: 发送消息的内容
channel.basicPublish("", "queue1", null, msg.getBytes());
}
System.out.println("消息发送成功!");
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
消费者1:
package com.alone.rabbitmqdemo.work.poll;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:50
* @Description:
*/
public class Work1 {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("消费者-Work1");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 5: 申明队列queue存储消息
/*
* 如果队列不存在,则会创建
* Rabbitmq不允许创建两个相同的队列名称,否则会报错。
*
* @params1: queue 队列的名称
* @params2: durable 队列是否持久化
* @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
* @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
* */
// 这里如果queue已经被创建过一次了,可以不需要定义
// channel.queueDeclare("queue1", false, false, false, null);
// 同一时刻,服务器只会推送一条消息给消费者
// 6: 定义接受消息的回调
Channel finalChannel = channel;
// finalChannel.basicQos(1);
finalChannel.basicConsume("queue1", true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
try {
System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
Thread.sleep(2000);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
System.out.println("Work1-开始接受消息");
System.in.read();
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
消费者2:
package com.alone.rabbitmqdemo.work.poll;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:50
* @Description:
*/
public class Work2 {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("消费者-Work2");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 5: 申明队列queue存储消息
/*
* 如果队列不存在,则会创建
* Rabbitmq不允许创建两个相同的队列名称,否则会报错。
*
* @params1: queue 队列的名称
* @params2: durable 队列是否持久化
* @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
* @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
* */
// 这里如果queue已经被创建过一次了,可以不需要定义
//channel.queueDeclare("queue1", false, true, false, null);
// 同一时刻,服务器只会推送一条消息给消费者
//channel.basicQos(1);
// 6: 定义接受消息的回调
Channel finalChannel = channel;
// finalChannel.basicQos(1);
finalChannel.basicConsume("queue1", true, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
try {
System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
Thread.sleep(200);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
System.out.println("Work2-开始接受消息");
System.in.read();
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
公平分发:
生产者:
package com.alone.rabbitmqdemo.work.fair;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:49
* @Description:
*/
public class Producer {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("生产者");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 6: 准备发送消息的内容
//===============================end topic模式==================================
for (int i = 1; i <= 20; i++) {
//消息的内容
String msg = "学相伴:" + i;
// 7: 发送消息给中间件rabbitmq-server
// @params1: 交换机exchange
// @params2: 队列名称/routingkey
// @params3: 属性配置
// @params4: 发送消息的内容
channel.basicPublish("", "queue1", null, msg.getBytes());
}
System.out.println("消息发送成功!");
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
消费者1:
package com.alone.rabbitmqdemo.work.fair;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:50
* @Description:
*/
public class Work1 {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("消费者-Work1");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 5: 申明队列queue存储消息
/*
* 如果队列不存在,则会创建
* Rabbitmq不允许创建两个相同的队列名称,否则会报错。
*
* @params1: queue 队列的名称
* @params2: durable 队列是否持久化
* @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
* @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
* */
// 这里如果queue已经被创建过一次了,可以不需要定义
// channel.queueDeclare("queue1", false, false, false, null);
// 同一时刻,服务器只会推送一条消息给消费者
// 6: 定义接受消息的回调
Channel finalChannel = channel;
//一次处理多少消息?
finalChannel.basicQos(1);
finalChannel.basicConsume("queue1", false, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
try {
System.out.println("Work1-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
Thread.sleep(2000);
//必须要用手动应答
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
System.out.println("Work1-开始接受消息");
System.in.read();
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
消费者2:
package com.alone.rabbitmqdemo.work.fair;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.work.poll
* @Author: Alone
* @CreateTime: 2021-03-24 15:50
* @Description:
*/
public class Work2 {
public static void main(String[] args) {
// 1: 创建连接工厂
ConnectionFactory connectionFactory = new ConnectionFactory();
// 2: 设置连接属性
connectionFactory.setHost("101.37.33.222");
connectionFactory.setPort(5672);
connectionFactory.setVirtualHost("/");
connectionFactory.setUsername("admin");
connectionFactory.setPassword("admin");
Connection connection = null;
Channel channel = null;
try {
// 3: 从连接工厂中获取连接
connection = connectionFactory.newConnection("消费者-Work2");
// 4: 从连接中获取通道channel
channel = connection.createChannel();
// 5: 申明队列queue存储消息
/*
* 如果队列不存在,则会创建
* Rabbitmq不允许创建两个相同的队列名称,否则会报错。
*
* @params1: queue 队列的名称
* @params2: durable 队列是否持久化
* @params3: exclusive 是否排他,即是否私有的,如果为true,会对当前队列加锁,其他的通道不能访问,并且连接自动关闭
* @params4: autoDelete 是否自动删除,当最后一个消费者断开连接之后是否自动删除消息。
* @params5: arguments 可以设置队列附加参数,设置队列的有效期,消息的最大长度,队列的消息生命周期等等。
* */
// 这里如果queue已经被创建过一次了,可以不需要定义
//channel.queueDeclare("queue1", false, true, false, null);
// 同一时刻,服务器只会推送一条消息给消费者
//channel.basicQos(1);
// 6: 定义接受消息的回调
Channel finalChannel = channel;
finalChannel.basicQos(1);
finalChannel.basicConsume("queue1", false, new DeliverCallback() {
@Override
public void handle(String s, Delivery delivery) throws IOException {
try {
System.out.println("Work2-收到消息是:" + new String(delivery.getBody(), "UTF-8"));
Thread.sleep(200);
finalChannel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
} catch (Exception ex) {
ex.printStackTrace();
}
}
}, new CancelCallback() {
@Override
public void handle(String s) throws IOException {
}
});
System.out.println("Work2-开始接受消息");
System.in.read();
} catch (Exception ex) {
ex.printStackTrace();
System.out.println("发送消息出现异常...");
} finally {
// 7: 释放连接关闭通道
if (channel != null && channel.isOpen()) {
try {
channel.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
if (connection != null && connection.isOpen()) {
try {
connection.close();
} catch (Exception ex) {
ex.printStackTrace();
}
}
}
}
}
公平分发需要消费者开启手动应答,关闭自动应答。
核心代码为:
channel.basicConsume("queue1", false, new DeliverCallback());
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
发布订阅模式
发布订阅模式可以将一条消息给多个消费者消费,示意图如下:
特点:
- 在此模式下,需要指定发送到的Exchange
- type为fanout
- 生产者发送消息时,不需要指定队列名,Exchange会自动转发到所绑定的队列
核心代码:
//生产者中
String message = "Alone";
String exchangeName = "fanout-exchange";
String routingKey = "";
channel.basicPublish(exchangeName, routingKey, null, message.getBytes());
路由模式
特点:
- 路由模式下Exchange的type为direct。
- 消息的目标队列可以由生产者按照routingKey规则指定。
- 消费者通过BindingKey绑定自己所关心的队列。
- 一条消息队可以被多个消息者获取。
- 只有RoutingKey与BindingKey相匹配的队列才会收到消息。
RoutingKey用于生产者指定Exchange最终将消息路由到哪个队列,BindingKey用于消费者绑定到某个队列。
示意图如下所示:
核心代码:
//生产者中
String message = "Alone";
String exchangeName = "direct-exchange";
String routingKey1 = "testKey";
String routingKey2 = "testKey2";
channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());
channel.basicPublish(exchangeName, routingKey2, null, message.getBytes());
主题模式
主题模式是在路由模式的基础上,将路由键和某模式进行匹配。其中#表示匹配多个词,*表示匹配一个词,消费者可以通过某种模式的BindKey来达到订阅某个主题消息的目的。主题模式Exchange的type取值为topic。
示意图:
核心代码:
//生产者中
String message = "Alone";
String exchangeName = "topic-exchange";
String routingKey1 = "com.course.order";
String routingKey2 = "com.order.user";
channel.basicPublish(exchangeName, routingKey1, null, message.getBytes());
与SpringBoot整合
以fanout模式为例:
生产者:
- 在
pom.xml
中引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-amqp</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
- 配置
application.yml
文件:
# 服务端口
server:
port: 8080
# 配置rabbitmq服务
spring:
rabbitmq:
username: admin
password: admin
virtual-host: /
host: 101.37.33.222
port: 5672
- 定义生产者
package com.alone.rabbitmq.springbootorderrabbitmqproducer.service;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import java.util.UUID;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqproducer.service
* @Author: Alone
* @CreateTime: 2021-03-24 20:07
* @Description:
*/
@Component
public class OrderService {
@Autowired
private RabbitTemplate rabbitTemplate;
// 1: 定义交换机
private String exchangeName = "fanout_order_exchange";
// 2: 路由key
private String routeKey = "";
public void makeOrder(Long userId, Long productId, int num) {
// 1: 模拟用户下单
String orderNumer = UUID.randomUUID().toString();
// 2: 根据商品id productId 去查询商品的库存
// int numstore = productSerivce.getProductNum(productId);
// 3:判断库存是否充足
// if(num > numstore ){ return "商品库存不足..."; }
// 4: 下单逻辑
// orderService.saveOrder(order);
// 5: 下单成功要扣减库存
// 6: 下单完成以后
System.out.println("用户 " + userId + ",订单编号是:" + orderNumer);
// 发送订单信息给RabbitMQ fanout
rabbitTemplate.convertAndSend(exchangeName, routeKey, orderNumer);
}
}
4.绑定关系
package com.alone.rabbitmq.springbootorderrabbitmqproducer.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.FanoutExchange;
import org.springframework.amqp.core.Queue;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqproducer.config
* @Author: Alone
* @CreateTime: 2021-03-24 20:12
* @Description:
*/
@Configuration
public class RabbitMqConfiguration {
private static final String exchangeName = "fanout_order_exchange";
//1.声明注册fanout模式的交换机
@Bean
public FanoutExchange fanoutExchange() {
// durable:是否持久化,默认是false,持久化队列:会被存储在磁盘上,当消息代理重启时仍然存在,暂存队列:当前连接有效
// exclusive:默认也是false,只能被当前创建的连接使用,而且当连接关闭后队列即被删除。此参考优先级高于durable
// autoDelete:是否自动删除,当没有生产者或者消费者使用此队列,该队列会自动删除。
// return new Queue("TestDirectQueue",true,true,false);
//一般设置一下队列的持久化就好,其余两个就是默认false
return new FanoutExchange(exchangeName, true, false);
}
//2.声明队列sms.fanout.queue email.fanout.queue duanxin.fanout.queue
@Bean
public Queue smsQueue() {
return new Queue("sms.fanout.queue", true);
}
@Bean
public Queue duanxinQueue() {
return new Queue("duanxin.fanout.queue", true);
}
@Bean
public Queue emailQueue() {
return new Queue("email.fanout.queue", true);
}
//3.完成绑定关系(队列和交换机完成绑定关系)
@Bean
public Binding smsBingding() {
return BindingBuilder.bind(smsQueue()).to(fanoutExchange());
}
@Bean
public Binding duanxinBingding() {
return BindingBuilder.bind(duanxinQueue()).to(fanoutExchange());
}
@Bean
public Binding emailBingding() {
return BindingBuilder.bind(emailQueue()).to(fanoutExchange());
}
}
5.测试:
package com.alone.rabbitmq.springbootorderrabbitmqproducer;
import com.alone.rabbitmq.springbootorderrabbitmqproducer.service.OrderService;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
@SpringBootTest
class SpringbootOrderRabbitmqProducerApplicationTests {
@Autowired
private OrderService orderService;
@Test
void contextLoads() {
orderService.makeOrder("1", "1", 12);
}
}
消费者:
邮件服务:
package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
* @Author: Alone
* @CreateTime: 2021-03-24 21:11
* @Description:
*/
@RabbitListener(queues = {"email.fanout.queue"})
@Service
public class FanoutEmailConsumer {
@RabbitHandler
public void receiveMessage(String message) {
System.out.println("email fanout ---> 接收到了订单信息是: ->" + message);
}
}
短信服务:
package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
* @Author: Alone
* @CreateTime: 2021-03-24 21:11
* @Description:
*/
@RabbitListener(queues = {"duanxin.fanout.queue"})
@Service
public class FanoutDuanxinConsumer {
@RabbitHandler
public void receiveMessage(String message) {
System.out.println("duanxin fanout ---> 接收到了订单信息是: ->" + message);
}
}
微信服务:
package com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout;
import org.springframework.amqp.rabbit.annotation.RabbitHandler;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Service;
/**
* @BelongsProject: study
* @BelongsPackage: com.alone.rabbitmq.springbootorderrabbitmqconsumer.service.fanout
* @Author: Alone
* @CreateTime: 2021-03-24 21:11
* @Description: 消息队列消费者
*/
@RabbitListener(queues = {"sms.fanout.queue"})
@Service
public class FanoutSMSConsumer {
@RabbitHandler
public void receiveMessage(String message) {
System.out.println("sms fanout ---> 接收到了订单信息是: ->" + message);
}
}
direct模式需要在绑定的时候指定路由:
public Binding bindingDirect(){
return BindingBuilder.bind(emailQueue()).to(directOrderExchange()).with("");
}
消费者使用注解绑定队列eg:
@Service
@RabbitListener(bindings = @QueueBinding(
value = @Queue(value = "sms.topic.queue", durable = "true", autoDelete = "false"),
exchange = @Exchange(value = "topic_order_exchange", type = ExchangeTypes.TOPIC),
key = "com.#"
))
public class TopicSMSConsumer {
@RabbitHandler
public void receiveMessage(String message) {
System.out.println("sms topic ---> 接收到了订单信息是: ->" + message);
}
}
关于创建队列时的附加参数的使用
在创建队列时的queueDeclare()
方法中的最后一个参数是可以用来传递一些队列附加参数,下面记录一下这个参数有关的使用:
设置过期时间
过期时间有两种设置方式:
- 通过队列属性设置,此时队列中所有消息都有相同的过期时间
@Bean
public Queue directTTLQueue() {
//设置过期时间
Map<String, Object> args = new HashMap<>();
args.put("x-message-ttl", 5000);//这里一定是int类型
return new Queue("ttl.direct.queue", true, false, false, args);
}
- 对消息进行单独设置,每条消息TTL可以不同
public void makeOrderTTLMessage(String userId, String productId, int num) {
//1.根据商品id查询库存是否充足
//2.保存订单
String orderId = UUID.randomUUID().toString();
System.out.println("订单生成成功:" + orderId);
//3.通过MQ来完成消息分发
//参数1: 交换机 参数2:路由key/queue队列名称 参数3:消息内容
String exchangeName = "ttl_direct_exchange";
String routingKey = "ttlmessage";
//给消息设置过期时间
MessagePostProcessor messagePostProcessor = new MessagePostProcessor() {
@Override
public Message postProcessMessage(Message message) throws AmqpException {
//这里就是字符串
message.getMessageProperties().setExpiration("5000");
message.getMessageProperties().setContentEncoding("UTF-8");
return message;
}
};
rabbitTemplate.convertAndSend(exchangeName, routingKey, orderId, messagePostProcessor);
}
如果上述两种方法同时使用,则消息的过期时间以两者之间TTL较小的那个数值为准。消息在队列的生存时间一旦超过设置的TTL值,就被投递到死信队列,消费者再也无法收到。
死信队列
DLX,全称为Dead-Letter-Exchange
,可以称之为死信交换机。当消息在一个队列中变成死信之后,它能重新发送到另一个交换机中,这个交换机就是DLX,绑定DLX的队列就称为死信队列。
消息变成死信的原因:
- 消息被拒绝
- 消息过期
- 队列达到最大长度
当队列中存在死信时,RabbitMQ就会自动将这个消息重新发布到设置的DLX上,进而被路由到死信队列。
//设置死信队列参数
args.put("x-dead-letter-exchange", "dead_direct_exchange");
args.put("x-dead-letter-routing-key", "dead"); //fanout模式不需要配置
原理
如何确保消息发送和接收
消息发送确认
ConfirmCallback
方法
ConfirmCallback
是一个回调接口,消息发送到Broker后触发回调,确认消息是否到达Broker服务器,也就是只确认是否正确到达Exchange中。
@Slf4j
@Component
public class ConfirmCallbackService implements RabbitTemplate.ConfirmCallback {
/**
* 消息从producer到broker的确认模式
* @param correlationData 对象内部只有一个id属性,表示当前消息的唯一性
* @param ack 消息投递到broker的状态,true代表成功
* @param cause 投递失败的原因
*/
@Override
public void confirm(CorrelationData correlationData, boolean ack, String cause) {
if(!ack) {
log.error("消息发送异常!");
} else {
log.info("生产者已经收到确认:correlationData={}, ack={}, cause={}", correlationData.getId(), ack, cause);
}
}
}
ReturnCallback
方法
通过实现ReturnCallback
接口,启动消息失败返回,此接口是在交换器路由不到队列时触发回调,该方法可以不使用,因为交换器和队列是在代码里绑定的,如果消息成功投递到Broker后几乎不存在绑定队列失败,除非你代码写错了。
@Slf4j
@Component
public class ReturnCallbackService implements RabbitTemplate.ReturnsCallback {
/**
* 如果消息从exchange投递到queue投递失败的退回模式
* @param returnedMessage
*/
@Override
public void returnedMessage(ReturnedMessage returnedMessage) {
// 方法包含五个参数:message-消息体,replyCode-响应code,replyText-响应内容,exchange-交换机,routingKey-队列
log.info("returnedMessage:replyCode={},replyText={},exchange={},routingKey={},message={}",
returnedMessage.getReplyCode(), returnedMessage.getReplyText(), returnedMessage.getExchange(), returnedMessage.getRoutingKey(), returnedMessage.getMessage());
}
}
消息接收确认
消息应答机制是指:消费者在接收到消息并且处理该消息之后,告诉rabbitmq它已经处理了,rabbitmq可以把该消息删除了。
RabbitMQ消息确认机制(ACK)默认是自动确认的,自动确认会在消息发送给消费者后立即确认,即消息发送后立即被认为已经传送成功。但存在丢失消息的可能:1. 如果消息在接收到之前,消费者出现连接或者channel关闭。2. 如果消费端消费逻辑抛出异常,假如你用回滚了也只是保证了数据的一致性,但是消息还是丢了,也就是消费端没有处理成功这条消息,那么久相当于丢失了消息。
消息确认模式有:
- AcknowledgeMode.NONE:自动确认
- AcknowledgeMode.AUTO:根据情况确认
- AcknowledgeMode.MANUAL:手动确认
消费者收到消息后,手动使用Basic.Ack或Basic.Nack或Basic.Reject后,RabbitMQ收到这些消息后,才认为本次投递完成。 - Basic.Ack命令:用于确认当前消息
- Basic.Nack命令:用于否定当前消息
- Basic.Reject命令:用于拒绝当前消息
Nack,Reject后都有能力要求是否requeue消息或者进入死信队列。
手动确认要在消费者服务开启参数:spring.rabbitmq.listener. simple.acknowledge-mode=manual
@Slf4j
@RabbitListener(queues = HandAckConsumerConfiguration.QUEUE_NAME)
@Component
public class HandAckConsumer {
@RabbitListener
public void messageConsumer(String msg, Channel channel, Message message) throws IOException {
// delivertag是一个唯一标识id,当一个消费者向RabbitMQ注册后,会建立起一个Channel
// RabbitMQ会用basic.deliver方法向消费者推送消息,这个方法携带一个deliveryTag作为唯一标识,它是一个单调递增的正整数
// multiple是控制是否可以进行批处理,如果为true,则一次性可以确认deliveryTag小于等于传入值的所有消息
try {
log.info("消费者收到消息:{}", msg);
channel.basicAck(message.getMessageProperties().getDeliveryTag(), false);
} catch (Exception e) {
if(message.getMessageProperties().getRedelivered()) {
log.error("消息已重复处理失败,拒绝再次接收...");
// 拒绝消息
channel.basicReject(message.getMessageProperties().getDeliveryTag(), false);
} else {
log.error("消息即将再次返回队列处理...");
channel.basicNack(message.getMessageProperties().getDeliveryTag(), false, true);
}
}
}
}
持久化
为了确保消息不会丢失,还需要将队列和消息都设置为持久化的,不然服务停止后消息就丢了。
声明队列为持久化配置下面这个属性即可:
注意如果原先队列不是持久化的,需要先删除原来队列,再重新建队列。
声明消息为持久化采用下面的方式:
应用场景
异步
将一个操作的多个步骤,拆分开,提高效率,比如一个注册步骤,以前是这么做的:
使用消息队列后:
这样可以更快的返回结果,加快服务器响应速度。
解耦
削峰
对于高并发的系统来说,在访问高峰时,突发的流量就像洪水般向应用系统涌过来,尤其是一些高并发写操作,随时会导致数据库服务器瘫痪,无法继续提供服务。而引入消息队列则可以减少突发流量对应用系统的冲击。
其他
kafka是由producer决定msg发送到哪个queue,rabbitmq是由Exchange决定msg应该怎样发送到目标queue,这就是binding及对应策略。