RabbitMQ发布订阅模式多实例消费者防止重复消费实现方式

书接上回。

上一篇文章中已经通过一个实际的业务场景结合RabbitMQ的四种交换机类型对RabbitMQ发布订阅模式同一消费者多个实例如何防止重复消费这个问题给出了解决方案。结尾的时候挖了个坑,水这篇的目的就是要把这个坑填上,给大家提供一个可以直接抄作业的代码。

先把一些参数提前公布出来,后面代码里面再遇到就不逐个解释了

  • RabbitMQ主机地址127.0.0.1,如果是部署在其他机器上的,就把IP地址替换成相应的主机IP或者域名。
  • 如果使用默认端口5672,连接地址可以不用写,映射到其他端口的话在主机地址上面加上。
  • 采用默认的用户名和密码guest,根据具体情况进行相应替换。
  • 预定交换机名称demo.event,根据具体情况进行相应替换。
  • 预定队列名称demo.event.queue,根据具体情况进行相应替换。
  • 队列demo.event.queue上有两个消费者,另外再生成一个只有一个消费者的随机名称队列。

要达到的效果

生产者发送一条消息到交换机demo.event,再由交换机分发到绑定给它的队列上,demo.event.queue队列上的消费者只有一个能收到,其他随机队列的消费者(只有一个)也能收到消息。

一、准备RabbitMQ环境

我是在docker上部署的,到官方网站下载Docker Desktop直接安装就可以了,这里不再赘述。RabbitMQ的环境准备不是本篇的重点,这里只提供最基本的用法,有其他需求可以略过这一章节,去RabbitMQ或者Docker官网按照文档配置即可。

拉取镜像

我用的是带管理后台的rabbitmq:management

打开控制台/终端,输入

bash 复制代码
docker pull rabbitmq:management

创建Volumes

bash 复制代码
docker volume create rabbitmq

创建容器

bash 复制代码
docker run -d --name rabbitmq -p 4369:4369 -p 5671:5671 -p 5672:5672 -p 15671:15671 -p 15672:15672 -p 25672:25672 -p 15691:15691 -p 15692:15692 -v rabbitmq:/var/lib/rabbitmq rabbitmq:management

执行完以上代码后,容器会自动启动。

二、.net + RabbitMQ.Client

先准备IDE,Visual Studio、VS Code、Rider都可以。

我这里用的LinqPad,这是一个可以快速执行C#、VB、F#代码块和SQL的工具,使用Microsoft Roslyn进行编译,支持引入NuGet包和三方DLL文件,可以直接运行Asp.Net Core服务。Ver9开始支持macOS,渲染层由之前的WPF改成了AvaloniaUI。有需要的可以使用下面的链接购买

https://www.linqpad.net/Purchase.aspx?affiliate=8ucu28vs

1、依赖包

RabbitMQ.Client

LinqPad通过自带工具添加,正经IDE通过NuGet Manager、CLI工具添加或者在csproj引入

xml 复制代码
<PackageReference Include="RabbitMQ.Client" Version="7.2.0" />

如果项目使用集中版本管理,需要在Directory.Packages.props添加

xml 复制代码
<PackageVersion Include="RabbitMQ.Client" Version="7.2.0" />

然后在具体项目的csproj添加

xml 复制代码
<PackageReference Include="RabbitMQ.Client" />

需要注意的是,7.x以后RabbitMQ.Client做了比较大的调整,原先的方法都改成了异步,EventingBasicConsumer也换成了AsyncEventingBasicConsumer,Consumer的Receive事件变成了ReceivedAsync,支持异步事件处理。

2、生产者代码

csharp 复制代码
async Task Main()
{
	var factory = new ConnectionFactory { Uri = new Uri("amqp://guest:guest@127.0.0.1") };
	using (var connection = await factory.CreateConnectionAsync())
	{
		using (var channel = await connection.CreateChannelAsync())
		{
			await channel.ExchangeDeclareAsync(exchange: "demo.event", type: ExchangeType.Fanout, durable: true, autoDelete: true);

			while (true)
			{
				var message = Console.ReadLine();
				var body = Encoding.UTF8.GetBytes(message);
				var properties = new BasicProperties();
				await channel.BasicPublishAsync(exchange: "demo.event", routingKey: "", mandatory: true, basicProperties: properties, body: body, cancellationToken: default);
			}
		}
	}
}

// Define other methods, classes and namespaces here

3、消费者代码

csharp 复制代码
async Task Main()
{
	var factory = new ConnectionFactory { Uri = new Uri("amqp://guest:guest@127.0.0.1") };
	await using (var connection = await factory.CreateConnectionAsync())
	{
		using (var channel = await connection.CreateChannelAsync())
		{
			await channel.ExchangeDeclareAsync(exchange: "demo.event", type: ExchangeType.Fanout, durable: true, autoDelete: true);
			var queueName = await channel.QueueDeclareAsync("demo.event.queue", true, false, true).ContinueWith(task => task.Result.QueueName); // 客户端制定队列名称。
			await channel.QueueBindAsync(queue: queueName,
							  exchange: "demo.event",
							  routingKey: "");
			Console.WriteLine(" [*] Waiting for logs.");

			var consumer = new AsyncEventingBasicConsumer(channel);
			consumer.ReceivedAsync += async (model, ea) =>
			{
				var body = ea.Body.ToArray();
				var message = Encoding.UTF8.GetString(body);
				Console.WriteLine(" [x] {0}", message);
			};
			await channel.BasicConsumeAsync(queue: "", autoAck: true, consumer: consumer);
			Console.WriteLine(" Press [enter] to exit.");
			Console.ReadLine();
		}
	}
}

// Define other methods, classes and namespaces here

3、开始测试

在LinqPad里面每个标签的代码是完全隔离的,无法引用,所以为了启动多个消费者,我需要把消费者的代码块再复制两份出来。其中一个标签把

csharp 复制代码
var queueName = await channel.QueueDeclareAsync("demo.event.queue", true, false, false).ContinueWith(task => task.Result.QueueName);

改成

csharp 复制代码
var queueName = await channel.QueueDeclareAsync().ContinueWith(task => task.Result.QueueName); // 由RabbitMQ生成队列名称

先运行三个消费者代码开始监听队列消息。再运行生产者代码。

消费者1、消费者2队列名称都是手动指定的demo.event.queue

消费者3队列名称是服务端生成的amq.gen-vFbhNYiLf4wDdKNdmP2WzQ

在生产者发送一条消息 Hello, World!,我们再看下接收情况

消费者1、3收到消息,消费者2没有收到,再发一条Second message.

消费者2、3收到消息,消费者1没有收到。

结论是:目标达成。

三、Java + Maven + amqp-client

1、依赖包

com.rabbitmq:amqp-client,版本号根据实际需要选择

使用Maven工具安装或者直接在pom.xml里添加

xml 复制代码
<dependencies>
  <dependency>
    <groupId>com.rabbitmq</groupId>
    <artifactId>amqp-client</artifactId>
    <version>5.25.0</version>
  </dependency>
</dependencies>

2、生产者代码

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        var factory = new ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");
        var connection = factory.newConnection();
        var channel = connection.createChannel();
        channel.exchangeDeclare("demo.event", "fanout", true, true, null);

        while (true) {
            var message = java.lang.IO.readln();
            var properties = new AMQP.BasicProperties.Builder().build();
            channel.basicPublish("demo.event", "", true, properties, message.getBytes());
            System.out.println(" [x] Sent '" + message + "'");
        }
    }
}

3、消费者代码

java 复制代码
public class Main {
    public static void main(String[] args) throws Exception {
        String recipientName = args[0];
        String queueName = args.length > 1 ? args[1] : "";

        var factory = new com.rabbitmq.client.ConnectionFactory();
        factory.setHost("127.0.0.1");
        factory.setUsername("guest");
        factory.setPassword("guest");

        var connection = factory.newConnection();
        var channel = connection.createChannel();
        channel.exchangeDeclare("demo.event", "fanout", true, true, null);
        if (queueName == null || queueName.isEmpty()) {
            queueName = channel.queueDeclare().getQueue();
        } else {
            queueName = channel.queueDeclare(queueName, true, false, true, null).getQueue();
        }

        System.out.println(recipientName + ": " + queueName);

        channel.queueBind(queueName, "demo.event", "");

        var consumer = new com.rabbitmq.client.DeliverCallback() {
            @Override
            public void handle(String consumerTag, com.rabbitmq.client.Delivery delivery)
                    throws java.io.IOException {
                var message = new String(delivery.getBody(), "UTF-8");
                System.out.println(" [x] Received '" + message + "'");
            }
        };

        channel.basicConsume(queueName, true, consumer, consumerTag -> {
        });

    }
}

消费者必须包含一个入参,用于指定当前消费者的名称,第二个参数可选,用于指定队列名称,如果不指定则由服务器生成。

因为只是做个demo帮助理解,图个方便这里面没有做任何的封装处理,都是直来直去的。其中有些参数根据实际应用场景灵活调整,比如是否自动删除队列和交换机,是否需要持久化消息等等。

3、测试结果

操作步骤

  1. 消费者代码导出成jar包recipient.jar。
  2. 通过以下指令启动三个消费者,其中消费者1、2指定队列名称demo.event.queue。
bash 复制代码
java -jar recipient.jar 消费者1 demo.event.queue
java -jar recipient.jar 消费者2 demo.event.queue
java -jar recipient.jar 消费者3
  1. 运行生产者端代码并发送消息。

具体的截图我就不贴了,结论是和上面.net版本的一样。

四、补充

因为RabbitMQ是跟开发平台无关的中间件,生产者端和消费者端可以采用任何语言开发,因此上面两种语言的例子可以混合运行,生产者端也可以同时运行多个,效果是一样的,不论哪种语言编写的客户端,只要queue相同RabbitMQ始终都只会锁定一个消费者实例进行投递。

上一篇简单讲了RabbitMQ的四种主要 Exchange 类型,其中有一个Topic 类型,它的特点是使用通配符(* 匹配一个词,# 匹配零个或多个词)与消费者的RoutingKey进行模式匹配路由,而我们的demo里面采用的Fanout类型的特点是完全忽略RoutingKey。

咱们思索一个问题,什么场景下可以用Fanout 、什么场景下该用Topic


点关注,不迷路。

如果您喜欢这篇文章,请不要忘记点赞、关注、投币、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论......

相关推荐
武子康2 小时前
Java-211 Spring Boot 2.4.1 整合 RabbitMQ 实战:DirectExchange + @RabbitListener 全流程
java·spring boot·分布式·消息队列·rabbitmq·rocketmq·java-rabbitmq
没有bug.的程序员3 小时前
Ribbon vs LoadBalancer 深度解析
jvm·后端·spring cloud·微服务·ribbon·架构·gc调优
想学后端的前端工程师3 小时前
【分布式系统架构设计实战:从单体到微服务】
微服务·云原生·架构
学海_无涯_苦作舟3 小时前
RabbitMQ Java Client源码解析——FrameHandler
java·rabbitmq·java-rabbitmq
没有bug.的程序员13 小时前
Nacos vs Eureka 服务发现深度对比
jvm·微服务·云原生·容器·eureka·服务发现
云和数据.ChenGuang17 小时前
Logstash配置文件的**语法解析错误**
运维·数据库·分布式·rabbitmq·jenkins
黄俊懿17 小时前
【深入理解SpringCloud微服务】Seata(AT模式)源码解析——全局事务的回滚
java·后端·spring·spring cloud·微服务·架构·架构师
wodet20 小时前
golang实现的批量审核文本服务
微服务·golang
喵了几个咪1 天前
开箱即用的 GoWind Admin|风行,企业级前后端一体中后台框架:分层设计的取舍之道(从 “简单粗暴” 到依赖倒置)
微服务·golang