【RabbitMQ】工作模式实现

目录

  • [一、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 生产者

生产者:

  1. 创建连接
  2. 创建Channel
  3. 声明⼀个队列Queue
  4. 生产消息
  5. 释放资源
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 消费者

我们创建两个消费者模拟。

消费者:

  1. 创建连接
  2. 创建Channel
  3. 声明一个队列Queue
  4. 消费消息
  5. 释放资源,为了造成两个消费者竞争,我们先不释放资源。
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 常⻅有三种类型,分别代表不同的路由规则,也就分别对应不同的⼯作模式:

  1. Fanout:⼴播,将消息交给所有绑定到交换机的队列(Publish/Subscribe模式)
  2. Direct:定向,把消息交给符合指定routing key的队列(Routing模式)
  3. Topic:通配符,把消息交给符合routing pattern(路由模式)的队列(Topics模式)

2.1 生产者

生产者

  1. 创建连接
  2. 创建信道
  3. 声明交换机
  4. 声明两个队列
  5. 交换机与队列进行绑定
  6. 生产消息
  7. 释放资源

声明交换机的方法是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 消费者

两个消费者分别消费两个队列的消息。

消费者:

  1. 创建连接
  2. 创建Channel
  3. 声明一个队列Queue
  4. 消费消息
  5. 释放资源。
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 生产者

生产者:

  1. 创建连接
  2. 创建信道
  3. 声明交换机,类型为direct
  4. 声明队列
  5. 队列与交换机绑定,绑定的时候加上BindingKey参数
  6. 生产消息,消息发送的时候指定routingKey
  7. 释放资源
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 消费者

两个消费者分别消费两个队列的消息。

消费者:

  1. 创建连接
  2. 创建Channel
  3. 声明一个队列Queue
  4. 消费消息
  5. 释放资源。
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 模式的区别是:

  1. topics 模式使⽤的交换机类型为topic(Routing模式使⽤的交换机类型为direct)
  2. topic 类型的交换机在匹配规则上进⾏了扩展,Binding Key⽀持通配符匹配(direct类型的交换机路由规则是BindingKey和RoutingKey完全匹配)。

在topic类型的交换机在匹配规则上,有些要求:

  1. RoutingKey 是⼀系列由点( . )分隔的单词,⽐如 " stock.usd.nyse "," nyse.vmw "," quick.orange.rabbit "
  2. BindingKey 和 RoutingKey⼀样,也是点( . )分割的字符串。
  3. Binding Key中可以存在两种特殊字符串,⽤于模糊匹配
    3.1 * 表⽰⼀个单词
    3.2 # 表⽰多个单词(0-N个)

4.1 生产者

生产者:

  1. 创建连接
  2. 创建信道
  3. 声明交换机,类型为topic
  4. 声明队列
  5. 队列与交换机绑定,绑定的时候加上BindingKey参数
  6. 生产消息,消息发送的时候指定routingKey
  7. 释放资源
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 消费者

两个消费者分别消费两个队列的消息。

消费者:

  1. 创建连接
  2. 创建Channel
  3. 声明一个队列Queue
  4. 消费消息
  5. 释放资源。
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通信的过程,⼤概是通过两个队列实现⼀个可回调的过程。

⼤概流程如下:

  1. 客⼾端发送消息到⼀个指定的队列,并在消息属性中设置 replyTo 字段,这个字段指定了⼀个回调队列,服务端处理后,会把响应结果发送到这个队列。
  2. 服务端接收到请求后,处理请求并发送响应消息到replyTo指定的回调队列
  3. 客⼾端在回调队列上等待响应消息,⼀旦收到响应,客⼾端会检查消息的 correlationId 属性,以确保它是所期望的响应。

5.1 客户端

客户端:

  1. 声明两个队列,包含回调队列 replyQueueName,声明本次请求的唯⼀标志 corrId
  2. 将 replyQueueName 和 corrId 配置到要发送的消息队列中
  3. 使⽤阻塞队列来阻塞当前进程,监听回调队列中的消息,把请求放到阻塞队列中
  4. 阻塞队列有消息后,主线程被唤醒,打印返回内容
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 服务器

服务器:

  1. 接收消息
  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(发布确认)

消息中间件都会有消息丢失的问题发生,大概分为以下三种丢失情况:

  1. 生产者问题:因为应用故障,网络等问题,生产者没有成功向消息中间件发送消息;
  2. 消息中间件问题:生产者成功发送了消息,消息中间件自己原因导致消息丢失;
  3. 消费者问题:消费者消费消息时,处理出现问题,导致消费者 消费失败的 消息 从消息队列中删除了。

RabbitMQ针对上面三种情况给出的解决方案:

  1. 针对生产者问题:采取Publisher Confirms(发布确认)机制 解决;
  2. 针对消息中间件问题:通过持久化机制解决;
  3. 针对消费者问题:通过消息应答机制解决;

生产者将信道设置成 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 的有序性来维护这个已发消息的集合。

  1. 当收到ack时,从序列中删除该消息的序号。如果为批量确认消息,表⽰⼩于等于当前序号deliveryTag的消息都收到了,则清除对应集合
  2. 当收到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));
        }
    }

结果:

相关推荐
Luke Ewin4 小时前
内网私有化分布式集群部署语音识别接口
人工智能·分布式·语音识别·asr·funasr·通话语音质检·区分说话人
小马爱打代码6 小时前
实战:分布式开源监控Zabbix
分布式·开源·zabbix
筑梦之人7 小时前
Spark-3.5.7文档2 - RDD 编程指南
大数据·分布式·spark
happy_king_zi8 小时前
RabbitMQ 是否也支持消费组
分布式·rabbitmq
兮动人9 小时前
PrettyZoo:优雅易用的 ZooKeeper 可视化管理工具
分布式·zookeeper·云原生·prettyzoo
回家路上绕了弯9 小时前
五分钟内重复登录 QQ 号定位:数据结构选型与高效实现方案
分布式·后端
一抓掉一大把12 小时前
RuoYi .net-实现商城秒杀下单(redis,rabbitmq)
redis·mysql·c#·rabbitmq·.net
豆浆whisky1 天前
Go分布式追踪实战:从理论到OpenTelemetry集成|Go语言进阶(15)
开发语言·分布式·golang
乌恩大侠1 天前
DGX Spark 恢复系统
大数据·分布式·spark