目录
文章很长,放个目录方便跳转
简介 AMQP角色 RabbitMQ内部结构 安装 简单使用 核心API RabbitMQ解决消息丢失 RabbitMQ解决重复消费
简介
RabbitMQ是实现了高级消息队列协议(AMQP:Advanced Message Queue)的开源消息代理软件(亦称面向消息的中间件)。
RabbitMQ服务器是用Erlang语言编写的,而集群和故障转移是构建在开放电信平台框架上的。所有主要的编程语言均有与代理接口通讯的客户端库。
主要特性
- 可靠性(Reliability)
RabbitMQ 使用一些机制来保证可靠性,如持久化、传输确认、发布确认。
- 灵活的路由(Flexible Routing)
在消息进入队列之前,通过 Exchange 来路由消息的。对于典型的路由功能,RabbitMQ 已经提供了一些内置的 Exchange 来实现。针对更复杂的路由功能,可以将多个 Exchange 绑定在一起,也通过插件机制实现自己的 Exchange 。
- 消息集群(Clustering)
多个 RabbitMQ 服务器可以组成一个集群,形成一个逻辑 Broker 。
高可用(Highly Available Queues)
队列可以在集群中的机器上进行镜像,使得在部分节点出问题的情况下队列仍然可用。
管理界面(Management UI)
RabbitMQ 提供了一个易用的用户界面,使得用户可以监控和管理消息 Broker 的许多方面。
- 跟踪机制(Tracing)
如果消息异常,RabbitMQ 提供了消息跟踪机制,使用者可以找出发生了什么。
AMQP角色
- Message(消息):消息服务器处理消息的原子单元,包括一个内容头,一组属性和一个内容体。
__使用AMQP协议,消息服务器不能修改内容体和内容头,但可以在内容头上添加额外信息。
_
- PubLisher(消息生产者):发送消息
- Consumer(消息消费者):消费消息
- Broker(消息代理):消息队列服务器,负责接收客户端连接,路由消息。
- Queue(消息队列):Broker中的一个角色,一个Broker中可以有多个Queue,负责保存消息直到发送给不同的消费者。算是消息的容器。一个消息可以被投入一个或多个队列中,每个队列的消息都会等待消费者连接到这个队列并被取走。
- Exchange(交换路由):Broker中的一个角色,负责接收生产者发送的消息,并路由给服务器中的队列。可以被理解成一个规则表,指明消息该被投到哪个队列中。
- Channel(信道):信道是一条独立的双向数据流通道。为了解决操作系统无法承受每秒建立特别多的TCP连接问题
RabbitMQ内部结构
1、Message
消息,消息是不具名的,它由消息头和消息体组成。消息体是不透明的,而消息头则由一系列的可选属性组成,这些属性包括routing-key(路由键)、priority(相对于其他消息的优先权)、delivery-mode(指出该消息可能需要持久性存储)等。
2、Publisher
消息的生产者,也是一个向交换器发布消息的客户端应用程序。
3、Exchange
交换器,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
4、Binding
绑定,用于消息队列和交换器之间的关联。一个绑定就是基于路由键将交换器和消息队列连接起来的路由规则,所以可以将交换器理解成一个由绑定构成的路由表。
5、Queue
消息队列,用来保存消息直到发送给消费者。它是消息的容器,也是消息的终点。一个消息可投入一个或多个队列。消息一直在队列里面,等待消费者连到这个队列将其取走。
6、Connection
网络连接,比如一个TCP连接。
7、Channel
信道,多路复用连接中的一条独立的双向数据流通道。信道是建立在真实的TCP连接内地虚拟连接,AMQP 命令都是通过信道发出去的,不管是发布消息、订阅队列还是接收消息,这些动作都是通过信道完成。
8、Consumer
消息的消费者,表示一个从消息队列中取得消息的客户端应用程序。
9、Virtual Host
虚拟主机,表示一批交换器、消息队列和相关对象。虚拟主机是共享相同的身份认证和加密环境的独立服务器域。每个 vhost 本质上就是一个 mini 版的RabbitMQ 服务器,拥有自己的队列、交换器、绑定和权限机制。vhost 是 AMQP概念的基础,必须在连接时指定,RabbitMQ 默认的 vhost 是 / 。
10、Broker
表示消息队列服务器实体。
相关名词解释
RoutingKey:指定当前消息被谁(哪个队列)接受【由生产者发送消息时携带发送给Exchange】
BindingKey: 指定当前Exchange下,什么样的RoutingKey会被下派到当前绑定的Queue中【由队列创建时与Exchange之间创建】
为什么还要引入信道呢
TCP的创建和销毁,开销大,创建需要三次握手,销毁需要四次分手
如果不使用信道,那么引用程序就会使用TCP的方式连接到rabbitmq,高峰时每秒成千上万条连接会造成资源的巨大浪费(一条tcp消耗资源,成千上万的tcp会非常消耗资源),而且操作系统每秒处理TCP连接数量也是有限的,必定会造成性能瓶颈
3.信道的原理是一条线程一条信道,多条线程多条信道共同使用一条TCP连接。一条TCP连接可以容纳无限的信道,及时每秒造成成千上万的请求也不会造成性能瓶颈。
四种交换器(Exchange)
在RabbitMQ中,交换机负责接收生产者发送的消息并将这些消息路由给服务器中的队列。
常用的交换器类型有direct、topic、fanout、headers四种。
Direct Exchange-直连交换器
该类型的交换器将所有发送到该交换器的消息被转发到RoutingKey指定的队列中,也就是说路由到BindingKey和RoutingKey完全匹配的队列中。
即如果一个队列绑定到交换机要求路由键为“order”,则只转发 routing key 标记为“order”的消息,不会转发“order.score”,也不会转发“order.sms”等等。它是完全匹配、单播的模式。
Topic Exchange-主题交换器
Topic 交换器通过模式匹配分配消息的路由键属性,将路由键和某个模式进行匹配,此时队列需要绑定到一个模式上。它将路由键和绑定键的字符串切分成单词,这些单词之间用点隔开。
它同样也会识别两个通配符:符号“#”和符号“”。#匹配0个或多个单词,”“匹配多个单词。
Fanout Exchange-扇形交换器
每个发到 fanout 类型交换器的消息都会分到所有绑定的队列上去。
fanout 交换器不处理路由键,只是简单的将队列绑定到交换器上,每个发送到交换器的消息都会被转发到与该交换器绑定的所有队列上。很像子网广播,每台子网内的主机都获得了一份复制的消息。
fanout 类型转发消息是最快的
Headers Exchange-头交换器
该类型的交换器不依赖路由规则来路由消息,而是根据消息内容中的headers属性进行匹配。
headers类型交换器性能差,在实际中并不常用。
六种工作模式
官网说明:https://www.rabbitmq.com/getstarted.html
简单队列模式(点对点模式)
一条消息由一个消费者进行消费。
P代表生产者,C代表消费者,红色代码消息队列。P将消息发送到消息队列,C对消息进行处理。
工作队列模式(Work Queues)
一个生产者,多个消费者共同消费同一个队列的消息,每个消费者获取到的消息唯一。
这种模式与简单模式的区别就是多了消费者
在一个队列中如果有多个消费者,那么消费者之间对于同一个消息的关系是竞争的关系,队列采用轮询的方式将消息是平均发送给消费者**,消费者在处理完某条消息后,才会收到下一条消息
发布/订阅模式(Publish/Subscribe)
相比工作队列模式,多了个交换机
生产端先把消息发送到交换机,再由交换机把消息发送到绑定的队列中,每个绑定的队列都能收到由生产端发送的消息。
使用的是Fanout扇形交换器**
应用场景:用户通知,当用户充值成功或转账完成系统通知用户,通知方式有短信、邮件多种方法;
路由模式(Routing)
该种模式除了要绑定交换机外,发消息的时候还要制定routing key,即路由key,队列通过通道绑定交换机的时候,需要指定自己的routing key,这样,生产端发送消息的时候也会指定routing key,通过routing key就可以把相应的消息发送到绑定相应routing key的队列中去。
使用的是direct直连交换器
主题模式(Topics)
Topics 模式和Routing 路由模式最大的区别就是,Topics 模式发送消息和消费消息的时候是通过通配符去进行匹配的。
使用的是topic主题交换器
**
RPC模式(Remote procedure call)
RPC即客户端远程调用服务端的方法 ,使用MQ可以实现RPC的异步调用,基于Direct交换机实现
1、客户端即是生产者也是消费者,向RPC请求队列发送RPC调用消息,同时监听RPC响应队列。
2、服务端监听RPC请求队列的消息,收到消息后执行服务端的方法,得到方法返回的结果。
3、服务端将RPC方法 的结果发送到RPC响应队列。
4、客户端(RPC调用方)监听RPC响应队列,接收到RPC调用结果。
安装
第一步:下载安装Erlang
RabbitMQ服务端代码是使用并发式语言Erlang编写的,安装Rabbit MQ的前提是安装Erlang。【就像安装ActiveMQ之前需要保证安装了JDK一样】
下载地址:http://www.erlang.org/downloads
选择合适版本下载,此处我选择64位20.3版本安装程序
下载完成,双击安装
默认点击Next即可
安装完成,配置环境变量
此电脑—>鼠标右键“属性”—>高级系统设置—>环境变量—>“新建”系统环境变量
变量名:ERLANG_HOME
变量值就是刚才erlang的安装地址,点击确定。
修改path环境变量,将%ERLANG_HOME%\bin加入到path中
验证是否安装成功,在Dos窗口输入erl,出现erlang的版本号即表示安装完成
第二步:下载并安装RabbitMQ
官网地址:https://www.rabbitmq.com/download.html
下载完成后双击安装,点击next即可
第三步:启用RabbitMQ管理插件
打开命令行cd,输入RabbitMQ的sbin目录。
输入命令:
rabbitmq-plugins enable rabbitmq_management
验证是否成功,输入rabbitmqctl status命令,出现如下输出则表明成功且处于启动状态
第四步:启动RabbitMQ服务器
前往sbin目录,双击rabbitmq-server.bat
启动成功
第五步:访问管理页面
访问http://localhost:15672,输入默认账密guest/guest
界面介绍
Overview-概览
统计内容:
- Connections:连接数
- Channels:通道数
- Exchanges:交换机数
- Queues:队列数
- Consumers:消费者数
Nodes中包含RabbitMQ节点的进程、内存、配置文件路径等内容
Ports and contexts中显示RabbitMQ所开启的端口号,amqp是RabbitMQ TCP端口号、clustering是集群端口号、下方还有管控台端口号
Impot / export definitions 用于导入、导出RabbitMQ的所有配置(建议经常备份配置,以便在版本升级等场景下进行现场还原)
Connects-连接管理
连接,无论生产者还是消费者,都需要与 RabbitMQ 建立连接后才可以完成消息的生产和消费,在这里可以查看连接情况
Channels-通道管理
Exchanges-交换机管理
交换机,用来接收生产者发送的消息并将这些消息路由给服务器中的队列。
Queues-队列管理
就是消息队列,消息存放在队列中,等待消费,消费后会被移除队列
Admin-用户管理
可以创建和管理访问管控台的用户,线上环境需要删除默认的guest用户重新创建。
- 超级管理员administrator,可以登录控制台,查看所有信息,可以对用户和策略进行操作
- 监控者monitoring,可以登录控制台,可以查看节点的相关信息,比如进程数,内存磁盘使用情况
- 策略制定者policymaker ,可以登录控制台,制定策略,但是无法查看节点信息
- 普通管理员 management 仅能登录控制台
- 其他, 无法登录控制台,一般指的是提供者和消费者
简单使用
创建用户
先在RabbitMQ新建一个用户mytest,授予管理员角色
初始创建时没有权限(No access),需要点击名称进行设置
进入权限设置页面后点击Set permission按钮即可,自动设置全读写权限
完成后mytest用户信息如下
引入依赖
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>4.1.0</version>
</dependency>
统一工具类
package com.mq.rabbitmq.queue;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @description 通用工具类
* @date: 2020-12-10 15:54
*/
public class CommonUtil {
private static Connection connection = null;
private static ConnectionFactory connectionFactory = null;
/**
* 获取RabbitMQ连接对象
* @return
*/
public static Connection getConnection() throws IOException, TimeoutException {
if(connection!=null){
return connection;
}
if(connectionFactory!=null){
connection = connectionFactory.newConnection();
return connection;
}
// 连接工厂
connectionFactory = new ConnectionFactory();
// 设置RabbitMQ服务器连接信息
factory.setHost("127.0.0.1");
// amqp的端口默认是5672
factory.setPort(5672);
factory.setUsername("mytest");
factory.setPassword("123456");
// rabbitmq默认虚拟机名称为“/”,虚拟机相当于一个独立的mq服务器
connectionFactory.setVirtualHost("/");
// 创建连接
connection = factory.newConnection();
return connection;
}
}
简单模式
消息生产者
package com.mq.rabbitmq.queue;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.util.Scanner;
/**
* @description rabbitMQ消息队列生产者
* @date: 2020-12-09 15:23
*/
public class RabbitMQSender {
// 队列名称
final static String QUEUE_NAME = "queueOne";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//消息发布
Scanner scanner = new Scanner(System.in);
String message = "";
do{
message = scanner.nextLine();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("生产者发送消息:"+message);
}while(!message.equals("bye")); // 当输入bye时结束输入,推出循环
//关闭资源
channel.close();
connection.close();
}
}
消息消费者
package com.mq.rabbitmq.queue;
import com.rabbitmq.client.*;
import java.io.IOException;
/**
* @description rabbitMQ消息队列消费者
* @date: 2020-12-09 16:10
*/
public class RabbitMQReceiver {
// 队列名称
final static String QUEUE_NAME = "queueOne";
//消费者消费消息
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
// 每次取5条消息
channel.basicQos(5);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消息
String msg = new String(body,"utf-8");
System.out.println("consume msg: "+msg);
}
};
//参数1:接收哪个队列的数据
//参数2:消息确认 是否应答,收到消息是否回复
//参数3:消费者
channel.basicConsume(QUEUE_NAME,true,consumer);
//关闭资源(这里一定要注意,别关闭通道【或者加个休眠再停止】,否则还没接收消息就停止程序了)
// channel.close();
// connection.close();
}
}
启动生产者者并发送消息
队列多了一条消息
启动消费者消费消息
队列中没有消息,且多了一个消费者
工作队列模式(Work Queues)
消息生产者
package com.mq.rabbitmq.workqueue;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.Scanner;
/**
* @description rabbitMQ消息队列生产者
* @date: 2020-12-09 15:23
*/
public class WorkQueueSender {
// 队列名称
final static String QUEUE_NAME = "workQueue";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//消息发布
for(int i=1;i<=20;i++){
String message = "第"+i+"条消息";
System.out.println("生产者发送消息:"+message);
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
}
//关闭资源
channel.close();
connection.close();
}
}
消息消费者(多个)
package com.mq.rabbitmq.workqueue;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @description 工作队列模式消费者
* @date: 2020-12-09 16:10
*/
public class WorkQueueReceiver {
// 队列名称
final static String QUEUE_NAME = "workQueue";
//消费者消费消息
public static void main(String[] args) throws Exception {
MyConsumerFactory myConsumerFactory = new MyConsumerFactory(QUEUE_NAME);
// 创建多个消费者,消费同一个队列
for(int i=1;i<=5;i++){
myConsumerFactory.createConsumer(i+"号");
}
}
/**
* 自定义消费者工厂类
*/
static class MyConsumerFactory{
String queueName; // 目标队列名称
/**
* 初始化消费者
* @param queueName 目标队列名称
*/
public MyConsumerFactory(String queueName) {
this.queueName = queueName;
}
/**
* 创建消费者
*/
public void createConsumer(final String consumerName){
try {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(queueName,false,false,false,null);
// 每次取1条消息
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消费
String msg = new String(body,"utf-8");
System.out.println("消费者"+consumerName+": "+msg);
// 休眠个1秒钟,将消息让给其他消费者
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动消息确认[如果自动确认消息,速度过快,根本不可能轮得到别的消费者,就一直都是1号消费者在消费]
getChannel().basicAck(envelope.getDeliveryTag(),false);
}
};
//参数1:接收哪个队列的数据
//参数2:消息确认 是否应答,收到消息是否回复
//参数3:消费者
channel.basicConsume(queueName,false,consumer);
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
启动生产者并发送消息
启动消费者并消费消息
消息队列workQueue里的消息被消费完了,总共有五个消费者
发布/订阅模式(Publish/Subscribe)
消息生产者
package com.mq.rabbitmq.PublishAndSubscribe;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
/**
* @description 发布订阅模式生产者
* @date: 2020-12-11 11:18
*/
public class Publisher {
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
// 交换机名称
final static String EXCHANGE_NAME = "MY_EXCHANGE";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 通道绑定交换机
* 1、交换机名称
* 2、交换机类型,fanout、topic、direct、headers
*/
//Publish/subscribe发布订阅模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.FANOUT);
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME_ONE,false,false,false,null); // 通道绑定队列1
channel.queueDeclare(QUEUE_NAME_TWO,false,false,false,null); // 通道绑定队列2
/**
* 交换机和队列绑定
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
//Publish/subscribe发布订阅模式
channel.queueBind(QUEUE_NAME_ONE,EXCHANGE_NAME,""); // 交换机绑定队列1
channel.queueBind(QUEUE_NAME_TWO,EXCHANGE_NAME,""); // 交换机绑定队列2
//消息发布
for(int i=1;i<=10;i++){
String message = "第"+i+"条消息";
System.out.println("生产者发送消息:"+message);
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
channel.basicPublish(EXCHANGE_NAME,"",null,message.getBytes());
}
//关闭资源
channel.close();
connection.close();
}
}
消息消费者(多个)
package com.mq.rabbitmq.PublishAndSubscribe;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @description 发布订阅模式消费者
* @date: 2020-12-11 11:18
*/
public class Subscriber {
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
public static void main(String[] args) {
MyConsumerFactory myConsumerFactory = new MyConsumerFactory();
// 创建队列1的消费者
myConsumerFactory.createConsumer("(队列1)", QUEUE_NAME_ONE);
// 创建队列2的消费者
myConsumerFactory.createConsumer("(队列2)", QUEUE_NAME_TWO);
}
/**
* 自定义消费者工厂类
*/
static class MyConsumerFactory{
/**
* 创建消费者
*/
public void createConsumer(final String consumerName,String queueName){
try {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(queueName,false,false,false,null);
// 每次取1条消息
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消费
String msg = new String(body,"utf-8");
System.out.println("消费者"+consumerName+" 收到消息: "+msg);
//手动消息确认[如果自动确认消息,速度过快,根本不可能轮得到别的消费者,就一直都是1号消费者在消费]
getChannel().basicAck(envelope.getDeliveryTag(),false);
}
};
//参数1:接收哪个队列的数据
//参数2:消息确认 是否自动确认
//参数3:消费者
channel.basicConsume(queueName,false,consumer);
System.out.println("消费者"+consumerName+"已就位");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
启动生产者并发送消息
启动消费者并消费消息
路由模式(Routing)
消息生产者
package com.mq.rabbitmq.routing;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.Scanner;
/**
* @description 路由模式生产者
* @auther: lai.guanfu
* @date: 2020-12-11 11:18
*/
public class Publisher {
// 声明两个队列和一个交换机
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
// 交换机名称
final static String EXCHANGE_NAME = "MY_ROUTING_EXCHANGE";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 通道绑定交换机
* 1、交换机名称
* 2、交换机类型,fanout、topic、direct、headers
*/
// Routing 路由模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME_ONE,false,false,false,null); // 通道绑定队列1
channel.queueDeclare(QUEUE_NAME_TWO,false,false,false,null); // 通道绑定队列2
/**
* 交换机和队列绑定
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
// Routing 路由模式
channel.queueBind(QUEUE_NAME_ONE,EXCHANGE_NAME,QUEUE_NAME_ONE); // 交换机绑定队列1
channel.queueBind(QUEUE_NAME_TWO,EXCHANGE_NAME,QUEUE_NAME_TWO); // 交换机绑定队列2
// 消息发布
Scanner scanner = new Scanner(System.in);
do {
System.out.println("请发布内容:");
String message = scanner.nextLine();
System.out.println("请选择目标接收队列(1 队列1,2 队列2):");
int i = scanner.nextInt();
if(i==1){
System.out.println("生产者向队列1发送消息:"+message);
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
channel.basicPublish(EXCHANGE_NAME,QUEUE_NAME_ONE,null,message.getBytes());
}else{
System.out.println("生产者向队列2发送消息:"+message);
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
channel.basicPublish(EXCHANGE_NAME,QUEUE_NAME_TWO,null,message.getBytes());
}
if(message.equals("bye")){
break;
}
scanner.nextLine();
}while(true);
//关闭资源
channel.close();
connection.close();
}
}
消息消费者(多个)
package com.mq.rabbitmq.routing;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @description 路由模式消费者
* @date: 2020-12-11 11:18
*/
public class Subscriber {
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
public static void main(String[] args) {
MyConsumerFactory myConsumerFactory = new MyConsumerFactory();
// 创建队列1的消费者
myConsumerFactory.createConsumer("(队列1)", QUEUE_NAME_ONE);
// 创建队列2的消费者
myConsumerFactory.createConsumer("(队列2)", QUEUE_NAME_TWO);
}
/**
* 自定义消费者工厂类
*/
static class MyConsumerFactory{
/**
* 创建消费者
*/
public void createConsumer(final String consumerName,String queueName){
try {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(queueName,false,false,false,null);
// 每次取1条消息
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消费
String msg = new String(body,"utf-8");
System.out.println("消费者"+consumerName+" 收到消息: "+msg);
}
};
//参数1:接收哪个队列的数据
//参数2:消息确认 是否自动确认
//参数3:消费者
channel.basicConsume(queueName,true,consumer);
System.out.println("消费者"+consumerName+"已就位");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
启动生产者并发送消息
启动消费者并消费消息
主题模式(Topics)
消息生产者
package com.mq.rabbitmq.topics;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.BuiltinExchangeType;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import java.util.Scanner;
/**
* @description 主题模式生产者
* @date: 2020-12-11 11:18
*/
public class TopicPublisher {
// 声明两个队列和一个交换机
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
// 交换机名称
final static String EXCHANGE_NAME = "MY_TOPIC_EXCHANGE";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 通道绑定交换机
* 1、交换机名称
* 2、交换机类型,fanout、topics、direct、headers
*/
// Topics 主题模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.TOPIC);
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME_ONE,false,false,false,null); // 通道绑定队列1
channel.queueDeclare(QUEUE_NAME_TWO,false,false,false,null); // 通道绑定队列2
/**
* 交换机和队列绑定
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
// Topics 主题模式
// 交换机绑定队列,注意此处的路由key使用#进行匹配,并不和队列名称一致
channel.queueBind(QUEUE_NAME_ONE,EXCHANGE_NAME,"info.#.one.#");
channel.queueBind(QUEUE_NAME_TWO,EXCHANGE_NAME,"info.#.two.#");
// 消息发布
Scanner scanner = new Scanner(System.in);
do {
System.out.println("请发布内容:");
String message = scanner.nextLine();
System.out.println("请输入目标routingKey:");
String routingKey = scanner.nextLine();
System.out.println("生产者发送消息:"+message+",目标routingKey为:"+routingKey);
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
// 指定routingKey由交换机和Binding去分析应该发送给哪个队列
channel.basicPublish(EXCHANGE_NAME,routingKey,null,message.getBytes());
if(message.equals("bye")){
break;
}
scanner.nextLine();
}while(true);
//关闭资源
channel.close();
connection.close();
}
}
消息消费者(多个)
package com.mq.rabbitmq.topics;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
/**
* @description 主题模式消费者
* @date: 2020-12-11 11:18
*/
public class TopicSubscriber {
// 队列1名称
final static String QUEUE_NAME_ONE = "QUEUE_ONE";
// 队列2名称
final static String QUEUE_NAME_TWO = "QUEUE_TWO";
public static void main(String[] args) {
MyConsumerFactory myConsumerFactory = new MyConsumerFactory();
// 创建队列1的消费者
myConsumerFactory.createConsumer("(队列1)", QUEUE_NAME_ONE);
// 创建队列2的消费者
myConsumerFactory.createConsumer("(队列2)", QUEUE_NAME_TWO);
}
/**
* 自定义消费者工厂类
*/
static class MyConsumerFactory{
/**
* 创建消费者
*/
public void createConsumer(final String consumerName,String queueName){
try {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
Channel channel = connection.createChannel();
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(queueName,false,false,false,null);
// 每次取1条消息
channel.basicQos(1);
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消费
String msg = new String(body,"utf-8");
System.out.println("消费者"+consumerName+" 收到消息: "+msg);
}
};
//参数1:接收哪个队列的数据
//参数2:消息确认 是否自动确认
//参数3:消费者
channel.basicConsume(queueName,true,consumer);
System.out.println("消费者"+consumerName+"已就位");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
}
启动生产者并发送消息
队列1和队列2都收到了两个消息
启动消费者并消费消息
RPC模式
客户端(Client)
package com.mq.rabbitmq.RPC;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Queue;
import java.util.Scanner;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
/**
* @description RPC模式客户端
* @date: 2020-12-11 11:18
*/
public class RPCClient {
// 声明队列和交换机
// 队列名称
final static String QUEUE_NAME = "QUEUE_RPC";
// 交换机名称
final static String EXCHANGE_NAME = "MY_RPC_EXCHANGE";
public static void main(String[] args) throws Exception {
// 获取连接对象
Connection connection = CommonUtil.getConnection();
// 创建信道,每个连接可以创建多个信道,每个信道代表一个会话任务
final Channel channel = connection.createChannel();
// 限制:每次最多给一个消费者发送1条消息
channel.basicQos(1);
/**
* 通道绑定交换机
* 1、交换机名称
* 2、交换机类型,fanout、topics、direct、headers
*/
// RPC模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME, false, false, false, null);// 通道绑定队列1
/**
* 交换机和队列绑定
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
// RPC模式
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,QUEUE_NAME);
// 本次请求唯一标志
final String correlationId = UUID.randomUUID().toString();
// 定义匿名的回调队列[由channel随机生成]
// 这样做的好处是:每个客户端有属于自己的唯一回复队列,生命周期同客户端
String replyToQueue = channel.queueDeclare().getQueue();
// 此处为返回的核心,设置发送消息携带的信息,对方才能知道应该怎么回复
final AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(correlationId)// 队列的唯一值认证
.replyTo(replyToQueue)// 回调队列(告诉对方应该回复到哪个队列)
.build();
System.out.println("创建连接,服务端回复队列为:"+replyToQueue);
System.out.println("====================================");
// 发送消息给Server
System.out.println("请输入传送内容:");
Scanner scanner = new Scanner(System.in);
String message = scanner.nextLine();
System.out.println("发送了一条消息:"+message);
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
// 将消息发送到 QUEUE_NAME 指定的队列中
channel.basicPublish(EXCHANGE_NAME, QUEUE_NAME, props, message.getBytes("utf8"));
System.out.println("等待回复中。。。。");
// 监听回调,接收返回来的消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) {
// 判断回传的消息是否与唯一id一致
if (properties.getCorrelationId().equals(correlationId)) {
System.out.println("收到回复:" + new String(body));
}else{
System.out.println("correlationID未对应上的消息:" + new String(body));
}
// 回复
try {
System.out.println("请输入回复内容:");
Scanner scanner = new Scanner(System.in);
String message = scanner.nextLine();
channel.basicPublish(EXCHANGE_NAME,QUEUE_NAME , props, message.getBytes("utf8"));
System.out.println("发送了一条消息:"+message);
System.out.println("等待回复中。。。。");
} catch (IOException e) {
e.printStackTrace();
}
}
};
// 注册消费监听,监听的是回复队列(Server端会把消息放到这个队列里面)
channel.basicConsume(replyToQueue, true,consumer );
}
}
服务端(Server)
package com.mq.rabbitmq.RPC;
import com.mq.rabbitmq.CommonUtil;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.Scanner;
import java.util.concurrent.TimeoutException;
/**
* @description RPC模式服务端
* @date: 2020-12-11 11:18
*/
public class RPCServer {
// 队列名称
final static String QUEUE_NAME = "QUEUE_RPC";
// 交换机名称
final static String EXCHANGE_NAME = "MY_RPC_EXCHANGE";
public static void main(String[] args) throws IOException, TimeoutException {
final Channel channel = CommonUtil.getConnection().createChannel();
/**
* 通道绑定交换机
* 1、交换机名称
* 2、交换机类型,fanout、topics、direct、headers
*/
// RPC 主题模式
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
/**
* 交换机和队列绑定
* 1、队列名称
* 2、交换机名称
* 3、路由key
*/
// RPC模式
channel.queueBind(QUEUE_NAME,EXCHANGE_NAME,QUEUE_NAME);
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME,false,false,false,null); // 通道绑定队列1
// 构造消费者
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("收到消息:"+new String(body)+"----[回复队列:"+properties.getReplyTo()+"]");
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
//我们在将要回复的消息属性中,放入从客户端传递过来的correlateId
builder.correlationId(properties.getCorrelationId());
AMQP.BasicProperties prop = builder.build();
Scanner scanner = new Scanner(System.in);
System.out.println("请输入回复内容:");
String replyMessage = scanner.nextLine();
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性
* param4:消息体
*/
// 回复消息,将消息放到replyTo指定的队列中
channel.basicPublish("", properties.getReplyTo(), prop, (replyMessage).getBytes());
System.out.println("发送回复:"+replyMessage);
}
};
// 注册消费,监听 QUEUE_NAME 指定的队列,Client端会将消息放到这个队列里面
channel.basicConsume(QUEUE_NAME, true, consumer);
}
}
启动客户端(Client)并发送消息
客户端消息队列有一条消息待消费,回复队列没有消息
启动服务端(Server)并消费消息
Server端收到消息,并回复
客户端收到来自服务端的回复
核心API
从上述代码对各种模式的实现中,我们知道主要的操作都是依靠Channel和Consumer类实现,包括绑定交换器,绑定队列,发送消息,接收消息等。
com.rabbitmq.client.Channel—信道
exchangeDeclare—-声明并绑定注册器
参数解析:
- exchange:交换器名称
- type:交换器类型,有direct、fanout、topic、headers四种【见BuiltinExchangeType枚举】
- durable:是否持久化交换器(true|false),true代表服务器重启会保留下来Exchange。警告:仅设置此选项,不代表消息持久化。即不保证重启后消息还在。
- autoDelete:是否自动删除交换器(true|false).true表示当已经没有消费者时,服务器是否可以删除该Exchange
- internal:是否内部交换消息(true|false),true表示交换是内部的,即客户端不能直接发布消息
arguments:其他附属属性
/**
* Declare an exchange, via an interface that allows the complete set of
* arguments.
* @see com.rabbitmq.client.AMQP.Exchange.Declare
* @see com.rabbitmq.client.AMQP.Exchange.DeclareOk
* @param exchange the name of the exchange
* @param type the exchange type
* @param durable true if we are declaring a durable exchange (the exchange will survive a server restart)
* @param autoDelete true if the server should delete the exchange when it is no longer in use
* @param internal true if the exchange is internal, i.e. can't be directly
* published to by a client.
* @param arguments other properties (construction arguments) for the exchange
* @return a declaration-confirm method to indicate the exchange was successfully declared
* @throws java.io.IOException if an error is encountered
*/
Exchange.DeclareOk exchangeDeclare(String exchange,
BuiltinExchangeType type,
boolean durable,
boolean autoDelete,
boolean internal,
Map<String, Object> arguments) throws IOException;
queueDeclare—-声明并绑定队列
参数解析:
queue:队列名称
- durable:是否持久化队列(true|false),true代表服务器重启会保留下该队列
- exclusive :是否为当前连接的专用队列,在连接断开后,会自动删除该队列
- autodelete:当没有任何消费者使用时,自动删除该队列
- arguments:其他附属属性
/**
* Declare a queue
* @see com.rabbitmq.client.AMQP.Queue.Declare
* @see com.rabbitmq.client.AMQP.Queue.DeclareOk
* @param queue the name of the queue
* @param durable true if we are declaring a durable queue (the queue will survive a server restart)
* @param exclusive true if we are declaring an exclusive queue (restricted to this connection)
* @param autoDelete true if we are declaring an autodelete queue (server will delete it when no longer in use)
* @param arguments other properties (construction arguments) for the queue
* @return a declaration-confirm method to indicate the queue was successfully declared
* @throws java.io.IOException if an error is encountered
*/
Queue.DeclareOk queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
Map<String, Object> arguments) throws IOException;
queueBind—-绑定队列与交换器
参数解析:
- queue:队列名称
- exchange:交换器名称
- routingKey:路由key,即指定队列在指定交换器的注册路由
- arguments:其他附属属性
/**
* Bind a queue to an exchange.
* @see com.rabbitmq.client.AMQP.Queue.Bind
* @see com.rabbitmq.client.AMQP.Queue.BindOk
* @param queue the name of the queue
* @param exchange the name of the exchange
* @param routingKey the routing key to use for the binding
* @param arguments other properties (binding parameters)
* @return a binding-confirm method if the binding was successfully created
* @throws java.io.IOException if an error is encountered
*/
Queue.BindOk queueBind(String queue, String exchange, String routingKey, Map<String, Object> arguments) throws IOException;
basicPublish—-发布消息
参数解析:
- exchange:交换器名称
- routingKey:路由键,#匹配0个或多个单词,*匹配一个单词,在topic exchange做消息转发用
- mandatory:true:如果exchange根据自身类型和消息routeKey无法找到一个符合条件的queue,那么会调用basic.return方法将消息返还给生产者。false:出现上述情形broker会直接将消息扔掉
- immediate:true:如果exchange在将消息route到queue(s)时发现对应的queue上没有消费者,那么这条消息不会放入队列中。当与消息routeKey关联的所有queue(一个或多个)都没有消费者时,该消息会通过basic.return方法返还给生产者。
- BasicProperties :需要注意的是BasicProperties.deliveryMode,0:不持久化 1:持久化 这里指的是消息的持久化,配合channel(durable=true),queue(durable=true)可以实现,即使服务器宕机,消息仍然保留
- body:消息内容
简单来说:mandatory标志告诉服务器至少将该消息route到一个队列中,否则将消息返还给生产者;immediate标志告诉服务器如果该消息关联的queue上有消费者,则马上将消息投递给它,如果所有queue都没有消费者,直接把消息返还给生产者,不用将消息入队列等待消费者了。
/**
* Publish a message.
*
* Publishing to a non-existent exchange will result in a channel-level
* protocol exception, which closes the channel.
*
* Invocations of <code>Channel#basicPublish</code> will eventually block if a
* <a href="https://www.rabbitmq.com/alarms.html">resource-driven alarm</a> is in effect.
*
* @see com.rabbitmq.client.AMQP.Basic.Publish
* @see <a href="https://www.rabbitmq.com/alarms.html">Resource-driven alarms</a>
* @param exchange the exchange to publish the message to
* @param routingKey the routing key
* @param mandatory true if the 'mandatory' flag is to be set
* @param immediate true if the 'immediate' flag is to be
* set. Note that the RabbitMQ server does not support this flag.
* @param props other properties for the message - routing headers etc
* @param body the message body
* @throws java.io.IOException if an error is encountered
*/
void basicPublish(String exchange, String routingKey, boolean mandatory, boolean immediate, BasicProperties props, byte[] body)
throws IOException;
basicConsume—-启动一个消息监听
参数解析:
- queue:队列的名称
- autoAck:设置是否自动确认。一般设置为false,即不自动确认。
- consumerTag:消费者标签,用来区分多个消费者。
- noLocal:设置为true,则表示不能将同一个Connection中生产者发送的消息传送给这个Connection中的消费者。
- exclusive:设置是否排他。
- arguments:设置消费者的其他参数
- deliverCallback:处理推送过来的消息的回调函数。比如DefaultConsumer,重写其中的方法实现我们对消息的处理逻辑。
- cancelCallback:当消费者取消订阅时的回调函数
- shutdownSignalCallback:当Connection/Channel关闭时的回调函数
/**
* Start a consumer. Calls the consumer's {@link Consumer#handleConsumeOk}
* method.
* Provide access to <code>basic.deliver</code>, <code>basic.cancel</code>
* and shutdown signal callbacks (which is sufficient
* for most cases). See methods with a {@link Consumer} argument
* to have access to all the application callbacks.
* @param queue the name of the queue
* @param autoAck true if the server should consider messages
* acknowledged once delivered; false if the server should expect
* explicit acknowledgements
* @param consumerTag a client-generated consumer tag to establish context
* @param noLocal True if the server should not deliver to this consumer
* messages published on this channel's connection. Note that the RabbitMQ server does not support this flag.
* @param exclusive true if this is an exclusive consumer
* @param arguments a set of arguments for the consume
* @param deliverCallback callback when a message is delivered
* @param cancelCallback callback when the consumer is cancelled
* @param shutdownSignalCallback callback when the channel/connection is shut down
* @return the consumerTag associated with the new consumer
* @throws java.io.IOException if an error is encountered
* @see com.rabbitmq.client.AMQP.Basic.Consume
* @see com.rabbitmq.client.AMQP.Basic.ConsumeOk
* @since 5.0
*/
String basicConsume(String queue, boolean autoAck, String consumerTag, boolean noLocal, boolean exclusive, Map<String, Object> arguments, DeliverCallback deliverCallback, CancelCallback cancelCallback, ConsumerShutdownSignalCallback shutdownSignalCallback) throws IOException;
basicAck—-确认消息
参数解析:
- deliveryTag:用来标识信道中投递的消息,它是 一个 64 位的长整型值,最大值是9223372036854775807。RabbitMQ 推送消息给 Consumer 时,会附带一个 Delivery Tag,以便 Consumer 可以在消息确认时告诉 RabbitMQ 到底是哪条消息被确认了。可以从消费消息方法的Envelope对象中获取。
- multiple:如果false,表示通知 RabbitMQ该deliveryTag消息被确认。如果为true,则将小于deliveryTag之前的所有消息全部确认。
/**
* Acknowledge one or several received
* messages. Supply the deliveryTag from the {@link com.rabbitmq.client.AMQP.Basic.GetOk}
* or {@link com.rabbitmq.client.AMQP.Basic.Deliver} method
* containing the received message being acknowledged.
* @see com.rabbitmq.client.AMQP.Basic.Ack
* @param deliveryTag the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
* @param multiple true to acknowledge all messages up to and
* including the supplied delivery tag; false to acknowledge just
* the supplied delivery tag.
* @throws java.io.IOException if an error is encountered
*/
void basicAck(long deliveryTag, boolean multiple) throws IOException;
basicReject—-拒绝消息
参数解析:
- deliveryTag:用来标识信道中投递的消息
- requeue:requeue 参数设置为true,则 RabbitMQ 会重新将这条消息存入队列,以便可以发送给下个订阅的消费者;如果 requeue 参数设置为 false,则 RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
/**
* Reject a message. Supply the deliveryTag from the {@link com.rabbitmq.client.AMQP.Basic.GetOk}
* or {@link com.rabbitmq.client.AMQP.Basic.Deliver} method
* containing the received message being rejected.
* @see com.rabbitmq.client.AMQP.Basic.Reject
* @param deliveryTag the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
* @param requeue true if the rejected message should be requeued rather than discarded/dead-lettered
* @throws java.io.IOException if an error is encountered
*/
void basicReject(long deliveryTag, boolean requeue) throws IOException;
basicNack—-批量拒绝消息
参数解析:
- deliveryTag:用来标识信道中投递的消息
- multiple: 如果false,表示通知 RabbitMQ 该deliveryTag消息被拒绝。如果为true,则将小于deliveryTag之前的所有消息全部拒绝。
- requeue:requeue 参数设置为true,则 RabbitMQ 会重新将这条消息存入队列,以便可以发送给下个订阅的消费者;如果 requeue 参数设置为 false,则 RabbitMQ立即会把消息从队列中移除,而不会把它发送给新的消费者。
/**
* Reject one or several received messages.
*
* Supply the <code>deliveryTag</code> from the {@link com.rabbitmq.client.AMQP.Basic.GetOk}
* or {@link com.rabbitmq.client.AMQP.Basic.GetOk} method containing the message to be rejected.
* @see com.rabbitmq.client.AMQP.Basic.Nack
* @param deliveryTag the tag from the received {@link com.rabbitmq.client.AMQP.Basic.GetOk} or {@link com.rabbitmq.client.AMQP.Basic.Deliver}
* @param multiple true to reject all messages up to and including
* the supplied delivery tag; false to reject just the supplied
* delivery tag.
* @param requeue true if the rejected message(s) should be requeued rather
* than discarded/dead-lettered
* @throws java.io.IOException if an error is encountered
*/
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
com.rabbitmq.client.Consumer—消息监听
handleDelivery
用来处理队列推送的消息
/**
* Called when a <code><b>basic.deliver</b></code> is received for this consumer.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
* @param envelope packaging data for the message
* @param properties content header data for the message
* @param body the message body (opaque, client-specific byte array)
* @throws IOException if the consumer encounters an I/O error while processing the message
* @see Envelope
*/
void handleDelivery(String consumerTag,
Envelope envelope,
AMQP.BasicProperties properties,
byte[] body)
throws IOException;
handleShutdownSignal
当Channel或者Connection关闭的时候会调用。
**
- consumerTag:消费者标签,用来区分多个消费者。
- sig:如果抛出异常同时传入
/**
* Called when either the channel or the underlying connection has been shut down.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
* @param sig a {@link ShutdownSignalException} indicating the reason for the shut down
*/
void handleShutdownSignal(String consumerTag, ShutdownSignalException sig);
handleConsumeOk
任意Channel#basicConsume调用导致消费者被注册时调用,返回消费者标签
/**
* Called when the consumer is registered by a call to any of the
* {@link Channel#basicConsume} methods.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
*/
void handleConsumeOk(String consumerTag);
handleCancelOk
Channel#basicCancel调用导致的订阅取消时被调用。
/**
* Called when the consumer is cancelled by a call to {@link Channel#basicCancel}.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
*/
void handleCancelOk(String consumerTag);
handleCancel
除了调用basicCancel的其他原因导致消息被取消时调用。
/**
* Called when the consumer is cancelled for reasons <i>other than</i> by a call to
* {@link Channel#basicCancel}. For example, the queue has been deleted.
* See {@link #handleCancelOk} for notification of consumer
* cancellation due to {@link Channel#basicCancel}.
* @param consumerTag the <i>consumer tag</i> associated with the consumer
* @throws IOException
*/
void handleCancel(String consumerTag) throws IOException;
RabbitMQ解决消息丢失
我们知道,消息队列的消息丢失可能发生在三个阶段:
- 生产者向Broker发送消息的过程中丢失
- Broker故障丢失
- 消费者丢失数据
RabbitMQ对这三种情况的预防措施如下:
生产者弄丢了数据—事务机制和Confirm模式
生产者将数据发送到rabbitmq的时候,可能因为网络问题导致数据就在半路给搞丢了。
事务机制
RabbitMQ中与事务机制有关的方法有三个:Chanel#txSelect, Chanel#txCommit()以及Chanel#txRollback(), txSelect用于将当前channel设置成transaction模式,txCommit用于提交事务,txRollback用于回滚事务。
在通过txSelect开启事务之后,我们便可以发布消息给broker代理服务器了,如果txCommit提交成功了,则消息一定到达了broker了,如果在txCommit执行之前broker异常崩溃或者由于其他原因抛出异常,这个时候我们便可以捕获异常通过txRollback回滚事务了。
do{
message = scanner.nextLine();
try {
// 开启事务
channel.txSelect();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("生产者发送消息:"+message);
channel.txCommit(); // 事务提交
} catch (IOException e) {
try {
channel.txRollback();
} catch (IOException e1) {
e1.printStackTrace();
}
e.printStackTrace();
}
}while(!message.equals("bye")); // 当输入bye时结束输入,推出循环
但是问题是,开启Rabbitmq事务机制,基本上吞吐量会下来,因为太耗性能。
confirm模式(推荐)
在生产者那里设置开启confirm模式之后,每次发送的消息都会分配一个唯一的id,然后如果成功写入了rabbitmq中,rabbitmq会回传一个ack消息,表明这个消息已确认存放。
如果rabbitmq没能处理这个消息,会回调你一个nack接口,告诉你这个消息接收失败,你可以重试。而且你可以结合这个机制自己在内存里维护每个消息id的状态,如果超过一定时间还没接收到这个消息的回调,那么你可以重发。
串行方式
每发送一条消息后,调用waitForConfirms()方法,等待服务器端confirm。
// 开启confirm模式
channel.confirmSelect();
do{
message = scanner.nextLine();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
// 等待确认
if(!channel.waitForConfirms()){
System.out.println("消息发送失败。");
break;
}
System.out.println("生产者发送消息:"+message);
}while(!message.equals("bye")); // 当输入bye时结束输入,推出循环
异步方式
提供一个回调方法,服务端confirm了一条或者多条消息后Client端会回调这个方法。
下述代码为channel添加了一个消息确认回调监听器,无论Broker接收消息成功与否,都会触发监听器内部的监听方法,并且不会阻塞channel继续发送消息
// 开启confirm模式
channel.confirmSelect();
// 装载待确认回调的消息id
final SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<Long>());
// 为channel添加确认回调监听器
channel.addConfirmListener(new ConfirmListener() {
/**
* 消息确认成功的回调函数
*/
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple) {
// 如果是批量确认,则清除从开始到当前消息的元素
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
System.out.println("消息发送成功");
}
/**
* 消息确认失败的回调函数
*/
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
// 这里可以处理消息确认发送失败的逻辑,如重发等
if (multiple) {
confirmSet.headSet(deliveryTag + 1).clear();
} else {
confirmSet.remove(deliveryTag);
}
System.out.println("消息发送失败");
}
});
do{
message = scanner.nextLine();
// 获取即将发送的消息的唯一id
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
// 将已发送的消息id加入待确认集合
confirmSet.add(nextPublishSeqNo);
System.out.println("生产者发送消息:"+message);
}while(!message.equals("bye")); // 当输入bye时结束输入,推出循环
事务机制和cnofirm机制最大的不同在于,事务机制是同步的,你提交一个事务之后会阻塞在那儿,但是confirm机制是异步的,你发送个消息之后就可以发送下一个消息,然后那个消息rabbitmq接收了之后会异步回调你一个接口通知你这个消息接收到了。
所以一般在生产者这块避免数据丢失,都是用**confirm模式**的。
Broker弄丢了数据
为了防止rabbitmq自己弄丢了数据,必须开启rabbitmq的持久化,就是消息写入之后会持久化到磁盘,哪怕是rabbitmq自己挂了,恢复之后会自动读取之前存储的数据,一般数据不会丢。除非极其罕见的是,rabbitmq还没持久化,自己就挂了,可能导致少量数据会丢失的,但是这个概率较小。
设置持久化有两个步骤,第一个是创建queue的时候将其设置为持久化的,这样就可以保证rabbitmq持久化queue的元数据,但是不会持久化queue里的数据;第二个是发送消息的时候将消息的deliveryMode设置为2,就是将消息设置为持久化的,此时rabbitmq就会将消息持久化到磁盘上去。必须要同时设置这两个持久化才行,rabbitmq哪怕是挂了,再次重启,也会从磁盘上重启恢复queue以及恢复这个queue里的数据。
而且持久化可以跟生产者那边的confirm机制配合起来,只有消息被持久化到磁盘之后,才会通知生产者ack了,所以哪怕是在持久化到磁盘之前,rabbitmq挂了,数据丢了,生产者收不到ack,你也是可以自己重发的。
队列持久化
/**
* 声明队列,如果Rabbit中没有此队列将自动创建
* param1:队列名称
* param2:是否持久化
* param3:队列是否独占此连接
* param4:队列不再使用时是否自动删除此队列
* param5:队列参数
* String common, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments
*/
channel.queueDeclare(QUEUE_NAME,true,false,false,null);
消息持久化
/**
* 消息发布方法
* param1:Exchange的名称,如果没有指定,则使用Default Exchange
* param2:routingKey,消息的路由Key,是用于Exchange(交换机)将消息转发到指定的消息队列
* param3:消息包含的属性,MessageProperties.PERSISTENT_BASIC实际上是一个设置了持久化的BasicProperties对象
* param4:消息体
*/
channel.basicPublish("",QUEUE_NAME, MessageProperties.PERSISTENT_BASIC,message.getBytes());
若生产者那边的confirm机制未开启的情况下,哪怕是你给rabbitmq开启了持久化机制,也有一种可能,就是这个消息写到了rabbitmq中,但是还没来得及持久化到磁盘上,结果不巧,此时rabbitmq挂了,就会导致内存里的一点点数据会丢失。
消费者弄丢了数据
主要是因为消费者消费的时候,刚消费到,还没处理,结果进程挂了,比如重启了,那么就尴尬了,rabbitmq认为消息已经被消费了,这数据就丢了。【在Broker端是完全说的过去的】
这个时候得用rabbitmq提供的ACK机制,简单来说,就是你关闭rabbitmq自动ACK,开启手动ACK,可以通过一个api来调用就行,然后每次你自己代码里确保处理完的时候,再程序里ACK一把。
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消费消费
String msg = new String(body,"utf-8");
System.out.println("consume msg: "+msg);
try {
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动消息确认
/**
* @param1 : 消息标识
* @param2 : 是否批量确认
*/
getChannel().basicAck(envelope.getDeliveryTag(),false);
}
};
/**
* 注册监听
* @param1 :接收哪个队列的数据
* @param2 :消息确认 是否应答,收到消息是否回复
* @param3 :消费者
*/
channel.basicConsume(QUEUE_NAME,false,consumer);
这样的话,如果你还没处理完,不就没有ack?那rabbitmq就认为你还没处理完,这个时候rabbitmq会把这个消费分配给别的consumer去处理,消息是不会丢的。
RabbitMQ解决重复消费
如上述场景,当消费端处理完成,打算返回ACK给Broker的时候,进程被干掉了,这就会导致Broker认为这条消息还没有被消费成功,重新派发消息给消费端去处理,这就是消息重复消费的一种原因
解决方案:保证消费者幂等性,重复消费也没有关系。
幂等性,通俗点说,就一个数据,或者一个请求,给你重复来多次,你得确保对应的数据是不会改变的,不能出错。
**
比如:一条数据重复出现两次,数据库里就只有一条数据,这就保证了系统的幂等性。
保证消费者消费的幂等性需要结合业务来分析:
- 比如你拿个数据要写库,你先根据主键查一下,如果这数据都有了,你就别插入了,update 一下好吧。
- 比如你是写 Redis,那没问题了,反正每次都是 set,天然幂等性。
比如你不是上面两个场景,那做的稍微复杂一点,你需要让生产者发送每条数据的时候,里面加一个全局唯一的 id,类似订单 id 之类的东西,然后你这里消费到了之后,先根据这个 id 去比如 Redis 里查一下,之前消费过吗?如果没有消费过,你就处理,然后这个 id 写Redis。如果消费过了,那你就别处理了,保证别重复处理相同的消息即可。
//定义消费者
DefaultConsumer consumer = new DefaultConsumer(channel){
// 监听消息
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
//消息
String msg = new String(body,"utf-8");
System.out.println("consume msg: "+msg);
// 前置判断是否已经消费过了
// consumedMap代表在redis缓存中存放的已消费的消息
Map<Long,Object> consumedMap = redisHelper.getMap("consumedMap");
if(consumedMap.containsKey(envelope.getDeliveryTag())){
// 如果已经消费过,啥都不做,直接确认
//手动消息确认
/**
* @param1 : 消息标识
* @param2 : 是否批量确认
*/
getChannel().basicAck(envelope.getDeliveryTag(),false);
}
try {
// 此处模拟业务处理
Thread.sleep(1000);
} catch (InterruptedException e) {
e.printStackTrace();
}
//手动消息确认
/**
* @param1 : 消息标识
* @param2 : 是否批量确认
*/
getChannel().basicAck(envelope.getDeliveryTag(),false);
// 加入已消费缓存
consumedMap.put(envelope.getDeliveryTag(),body);
}
};