名词介绍
应用场景
#!/usr/bin/sh
sudo apt-get install curl gnupg apt-transport-https -y
## Team RabbitMQ's main signing key
curl -1sLf "https://keys.openpgp.org/vks/v1/by-fingerprint/0A9AF2115F4687BD29803A206B73A36E6026DFCA" | sudo gpg --dearmor | sudo tee /usr/share/keyrings/com.rabbitmq.team.gpg > /dev/null
## Cloudsmith: modern Erlang repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/gpg.E495BB49CC4BBE5B.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg > /dev/null
## Cloudsmith: RabbitMQ repository
curl -1sLf https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/gpg.9F4587F226208342.key | sudo gpg --dearmor | sudo tee /usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg > /dev/null
## Add apt repositories maintained by Team RabbitMQ
sudo tee /etc/apt/sources.list.d/rabbitmq.list <<EOF
## Provides modern Erlang/OTP releases
##
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu focal main
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.E495BB49CC4BBE5B.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-erlang/deb/ubuntu focal main
## Provides RabbitMQ
##
deb [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu focal main
deb-src [signed-by=/usr/share/keyrings/io.cloudsmith.rabbitmq.9F4587F226208342.gpg] https://dl.cloudsmith.io/public/rabbitmq/rabbitmq-server/deb/ubuntu focal main
EOF
## Update package indices
sudo apt-get update -y
## Install Erlang packages
sudo apt-get install -y erlang-base \
erlang-asn1 erlang-crypto erlang-eldap erlang-ftp erlang-inets \
erlang-mnesia erlang-os-mon erlang-parsetools erlang-public-key \
erlang-runtime-tools erlang-snmp erlang-ssl \
erlang-syntax-tools erlang-tftp erlang-tools erlang-xmerl
## Install rabbitmq-server and its dependencies
sudo apt-get install rabbitmq-server -y --fix-missing
#! /root/bin
# 安装erlang
curl -s https://packagecloud.io/install/repositories/rabbitmq/erlang/script.rpm.sh | sudo bash
yum install -y erlang
# 安装rabbitmq之前导入相关依赖
rpm --import https://packagecloud.io/rabbitmq/rabbitmq-server/gpgkey
rpm --import https://packagecloud.io/gpg.key
curl -s https://packagecloud.io/install/repositories/rabbitmq/rabbitmq-server/script.rpm.sh | sudo bash
# 下载rabbitmq的安装包
# wget https://github.com/rabbitmq/rabbitmq-server/releases/download/v3.8.5/rabbitmq-server-3.8.5-1.el7.noarch.rpm
# 正式安装之前导入相关依赖
rpm --import https://www.rabbitmq.com/rabbitmq-release-signing-key.asc
yum -y install epel-release
yum -y install socat
# 安装rabbimq的rpm包
rpm -ivh /opt/packages/rabbitmq-server-3.8.5-1.el7.noarch.rpm
开启管理页面插件
rabbitmq-plugins enable rabbitmq_management
启动rabbitmq服务
#同时开启erlang服务
systemctl start rabbitmq-server
#开启rabbitmq应用
rabbitmqctl start_app
停止rabbitmq服务
#同时停止erlang服务
systemctl stop rabbitmq-server
#停止rabbitmq应用
rabbitmqctl stop_app
查看当前rabbitmq服务的状态
systemctl status rabbitmq-server
默认的guest用户无法访问管理页面,需要创建新的用户并赋予管理员权限
# 创建新的用户并授予所有权限
rabbitmqctl add_user admin admin
rabbitmqctl set_user_tags admin administrator
rabbitmqctl set_permissions -p / admin "." "." ".*"
重启rabbitmq服务
systemctl restart rabbitmq-server
单生产者及单消费者简单实现
生产者代码示例
package com.lzx.hello_rabbitmq.producer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Producer {
//声明消息队列的名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) {
//构建连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("workstation");
factory.setUsername("root");
factory.setPassword("521520");
try {
//获取连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
String message = "hello, rabbitmq";
//发布消息
channel.basicPublish("",QUEUE_NAME,null,message.getBytes());
System.out.println("消息发送完毕");
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
}
消费者代码示例
package com.lzx.hello_rabbitmq.consumer;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import com.rabbitmq.client.DeliverCallback;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer {
//声明消息队列名称
public static final String QUEUE_NAME="hello";
public static void main(String[] args) throws IOException, TimeoutException {
//构造连接工厂
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("workstation");
factory.setUsername("root");
factory.setPassword("521520");
//获取连接
Connection connection = factory.newConnection();
//获取信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//声明消息回调函数,该回调将缓冲消息直到消费者有时间消费它
DeliverCallback deliverCallback = (consumerTag,message)->{
System.out.println("消费消息成功,消息体为:"+new String(message.getBody()));
};
//消费者消费消息
channel.basicConsume(QUEUE_NAME,true,deliverCallback,consumerTag ->{
System.out.println("消费失败");
});
}
}
单生产者,多消费者代码实例
封装工具类用于获取信道
package com.lzx.utils;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class RabbitMqUtils {
private static ConnectionFactory factory;
private static Connection connection;
static {
factory = new ConnectionFactory();
factory.setHost("workstation");
factory.setUsername("root");
factory.setPassword("521520");
try {
connection=factory.newConnection();
} catch (IOException e) {
e.printStackTrace();
} catch (TimeoutException e) {
e.printStackTrace();
}
}
public static Channel getChannel() throws IOException {
if(connection!=null){
return connection.createChannel();
}
return null;
}
}
生产者代码实现
package com.lzx.hello_rabbitmq.producer;
import com.lzx.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
import java.util.Scanner;
/**
* 用于演示轮询消费
*/
public class Task01 {
private static final String QUEUE_NAME="work_queue";
public static void main(String[] args) throws IOException {
//获取连接信道
Channel channel = RabbitMqUtils.getChannel();
//声明消息队列
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
//从控制台输入要发送的消息
Scanner scanner = new Scanner(System.in);
while(scanner.hasNext()){
String message = scanner.next();
//发送消息到指定工作队列
channel.basicPublish("",QUEUE_NAME,null,message.getBytes(StandardCharsets.UTF_8));
System.out.println("发送消息:"+message + "成功");
}
}
}
消费者代码实现,通过开启允许多实例运行实现多个消费者
package com.lzx.hello_rabbitmq.consumer;
import com.lzx.utils.RabbitMqUtils;
import com.rabbitmq.client.Channel;
import java.io.IOException;
public class Work01 {
private static final String QUEUE_NAME = "work_queue";
public static void main(String[] args) throws IOException {
Channel channel = RabbitMqUtils.getChannel();
channel.queueDeclare(QUEUE_NAME,false,false,false,null);
System.out.println("工作线程work02准备接收任务");
channel.basicConsume(QUEUE_NAME,true,((consumerTag, message) -> {
System.out.println("接收到消息"+new String(message.getBody()));
}),consumerTag -> {
System.out.println("取消消费");
});
}
}
完成一项任务可能只需要几秒钟。您可能想知道,如果一个消费者开始了一项很长的任务,而任务只完成了一部分就结束了,会发生什么情况。在我们之前的代码中,一旦RabbitMQ将一条消息发送给消费者,它就会立即标记为删除。在这种情况下,如果工作线程死亡,我们将丢失它正在处理的消息。我们还会丢失所有已发送给这个特定工作线程但尚未处理的消息。
为了确保消息不会丢失,RabbitMQ支持消息确认。消费者会返回一个确认信息,告诉RabbitMQ一个特定的消息已经被接收、处理,并且RabbitMQ可以随意删除它。
手动应答
void basicAck(long deliveryTag, boolean multiple) throws IOException;
注:
deliveryTag 表示消息标记,通过调用 delivery.getEnvelope().getDeliveryTag()得到
boolean multiple 用于批量应答
true表示应答该消息之前的所有消息,false表示只应答当前的消息
void basicNack(long deliveryTag, boolean multiple, boolean requeue)
throws IOException;
void basicReject(long deliveryTag, boolean requeue) throws IOException;
手动肯定确认代码实现
DeliverCallback deliverCallback =(consumerTag, message) -> {
try {
Thread.sleep(10000);
} catch (InterruptedException e) {
e.printStackTrace();
}
System.out.println("接收到消息"+new String(message.getBody()));
//处理完消息以后进行手动应答
channel.basicAck(message.getEnvelope().getDeliveryTag(),false);
}
消息自动重新入队:如果一个消费者在没有发送ack的情况下死亡(通道被关闭,连接被关闭,或者TCP连接丢失),RabbitMQ会认为消息没有被完全处理,并将其重新排队。如果有其他消费者同时在线,它会迅速将其重新交付给另一个消费者,这样就可以确保没有消息丢失。
当RabbitMQ退出或崩溃时,它会忘记队列和消息,除非你告诉它不要这样做。要确保消息不会丢失,需要做两件事:将队列和消息都标记为持久的。
队列持久化
boolean durable = true;
channel.queueDeclare("durable_queue", durable, false, false, null);
消息持久化
import com.rabbitmq.client.MessageProperties;
channel.basicPublish("", "durable_queue",
MessageProperties.PERSISTENT_TEXT_PLAIN,
message.getBytes());
注:将消息标记为持久性并不完全保证消息不会丢失。虽然它告诉RabbitMQ将消息保存到磁盘,但当RabbitMQ接受了消息但还没有保存它时,仍然会有一个很短的时间窗口。此外,RabbitMQ不会对每条消息进行刷盘——它可能只是保存在缓存中,而不是真正写入磁盘。持久性保证并不强,但对于简单任务队列来说已经足够了。如果你需要更强大的保证,需要使用发布确认。
注意到RabbitMQ的调度仍然不完全像我们希望的那样工作。例如,在有两个消费者的情况下,当所有奇数消息都很重,偶数消息都很轻时,一个消费者将一直很忙,而另一个几乎不做任何工作。然而RabbitMQ对此一无所知,仍然会均匀地分发消息。这是因为RabbitMQ只在消息进入队列时分派消息。它不为消费者查看未确认消息的数量。它只是盲目地将第n个消息发送给第n个消费者。
为了解决上面这个问题,我们可以使用basicQos方法设置prefetchCount = 1。这告诉RabbitMQ一次不要给一个worker发送多条消息。或者,换句话说,在worker处理并确认了前一条消息之前,不要向它发送一条新消息。相反,它将把它分配给下一个不忙碌的工作者。
int prefetchCount = 1;
channel.basicQos(prefetchCount);
注1:prefetchCount 预取值,该值表示未确认的消息的最大数量,未确认的消息存放在一个未确认的消息缓冲区内,当未确认的消息达到预取值时,RabbitMQ将停止在channel上传递更多的消息。
注2:当所有的消费者线程都处于忙碌状态时,新进来的消息将会存放到消息队列中,但是消息队列的长度是有限的,当消息队列满时,你需要考虑增加消费者或者采取其他的有效措施。
交换机是一个非常简单的东西,生产者只能向交换机发送消息。交换机一方面接收来自生产者的消息,另一方面将消息推送到队列中。交换器必须确切地知道如何处理接收到的消息。例如:消息应该被追加到一个特定的队列吗?消息应该被追加到多个队列吗?或者消息应该被丢弃吗?这些实现由交换机的种类进行决定。
[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传(img-CV91ggli-1652692910467)(https://gitee.com/lzx143421/figures/raw/master/img/image-20220515102122560.png)]
交换机的种类
channel.exchangeDeclare("logs", "fanout");
注:之前我们发布消息的时候使用的是用空字符串(“”)进行标识的默认交换机或是无名交换机,消息能路由发送到队列中其实是由routingKey(bindingkey)绑定key指定的,如果它存在的话。
具有随机名称,并且当断开连接时能够自动删除的队列。
通过如下命令可以生成一个具有随机名称的非持久化,排他的,自动删除的队列
String queueName = channel.queueDeclare().getQueue();
一个消费者将日志信息打印到控制台,另一个消费者将日志信息输出到磁盘
生产者代码实现
public class Producer {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
//声明名为logs,类型为fanout的交换机
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
Scanner sc = new Scanner(System.in);
System.out.println("请输入信息");
//控制台输入模拟日志信息
while (sc.hasNext()) {
String message = sc.nextLine();
channel.basicPublish(EXCHANGE_NAME, "", null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息" + message); }
}
}
}
消费者代码实现
public class LogConsumer1 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取随机队列
String queueName = channel.queueDeclare().getQueue();
//将随机队列与Logs交换机绑定
channel.queueBind(queueName,EXCHANGE_NAME,"")
System.out.println("等待接收消息,把接收到的消息打印在屏幕.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("控制台打印接收到的消息"+message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
public class LogConsumer2 {
private static final String EXCHANGE_NAME = "logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "fanout");
//获取随机队列
String queueName = channel.queueDeclare().getQueue();
//将队列与logs交换机绑定
channel.queueBind(queueName,EXCHANGE_NAME,"")
System.out.println("等待接收消息,把接收到的消息写到文件.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
//将日志信息写入到磁盘
String message = new String(delivery.getBody(), "UTF-8");
File file = new File("D:\\rabbitmq_info.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("数据写入文件成功");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
注1:binding key 根据交换机类型的不同有不一样的意义,对fanout类型的交换机而言,它将忽略 binding key
注2:routing key 消息发送时的路由key,binding key 队列与交换机绑定时的key,表示队列只对来自该交换机的routing key == binding key的消息感兴趣
将消息发送到消息的routing key与其绑定的队列的 binding key一致的队列中
channel.exchangeDeclare(EXCHANGE_NAME, "direct");
日志发送者代码实现
public class Producer {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
//创建多个routing_key
Map<String, String> bindingKeyMap = new HashMap<>();
bindingKeyMap.put("info","普通info信息");
bindingKeyMap.put("warning","警告warning信息");
bindingKeyMap.put("error","错误error信息");
//debug没有消费这接收这个消息 所有就丢失了
bindingKeyMap.put("debug","调试debug信息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
}
日志接收者代码实现
public class LogConsumer1 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "disk";
channel.queueDeclare(queueName, false, false, false, null);
//绑定error消息,只接受error日志
channel.queueBind(queueName, EXCHANGE_NAME, "error");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message;
File file = new File("D:\\rabbitmq_error.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("错误日志已经接收");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
public class LogConsumer2 {
private static final String EXCHANGE_NAME = "direct_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, BuiltinExchangeType.DIRECT);
String queueName = "console";
channel.queueDeclare(queueName, false, false, false, null);
//绑定info warning消息,将info及warning消息打印到控制台
channel.queueBind(queueName, EXCHANGE_NAME, "info");
channel.queueBind(queueName, EXCHANGE_NAME, "warning");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
topic exchange
的消息不能使用任意的 routing_key
,它必须是由点分隔的单词列表。这些词可以是任何东西,但通常它们指定了与消息相关的一些特征。例如 “quick.orange.rabbit”,“stock.usd.nyse”。routing_key
中可以有任意多的单词,最大限制为255字节。routing_key
中两个特殊的符号
注1:当路由键只有“#”号时,此时的topic交换机与fanout交换机一致
注2:当“*”和“#”号都没有使用时,此时的topic交换机和direct交换机一致
public class Producer {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
try (Channel channel = RabbitMqUtils.getChannel()) {
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
//创建多个routing_key,包含来自认证和来自内核的消息
Map<String, String> bindingKeyMap = new HashMap<>();
//来自用户认证的日志消息
bindingKeyMap.put("auth.info","用户的普通info信息");
bindingKeyMap.put("auth.warning","用户的警告warning信息");
bindingKeyMap.put("auth.error","用户的错误error信息");
bindingKeyMap.put("auth.debug","用户的调试debug信息");
//来自内核的日志消息
bindingKeyMap.put("kern.info","内核的普通info信息");
bindingKeyMap.put("kern.warning","内核的警告warning信息");
bindingKeyMap.put("kern.error","内核的错误error信息");
bindingKeyMap.put("kern.debug","内核的调试debug信息");
for (Map.Entry<String, String> bindingKeyEntry: bindingKeyMap.entrySet()){
String bindingKey = bindingKeyEntry.getKey();
String message = bindingKeyEntry.getValue();
channel.basicPublish(EXCHANGE_NAME,bindingKey, null, message.getBytes("UTF-8"));
System.out.println("生产者发出消息:" + message);
}
}
}
}
/**
*用于记录用户认证和内核的错误信息,将错误信息输出到磁盘
*/
public class LogConsumer1 {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String queueName = "disk";
channel.queueDeclare(queueName, false, false, false, null);
//绑定error消息,接受来自不同主体的error日志
channel.queueBind(queueName, EXCHANGE_NAME, "*.error");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
message="接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message;
File file = new File("D:\\rabbitmq_error.txt");
FileUtils.writeStringToFile(file,message,"UTF-8");
System.out.println("错误日志已经接收");
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
}
/**
*用于将用户认证的所有日志信息打印到工作台
*/
public class LogConsumer2 {
private static final String EXCHANGE_NAME = "topic_logs";
public static void main(String[] argv) throws Exception {
Channel channel = RabbitMqUtils.getChannel();
channel.exchangeDeclare(EXCHANGE_NAME, "topic");
String queueName = "console";
channel.queueDeclare(queueName, false, false, false, null);
//绑定用户认证的所有消息
channel.queueBind(queueName, EXCHANGE_NAME, "auth.*");
System.out.println("等待接收消息.....");
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
String message = new String(delivery.getBody(), "UTF-8");
System.out.println("接收绑定键:"+delivery.getEnvelope().getRoutingKey()+",消息:"+message);
};
channel.basicConsume(queueName, true, deliverCallback, consumerTag -> { });
}
远程调用:客户端发起一个耗时请求,放到请求队列中,远程服务器从请求队列中取出任务进行执行,然后将执行的结果放到请求响应队列中,客户端检查响应队列中的结果是否与自己发送的请求匹配,若匹配则取出结果,否则就忽略这个未知结果。
尽管RPC在计算中是一个相当常见的模式,但它经常受到批评。当程序员不知道函数调用是本地的还是缓慢的RPC时,问题就会出现。这样的混淆会导致不可预测的系统,并为调试增加不必要的复杂性。误用RPC不会简化软件,反而会导致不可维护的意大利面条式代码。
考虑到这一点,请考虑下列建议:
确保哪个函数调用是本地的,哪个调用是远程的。
记录你的系统。明确组件之间的依赖关系。
处理错误情况。当RPC服务器长时间关闭时,客户端应该如何反应?
回调队列:为了接收响应,我们需要在请求中发送一个’callback’队列地址。
callbackQueueName = channel.queueDeclare().getQueue();
BasicProperties props = new BasicProperties
.Builder()
.replyTo(callbackQueueName)
.build();
//replyTo 用于命名回调队列
channel.basicPublish("", "rpc_queue", props, message.getBytes());
correlation Id:我们可以为每个客户端建立一个回调队列,然后利用correlation Id将请求与响应相互关联。
使用远程调用实现计算Fibonacci值
RPC客户端代码
import com.rabbitmq.client.AMQP;
import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;
import java.io.IOException;
import java.util.UUID;
import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.BlockingQueue;
import java.util.concurrent.TimeoutException;
public class RPCClient implements AutoCloseable {
private Connection connection;
private Channel channel;
private String requestQueueName = "rpc_queue";
public RPCClient() throws IOException, TimeoutException {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
connection = factory.newConnection();
channel = connection.createChannel();
}
public static void main(String[] argv) {
try (RPCClient fibonacciRpc = new RPCClient()) {
for (int i = 0; i < 32; i++) {
String i_str = Integer.toString(i);
System.out.println(" [x] Requesting fib(" + i_str + ")");
String response = fibonacciRpc.call(i_str);
System.out.println(" [.] Got '" + response + "'");
}
} catch (IOException | TimeoutException | InterruptedException e) {
e.printStackTrace();
}
}
public String call(String message) throws IOException, InterruptedException {
final String corrId = UUID.randomUUID().toString();
String replyQueueName = channel.queueDeclare().getQueue();
AMQP.BasicProperties props = new AMQP.BasicProperties
.Builder()
.correlationId(corrId)
.replyTo(replyQueueName)
.build();
channel.basicPublish("", requestQueueName, props, message.getBytes("UTF-8"));
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
if (delivery.getProperties().getCorrelationId().equals(corrId)) {
response.offer(new String(delivery.getBody(), "UTF-8"));
}
}, consumerTag -> {
});
String result = response.take();
channel.basicCancel(ctag);
return result;
}
public void close() throws IOException {
connection.close();
}
}
RPC服务端代码
import com.rabbitmq.client.*;
public class RPCServer {
private static final String RPC_QUEUE_NAME = "rpc_queue";
private static int fib(int n) {
if (n == 0) return 0;
if (n == 1) return 1;
return fib(n - 1) + fib(n - 2);
}
public static void main(String[] argv) throws Exception {
ConnectionFactory factory = new ConnectionFactory();
factory.setHost("localhost");
try (Connection connection = factory.newConnection();
Channel channel = connection.createChannel()) {
channel.queueDeclare(RPC_QUEUE_NAME, false, false, false, null);
channel.queuePurge(RPC_QUEUE_NAME);
channel.basicQos(1);
System.out.println(" [x] Awaiting RPC requests");
Object monitor = new Object();
DeliverCallback deliverCallback = (consumerTag, delivery) -> {
AMQP.BasicProperties replyProps = new AMQP.BasicProperties
.Builder()
.correlationId(delivery.getProperties().getCorrelationId())
.build();
String response = "";
try {
String message = new String(delivery.getBody(), "UTF-8");
int n = Integer.parseInt(message);
System.out.println(" [.] fib(" + message + ")");
response += fib(n);
} catch (RuntimeException e) {
System.out.println(" [.] " + e.toString());
} finally {
channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps,
response.getBytes("UTF-8"));
channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
// RabbitMq consumer worker thread notifies the RPC server owner thread
synchronized (monitor) {
monitor.notify();
}
}
};
channel.basicConsume(RPC_QUEUE_NAME, false, deliverCallback, (consumerTag -> { }));
// Wait and be prepared to consume the message from RPC client.
while (true) {
synchronized (monitor) {
try {
monitor.wait();
} catch (InterruptedException e) {
e.printStackTrace();
}
}
}
}
}
}
生产者通过调用信道的 confirmSelect()
方法开启发布确认功能,每个需要开启发布确认的信道只能调用一次该方法,一旦信道开启发布确认,所有在该信道上发布的消息都会唯一关联一个序列号(序列号从1开始),当消息被投递到相应的队列后,broker就会给生产者发送肯定确认,生产者就直到消息已经到达了正确的队列。
Channel channel = connection.createChannel();
channel.confirmSelect();
发布确认是异步的等待确认消息的,但在部分模式下需要使用基于异步通知的同步助手。
发布确认有三个不同的策略
waitForConfirmOrDie(timeout)
方法来等待确认,当消息在timeout时间内没有确认或者得到了否定确认时,该方法将会产生异常供客户端进行处理。注:在这种模式下,客户端实际上异步接收confirm并相应地解除对waitForConfirmsOrDie的调用的阻塞。可以将waitForConfirmsOrDie看作是一个依赖于异步通知的同步助手。
代码示例
public static void publishMessagesIndividually() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
//声明随机名称的队列
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
//开启发布确认
ch.confirmSelect();
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
ch.basicPublish("", queue, null, body.getBytes());
//等待confirm
ch.waitForConfirmsOrDie(5_000);
}
long end = System.nanoTime();
System.out.format("Published %,d messages individually in %,d ms%n", MESSAGE_COUNT,
Duration.ofNanos(end - start).toMillis());
}
}
与等待单个消息的确认相比,等待一批消息被确认大大提高了吞吐量。一个缺点是,在失败的情况下,我们不能确切地知道哪里出了问题,因此我们可能不得不在内存中保留整批消息来记录有意义的内容或重新发布消息。而且这个解决方案仍然是同步的,所以它阻止了消息的发布。
代码示例
/**
*批量确认(一次100条消息)
*/
public static void publishMessagesInBatch() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
//声明随机名称的队列
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
//开启发布确认
ch.confirmSelect();
//声明批量数以及未确认的消息数
int batchSize = 100;
int outstandingMessageCount = 0;
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
ch.basicPublish("", queue, null, body.getBytes());
outstandingMessageCount++;
//达到规定的批量之后等待确认
if (outstandingMessageCount == batchSize) {
ch.waitForConfirmsOrDie(5_000);
outstandingMessageCount = 0;
}
}
//消息发送完,但是批数不够100,但是大于0,等待确认
if (outstandingMessageCount > 0) {
ch.waitForConfirmsOrDie(5_000);
}
long end = System.nanoTime();
System.out.format("Published %,d messages in batch in %,d ms%n", MESSAGE_COUNT,
Duration.ofNanos(end - start).toMillis());
}
}
调用channel的 addConfirmListener()
方法注册确认监听器,使用两个回调函数来处理肯定确认和否定确认
Channel channel = connection.createChannel();
channel.confirmSelect();
channel.addConfirmListener((sequenceNumber, multiple) -> {
// code when message is confirmed
}, (sequenceNumber, multiple) -> {
// code when message is nack-ed
});
sequenceNumber:消息的序列号,可以同消息体进行关联,通过调用channel的getNextPublishSeqNo()方法可以获得将要发布的消息的序列号
int sequenceNumber = channel.getNextPublishSeqNo());
ch.basicPublish(exchange, queue, properties, body);
multiple:一个布尔值,false表示只处理当前sequenceNumber的消息,true表示处理小于等于sequenceNumber的消息
使用ConcurrentNavigableMap来跟踪未完成的确认。这种数据结构之所以方便,有几个原因。它允许轻松地将序列号与消息(无论消息数据是什么)关联起来,并轻可以松地把给定序列Id之前的消息进行清除(以处理多个confirm /nacks)。最后,它支持并发访问,因为确认回调是在客户端库所拥有的线程中调用的,应该与发布线程保持不同。
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
// ... code for confirm callbacks will come later
String body = "...";
outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
channel.basicPublish(exchange, queue, properties, body.getBytes());
异步发布确认示例
public static void handlePublishConfirmsAsynchronously() throws Exception {
try (Connection connection = createConnection()) {
Channel ch = connection.createChannel();
String queue = UUID.randomUUID().toString();
ch.queueDeclare(queue, false, false, true, null);
ch.confirmSelect();
//将消息id与消息体进行绑定
ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
//肯定确认消息的处理
ConfirmCallback cleanOutstandingConfirms = (sequenceNumber, multiple) -> {
if (multiple) {
//批量清除sequenceNumber之前的所有消息
ConcurrentNavigableMap<Long, String> confirmed =
outstandingConfirms.headMap(sequenceNumber, true);
confirmed.clear();
} else {
//清除单个消息
outstandingConfirms.remove(sequenceNumber);
}
};
ch.addConfirmListener(cleanOutstandingConfirms, (sequenceNumber, multiple) -> {
//以日志形式记录否定确认的消息
String body = outstandingConfirms.get(sequenceNumber);
System.err.format(
"Message with body %s has been nack-ed. Sequence number: %d, multiple: %b%n",
body, sequenceNumber, multiple
);
//记录之后交给肯定确认的回调函数进行处理,将否定确认的消息进行清除
cleanOutstandingConfirms.handle(sequenceNumber, multiple);
});
//发布消息并将消息id与消息体进行绑定
long start = System.nanoTime();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String body = String.valueOf(i);
outstandingConfirms.put(ch.getNextPublishSeqNo(), body);
ch.basicPublish("", queue, null, body.getBytes());
}
if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
throw new IllegalStateException("All messages could not be confirmed in 60 seconds");
}
long end = System.nanoTime();
System.out.format("Published %,d messages and handled confirms asynchronously in %,d ms%n",
MESSAGE_COUNT, Duration.ofNanos(end - start).toMillis());
}
}
public static boolean waitUntil(Duration timeout, BooleanSupplier condition) throws InterruptedException {
int waited = 0;
while (!condition.getAsBoolean() && waited < timeout.toMillis()) {
Thread.sleep(100L);
waited = +100;
}
return condition.getAsBoolean();
}
注:从相应的回调中重新发布一个否定确认的消息可能很诱人,但这应该避免,因为确认回调是在I/O线程中分派的,而通道不应该做操作。更好的解决方案是将消息放入由发布线程轮询的内存队列中。像ConcurrentLinkedQueue这样的类很适合在确认回调和发布线程之间传输消息。
配置信息
spring.rabbitmq.publisher-confirm-type=correlated
##NONE 禁用发布确认模式,默认值
##CORRELATED 发布消息成功到交换器后会触发回调方法
##SIMPLE
RabbitTemplate.ConfirmCallback 接口(交换机不管是否收到消息时的回调接口)
//交换机不管是否收到消息时的回调
rabbitTemplate.setConfirmCallback(myCallBack)
public class MyCallBack implements RabbitTemplate.ConfirmCallback{
@Override
public void confirm(CorrelationData correlationData,boolean ack,String cause){
//correlationData 表示消息相关的数据
//ack 表示交换机是否收到消息
//cause 交换机没有收到消息的原因
...
}
}
RabbitTemplate.ReturnCallback(交换机收到但是不能成功路由,即交换机不能将消息转发到消息队列时的回调接口)
//设置mandatory参数
/**
*true: 交换机无法路由时会将消息返回给生产者
*false:交换机无法路由消息时,直接丢弃消息
*/
rabbitTemplate.setMandatory(true);
rabbitTemplate.setReturnCallback(myCallback)
public class MyCallBack implements RabbitTemplate.ReturnCallback{
@Override
public void returnedMessage(Message message, int replyCode,String replyText,String exchange,String routingKey){
//message 被退回消息
//replyCode 退回代码
//replyText 退回原因
//exchange 退回交换机
//routingKey 消息路由key
...
}
}
备份交换机,当消息被交换机退回时,交换机会把这条消息转发到它的备份交换机中,再由备份交换机进行转发和处理,备份交换机的类型通常是"Fanout",这样就能把所有的消息投递到与其绑定的队列中。
//设置确认交换机的备份交换机
ExchangeBuilder exchangeBuilder =
ExchangeBuilder.directExchange("confirm.exchange").durable(true)
.withArgument("alternate-exchange","back.exchange");
FountExchange backExchange = new FanountExcahnge("back.exchange");
TTL(time to live)(存活时间)
设置消息过期时间的方式
使用策略声明(Define Message TTL for Queues Using a Policy)
rabbitmqctl set_policy TTL ".*" '{"message-ttl":60000}' --apply-to queues
创建队列时声明(Define Message TTL for Queues Using x-arguments During Declaration)
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-message-ttl", 60000);
channel.queueDeclare("myqueue", false, false, false, args);
发布消息时声明(Per-Message TTL in Publishers)
byte[] messageBodyBytes = "Hello, world!".getBytes();
AMQP.BasicProperties properties = new AMQP.BasicProperties.Builder()
.expiration("60000")
.build();
channel.basicPublish("my-exchange", "routing-key", properties, messageBodyBytes);
注:通过发布消息时为消息设置过期时间时,当消息过期后不一定被马上丢弃,只有当消息到队头时才会判断消息是否过期,如果队列消息积压则过期的消息也还能存活很长时间
设置队列的过期时间的方式
使用策略声明(Define Queue TTL for Queues Using a Policy)
rabbitmqctl set_policy expiry ".*" '{"expires":1800000}' --apply-to queues
声明队列时是使用x-args设置(Define Queue TTL for Queues Using x-arguments During Declaration)
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-expires", 1800000);
channel.queueDeclare("myqueue", false, false, false, args);
由于某些特定的原因导致消息无法被消费,需要重新发布到其它的交换机时,消息就成为了死信。
死信来源
消息TTL过期
队列到达最大长度,无法再添加数据到mq中
通过策略声明队列的最大长度
rabbitmqctl set_policy my-pol "^one-meg$" \
'{"max-length-bytes":1048576}' \
--apply-to queues
rabbitmqctl set_policy my-pol "^two-messages$" \
'{"max-length":2,"overflow":"reject-publish"}' \
--apply-to queues
通过队列的可选参数声明队列的最大长度
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-max-length", 10);
channel.queueDeclare("myqueue", false, false, false, args);
消息被消费者拒绝(basic.reject或者basic.nack)并且requeue=false
注:队列的过期不会对其中的消息造成死信。
成为死信的消息,需要由死信交换机(DLX)重新路由到死信队列进行消费。对于任何给定的队列,DLX可以由客户端使用队列的参数定义,也可以在服务器中使用策略定义。如果策略和参数都指定了一个DLX,则参数中指定的DLX会覆盖策略中指定的DLX。
使用策略的方式指定队列的死信交换机与死信路由键
rabbitmqctl set_policy DLX ".*" '{"dead-letter-exchange":"some.exchange.name"}' --apply-to queues
rabbitmqctl set_policy DLX ".*" '{"dead-letter-routing-key":"some-routing-key"}' --apply-to queues
使用队列的可选参数指定队列的死信交换机和死信路由键
channel.exchangeDeclare("dead-exchange", "direct");
Map<String, Object> args = new HashMap<String, Object>();
args.put("x-dead-letter-exchange", "some.exchange.name");
args.put("x-dead-letter-routing-key", "some-routing-key");
channel.queueDeclare("myqueue", false, false, false, args);
注:你需要指定死信消息的路由键。如果没有设置,则将使用消息本身的路由键。
用于存放需要在指定时间被处理的元素的队列,也可以理解为在某事件发生多久之后处理消息。(例如下单30分钟后查看订单状态)
实现延迟队列的方式
通过设置消息的TLL,使消息过期成为死信消息,然后由死信交换机转发给死信队列,消费者消费死信队列中的消息即可
注:当通过发布消息时为消息设置过期时间实现延迟队列时,当消息过期后不一定马上成为死信,只有当消息到队头时才会判断消息是否过期,如果队列消息积压则过期的消息也还能存活很长时间,因此若先入队的消息存活时间过长,后入队的消息存活时间短,可能实现的延迟队列不准确。
使用rabbitmq插件实现延迟队列(插件列表,延迟队列插件下载地址)
启用下载的插件
#将下载的插件文件放到/usr/lib/rabbitmq/lib/rabbitmq_server-3.10.1/plugins/ 目录下
mv rabbitmq_delayed_message_exchange-3.10.0.ez /usr/lib/rabbitmq/lib/rabbitmq_server-3.10.1/plugins/
#启用插件
rabbitmq-plugins enable rabbitmq_delayed_message_exchange
代码示例
package com.lzx.delay_queue.config;
import org.springframework.amqp.core.Binding;
import org.springframework.amqp.core.BindingBuilder;
import org.springframework.amqp.core.CustomExchange;
import org.springframework.amqp.core.Queue;
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;
@Configuration
public class DelayedQueueConfig {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Bean
public Queue delayedQueue() {
return new Queue(DELAYED_QUEUE_NAME);
}
//自定义交换机 我们在这里定义的是一个延迟交换机
@Bean("delayedExchange")
public CustomExchange delayedExchange() {
Map<String, Object> args = new HashMap<>();
//自定义延迟交换机的类型
args.put("x-delayed-type", "direct");
return new CustomExchange(DELAYED_EXCHANGE_NAME, "x-delayed-message", true, false, args);
}
@Bean
public Binding bindingDelayedQueue(@Qualifier("delayedQueue") Queue queue,
@Qualifier("delayedExchange") CustomExchange delayedExchange) {
return BindingBuilder.bind(queue).to(delayedExchange).with(DELAYED_ROUTING_KEY).noargs();
}
}
/**
*生产者代码
*/
package com.lzx.delay_queue.controller;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.rabbit.core.RabbitTemplate;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Date;
@Slf4j
@RequestMapping("ttl")
@RestController
public class SendMsgController {
public static final String DELAYED_EXCHANGE_NAME = "delayed.exchange";
public static final String DELAYED_ROUTING_KEY = "delayed.routingkey";
@Autowired
private RabbitTemplate rabbitTemplate;
@GetMapping("sendDelayMsg/{message}/{delayTime}")
public void sendMsg(@PathVariable String message, @PathVariable Integer delayTime) {
rabbitTemplate.convertAndSend(DELAYED_EXCHANGE_NAME, DELAYED_ROUTING_KEY, message, correlationData -> {
correlationData.getMessageProperties().setDelay(delayTime);
return correlationData;
});
log.info("当前时间:{},发送一条延迟{}毫秒的信息给队列delayed.queue:{}", new Date(),delayTime, message);
}
}
/**
*消费者代码
*/
package com.lzx.delay_queue.consummer;
import com.rabbitmq.client.Channel;
import lombok.extern.slf4j.Slf4j;
import org.springframework.amqp.core.Message;
import org.springframework.amqp.rabbit.annotation.RabbitListener;
import org.springframework.stereotype.Component;
import java.util.Date;
@Slf4j
@Component
public class DeadLetterQueueConsumer {
public static final String DELAYED_QUEUE_NAME = "delayed.queue";
@RabbitListener(queues = DELAYED_QUEUE_NAME)
public void receiveDelayedQueue(Message message) {
String msg = new String(message.getBody());
log.info("当前时间:{},收到延时队列的消息:{}", new Date().toString(), msg);
}
}
任何队列都可以使用客户端提供的可选参数转换为优先级队列。目前的实现支持有限数量的优先级:255。建议取值为1 ~ 10。
使用前提,只有满足以下条件才能对消息进行排序
队列需要设置为优先级队列
Map<String,Object> params = new HashMap<>();
params.put("x-max-priority",10);
channel.queueDeclare("priority_queue",true,false,false,params);
消息需要设置优先级
//
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
//
correlationData.getMessageProperties().setPriority(5);
消息已经发送到队列中
注意事项
从RabbitMQ 3.6.0开始,就有了惰性队列的概念——这些队列会尽可能早地将它们的内容移动到磁盘上,并且只在用户请求时才将它们加载到RAM中,因此有惰性的说法。
延迟队列的主要目标之一是能够支持非常长的队列(数百万条消息)。由于各种原因,队列可能会变得很长:
声明方式
Map<String,Object> args = new HashMap<String,Object>();
args.put("x-queue-mode","lazy");
channel.queueDeclare("lazy_queue",false,false,false,args);
注:如果在声明时通过可选参数设置队列模式,则只能通过删除队列并使用不同的参数重新声明来更改它
rabbitmqctl set_policy Lazy "^lazy-queue$" '{"queue-mode":"lazy"}' --apply-to queues
注:使用策略声明时可以在运行时修改队列的模式 rabbitmqctl set_policy Lazy “^lazy-queue$” ‘{“queue-mode”:“default”}’ --apply-to queues
任何队列都可以使用客户端提供的可选参数转换为优先级队列。目前的实现支持有限数量的优先级:255。建议取值为1 ~ 10。
使用前提,只有满足以下条件才能对消息进行排序
队列需要设置为优先级队列
Map<String,Object> params = new HashMap<>();
params.put("x-max-priority",10);
channel.queueDeclare("priority_queue",true,false,false,params);
消息需要设置优先级
//
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder().priority(5).build();
//
correlationData.getMessageProperties().setPriority(5);
消息已经发送到队列中
注意事项