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。
打开控制台/终端,输入
docker pull rabbitmq:management
创建Volumes
docker volume create rabbitmq
创建容器
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引入
<PackageReference Include="RabbitMQ.Client" Version="7.2.0" />
如果项目使用集中版本管理,需要在Directory.Packages.props添加
<PackageVersion Include="RabbitMQ.Client" Version="7.2.0" />
然后在具体项目的csproj添加
<PackageReference Include="RabbitMQ.Client" />
需要注意的是,7.x以后RabbitMQ.Client做了比较大的调整,原先的方法都改成了异步,EventingBasicConsumer也换成了AsyncEventingBasicConsumer,Consumer的Receive事件变成了ReceivedAsync,支持异步事件处理。
2、生产者代码
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、消费者代码
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里面每个标签的代码是完全隔离的,无法引用,所以为了启动多个消费者,我需要把消费者的代码块再复制两份出来。其中一个标签把
var queueName = await channel.QueueDeclareAsync("demo.event.queue", true, false, false).ContinueWith(task => task.Result.QueueName);
改成
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里添加
<dependencies>
<dependency>
<groupId>com.rabbitmq</groupId>
<artifactId>amqp-client</artifactId>
<version>5.25.0</version>
</dependency>
</dependencies>
2、生产者代码
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、消费者代码
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、测试结果
操作步骤
- 消费者代码导出成jar包recipient.jar。
- 通过以下指令启动三个消费者,其中消费者1、2指定队列名称demo.event.queue。
java -jar recipient.jar 消费者1 demo.event.queue
java -jar recipient.jar 消费者2 demo.event.queue
java -jar recipient.jar 消费者3
- 运行生产者端代码并发送消息。
具体的截图我就不贴了,结论是和上面.net版本的一样。
四、补充
因为RabbitMQ是跟开发平台无关的中间件,生产者端和消费者端可以采用任何语言开发,因此上面两种语言的例子可以混合运行,生产者端也可以同时运行多个,效果是一样的,不论哪种语言编写的客户端,只要queue相同RabbitMQ始终都只会锁定一个消费者实例进行投递。
上一篇简单讲了RabbitMQ的四种主要 Exchange 类型,其中有一个Topic类型,它的特点是使用通配符(* 匹配一个词,# 匹配零个或多个词)与消费者的RoutingKey进行模式匹配路由,而我们的demo里面采用的Fanout类型的特点是完全忽略RoutingKey。
咱们思索一个问题,什么场景下可以用Fanout、什么场景下该用Topic?
点关注,不迷路。
如果您喜欢这篇文章,请不要忘记点赞、关注、投币、转发,谢谢!如果您有任何高见,欢迎在评论区留言讨论……


上一篇文章中已经通过一个实际的业务场景结合RabbitMQ的四种交换机类型对RabbitMQ发布订阅模式同一消费者多个实例如何防止重复消费这个问题给出了解决方案。水这篇的目的主要是再给出一个可以直接抄作业的代码。
浙公网安备 33010602011771号