简单模式
P代表生产者,C代表消费者,Queue代表消息队列.
特点:一个生产者,一个消费者,消息只能被消费一次,也称为点对点模式.
适用场景:消息只能被单个消费者处理.
代码案例
引入依赖
<dependency> <groupId>com.rabbitmq</groupId> <artifactId>amqp-client</artifactId> <version>5.20.0</version> </dependency>
编写生产者代码
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
//Constants为封装的常量类
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号,默认为5672
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明交换机 使用内置的交换机
//4. 声明队列
/**
* queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete,
* Map<String, Object> arguments)
* 参数说明:
* queue: 队列名称
* durable: 可持久化
* exclusive: 是否独占,只能有一个消费者监听队列
* autoDelete: 是否自动删除,当没有消费者时,自动删除掉
* arguments: 参数
*/
channel.queueDeclare("hello", true, false, false, null);
//5. 发送消息
/**
* basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* 参数说明:
* exchange: 交换机名称
* routingKey: 内置交换机, routingkey和队列名称保持一致
* props: 属性配置
* body: 消息
*/
for (int i = 0; i < 10; i++) {
String msg = "hello rabbitmq~"+i;
channel.basicPublish("","hello", null, msg.getBytes());
}
System.out.println("消息发送成功~");
//6. 资源释放
channel.close();
connection.close();
}
编写消费者代码
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1. 创建连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 创建Channel
Channel channel = connection.createChannel();
//3. 声明队列(可以省略)
channel.queueDeclare("hello",true, false, false, null);
//4. 消费消息
/**
* basicConsume(String queue, boolean autoAck, Consumer callback)
* 参数说明:
* queue: 队列名称
* autoAck: 是否自动确认
* callback: 接收到消息后, 执行的逻辑
*/
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));
}
};
channel.basicConsume("hello", true, consumer);
//等待程序执行完成
Thread.sleep(2000);
//5. 释放资源
channel.close();
connection.close();
}
工作队列模式
支持多个消费者接收消息,消费者之间是竞争关系,每个消息只能被一个消费者接收.
代码和简单模式相差不大,开启两个消费者服务即可.
发布订阅模式
发布订阅模式,需要手动创建交换机,而不在使用内置的交换机.
交换机有常见的三种类型,分别代表不同的路由规则
a)Fanout:⼴播,将消息交给所有绑定到交换机的队列(Publish/Subscribe模式,发布订阅模式)
b)Direct:定向,把消息交给符合指定routingkey的队列(Routing模式,路由模式)
c)Topic:通配符,把消息交给符合routing pattern(路由模式)的队列(Topics模式,通配符模式)
不同的交换机类型,也对应着不同的工作模式.
代码案例
生产者代码
/**
* 和前两个模式的区别:创建交换机并且绑定队列和交换机
*/
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明交换机
/**
* exchangeDeclare(String exchange, BuiltinExchangeType type, boolean durable, boolean autoDelete, boolean internal, Map<String, Object> arguments)
* exchange:交换机名称
* type:交换机类型,fanout,direct,topic,headers
* durable:是否持久化,true表示持久化,false表示不持久化
* autoDelete:是否自动删除,⾃动删除的前提是⾄少有⼀个队列或者交换器与这个交换器绑定, 之后所有与这个交换器绑定的
* 队列或者交换器都与此解绑.
* ⽽不是这种理解: 当与此交换器连接的客⼾端都断开时,RabbitMQ会⾃动删除本交换器.
* internal:是否内置,true表示内置,false表示非内置.
* 如果设置为true, 表⽰内部使⽤.
* 客⼾端程序⽆法直接发送消息到这个交换器中,只能通过交换器路由到交换器这种⽅式
* arguments:参数
*/
channel.exchangeDeclare(Constants.FANOUT_EXCHANGE, BuiltinExchangeType.FANOUT, true,false,false,null);
//4. 声明队列
//如果没有一个这样的队列,则自动创建;如果有,则不创建
/**
* queueDeclare(String queue, boolean durable, boolean exclusive, boolean autoDelete, Map<String, Object> arguments)
* queue:队列名称
* durable:是否持久化,true表示持久化,false表示不持久化
* exclusive:是否独占,true表示独占,false表示共享
* autoDelete:是否自动删除,true表示自动删除,false表示不自动删除
* arguments:参数,比如设置队列的过期时间,或者设置队列的最大长度等
*/
channel.queueDeclare(Constants.FANOUT_QUEUE1,true,false,false,null);
channel.queueDeclare(Constants.FANOUT_QUEUE2,true,false,false,null);
//5. 交换机和队列绑定
/**
* queueBind(String queue, String exchange, String routingKey)
* queue:队列名称
* exchange:交换机名称
* routingKey:路由规则(BindingKey)
* 如果为fanout,BindingKey设置为"",表示所有与交换机绑定的队列都能收到消息
*/
channel.queueBind(Constants.FANOUT_QUEUE1,Constants.FANOUT_EXCHANGE,"");
channel.queueBind(Constants.FANOUT_QUEUE2,Constants.FANOUT_EXCHANGE,"");
//6. 发布消息
String msg = "hello fanout....";
/**
* basicPublish(String exchange, String routingKey, BasicProperties props, byte[] body)
* exchange:交换机名称
* routingKey:路由键
* props:消息属性
* body:消息体
*/
channel.basicPublish(Constants.FANOUT_EXCHANGE,"", null, msg.getBytes());
System.out.println("消息发送成功");
//7. 释放资源
channel.close();
connection.close();
}
}
消费者代码
在确保生产者已经声明队列的前提下,消费者可以不用声明队列.
但是如果生产者还没有上线,而消费者也没有声明队列,直接进行消息的消费是会报错的.
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.FANOUT_QUEUE1,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.FANOUT_QUEUE1, true, consumer);
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.FANOUT_QUEUE2,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.FANOUT_QUEUE2, true, consumer);
}
}
路由模式
队列和交换机的绑定不再是任意的绑定了,而是要指定一个bindingKey.
消息在由发送方发送时,需要指定消息的routingKey.
在消息到达交换机之后,交换机会根据消息的routingKey进行判断,只有和队列绑定的bindingKey和消息的routingKey完全一致时,交换机才会将消息转发到指定的队列当中.
代码案例
生产者代码
和发布订阅模式不同的是:交换机的类型不同了,绑定队列的bindingKey也不同了.
/**
* 路由模式生产者
*/
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明交换机
channel.exchangeDeclare(Constants.DIRECT_EXCHANGE, BuiltinExchangeType.DIRECT, true);
//4. 声明队列
channel.queueDeclare(Constants.DIRECT_QUEUE1, true, false, false, null);
channel.queueDeclare(Constants.DIRECT_QUEUE2, true, false, false, null);
//5. 绑定交换机和队列
channel.queueBind(Constants.DIRECT_QUEUE1, Constants.DIRECT_EXCHANGE, "a");
channel.queueBind(Constants.DIRECT_QUEUE2, Constants.DIRECT_EXCHANGE, "a");
channel.queueBind(Constants.DIRECT_QUEUE2, Constants.DIRECT_EXCHANGE, "b");
channel.queueBind(Constants.DIRECT_QUEUE2, Constants.DIRECT_EXCHANGE, "c");
//6. 发送消息
String msg = "hello direct, my routingkey is a....";
channel.basicPublish(Constants.DIRECT_EXCHANGE,"a", null, msg.getBytes());
String msg_b = "hello direct, my routingkey is b....";
channel.basicPublish(Constants.DIRECT_EXCHANGE,"b", null, msg_b.getBytes());
String msg_c = "hello direct, my routingkey is c....";
channel.basicPublish(Constants.DIRECT_EXCHANGE,"c", null, msg_c.getBytes());
System.out.println("消息发送成功");
//7. 释放资源
channel.close();
connection.close();
}
}
可以在管理界面,看到交换机和队列的绑定关系.
队列中已经有消息了.符合预期.
消费者代码
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.DIRECT_QUEUE1,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.DIRECT_QUEUE1, true, consumer);
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.DIRECT_QUEUE2,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.DIRECT_QUEUE2, true, consumer);
}
}
运行结果
通配符模式
topic模式交换机类型为topic.
topic类型的交换机在匹配的规则上进行了扩展,bindingKey支持通配符的匹配.
匹配规则
- routingKey是一系列由点分隔的单词
- bindingKey和routingKey一样,也是点分隔的字符串
- bindingKey存在两种特殊的字符串,用于模糊匹配:*表示一个单词;#表示多个单词(0-N个)
生产者代码
/**
* 通配符模式生产者
*/
public class Producer {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明交换机
channel.exchangeDeclare(Constants.TOPIC_EXCHANGE, BuiltinExchangeType.TOPIC, true);
//4. 声明队列
channel.queueDeclare(Constants.TOPIC_QUEUE1, true, false, false, null);
channel.queueDeclare(Constants.TOPIC_QUEUE2, true, false, false, null);
//5. 绑定交换机和队列
channel.queueBind(Constants.TOPIC_QUEUE1, Constants.TOPIC_EXCHANGE, "*.a.*");
channel.queueBind(Constants.TOPIC_QUEUE2, Constants.TOPIC_EXCHANGE, "*.*.b");
channel.queueBind(Constants.TOPIC_QUEUE2, Constants.TOPIC_EXCHANGE, "c.#");
//6. 发送消息
String msg = "hello topic, my routingkey is ae.a.f....";
channel.basicPublish(Constants.TOPIC_EXCHANGE,"ae.a.f", null, msg.getBytes()); //转发到Q1
String msg_b = "hello topic, my routingkey is ef.a.b....";
channel.basicPublish(Constants.TOPIC_EXCHANGE,"ef.a.b", null, msg_b.getBytes()); //转发到Q1和Q2
String msg_c = "hello topic, my routingkey is c.ef.d....";
channel.basicPublish(Constants.TOPIC_EXCHANGE,"c.ef.d", null, msg_c.getBytes());//转发Q2
System.out.println("消息发送成功");
//7. 释放资源
channel.close();
connection.close();
}
}
运行代码,查看管理界面
消费者代码
public class Consumer1 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.TOPIC_QUEUE1,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.TOPIC_QUEUE1, true, consumer);
}
}
public class Consumer2 {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 声明队列
channel.queueDeclare(Constants.TOPIC_QUEUE2,true,false,false,null);
//4. 消费消息
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));
}
};
channel.basicConsume(Constants.TOPIC_QUEUE2, true, consumer);
}
}
运行代码:
RPC模式
RPC(Remote Procedure Call),即远程过程调⽤.它是⼀种通过⽹络从远程计算机上请求服务,⽽不需要了解底层⽹络的技术.类似于Http远程调⽤.
RabbitMQ实现RPC通信的过程,⼤概是通过两个队列实现⼀个可回调的过程.
大致流程
- 客户端发送消息到一个指定的队列,并在消息的属性中设置reply_to字段,这个字段指定了一个回调队列.服务端在处理完响应之后,会把响应结果发送到这个队列当中.
- 服务端收到请求,处理请求并把响应结果发送到replyTo指定的队列.
- 客户端在回调队列上等待响应的消息,一旦收到响应,客户端会检查消息的correlation_id属性,来确保它是和请求匹配的响应.
客户端代码
/**
* rpc 客户端
* 1. 发送请求
* 2. 接收响应
*/
public class RpcClient {
public static void main(String[] args) throws IOException, TimeoutException, InterruptedException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
channel.queueDeclare(Constants.RPC_REQUEST_QUEUE, true, false, false, null);
channel.queueDeclare(Constants.RPC_RESPONSE_QUEUE, true, false, false, null);
//3. 发送请求
String msg = "hello rpc...";
//设置请求的唯一标识
String correlationID = UUID.randomUUID().toString();
//设置请求的相关属性
AMQP.BasicProperties props = new AMQP.BasicProperties().builder()
.correlationId(correlationID)
.replyTo(Constants.RPC_RESPONSE_QUEUE)
.build();
channel.basicPublish("", Constants.RPC_REQUEST_QUEUE, props, msg.getBytes());
//4. 接收响应
//使用阻塞队列, 来存储响应信息
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 {
String respMsg = new String(body);
System.out.println("接收到回调消息: "+ respMsg);
if (correlationID.equals(properties.getCorrelationId())){
//如果correlationID校验一致
response.offer(respMsg);
}
}
};
channel.basicConsume(Constants.RPC_RESPONSE_QUEUE, true, consumer);
String result = response.take();
System.out.println("[RPC Client 响应结果]:"+ result);
}
}
服务端代码
/**
* RPC server
* 1. 接收请求
* 2. 发送响应
*/
public class RpcServer {
public static void main(String[] args) throws IOException, TimeoutException {
//1. 建立连接
ConnectionFactory connectionFactory = new ConnectionFactory();
connectionFactory.setHost(Constants.HOST);
connectionFactory.setPort(Constants.PORT); //需要提前开放端口号
connectionFactory.setUsername(Constants.USER_NAME);//账号
connectionFactory.setPassword(Constants.PASSWORD); //密码
connectionFactory.setVirtualHost(Constants.VIRTUAL_HOST); //虚拟主机
Connection connection = connectionFactory.newConnection();
//2. 开启信道
Channel channel = connection.createChannel();
//3. 接收请求
channel.basicQos(1); //设置同时最多只能获取⼀个消息
DefaultConsumer consumer = new DefaultConsumer(channel){
@Override
public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
String request = new String(body,"UTF-8");
System.out.println("接收到请求:"+ request);
String response = "针对request:"+ request +", 响应成功";
AMQP.BasicProperties basicProperties = new AMQP.BasicProperties().builder()
.correlationId(properties.getCorrelationId())
.build();
channel.basicPublish("", Constants.RPC_RESPONSE_QUEUE, basicProperties, response.getBytes());
//手动确认
channel.basicAck(envelope.getDeliveryTag(), false);
}
};
channel.basicConsume(Constants.RPC_REQUEST_QUEUE, false, consumer);
}
}
发布确认模式
作为消息中间件,都会面临消息丢失的问题.
消息丢失大概分为三种情况:
1. ⽣产者问题:因为应⽤程序故障,⽹络抖动等各种原因,⽣产者没有成功向broker发送消息.
2. 消息中间件⾃⾝问题.⽣产者成功发送给了Broker,但是Broker没有把消息保存好,导致消息丢失.
3. 消费者问题.Broker发送消息到消费者,消费者在消费消息时,因为没有处理好,导致broker将消费 失败的消息从队列中删除了.
针对问题1,可以采⽤发布确认(Publisher Confirms)机制实现.
针对问题2可以通过持久化机制.
针对问题3可以采⽤消息应答机制.
发布确认模式是RabbitMQ的七大工作模式之一.
⽣产者将信道设置成confirm(确认)模式,⼀旦信道进⼊confirm模式,所有在该信道上⾯发布的消息都 会被指派⼀个唯⼀的ID(从1开始),⼀旦消息被投递到所有匹配的队列之后,RabbitMQ就会发送⼀个确认给⽣产者(包含消息的唯⼀ID),这就使得⽣产者知道消息已经正确到达⽬的队列了,如果消息和队列是可 持久化的,那么确认消息会在将消息写⼊磁盘之后发出.broker回传给⽣产者的确认消息中deliveryTag包含了确认消息的序号,此外broker也可以设置channel.basicAck⽅法中的multiple参 数,表⽰到这个序号之前的所有消息都已经得到了处理.
发布确认模式的三种策略
关键方法
设置信道为confirm模式 channel.confirmSelect();//等待确认消息.只要消息被确认,这个⽅法就会被返回 //如果超时过期, 则抛出TimeoutException。如果任何消息被nack(丢失),waitForConfirmsOrDie将抛出IOException。 channel.waitForConfirmsOrDie(5000);
单独确认
/**
* 单独确认
*/
private static void publishingMessagesIndividually() throws Exception {
try(Connection connection = createConnection()) {
//1. 开启信道
Channel channel = connection.createChannel();
//2. 设置信道为confirm模式
channel.confirmSelect();
//3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE1, true, false, false, null);
//4. 发送消息, 并等待确认
long start = System.currentTimeMillis();
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = "hello publisher confirms"+i;
channel.basicPublish("",Constants.PUBLISHER_CONFIRMS_QUEUE1, null, msg.getBytes());
//等待确认
//等待确认消息.只要消息被确认,这个⽅法就会被返回
//如果超时过期, 则抛出TimeoutException。如果任何消息被nack(丢失),waitForConfirmsOrDie将抛出IOException。
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.printf("单独确认策略, 消息条数: %d, 耗时: %d ms \n",MESSAGE_COUNT, end-start);
}
}
批量确认
/**
* 批量确认
* @throws Exception
*/
private static void publishingMessagesInBatches() throws Exception{
try(Connection connection = createConnection()) {
//1. 开启信道
Channel channel = connection.createChannel();
//2. 设置信道为confirm模式
channel.confirmSelect();
//3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE2, true, false, false, null);
//4. 发送消息, 并进行确认
long start = System.currentTimeMillis();
int batchSize = 100;
int outstandingMessageCount = 0;
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = "hello publisher confirms"+i;
channel.basicPublish("",Constants.PUBLISHER_CONFIRMS_QUEUE2, null, msg.getBytes());
outstandingMessageCount++;
if (outstandingMessageCount==batchSize){
channel.waitForConfirmsOrDie(5000);
outstandingMessageCount = 0;
}
}
if (outstandingMessageCount>0){
channel.waitForConfirmsOrDie(5000);
}
long end = System.currentTimeMillis();
System.out.printf("批量确认策略, 消息条数: %d, 耗时: %d ms \n",MESSAGE_COUNT, end-start);
}
}
异步确认
/**
* 异步确认
*/
private static void handlingPublisherConfirmsAsynchronously() throws Exception{
try (Connection connection = createConnection()){
//1. 开启信道
Channel channel = connection.createChannel();
//2. 设置信道为confirm模式
channel.confirmSelect();
//3. 声明队列
channel.queueDeclare(Constants.PUBLISHER_CONFIRMS_QUEUE3, true, false, false, null);
//4. 监听confirm
//集合中存储的是未确认的消息ID
long start = System.currentTimeMillis();
SortedSet<Long> confirmSeqNo = Collections.synchronizedSortedSet(new TreeSet<>());
channel.addConfirmListener(new ConfirmListener() {
@Override
public void handleAck(long deliveryTag, boolean multiple) throws IOException {
if (multiple){
confirmSeqNo.headSet(deliveryTag+1).clear();
}else {
confirmSeqNo.remove(deliveryTag);
}
}
@Override
public void handleNack(long deliveryTag, boolean multiple) throws IOException {
if (multiple){
confirmSeqNo.headSet(deliveryTag+1).clear();
}else {
confirmSeqNo.remove(deliveryTag);
}
//业务需要根据实际场景进行处理, 比如重发, 此处代码省略
}
});
//5. 发送消息
for (int i = 0; i < MESSAGE_COUNT; i++) {
String msg = "hello publisher confirms"+i;
long seqNo = channel.getNextPublishSeqNo();
channel.basicPublish("",Constants.PUBLISHER_CONFIRMS_QUEUE3, null, msg.getBytes());
confirmSeqNo.add(seqNo);
}
while (!confirmSeqNo.isEmpty()){
Thread.sleep(10);
}
long end = System.currentTimeMillis();
System.out.printf("异步确认策略, 消息条数: %d, 耗时: %d ms \n",MESSAGE_COUNT, end-start);
}
}
三种策略耗时比较
单独确认模式是每发送⼀条消息后就调⽤channel.waitForConfirmsOrDie⽅法,之后 等待服务端的确认,这实际上是⼀种串⾏同步等待的⽅式.尤其对于持久化的消息来说,需要等待消息确认存储在磁盘之后才会返回(调⽤Linux内核的fsync⽅法).
批量确认模式相⽐于单独确认策略,极⼤地提升了confirm的效率,缺点是出现Basic.Nack或者超时时,我们不清楚具体哪条消息出了问题.客⼾端需要将这⼀批次的消息全部重发,这会带来明显的重复消息数量.当消息经常丢失时,批量确认的性能应该是不升反降的.
异步确认模式,我们需要为每⼀个Channel维护⼀个已发送消息的序号集合.当收到RabbitMQ的confirm回调时,从集合中删除对应的消息.当Channel开启confirm模式后,channel上发送消息都会附带⼀个从1开始递增的 deliveryTag序号.我们可以使⽤SortedSet的有序性来维护这个已发消息的集合.
1. 当收到ack时,从序列中删除该消息的序号.如果为批量确认消息,表⽰⼩于等于当前序号 deliveryTag的消息都收到了,则清除对应集合
2. 当收到nack时,处理逻辑类似,不过需要结合具体的业务情况,进⾏消息重发等操作.