rabbitMQ入门指南:基于Java客户端实现常用的消息场景

1.引言

  RabbitMQ是一个功能强大的消息中间件,可以帮助构建高效的消息系统。上一篇文章,介绍了在java中如何使用RabbitMQ提供的客户端,本文将围绕RabbitMQ官网提供的七个消息使用场景,使用官方的Java客户端进行示例演示,探索每个场景的应用和优势。下面的代码环境,继续使用管理后台搭建过程使用的配置。

在这里插入图片描述

2. 基本模式(hello world体验模式)

  “Hello World” 模式是 RabbitMQ 入门级别的一个简单示例,用于演示如何在消息队列中发送和接收消息。在这个示例中,我们会创建一个生产者(Producer),它发送消息到名为hello 的队列(Queue),同时创建一个消费者(Consumer),它从同一个队列接收并处理消息。下图是官网的一个交互图,P就是我们的生产者,C就是消费者,在这个模式下,只有生产者-队列-消费者,没有交换机
在这里插入图片描述

2.1 创建虚拟机

本文使用的虚拟机scenarios是,需提前在管理后台创建好
在这里插入图片描述
创建完成后,切换到Users查看用户是否有权限
在这里插入图片描述

2.2 通用的连接

下面是获取连接的代码,本文使用的都是该配置,所以封装到一个工具类中,后续都可以通过该方法获取连接。

import com.rabbitmq.client.Channel;
import com.rabbitmq.client.Connection;
import com.rabbitmq.client.ConnectionFactory;

public class ConnectionUtils {
    
    
    private static Connection connection;
    //获取连接
	public static Connection getConnection() throws Exception {
    
    
        if (null == connection) {
    
    
            ConnectionFactory factory = new ConnectionFactory();
            //服务地址
            factory.setHost("192.168.1.11");
            //服务所在端口,不填默认5672
            factory.setPort(ConnectionFactory.DEFAULT_AMQP_PORT);
            //管理员账号
            factory.setUsername("tom");
            //管理员密码
            factory.setPassword("abc123");
            //虚拟机
            factory.setVirtualHost("scenarios");
            connection = factory.newConnection();
        }
        return connection;
	}
	//关闭连接
	public static void closeConnection() throws Exception {
    
    
        if (null != connection) {
    
    
            connection.close();
        }
    }
}

2.3 生产者发送消息

生产者往hello队列中发送一条含有uuid的消息

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
channel.queueDeclare("hello", true, false, false, null);
channel.basicPublish("", "hello", null, UUID.randomUUID().toString().getBytes());
channel.close();
ConnectionUtils.closeConnection();

执行完成后,到管理后台查看队列的状态,可以看到队列中已经收到一条消息
在这里插入图片描述

2.4 消费者消费消息

这里使用消费者主动拉取的方式获取hello队列的消息

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
GetResponse getResponse = channel.basicGet("hello", true);
byte[] body = getResponse.getBody();
System.out.println("消息内容:" + new String(body));
channel.close();
ConnectionUtils.closeConnection();

执行后控制台的内容

消息内容:1573f3da-7e49-4fbe-b01c-9ccc9644a16c

使用异步方式获取

new Thread(() -> {
    
    
	try {
    
    
		Connection connection = ConnectionUtils.getConnection();
		Channel channel = connection.createChannel();
		channel.basicConsume("hello",
				true,
				new DefaultConsumer(channel) {
    
    
					@Override
					public void handleConsumeOk(String consumerTag) {
    
    
						System.out.println("消费者启动成功");
					}
					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
						System.out.println("消息内容:" + new String(body));
					}
				});


	} catch (Exception e) {
    
    
		e.printStackTrace();
	}
}).start();

3 Work Queues(工作队列模式)

  工作队列(又称任务队列)的主要思想是避免立即执行资源密集型的任务并等待其完成。也被称为 “Task Queue” 或 “Work Queue” 模式,是一种常见的消息队列应用模式,用于在分布式系统中实现任务的异步处理。该模式中,任务(或称为消息)由生产者发布到一个队列中,然后由多个消费者并发地接收和处理这些任务。多个消费者可以同时处理队列中的任务,从而实现任务的并行处理和负载均衡。下面是官网给我们提供的一个交互图,消费者有两个或者多个,他们共同承担着同一个队列中消息的消费,即合作完成任务
在这里插入图片描述

3.1 生产者发送消息

worker队列中发送10条消息

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String queueName = "worker";
channel.queueDeclare(queueName, true, false, false, null);
for (int i = 0; i < 10; i++){
    
    
	channel.basicPublish("", queueName, null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();

3.2 消费者消费消息

在该模式下,启用两个进程模拟两个消费者进行消息的消费,

public static void main(String[] args) throws Exception {
    
    
	String queueName = "worker";
	new Thread(() -> {
    
    
		asyncGet(queueName);
	}, "worker1").start();
	new Thread(() -> {
    
    
		asyncGet(queueName);
	}, "worker2").start();
}
private static void asyncGet(String queueName) {
    
    
	try {
    
    
		String workerName = Thread.currentThread().getName();
		Connection connection = ConnectionUtils.newConnection();
		Channel channel = connection.createChannel();
		channel.basicConsume(queueName,
				true,
				new DefaultConsumer(channel) {
    
    
					@Override
					public void handleConsumeOk(String consumerTag) {
    
    
						System.out.printf("消费者%s - 启动成功%n", workerName);
					}

					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
						System.out.printf("消息者%s - 获取消息:%s%n", workerName, new String(body));
					}
				});
	} catch (Exception e) {
    
    
		e.printStackTrace();
	}
}

4. Publish/Subscribe(发布/订阅模式)

  “Publish/Subscribe” 用于实现消息的广播和多播,使多个消费者同时接收并处理相同的消息。在 “Publish/Subscribe” 模式中,消息由生产者发布到一个交换机(Exchange)中,而不是直接发布到队列。交换机负责将接收到的消息广播给绑定到它上面的所有队列。每个队列都有自己的消费者,当交换机广播消息时,所有绑定到它的队列上的消费者都会同时接收到消息。
在这里插入图片描述

4.1 生产者发送消息

生产者只能将消息发送到交换机publishSubscribe中,不再像前面发送到队列,交换机只负责从生产者接收消息,至于发送到哪个队列,生产者这把不关注。交换机的类型必须为fanout

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "publishSubscribe";
channel.exchangeDeclare(exchangeName,"fanout");
for (int i = 0; i < 10; i++){
    
    
	channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();

4.2 消费者消费消息

“Publish/Subscribe” 模式中,每个队列都可以有自己的消费者,并且消费者之间是相互独立的。这样就可以实现消息的广播和多播,使得多个消费者都能同时接收并处理相同的消息。交换机的声明必须跟生产者一直,并且最后需要将获取到的队列绑定到交换机上才能正确消费到消息。

public static void main(String[] args) throws Exception {
    
    
	String exchangeName = "publishSubscribe";
	new Thread(() -> {
    
    
		asyncGet(exchangeName);
	}, "Subscribe1").start();
	new Thread(() -> {
    
    
		asyncGet(exchangeName);
	}, "Subscribe2").start();
}

private static void asyncGet(String exchangeName) {
    
    
	try {
    
    
		String workerName = Thread.currentThread().getName();
		Connection connection = ConnectionUtils.newConnection();
		Channel channel = connection.createChannel();
		channel.exchangeDeclare(exchangeName,"fanout");
		String queueName = channel.queueDeclare().getQueue();
		channel.queueBind(queueName, exchangeName, "");
		channel.basicConsume(queueName,
				true,
				new DefaultConsumer(channel) {
    
    
					@Override
					public void handleConsumeOk(String consumerTag) {
    
    
						System.out.printf("消费者%s - 启动成功%n", workerName);
					}

					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
						System.out.printf("消息者%s - 获取消息:%s%n", workerName, new String(body));
					}
				});
	} catch (Exception e) {
    
    
		e.printStackTrace();
	}
}

5. Routing(路由模式)

  “Routing” 模式是一种消息队列应用模式,用于根据路由键(Routing Key)的匹配关系将消息从交换机(Exchange)路由到指定的队列,可以说是对发布/订阅的一种更细化的定制,该模式下,并不是订阅了就可以消费到消息,还需要匹配路由键。(同时交换机类型从fanout改为direct
在这里插入图片描述

5.1 生产者发送消息

生产者发布的消息会带有一个特定的路由键。交换机根据这个路由键将消息路由到绑定到它上面的特定队列。这样,消费者只需要关注特定的队列,从而只接收和处理与其路由键匹配的消息。交换机的类型需设置为direct

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "routing";
channel.exchangeDeclare(exchangeName,"direct");
for (int i = 0; i < 10; i++){
    
    
	//发送到demo的路由
	channel.basicPublish(exchangeName, "demo", null, UUID.randomUUID().toString().getBytes());
	//发送到test的路由
    channel.basicPublish(exchangeName, "test", null, UUID.randomUUID().toString().getBytes());
    //发送到super的路由
    channel.basicPublish(exchangeName, "super", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();

5.1 消费者消费消息

交换机根据消息的路由键和队列绑定的路由键进行匹配。如果消息的路由键与某个队列绑定的路由键相匹配,则该消息将被路由到该队列,从而由该队列上的消费者进行处理。下面的例子定义了支持demotest,只要包含这两种路由规则都可以消费到消息

public static void main(String[] args) throws Exception {
    
    
        String exchangeName = "routing";
        new Thread(() -> {
    
    
            asyncGet(exchangeName);
        }, "Subscribe").start();

    }

    private static void asyncGet(String exchangeName) {
    
    
        try {
    
    
            String workerName = Thread.currentThread().getName();
            Connection connection = ConnectionUtils.newConnection();
            Channel channel = connection.createChannel();
            channel.exchangeDeclare(exchangeName,"direct");
            String queueName = channel.queueDeclare().getQueue();
            channel.queueBind(queueName, exchangeName, "demo");
            channel.queueBind(queueName, exchangeName, "test");
            channel.basicConsume(queueName,
                    true,
                    new DefaultConsumer(channel) {
    
    
                        @Override
                        public void handleConsumeOk(String consumerTag) {
    
    
                            System.out.printf("消费者%s - 启动成功%n", workerName);
                        }

                        @Override
                        public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                            System.out.printf("消息者%s - 获取消息:%s,路由:%s%n", workerName, new String(body),envelope.getRoutingKey());
                        }
                    });
        } catch (Exception e) {
    
    
            e.printStackTrace();
        }
    }

通过控制台打印可以看到该消费者不能获取到super路由的消息

消费者Subscribe - 启动成功
消息者Subscribe - 获取消息:a8a6d061-20a9-4af0-ba5e-e9f484180b8d,路由:demo
消息者Subscribe - 获取消息:9eb5e5ce-6530-4de3-bd31-06390d13d2d2,路由:test
消息者Subscribe - 获取消息:954bf76f-7b3e-429c-8761-1d064530b1c2,路由:demo
消息者Subscribe - 获取消息:811e8891-4945-4972-8a5c-340362e4b76b,路由:test
消息者Subscribe - 获取消息:240083af-ce7b-4587-8bec-cfd75c4ca69d,路由:demo
消息者Subscribe - 获取消息:a38c46fd-e5c6-4a60-bd7b-000cb258c10e,路由:test

6. topics(主题模式)

  “Topics” 模式是一种灵活且功能强大的消息队列应用模式,它允许生产者将消息发布到交换机中,并通过使用通配符的路由键(Routing Key)将消息路由到不同的队列。也算是对路由模式的一种个性定制,支持键的规则匹配。
在这里插入图片描述

6.1 生产者发送消息

在 “Topics” 模式中,生产者发布的消息携带一个路由键,该路由键是一个由点号分隔的字符串,例如:“user.create”, "order.update"等。交换机将根据消息的路由键和队列绑定的路由键进行匹配,使用通配符的方式将消息路由到一个或多个匹配的队列。
“Topics” 模式使用两种通配符:

  • *(星号):表示一个单词的通配符。例如,路由键 “user.*” 可以匹配 “user.create”、“user.update” 等。
  • #(井号):表示零个或多个单词的通配符。例如,路由键 “order.#” 可以匹配 “order”、“order.update”、“order.details” 等。
Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "topics";
channel.exchangeDeclare(exchangeName,"topic");
for (int i = 0; i < 3; i++){
    
    
	//发送到路由带有order.create的队列
	channel.basicPublish(exchangeName, "order.create", null, UUID.randomUUID().toString().getBytes());
	//发送到路由带有user.create的队列
	channel.basicPublish(exchangeName, "user.create", null, UUID.randomUUID().toString().getBytes());
	//发送到路由带有user.vip.create的队列
	channel.basicPublish(exchangeName, "user.vip.create", null, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();

6.2 消费者消费消息

在 “Topics” 模式中,交换机根据消息的路由键和队列绑定的路由键进行灵活的通配符匹配。消费者通过自己声明的路由规则绑定到交换机上,进而获取匹配到路由规则的消息。

public static void main(String[] args) throws Exception {
    
    
	String exchangeName = "topics";
	String routingKey1 = "*.create";
	new Thread(() -> {
    
    
		asyncGet(exchangeName, routingKey1);
	}, "Subscribe1").start();

	String routingKey2 = "#.create";
	new Thread(() -> {
    
    
		asyncGet(exchangeName, routingKey2);
	}, "Subscribe2").start();

}

private static void asyncGet(String exchangeName, String routingKey) {
    
    
	try {
    
    
		String workerName = Thread.currentThread().getName();
		Connection connection = ConnectionUtils.newConnection();
		Channel channel = connection.createChannel();
		channel.exchangeDeclare(exchangeName, "topics");
		String queueName = channel.queueDeclare().getQueue();
		channel.queueBind(queueName, exchangeName, routingKey);
		channel.basicConsume(queueName,
				true,
				new DefaultConsumer(channel) {
    
    
					@Override
					public void handleConsumeOk(String consumerTag) {
    
    
						System.out.printf("消费者%s - 启动成功%n", workerName);
					}

					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
						System.out.printf("消息者%s - 获取消息:%s,路由:%s%n", workerName, new String(body), envelope.getRoutingKey());
					}
				});
	} catch (Exception e) {
    
    
		e.printStackTrace();
	}
}

通过控制台可以看到消息者Subscribe1只能消费到路由键为order.createuser.create的消息;而消息者Subscribe2能消费到路由键为order.createuser.createuser.vip.create的消息。

消费者Subscribe1 - 启动成功
消费者Subscribe2 - 启动成功
消息者Subscribe2 - 获取消息:9aaf5e63-d994-4fd4-b285-4e2be6f27018,路由:order.create
消息者Subscribe2 - 获取消息:7c789c6e-456f-4682-b7ec-01ba4db489b4,路由:user.create
消息者Subscribe1 - 获取消息:9aaf5e63-d994-4fd4-b285-4e2be6f27018,路由:order.create
消息者Subscribe2 - 获取消息:d3a35a50-7d3e-4506-8e69-b732f43dfe1c,路由:user.vip.create
消息者Subscribe1 - 获取消息:7c789c6e-456f-4682-b7ec-01ba4db489b4,路由:user.create
消息者Subscribe1 - 获取消息:27848958-6824-49e2-953e-0d37443b69b6,路由:order.create
消息者Subscribe1 - 获取消息:ed0c9123-7d44-458c-859f-e825de42f01a,路由:user.create
消息者Subscribe2 - 获取消息:27848958-6824-49e2-953e-0d37443b69b6,路由:order.create
消息者Subscribe2 - 获取消息:ed0c9123-7d44-458c-859f-e825de42f01a,路由:user.create
消息者Subscribe2 - 获取消息:95f28ecb-f79a-4758-92df-043b4d443621,路由:user.vip.create
消息者Subscribe2 - 获取消息:9bf03771-72fa-4580-a421-f311dd8fb1ce,路由:order.create
消息者Subscribe1 - 获取消息:9bf03771-72fa-4580-a421-f311dd8fb1ce,路由:order.create
消息者Subscribe1 - 获取消息:393dc53f-d93c-4c8d-93f9-5b42d030376f,路由:user.create
消息者Subscribe2 - 获取消息:393dc53f-d93c-4c8d-93f9-5b42d030376f,路由:user.create
消息者Subscribe2 - 获取消息:17bb98f8-12cd-4ac1-84a9-f33006abc125,路由:user.vip.create

7. rpc(远程过程调用模式)

  在前面工作队列模式中,我们学习了如何使用工作队列将耗时的任务分发给多个工作进程。但是,如果我们需要在远程计算机上运行一个函数并等待结果呢?这就是一个不同的需求。这种模式通常称为远程过程调用(RPC)。RPC模式中,一个客户端和一个可伸缩的RPC服务器。
在这里插入图片描述
从上面的交互图可以看出来,RabbitMQ服务器作为消息代理,负责将客户端的请求转发到服务端,接收服务端的响应并返回给客户端;客户端(Client)发送请求到RabbitMQ服务器,并等待响应服务端(Server)监听RabbitMQ服务器上的队列,并处理来自客户端的请求,执行相应的远程方法,并将结果作为响应返回给客户端。

7.1 服务器启动并监听

在rpc模式下,没有生产者与消费者的概念,转而出现的是服务端和客户端,在下面的示例代码中,服务的监听rpc_queue队列,如果收到消息(一个整数),会调用计算斐波那契数列的方法计算消息中的数据并响应给客户端。

Connection connection = ConnectionUtils.newConnection();
        Channel channel = connection.createChannel();
        String queueName = "rpc_queue";
        channel.queueDeclare(queueName, false, false, false, null);
        //清空队列中的所有消息
        channel.queuePurge(queueName);
        channel.basicQos(1);
        //接收消息的回调
        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(), StandardCharsets.UTF_8);
                Integer i = Integer.parseInt(message);
                System.out.printf("服务端 - 获取消息:%s%n", message);
                response += fib(i);
            } catch (RuntimeException e) {
    
    
                System.out.println(" [.] " + e);
            } finally {
    
    
            	//响应客户端
                channel.basicPublish("", delivery.getProperties().getReplyTo(), replyProps, response.getBytes(StandardCharsets.UTF_8));
                channel.basicAck(delivery.getEnvelope().getDeliveryTag(), false);
            }
        };
        //监听队列
        channel.basicConsume(queueName, false, deliverCallback, (consumerTag -> {
    
    }));
    }

    //计算斐波那契数列
    private static int fib(int n) {
    
    
        if (n == 0) return 0;
        if (n == 1) return 1;
        return fib(n - 1) + fib(n - 2);
    }

7.2 客户端请求并等待

客户端发送一个整数到队列中,并且阻塞等待服务的计算结果

Connection connection = ConnectionUtils.newConnection();
        Channel channel = connection.createChannel();
        String replyQueueName = channel.queueDeclare().getQueue();
        String corrId = UUID.randomUUID().toString();
        AMQP.BasicProperties props = new AMQP.BasicProperties
                .Builder()
                .correlationId(corrId)
                .replyTo(replyQueueName)
                .build();
        String queueName = "rpc_queue";
        String message = new Random().nextInt(30) + "";
        //发送消息
        channel.basicPublish("", queueName, props, message.getBytes(StandardCharsets.UTF_8));
        CompletableFuture<String> response = new CompletableFuture<>();
        //等待结果
        String ctag = channel.basicConsume(replyQueueName, true, (consumerTag, delivery) -> {
    
    
            if (delivery.getProperties().getCorrelationId().equals(corrId)) {
    
    
                response.complete(new String(delivery.getBody(), StandardCharsets.UTF_8));
            }
        }, consumerTag -> {
    
    
        });
        channel.basicCancel(ctag);
        String result = response.get();
        System.out.printf("客户端收到响应:%s", result);

ctag 是消费者的标签(Consumer Tag),用于唯一标识一个消费者。
replyQueueName 是客户端发送消息到服务端rpc_queue队列时,携带的一个回复队列,后续服务端会通过该队列回复结果给客户端。

8. Publisher Confirms(发送确认机制)

  Publisher Confirms模式是一种消息发送确认机制(RabbitMQ 3.0 版本及以上开始支持),用于保证消息的可靠性投递。在传统的消息发送模式中,生产者将消息发送到RabbitMQ,然后就认为消息已经被成功地发送了,但实际上,由于网络等因素,消息有可能在传输过程中丢失,导致消息未被正确投递到队列。

  为了解决这个问题,RabbitMQ引入了Publisher Confirms机制。在Publisher Confirms模式中,当生产者将消息发送到RabbitMQ时,会等待RabbitMQ的确认反馈。如果消息成功地被RabbitMQ接收并投递到队列中,RabbitMQ会发送一个确认消息给生产者。如果消息未被正确处理,则RabbitMQ会发送一个否定消息给生产者。生产者在收到RabbitMQ的确认反馈后,就能确定消息是否成功被投递。
在Publisher Confirms模式中,生产者可以通过以下两种方式来确认消息的投递状态。

8.1 同步确认

  生产者发送一条消息后,会等待RabbitMQ的确认消息,直到收到确认消息或超时。这种方式确保了消息的可靠性投递,但在等待确认消息的过程中,生产者会阻塞,可能影响发送消息的性能。

8.1.1 单条同步确认

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "syncProducer";
channel.exchangeDeclare(exchangeName,"fanout");
//启用确认机制
channel.confirmSelect();
for (int i = 0; i < 3; i++){
    
    
	channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
	channel.waitForConfirmsOrDie(5_000);
}
channel.close();
ConnectionUtils.closeConnection();

8.1.2 批量同步确认

由于同步确认是需要阻塞等待结果的,多以单条确认影响发送效率,如果不是很关心数据的完整性,可以使用批量确认来提高发送效率

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "bactcSyncProducer";
channel.exchangeDeclare(exchangeName,"fanout");
//启用确认机制
channel.confirmSelect();
//批量确认数量
int batchSize = 3;
//积累的发送数量
int outstandingMessageCount = 0;
for (int i = 0; i < 10; i++){
    
    
	channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
	outstandingMessageCount++;
	//当积累的发送数量达到批量确认数量时,进行批量确认
	if (outstandingMessageCount == batchSize) {
    
    
		//等待确认消息的超时时间 5000毫秒
		channel.waitForConfirmsOrDie(5_000);
		outstandingMessageCount = 0;
	}
}
if (outstandingMessageCount > 0) {
    
    
	channel.waitForConfirmsOrDie(5_000);
}
channel.close();
ConnectionUtils.closeConnection();

批量同步确认模式要求所有消息都被正确确认后,才会继续执行后续代码,如果有任何一条消息未能正确确认,则整个批量发送过程被视为失败,生产者应该根据具体情况进行相应的处理,可能是重试发送失败的消息,或者记录失败的消息进行后续处理。

8.2 异步确认

  生产者发送一条消息后,不会阻塞等待确认消息,而是继续发送下一条消息。同时,生产者注册一个回调函数来处理RabbitMQ发送的确认消息。这样可以提高消息发送的性能,但需要注意处理确认消息的回调函数,以确保消息的可靠性。

public static void main(String[] args) throws Exception {
    
    
	Connection connection = ConnectionUtils.getConnection();
	Channel channel = connection.createChannel();
	String exchangeName = "asyncProducer";
	channel.exchangeDeclare(exchangeName, "fanout");
	//启用确认机制
	channel.confirmSelect();

	ConcurrentNavigableMap<Long, String> outstandingConfirms = new ConcurrentSkipListMap<>();
	//消息被RabbitMQ 确认接收后调用的回调方法
	ConfirmCallback ackCallback = (sequenceNumber, multiple) -> {
    
    
		if (multiple) {
    
    
			ConcurrentNavigableMap<Long, String> confirmed = outstandingConfirms.headMap(
					sequenceNumber, true
			);
			confirmed.clear();
		} else {
    
    
			outstandingConfirms.remove(sequenceNumber);
		}
	};

	ConfirmCallback nackCallback =  (sequenceNumber, multiple) -> {
    
    
		//获取未被确认的消息
		String body = outstandingConfirms.get(sequenceNumber);
		//没有被确认执行的方法,先移除
		ackCallback.handle(sequenceNumber, multiple);
		//TODO 再做其他逻辑处理
	};
	//添加确认监听
	channel.addConfirmListener(ackCallback, nackCallback);
	int msgCount = 30;
	long start = System.nanoTime();
	for (int i = 0; i < msgCount; i++) {
    
    
		String body = String.valueOf(i);
		outstandingConfirms.put(channel.getNextPublishSeqNo(), body);
		channel.basicPublish(exchangeName, "", null, UUID.randomUUID().toString().getBytes());
	}
	
	//等待所有消息被确认,超时时间为60秒
	if (!waitUntil(Duration.ofSeconds(60), () -> outstandingConfirms.isEmpty())) {
    
    
		//TODO 存在未被确认的消息,做其他处理
	}
	channel.close();
    ConnectionUtils.closeConnection();
	
}

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();
}

该例子中,通道添加一个确认监听,在通道每发送一条消息就往outstandingConfirms中添加一行记录,key是消息的序列号,序列号是一个递增的整数,每发送一条消息,序列号就会递增,由RabbitMQ Java 客户端提供;最后等所有的消息发送完成之后,等待60秒后检查是否存在有未被确认的消息。

9. Consume Confirms(消费确认机制)

消费确认机制官方没有提到,也许比较简单,官方没有纳入到他的特色使用场景介绍中。
在客户端的编程模式里,确认消息有两种方式:
一是在调用消费时,设置autoAck为true,启用默认应答机制

channel.basicConsume("q-1",true,new DefaultConsumer(channel){
    
    
                @Override
                public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
                }
            });

二是手动应答,使用basicAck

basicAck(long deliveryTag, boolean multiple)

参数说明:

  • deliveryTag:消息的唯一标识。每条消息都有一个唯一的 deliveryTag,用于标识消息在队列中的位置。

  • multiple:是否确认多条消息。如果设置为 false,则表示仅确认指定 deliveryTag 的消息;如果设置为 true,则表示确认所有 deliveryTag 小于或等于指定值的消息。

需要注意的是,消息确认机制需要在消费者端进行配置,在调用 channel.basicConsume(queue, autoAck, consumer) 方法时,将 autoAck 参数设置为 false,才能使用消息确认功能。

10. headers queues(头部路由模式)

  在教程模式中并没有该模式,但是官方github地址中的实例却有该模式。在java客户端的枚举类BuiltinExchangeType中也可以看到存在headers交换机类型

public enum BuiltinExchangeType {
    DIRECT("direct"), FANOUT("fanout"), TOPIC("topic"), HEADERS("headers");
	...
}

  在Headers模式中,交换机(Exchange)不使用消息的Routing Key来决定将消息发送到哪个队列,而是根据消息的Header(头部)属性来匹配队列。Header属性是一个包含键值对的字典,消息发送者可以在消息的Header中设置多个键值对。

10.1 生产者发送消息

  当消息发送到Headers类型的交换机时,交换机会检查消息的Header属性与绑定(Binding)的队列的Header属性是否匹配。只有当消息的Header属性和绑定的队列的Header属性完全匹配时,消息才会被路由到对应的队列。

Connection connection = ConnectionUtils.getConnection();
Channel channel = connection.createChannel();
String exchangeName = "headers";
// 声明交换器类型为headers
channel.exchangeDeclare(exchangeName,BuiltinExchangeType.HEADERS);
// 设置消息头
Map<String, Object> headers = new HashMap<String, Object>();
headers.put("project", "rabbitmq");
headers.put("env", "test");
// 设置消息属性
AMQP.BasicProperties.Builder builder = new AMQP.BasicProperties.Builder();
// 设置消息持久化,如果声明队列时durable为true时,消息将自动继承队列的持久性属性,即使不单独设置消息的 Delivery Mode 属性,消息仍然会被持久化到磁盘
builder.deliveryMode(MessageProperties.PERSISTENT_TEXT_PLAIN.getDeliveryMode());
//消息的优先级用于标记消息的重要性,范围从 0 到 9,0 为最低优先级,9 为最高优先级。
builder.priority(MessageProperties.PERSISTENT_TEXT_PLAIN.getPriority());
// Add the headers to the builder.
builder.headers(headers);
AMQP.BasicProperties theProps = builder.build();
for (int i = 0; i < 3; i++){
    
    
	channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
	channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
	channel.basicPublish(exchangeName, "head", theProps, UUID.randomUUID().toString().getBytes());
}
channel.close();
ConnectionUtils.closeConnection();

交换机声明为headers类型后,在管理后台同样也可以查看
在这里插入图片描述

10.2 消费者消费消息

  在 Headers 模式下,消费者通过设置队列的绑定(Binding)来消费消息。与其他模式不同,Headers 模式中,交换机(Exchange)不使用消息的 Routing Key 进行消息路由,而是根据消息的 Header 属性来匹配队列。

public static void main(String[] args) throws Exception {
    
    
	String exchangeName = "headers";
	// 设置Subscribe1的消息头
	Map<String, Object> headers1 = new HashMap<>();
	headers1.put("project", "java");
	headers1.put("env", "test");
	new Thread(() -> {
    
    
		asyncGet(exchangeName,headers1);
	}, "Subscribe1").start();
	// 设置Subscribe2的消息头
	Map<String, Object> headers2 = new HashMap<>();
	headers2.put("project", "rabbitmq");
	headers2.put("env", "test");
	new Thread(() -> {
    
    
		asyncGet(exchangeName,headers2);
	}, "Subscribe2").start();

}

private static void asyncGet(String exchangeName,Map<String, Object> headers) {
    
    
	try {
    
    
		String workerName = Thread.currentThread().getName();
		Connection connection = ConnectionUtils.newConnection();
		Channel channel = connection.createChannel();
		// 声明交换器类型为headers
		channel.exchangeDeclare(exchangeName,BuiltinExchangeType.HEADERS);
		String queueName = channel.queueDeclare().getQueue();
		channel.queueBind(queueName, exchangeName, "head",headers);
		channel.basicConsume(queueName,
				true,
				new DefaultConsumer(channel) {
    
    
					@Override
					public void handleConsumeOk(String consumerTag) {
    
    
						System.out.printf("消费者%s - 启动成功%n", workerName);
					}
					@Override
					public void handleDelivery(String consumerTag, Envelope envelope, AMQP.BasicProperties properties, byte[] body) throws IOException {
    
    
						System.out.printf("消息者%s - 获取消息:%s,路由:%s,headers:%s%n", workerName, new String(body),envelope.getRoutingKey(),properties.getHeaders().toString());
					}
				});
	} catch (Exception e) {
    
    
		e.printStackTrace();
	}
}

运行后通过控制台结果的打印得知,只有Subscribe2才能消费到消息,因为默认规则下需要全匹配才能消费到消息,可以通过设置headers.put("x-match", "any");让只要匹配一个规则就能消费到消息。

注意:Headers模式并不常用,因为通常情况下使用DirectTopicFanout模式就可以满足大部分消息路由的需求。Headers模式适用于特殊的路由需求,需要在消息的Header属性中携带丰富的信息,并且匹配规则较为复杂的情况。在大多数情况下,推荐使用Direct、Topic或Fanout模式来实现消息的路由和分发。

11. 总结

  至此,本博文已经全面介绍了 RabbitMQ 消息队列在不同场景下的应用。从基本模式(Hello World)、Work Queues、Publish/Subscribe、Routing、topics、rpc、Publisher Confirms、Consume Confirms,再到headers queues,每个部分都详细阐述了其用法和实例。通过这些不同的模式和特性,希望可以帮助您更加了解 RabbitMQ 的功能和灵活性,为构建可靠的消息传递系统提供指南。

猜你喜欢

转载自blog.csdn.net/dougsu/article/details/131878503