目录
- [一、Work Queues (工作队列模式)](#一、Work Queues (工作队列模式))
-
- [1.1 生产者](#1.1 生产者)
- [1.2 消费者](#1.2 消费者)
- 二、Publish/Subscribe(发布/订阅)
-
- [2.1 生产者](#2.1 生产者)
- [2.2 消费者](#2.2 消费者)
- 三、Routing(路由模式)
-
- [3.1 生产者](#3.1 生产者)
- [3.2 消费者](#3.2 消费者)
- 四、Topics(通配符模式)
-
- [4.1 生产者](#4.1 生产者)
- [4.2 消费者](#4.2 消费者)
- 五、RPC通信
-
- [5.1 客户端](#5.1 客户端)
- [5.2 服务器](#5.2 服务器)
- [六、Publisher Confirms(发布确认)](#六、Publisher Confirms(发布确认))
-
- [6.1 Publishing Messages Individually(单独确认)](#6.1 Publishing Messages Individually(单独确认))
- [6.2 Publishing Messages in Batches(批量确认)](#6.2 Publishing Messages in Batches(批量确认))
- [6.3 Handling Publisher Confirms Asynchronously(异步确认)](#6.3 Handling Publisher Confirms Asynchronously(异步确认))

一、Work Queues (工作队列模式)
简单模式在这个系列第一个文章,上手程序就是一个Simple (简单模式)的实现。

⼯作队列模式⽀持多个消费者接收消息,消费者之间是竞争关系,每个消息只能被⼀个消费者接收。
每个工作模式的实现,都先需要引入RabbitMQ的依赖:
xml
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.7.3</version>
</dependency>
1.1 生产者
生产者:
- 创建连接
- 创建Channel
- 声明⼀个队列Queue
- 生产消息
- 释放资源
java
package org.example.rabbitmq.workqueues;
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 ProducerDemo {
public static void main(String[] args) throws IOException, TimeoutException {
//建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//开启信道
Channel channel = connection.createChannel();
//声明信道
channel.queueDeclare("workQueues",true,false,true,null);
//声明队列,发送消息
for(int i = 0; i < 10; i++) {
String msg = "hello workQueues"+i;
channel.basicPublish("","workQueues",null,msg.getBytes());
}
System.out.println("消息发送成功");
//资源释放
channel.close();
connection.close();
}
}
1.2 消费者
我们创建两个消费者模拟。
消费者:
- 创建连接
- 创建Channel
- 声明一个队列Queue
- 消费消息
- 释放资源,为了造成两个消费者竞争,我们先不释放资源。
java
package org.example.rabbitmq.workqueues;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("workQueues",false,false,true,null);
//消费消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息: " + new String(body));
}
};
channel.basicConsume("workQueues",true,consumer);
Thread.sleep(100);
////释放资源
//channel.close();
//connection.close();
}
}
效果:

二、Publish/Subscribe(发布/订阅)

在发布/订阅模型中,多了⼀个Exchange⻆⾊。
Exchange 常⻅有三种类型,分别代表不同的路由规则,也就分别对应不同的⼯作模式:
- Fanout:⼴播,将消息交给所有绑定到交换机的队列(Publish/Subscribe模式)
- Direct:定向,把消息交给符合指定routing key的队列(Routing模式)
- Topic:通配符,把消息交给符合routing pattern(路由模式)的队列(Topics模式)
2.1 生产者
生产者
- 创建连接
- 创建信道
- 声明交换机
- 声明两个队列
- 交换机与队列进行绑定
- 生产消息
- 释放资源
声明交换机的方法是Channel类下的exchangeDeclare方法

- exchange -- the name of the exchange,交换机名称
- type -- the exchange type ,交换机类型
- durable -- true if we are declaring a durable exchange (the exchange will survive a server restart),是否可持久化
- autoDelete -- true if the server should delete the exchange when it is no longer in use ,是否自动删除
- internal -- true if the exchange is internal, i.e. can't be directly published to by a client,是否内部使用,内部使用客户端发不进去消息
- arguments -- other properties (construction arguments) for the exchange,参数
java
package org.example.rabbitmq.fanout;
import com.rabbitmq.client.BuiltinExchangeType;
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 void main(String[] args) throws IOException, TimeoutException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建信道
Channel channel = connection.createChannel();
//3. 声明交换机
channel.exchangeDeclare("fanout.exchange", BuiltinExchangeType.FANOUT,false);
//4. 声明队列
channel.queueDeclare("fanout.queue1",true,false,false,null);
channel.queueDeclare("fanout.queue2",true,false,false,null);
//5.交换机与队列绑定
channel.queueBind("fanout.queue1","fanout.exchange","");
channel.queueBind("fanout.queue2","fanout.exchange","");
//6. 生产消息
for(int i = 0; i < 10; i++) {
String msg = "hello fanout"+i;
channel.basicPublish("fanout.exchange","",null,msg.getBytes());
}
System.out.println("发送消息成功");
//7. 释放资源
channel.close();
connection.close();
}
}
2.2 消费者
两个消费者分别消费两个队列的消息。
消费者:
- 创建连接
- 创建Channel
- 声明一个队列Queue
- 消费消息
- 释放资源。
java
package org.example.rabbitmq.fanout;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("fanout.queue1",true,false,false,null);
//消费消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息: " + new String(body));
}
};
channel.basicConsume("fanout.queue1",consumer);
/* //释放资源
channel.close();
connection.close();*/
}
}
结果:

三、Routing(路由模式)

Routing(路由模式):
队列和交换机的绑定,不能是任意的绑定了,⽽是要指定⼀个BindingKey (RoutingKey的⼀种),消息的发送⽅在向Exchange发送消息时,也需要指定消息的RoutingKey,Exchange也不再把消息交给每⼀个绑定的key,⽽是根据消息的RoutingKey进⾏判断,只有队列绑定时的BindingKey和发送消息的RoutingKey 完全⼀致,才会接收到消息。
3.1 生产者
生产者:
- 创建连接
- 创建信道
- 声明交换机,类型为direct
- 声明队列
- 队列与交换机绑定,绑定的时候加上BindingKey参数
- 生产消息,消息发送的时候指定routingKey
- 释放资源
java
package org.example.rabbitmq.direct;
import com.rabbitmq.client.BuiltinExchangeType;
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 void main(String[] args) throws IOException, TimeoutException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建信道
Channel channel = connection.createChannel();
//3. 声明交换机
channel.exchangeDeclare("direct.exchange", BuiltinExchangeType.DIRECT,true);
//4. 声明队列
channel.queueDeclare("direct.queue1",true,false,false,null);
channel.queueDeclare("direct.queue2",true,false,false,null);
//5. 队列与交换机绑定
channel.queueBind("direct.queue1","direct.exchange","a");
channel.queueBind("direct.queue2","direct.exchange","a");
channel.queueBind("direct.queue2","direct.exchange","b");
channel.queueBind("direct.queue2","direct.exchange","c");
//6. 生产消息
String msgA = "hello direct routingKey is a";
channel.basicPublish("direct.exchange","a",null,msgA.getBytes());
String msgB = "hello direct routingKey is b";
channel.basicPublish("direct.exchange","b",null,msgB.getBytes());
String msgC = "hello direct routingKey is c";
channel.basicPublish("direct.exchange","c",null,msgC.getBytes());
System.out.println("发送消息成功");
//7. 释放资源
channel.close();
connection.close();
}
}
3.2 消费者
两个消费者分别消费两个队列的消息。
消费者:
- 创建连接
- 创建Channel
- 声明一个队列Queue
- 消费消息
- 释放资源。
java
package org.example.rabbitmq.direct;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("direct.queue1",true,false,false,null);
//消费消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息: " + new String(body));
}
};
channel.basicConsume("direct.queue1",consumer);
/* //释放资源
channel.close();
connection.close();*/
}
}
结果:

四、Topics(通配符模式)

Topics 和 Routing 模式的区别是:
- topics 模式使⽤的交换机类型为topic(Routing模式使⽤的交换机类型为direct)
- topic 类型的交换机在匹配规则上进⾏了扩展,Binding Key⽀持通配符匹配(direct类型的交换机路由规则是BindingKey和RoutingKey完全匹配)。
在topic类型的交换机在匹配规则上,有些要求:
- RoutingKey 是⼀系列由点( . )分隔的单词,⽐如 " stock.usd.nyse "," nyse.vmw "," quick.orange.rabbit "
- BindingKey 和 RoutingKey⼀样,也是点( . )分割的字符串。
- Binding Key中可以存在两种特殊字符串,⽤于模糊匹配
3.1*表⽰⼀个单词
3.2#表⽰多个单词(0-N个)
4.1 生产者
生产者:
- 创建连接
- 创建信道
- 声明交换机,类型为topic
- 声明队列
- 队列与交换机绑定,绑定的时候加上BindingKey参数
- 生产消息,消息发送的时候指定routingKey
- 释放资源
java
package org.example.rabbitmq.topics;
import com.rabbitmq.client.BuiltinExchangeType;
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 void main(String[] args) throws IOException, TimeoutException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建信道
Channel channel = connection.createChannel();
//3. 声明交换机
channel.exchangeDeclare("topic.exchange", BuiltinExchangeType.TOPIC,true);
//4. 声明队列
channel.queueDeclare("topic.queue1",true,false,false,null);
channel.queueDeclare("topic.queue2",true,false,false,null);
//5. 队列与交换机绑定
channel.queueBind("topic.queue1","topic.exchange","*.a.*");
channel.queueBind("topic.queue2","topic.exchange","*.*.b");
channel.queueBind("topic.queue2","topic.exchange","c.#");
//6. 生产消息
String msgA = "hello topic routingKey is word.a.word";
channel.basicPublish("topic.exchange","word.a.word",null,msgA.getBytes());
String msgB = "hello topic routingKey is word.word.b";
channel.basicPublish("topic.exchange","word.word.b",null,msgB.getBytes());
String msgC = "hello topic routingKey is c.word.word.word.word.b";
channel.basicPublish("topic.exchange","c.word.word.word.word.b",null,msgC.getBytes());
String msgD = "hello topic routingKey is c.a.b";
channel.basicPublish("topic.exchange","c.a.b",null,msgD.getBytes());
System.out.println("发送消息成功");
//7. 释放资源
channel.close();
connection.close();
}
}
4.2 消费者
两个消费者分别消费两个队列的消息。
消费者:
- 创建连接
- 创建Channel
- 声明一个队列Queue
- 消费消息
- 释放资源。
java
package org.example.rabbitmq.topics;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//创建信道
Channel channel = connection.createChannel();
//声明队列
channel.queueDeclare("topic.queue1",true,false,false,null);
//消费消息
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Consumer1 接收到消息: " + new String(body));
}
};
channel.basicConsume("topic.queue1",consumer);
//释放资源
// channel.close();
// connection.close();
}
}
结果:

五、RPC通信

RPC(Remote Procedure Call),即远程过程调⽤。它是⼀种通过⽹络从远程计算机上请求服务,⽽不需要了解底层⽹络的技术。类似于Http远程调⽤。
RabbitMQ实现RPC通信的过程,⼤概是通过两个队列实现⼀个可回调的过程。
⼤概流程如下:
- 客⼾端发送消息到⼀个指定的队列,并在消息属性中设置 replyTo 字段,这个字段指定了⼀个回调队列,服务端处理后,会把响应结果发送到这个队列。
- 服务端接收到请求后,处理请求并发送响应消息到replyTo指定的回调队列
- 客⼾端在回调队列上等待响应消息,⼀旦收到响应,客⼾端会检查消息的 correlationId 属性,以确保它是所期望的响应。
5.1 客户端
客户端:
- 声明两个队列,包含回调队列 replyQueueName,声明本次请求的唯⼀标志 corrId
- 将 replyQueueName 和 corrId 配置到要发送的消息队列中
- 使⽤阻塞队列来阻塞当前进程,监听回调队列中的消息,把请求放到阻塞队列中
- 阻塞队列有消息后,主线程被唤醒,打印返回内容
java
package org.example.rabbitmq.rpc;
import com.rabbitmq.client.*;
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 Client {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare("rpc.request.queue",true,false,false,null);
channel.queueDeclare("rpc.response.queue",true,false,false,null);
//4. 发送消息
//设置请求标识
String correlationId = UUID.randomUUID().toString();
AMQP.BasicProperties properties = new AMQP.BasicProperties().builder()
.correlationId(correlationId)
.replyTo("rpc.response.queue")
.build();
String msg = "hello rpc";
channel.basicPublish("","rpc.request.queue", properties,msg.getBytes());
//5. 接收响应
//使用阻塞队列存储响应
final BlockingQueue<String> response = new ArrayBlockingQueue<>(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Client 接收到消息: " + new String(body));
//判断唯⼀标识正确, 放到阻塞队列中
if(correlationId.equals(properties.getCorrelationId())) {
response.offer(new String(body));
}
System.out.println("Client 接收到消息: " + new String(body));
}
};
channel.basicConsume("rpc.response.queue",true,consumer);
// 获取响应的结果
String result = response.take();
System.out.println(" [RPC_Client] Result:" + result);
}
}
5.2 服务器
服务器:
- 接收消息
- 根据消息内容进⾏响应处理,把应答结果返回到回调队列中
java
package org.example.rabbitmq.rpc;
import com.rabbitmq.client.*;
import java.io.IOException;
import java.util.concurrent.TimeoutException;
public class Server {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost("101.43.47.137");//ip地址
connectionFactory.setPort(5672);//默认端口号
connectionFactory.setUsername("study");//用户
connectionFactory.setPassword("study");//用户密码
connectionFactory.setVirtualHost("study");//虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建信道
Channel channel = connection.createChannel();
//接收请求
//每次接受一条
channel.basicQos(1);
DefaultConsumer consumer = new DefaultConsumer(channel) {
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
System.out.println("Server 接收到请求: " + new String(body));
//响应请求
String response = "响应";
AMQP.BasicProperties props = new AMQP.BasicProperties().builder()
.correlationId(properties.getCorrelationId())
.replyTo("rpc.request.queue")
.build();
channel.basicPublish("","rpc.response.queue",props,response.getBytes());
//手动确认
channel.basicAck(envelope.getDeliveryTag(),false);
}
};
channel.basicConsume("rpc.request.queue",false,consumer);
}
}
结果:

六、Publisher Confirms(发布确认)
消息中间件都会有消息丢失的问题发生,大概分为以下三种丢失情况:
- 生产者问题:因为应用故障,网络等问题,生产者没有成功向消息中间件发送消息;
- 消息中间件问题:生产者成功发送了消息,消息中间件自己原因导致消息丢失;
- 消费者问题:消费者消费消息时,处理出现问题,导致消费者 消费失败的 消息 从消息队列中删除了。
RabbitMQ针对上面三种情况给出的解决方案:
- 针对生产者问题:采取Publisher Confirms(发布确认)机制 解决;
- 针对消息中间件问题:通过持久化机制解决;
- 针对消费者问题:通过消息应答机制解决;

生产者将信道设置成 confirm(确认)模式,一旦信道进入 confirm 模式,所有在该信道上面发布的消息都会被指派一个唯一的ID(从1开始);
一旦消息被投递到所有匹配的队列之后,RabbitMO就会发送一个确认给生产者(包含消息的唯一ID),这就使得生产者知道消息已经正确到达目的队列了;
如果消息和队列是可持久化的,那么确认消息会在将消息写入磁盘之后发出.
brokerl(消息中间件)回传给生产者的确认消息中 deliveryTag 包含了确认消息的序号,
此外 broker 也可以设置 channel.basicAck() 方法中的 multiple 参数,表示到这个序号之前的所有消息都已经得到了处理.
6.1 Publishing Messages Individually(单独确认)
跟生产者发送消息,只有调用Channel类confirmSelect()设置信道为confirm模式,和Channel类waitForConfirmsOrDie()方法等待手动确认。
java
private static void publishingMessagesIndividually() throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()){
//设置信道为confirm模式
channel.confirmSelect();
//声明队列
channel.queueDeclare("publish.confirm.queue1",true,false,false,null);
long start = System.currentTimeMillis();
//发送消息
for (int i = 0; i < 200; i++) {
String msg = "Publishing Messages Individually " + i;
channel.basicPublish("","publish.confirm.queue1",null,msg.getBytes());
//等待确认
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.println("Publishing Messages Individually(单独确认) 发送200条消息耗时 "+ (end - start));
}
}
结果:

可以发现,发送200条消息,耗时很⻓。
观察上⾯代码,会发现这种策略是每发送⼀条消息后就调⽤ channel.waitForConfirmsOrDie() ⽅法,之后 等待服务端的确认,这实际上是⼀种串⾏同步等待的⽅式。
尤其对于持久化的消息来说,需要等待消息确认存储在磁盘之后才会返回(调⽤Linux内核的fsync⽅法)。
但是发布确认机制是⽀持异步的。可以⼀边发送消息,⼀边等待消息确认。
6.2 Publishing Messages in Batches(批量确认)
每发送⼀批消息后,调⽤ channel.waitForConfirms ⽅法,等待服务器的确认返回。
跟单独确认区别就是,发送到一定消息再进行等待确认。
java
private static void publishingMessagesInBatches()throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()){
//设置信道为confirm模式
channel.confirmSelect();
//声明队列
channel.queueDeclare("publish.confirm.queue2",true,false,false,null);
long start = System.currentTimeMillis();
//发送消息
int batchSize = 100;
int flag = 0;
for (int i = 0; i < 200; i++) {
String msg = "Publishing Messages in Batches " + i;
channel.basicPublish("","publish.confirm.queue2",null,msg.getBytes());
//批量 等待确认
if(flag == batchSize) {
channel.waitForConfirmsOrDie(5000);
flag = 0;
}
flag++;
}
if(flag > 0) {
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.println("Publishing Messages in Batches(批量确认) 发送200条消息耗时 "+ (end - start));
}
}
结果:

相⽐于单独确认策略,批量确认极⼤地提升了confirm的效率,
缺点是出现Basic.Nack或者超时时,我们不清楚具体哪条消息出了问题。客⼾端需要将这⼀批次的消息全部重发,这会带来明显的重复消息数量。
当消息经常丢失时,批量确认的性能应该是不升反降的。
6.3 Handling Publisher Confirms Asynchronously(异步确认)
提供⼀个回调⽅法,服务端确认了⼀条或者多条消息后客⼾端会回这个⽅法进⾏处理。
异步confirm⽅法的编程实现最为复杂。Channel 接⼝提供了⼀个⽅法 addConfirmListener,这个⽅法可以添加ConfirmListener 回调接⼝。
ConfirmListener 接⼝中包含两个⽅法:handleAck(long deliveryTag, boolean multiple) 和 handleNack(long deliveryTag, boolean multiple) ,分别对应处理RabbitMQ发送给⽣产者的 ack 和 nack。
deliveryTag 表⽰发送消息的序号,multiple 表⽰是否批量确认。我们需要为每⼀个Channel 维护⼀个已发送消息的序号集合。
当收到RabbitMQ的confirm 回调时,从集合中删除对应的消息。当Channel开启confirm模式后,channel上发送消息都会附带⼀个从1开始递增的deliveryTag序号。
我们可以使⽤SortedSet 的有序性来维护这个已发消息的集合。
- 当收到ack时,从序列中删除该消息的序号。如果为批量确认消息,表⽰⼩于等于当前序号deliveryTag的消息都收到了,则清除对应集合
- 当收到nack时,处理逻辑类似,不过需要结合具体的业务情况,进⾏消息重发等操作。
java
private static void handlingPublisherConfirmsAsynchronously() throws IOException, TimeoutException, InterruptedException {
try (Channel channel = createChannel()){
//设置信道为confirm模式
channel.confirmSelect();
//声明队列
channel.queueDeclare("publish.confirm.queue3",true,false,false,null);
//有序集合,元素按照⾃然顺序进⾏排序,存储未confirm消息序号
SortedSet<Long> confirmSet = Collections.synchronizedSortedSet(new TreeSet<>());
long start = System.currentTimeMillis();
//监听
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
if(multiple) {
confirmSet.headSet(deliveryTag+1).clear();
}else { //单独确认
confirmSet.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
//批量确认:将集合中⼩于等于当前序号deliveryTag元素的集合清除,表⽰这批序号的消息都已经被ack了
if(multiple) {
confirmSet.headSet(deliveryTag+1).clear();
}else { //单独确认
confirmSet.remove(deliveryTag);
}
//根据业务处理
}
});
//发送消息
int flag = 0;
for (int i = 0; i < 200; i++) {
String msg = "Handling Publisher Confirms Asynchronously " + i;
//得到下次发送消息的序号, 从1开始
long nextPublishSeqNo = channel.getNextPublishSeqNo();
channel.basicPublish("","publish.confirm.queue3",null,msg.getBytes());
//存入集合
confirmSet.add(nextPublishSeqNo);
}
while(confirmSet.isEmpty()) {
Thread.sleep(20);
}
long end = System.currentTimeMillis();
System.out.println("Handling Publisher Confirms Asynchronously(异步确认) 发送200条消息耗时 "+ (end - start));
}
}
结果:
