【前言】

RabbitMQ提供了五种消息模型,分别是简单模型、工作队列模型、发布/订阅模型、路由模型和主题模型。‌‌

  • 简单模型(Simple)‌:在这种模式下,一个生产者将消息发送到一个队列,只有一个消费者监听并处理该队列中的消息。这种模型适用于简单的场景,但存在消息可能未被正确处理就被删除的风险。

  • 工作队列模型(Work Queue)‌:此模型允许多个消费者共同处理队列中的任务,实现负载均衡。生产者将消息发送到队列,多个消费者竞争获取并处理这些消息。这种模型适用于需要高效处理大量任务的场景。

  • ‌发布/订阅模型(Publish/Subscribe)‌:在这种模式下,生产者将消息发送到一个交换机,交换机以广播(Fanout)的形式将消息发送给所有订阅了相应队列的消费者。这种模型适用于需要广播消息给所有感兴趣消费者的场景。

  • 路由模型(Routing)‌:使用direct交换机,生产者发送消息时需要指定路由键,交换机根据路由键将消息路由到相应的队列。这种模型适用于需要对消息进行精确控制的场景。

  • ‌主题模型(Topics)‌:使用topic交换机,支持使用通配符进行模式匹配,生产者发送的消息可以通过特定的路由键匹配到多个队列。这种模型适用于需要灵活匹配消息的场景。

  • 这些模型在应用场景、消息传递方式和交换机使用上有所不同,用户可以根据具体需求选择合适的模型来优化系统的性能和可靠性。


在之前我使用RabbitMQ实现了Direct类型的交换机实现了基础的生产消费。

RabbitMQ的部署请见:https://www.cnblogs.com/sq1201/p/18635209

这篇文章我将基于.NET8.0来实现RabbitMQ的广播模式,以及死信队列的基础用法。

【一】首先创建项目,安装RabbitMQ的包(此处我没有选择最新版,因为最新版全面使用异步,关于IModel也改为了IChannel,最新版的语法有待研究),我的项目结构如下:

在这里延伸一下Asp.netcore的小知识点,以实现灵活而强大的配置管理系统。

1. Microsoft.Extensions.Configuration.Abstractions

功能:

  • 提供配置的基础接口和抽象,定义了与应用程序配置相关的核心机制。

  • 是依赖注入(DI)配置的一部分,为应用程序提供了对配置源的抽象访问。

    包含的核心功能和接口:

  • IConfiguration: 表示应用程序的配置,支持按层级结构访问配置值。

    var value = configuration["MySetting"];

  • IConfigurationSection: 表示配置中的一个具体部分,用于访问嵌套的层级配置。

    var section = configuration.GetSection("MySection");

    var subValue = section["SubSetting"];

  • IConfigurationProvider: 表示一个提供配置值的源(如文件、环境变量等)。

  • IConfigurationRoot: 是配置的根对象,支持动态监控配置变更。

适用场景:

  • 基础配置系统的搭建。
  • 当你需要自定义自己的配置提供程序时(例如:将数据库或远程 API 作为配置源)。

    示例:
using Microsoft.Extensions.Configuration;

var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json")
.AddEnvironmentVariables(); var configuration = builder.Build();
Console.WriteLine(configuration["MySetting"]);

2. Microsoft.Extensions.Configuration.Binder

功能:

  • 提供了扩展方法,用于 将配置绑定到强类型对象。
  • 在读取配置时,通常需要将配置值转换为 C# 对象(例如类或结构),而这个包提供了关键的绑定功能。

包含的核心功能:

  • Bind 方法: 将配置值绑定到自定义类型。
var myOptions = new MyOptions();
configuration.Bind("MySection", myOptions);
  • Get 方法: 从配置中直接返回类型化对象。
var myOptions = configuration.Get<MyOptions>("MySection");
  • GetValue 方法: 直接从配置中获取某个特定的值,并将其转换为指定的类型。
int timeout = configuration.GetValue<int>("Timeout");

适用场景:

  • 强类型配置的支持:当需要将配置文件内容(如 JSON、环境变量)与代码中的类型对应时。
  • 简化复杂配置读取逻辑。

    示例:
using Microsoft.Extensions.Configuration;

public class MyOptions
{
public string Setting1 { get; set; }
public int Setting2 { get; set; }
} // 读取配置
var builder = new ConfigurationBuilder()
.AddJsonFile("appsettings.json"); var configuration = builder.Build(); // 将配置绑定到强类型对象
var options = new MyOptions();
configuration.Bind("MySection", options); Console.WriteLine(options.Setting1);
Console.WriteLine(options.Setting2); // 或者直接获取类型化对象
var options2 = configuration.Get<MyOptions>("MySection");

【二】编写appsettings.json文件

【三】定义有关于配置文件信息的DTO



【四】编写RabbitMQ生产消费通用类,RabbitMQManager类。

点击查看代码
using RabbitMQ.Client.Events;
using RabbitMQ.Client;
using RabbitMQ.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks;
using Microsoft.Extensions.Configuration; namespace RabbitMQ.Service
{
public class RabbitMQManager
{
//使用数组的部分,是给消费端用的,目前生产者只设置了一个,消费者可能存在多个。
/// <summary>
/// RabbitMQ工厂发送端
/// </summary>
private IConnectionFactory _connectionSendFactory;
/// <summary>
/// RabbitMQ工厂接收端
/// </summary>
private IConnectionFactory _connectionReceiveFactory; /// <summary>
/// 连接 发送端
/// </summary>
private IConnection _connectionSend;
/// <summary>
/// 连接 消费端
/// </summary>
private IConnection[] _connectionReceive; /// <summary>
/// MQ配置信息
/// </summary>
public MqConfigInfo _mqConfigs; /// <summary>
/// 通道 发送端
/// </summary>
private IModel _modelSend;
/// <summary>
/// 通道 消费端
/// </summary>
private IModel[] _modelReceive; /// <summary>
/// 事件
/// </summary>
private EventingBasicConsumer[] _basicConsumer; /// <summary>
/// 消费者个数
/// </summary>
public int _costomerCount;
public RabbitMQManager(IConfiguration configuration)
{
_mqConfigs = new MqConfigInfo
{
Host = configuration["MQ:Host"],
Port = Convert.ToInt32(configuration["MQ:Port"]),
User = configuration["MQ:User"],
Password = configuration["MQ:Password"],
ExchangeName = configuration["MQ:ExchangeName"],
DeadLetterExchangeName = configuration["MQ:DeadLetterExchangeName"],
DeadLetterQueueName = configuration["MQ:Queues:2:QueueName"]
};
} /// <summary>
/// 初始化生产者连接
/// </summary>
public void InitProducerConnection()
{
Console.WriteLine("【开始】>>>>>>>>>>>>>>>生产者连接"); _connectionSendFactory = new ConnectionFactory
{
HostName = _mqConfigs.Host,
Port = _mqConfigs.Port,
UserName = _mqConfigs.User,
Password = _mqConfigs.Password
};
if (_connectionSend != null && _connectionSend.IsOpen)
{
return; //已有连接
} _connectionSend = _connectionSendFactory.CreateConnection(); //创建生产者连接 if (_modelSend != null && _modelSend.IsOpen)
{
return; //已有通道
} _modelSend = _connectionSend.CreateModel(); //创建生产者通道 // 声明主交换机 为 Fanout 类型,持久化
_modelSend.ExchangeDeclare(
exchange: _mqConfigs.ExchangeName,
type: ExchangeType.Fanout,
durable: true, // 明确设置为持久化
autoDelete: false,
arguments: null
); // 声明死信交换机 为Fanout类型,持久化
_modelSend.ExchangeDeclare(
exchange: _mqConfigs.DeadLetterExchangeName,
type: ExchangeType.Fanout,
durable: true, // 明确设置为持久化
autoDelete: false,
arguments: null
); // 声明死信队列
_modelSend.QueueDeclare(
queue: _mqConfigs.DeadLetterQueueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null
); // 绑定死信队列到死信交换机
_modelSend.QueueBind(_mqConfigs.DeadLetterQueueName, _mqConfigs.DeadLetterExchangeName, routingKey: ""); Console.WriteLine("【结束】>>>>>>>>>>>>>>>生产者连接");
} /// <summary>
/// 消息发布到交换机(Fanout模式)
/// </summary>
/// <param name="message">消息内容</param>
/// <param name="exchangeName">交换机名称</param>
/// <returns>发布结果</returns>
public async Task<(bool Success, string ErrorMessage)> PublishAsync(string message, string exchangeName)
{
try
{
byte[] body = Encoding.UTF8.GetBytes(message); await Task.Run(() =>
{
_modelSend.BasicPublish(
exchange: exchangeName,
routingKey: string.Empty, // Fanout 模式无需 RoutingKey
basicProperties: null,
body: body
);
}); return (true, string.Empty);
}
catch (Exception ex)
{
return (false, $"发布消息时发生错误: {ex.Message}");
}
} /// <summary>
/// 消费者初始化连接配置
/// </summary>
public void InitConsumerConnections(List<QueueConfigInfo> queueConfigs)
{
Console.WriteLine("【开始】>>>>>>>>>>>>>>>消费者连接"); //创建单个连接工厂
_connectionReceiveFactory = new ConnectionFactory
{
HostName = _mqConfigs.Host,
Port = _mqConfigs.Port,
UserName = _mqConfigs.User,
Password = _mqConfigs.Password
};
_costomerCount = queueConfigs.Sum(q => q.ConsumerCount); // 获取所有队列的消费者总数 // 初始化数组
_connectionReceive = new IConnection[_costomerCount];
_modelReceive = new IModel[_costomerCount];
_basicConsumer = new EventingBasicConsumer[_costomerCount]; int consumerIndex = 0; // 用于跟踪当前消费者索引 foreach (var queueConfig in queueConfigs)
{
for (int i = 0; i < queueConfig.ConsumerCount; i++)
{
string queueName = queueConfig.QueueName; // 创建连接
_connectionReceive[consumerIndex] = _connectionReceiveFactory.CreateConnection();
_modelReceive[consumerIndex] = _connectionReceive[consumerIndex].CreateModel();
_basicConsumer[consumerIndex] = new EventingBasicConsumer(_modelReceive[consumerIndex]); // 声明主交换机(确保交换机存在)
_modelReceive[consumerIndex].ExchangeDeclare(_mqConfigs.ExchangeName, ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null); // 声明死信交换机为 Fanout 类型
_modelReceive[consumerIndex].ExchangeDeclare(_mqConfigs.DeadLetterExchangeName, ExchangeType.Fanout, durable: true, autoDelete: false, arguments: null); if (queueName == _mqConfigs.DeadLetterQueueName)
{
// 死信队列的声明和绑定
_modelReceive[consumerIndex].QueueDeclare(
queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: null
); // 只将死信队列绑定到死信交换机
_modelReceive[consumerIndex].QueueBind(queueName, _mqConfigs.DeadLetterExchangeName, routingKey: "");
}
else
{
// 业务队列的声明和绑定
_modelReceive[consumerIndex].QueueDeclare(
queue: queueName,
durable: true,
exclusive: false,
autoDelete: false,
arguments: new Dictionary<string, object>
{
{ "x-dead-letter-exchange", _mqConfigs.DeadLetterExchangeName },
{ "x-dead-letter-routing-key", "" }
}
); // 只将业务队列绑定到主交换机
_modelReceive[consumerIndex].QueueBind(queueName, _mqConfigs.ExchangeName, routingKey: "");
} // 设置QoS,确保每次只处理一个消息
_modelReceive[consumerIndex].BasicQos(prefetchSize: 0, prefetchCount: 1, global: false); consumerIndex++;
}
} Console.WriteLine("【结束】>>>>>>>>>>>>>>>消费者连接初始化完成");
} /// <summary>
/// 消费者连接
/// </summary>
public async Task ConncetionReceive(int consumeIndex, string exchangeName, string queueName, Func<string, Task> action)
{ await StartListenerAsync(async (model, ea) =>
{
try
{
byte[] message = ea.Body.ToArray();
string msg = Encoding.UTF8.GetString(message);
Console.WriteLine($"队列 {queueName},消费者索引 {consumeIndex} 接收到消息:{msg}"); await action(msg);
_modelReceive[consumeIndex].BasicAck(ea.DeliveryTag, true);//确认消息
}
catch (Exception ex)
{
Console.WriteLine($"处理消息时发生错误: {ex.Message}");
// 拒绝消息且不重新入队,触发死信机制
_modelReceive[consumeIndex].BasicNack(ea.DeliveryTag, false, false);
} }, queueName, consumeIndex);
} /// <summary>
/// 手动确认消费机制
/// </summary>
/// <param name="handler"></param>
/// <param name="queueName"></param>
/// <param name="consumeIndex"></param>
/// <returns></returns>
private async Task StartListenerAsync(AsyncEventHandler<BasicDeliverEventArgs> handler, string queueName, int consumeIndex)
{
_basicConsumer[consumeIndex].Received += async (sender, ea) => await handler(sender, ea);
_modelReceive[consumeIndex].BasicConsume(
queue: queueName,
autoAck: false,
consumer: _basicConsumer[consumeIndex]
); Console.WriteLine($"队列 {queueName} 的消费者 {consumeIndex} 已启动监听");
} }
}

【五】编写 RabbitMQService服务类,负责初始化 RabbitMQManager,并调用其方法完成 RabbitMQ 的连接和配置。

using Microsoft.Extensions.Configuration;
using RabbitMQ.Model;
using System;
using System.Collections.Generic;
using System.Linq;
using System.Text;
using System.Threading.Tasks; namespace RabbitMQ.Service
{
public class RabbitMQService
{
private readonly RabbitMQManager _rabbitmqManager; public RabbitMQService(IConfiguration configuration)
{
_rabbitmqManager = new RabbitMQManager(configuration); //初始化生产者连接
_rabbitmqManager.InitProducerConnection(); var queueConfigs = configuration.GetSection("MQ:Queues").Get<List<QueueConfigInfo>>(); //初始化消费者连接
_rabbitmqManager.InitConsumerConnections(queueConfigs);
} public RabbitMQManager Instance => _rabbitmqManager;
}
}

【六】编写ActionService服务类,来实现模拟的消费者调用方法以及死信队列处理死信消息的逻辑

点击查看代码
namespace RabbitMQ.Service
{
public class ActionService
{
/// <summary>
/// 付款
/// </summary>
public async Task ExActionOne(string msg)
{
Console.WriteLine($"消费成功了【{msg}】消息以后正在执行付款操作");
// 模拟失败条件
if (msg.Contains("fail"))
{
throw new Exception("Simulated processing failure in ExActionOne");
}
await Task.Delay(1000); // 替换 Thread.Sleep
}
/// <summary>
/// 库存扣减
/// </summary>
public async Task ExActionTwo(string msg)
{
Console.WriteLine($"消费成功了【{msg}】消息以后正在执行库存扣减操作");
// 模拟失败条件
if (msg.Contains("fail"))
{
throw new Exception("Simulated processing failure in ExActionTwo");
}
await Task.Delay(1000); // 替换 Thread.Sleep
}
/// <summary>
/// 处理死信队列的消息
/// </summary>
public async Task ExActionDeadLetter(string message)
{
Console.WriteLine($"处理死信消息: {message}");
// 在这里可以记录日志、发送通知等
await Task.Delay(1000);
}
}
}

【七】在启动项Program文件中注入我们的服务注册为单例模式

【八】在Webapi控制器中编写测试方法,模拟实现给主交换机发送消息,当消费失败的时候,消息被发送到死信队列由死信队列消费进行后续操作

namespace RabbitMQ.Core.Controllers
{
[Route("api/[controller]/[action]")]
[ApiController]
public class TestController : ControllerBase
{
private readonly RabbitMQService _rabbitmqService;
private readonly IConfiguration _configuration; public TestController(RabbitMQService rabbitmqService, IConfiguration configuration)
{
_rabbitmqService = rabbitmqService;
_configuration = configuration;
}
/// <summary>
/// 测试rabbitmq发送消息
/// </summary>
/// <returns></returns>
[HttpPost]
public async Task<IActionResult> TestRabbitMqPublishMessage(string pubMessage)
{ var result = await _rabbitmqService.Instance.PublishAsync(
pubMessage,
_configuration["MQ:ExchangeName"]
); if (!result.Success)
{
Console.WriteLine($"【生产者】消息发送失败:{result.ErrorMessage}");
} Console.WriteLine("【生产者】消息发送完成"); return Ok();
}
}
}

【九】演示结果



PS:我这里的死信交换机声明的还是广播模式,其实对于死信队列来说,交换机声明为Direct模式,使用RouteKey去匹配队列也是完全没问题的,而且针对于一条消息,不同队列有不同的消费结果,具体实现场景是所有队列的消费者都消费失败以后才算是一个失败的消息还是说有队列消费成功就不算是失败消息,这都是要结合实际业务场景去进行构思的。

基于.NET8.0实现RabbbitMQ的Publish/Subscribe发布订阅以及死信队列的更多相关文章

  1. php redis pub/sub(Publish/Subscribe,发布/订阅的信息系统)之基本使用

    一.场景介绍 最近的一个项目需要用到发布/订阅的信息系统,以做到最新实时消息的通知.经查找后发现了redis pub/sub(发布/订阅的信息系统)可以满足我的开发需求,而且学习成本和使用成本也比较低 ...

  2. 译: 3. RabbitMQ Spring AMQP 之 Publish/Subscribe 发布和订阅

    在第一篇教程中,我们展示了如何使用start.spring.io来利用Spring Initializr创建一个具有RabbitMQ starter dependency的项目来创建spring-am ...

  3. SpringCloud 2020.0.4 系列之 Stream 消息出错重试 与 死信队列 的实现

    1. 概述 老话说的好:出错不怕,怕的是出了错,却不去改正.如果屡次出错,无法改对,就先记下了,然后找援军解决. 言归正传,今天来聊一下 Stream 组件的 出错重试 和 死信队列. RabbitM ...

  4. 理解 Redis(9) - Publish Subscribe 消息订阅

    在窗口1开通一个名为 redis 的通道: 127.0.0.1:6379> SUBSCRIBE redis Reading messages... (press Ctrl-C to quit) ...

  5. 就publish/subscribe功能看redis集群模式下的队列技术(一)

    Redis 简介 Redis 是完全开源免费的,是一个高性能的key-value数据库. Redis 与其他 key - value 缓存产品有以下三个特点: Redis支持数据的持久化,可以将内存中 ...

  6. 把酒言欢话聊天,基于Vue3.0+Tornado6.1+Redis发布订阅(pubsub)模式打造异步非阻塞(aioredis)实时(websocket)通信聊天系统

    原文转载自「刘悦的技术博客」https://v3u.cn/a_id_202 "表达欲"是人类成长史上的强大"源动力",恩格斯早就直截了当地指出,处在蒙昧时代即低 ...

  7. 使用Guava EventBus构建publish/subscribe系统

    Google的Guava类库提供了EventBus,用于提供一套组件内publish/subscribe的解决方案.事件总线EventBus,用于管理事件的注册和分发.在系统中,Subscribers ...

  8. 【RabbitMQ】Publish/Subscribe

    Publish/Subscribe 在上一节我们创建了一个work queue.背后的设想为每个任务被分发给明确的消费者.这节内容我们将做一些完全不同的事情 -- 我们将发送一条消息给多个消费者.这种 ...

  9. publish/subscribe

    Pub/Sub功能 Pub/Sub功能(means Publish, Subscribe)即发布及订阅功能.基于事件的系统中,Pub/Sub是目前广泛使用的通信模型,它采用事件作为基本的通信机制,提供 ...

  10. Mina、Netty、Twisted一起学(七):发布/订阅(Publish/Subscribe)

    消息传递有很多种方式,请求/响应(Request/Reply)是最常用的.在前面的博文的例子中,很多都是采用请求/响应的方式,当服务器接收到消息后,会立即write回写一条消息到客户端.HTTP协议也 ...

随机推荐

  1. JS 通过年份获取月,季度,半年度,年度

    原文请关注公众号 "酒酒酒酒"​,关注公众号 回复  "JS 通过年份获取月,季度,半年度,年度" 可获取源代码 功能描述: 实例化一个函数,给函数内传递不同的 ...

  2. Dbeaver24.2.2安装和使用教程(免费的数据库管理工具)

    前言 DBeaver是免费和开源(GPL)为开发人员和数据库管理员通用数据库工具. DBeaver 通过 JDBC 连接到数据库,可以支持几乎所有的数据库产品,包括:MySQL.PostgreSQL. ...

  3. 一文彻底搞定Spring Security 认证,实现登陆登出功能

    Spring Security 是一个强大且灵活的安全框架,提供了身份验证(认证)和授权(授权)功能.下面我们将详细介绍 Spring Security 的认证功能流程,并提供自定义实现登录接口的示例 ...

  4. 3.12 Linux创建文件及修改文件时间戳(touch命令)

    既然知道了如何在 Linux 系统中创建目录,接下来你可能会想在这些目录中创建一些文件,可以使用 touch 命令. 需要注意的是,touch 命令不光可以用来创建文件(当指定操作文件不存在时,该命令 ...

  5. 连接数据库报错的异常可以用mysqli_report来捕获

    有时候数据库密码改了或者数据库删了,就会有一个mysqli的链接报错,是因为直接使用了类似代码 $connection = new mysqli('127.0.0.1', 'test_user', ' ...

  6. mysql8创建用户

    create user test_user@'%' identified by 'test2022@'; grant all privileges on test.* to test_user@'%' ...

  7. java中并发包简要分析01

    参考<分布式java应用>一书,简单过一遍并发包(java.util.concurrent) ConcurrentHashMap ConcurrentHashMap是线程安全的HashMa ...

  8. Blazor 组件库 BootstrapBlazor 中Carousel组件介绍

    组件介绍 Carousel 走马灯的作用是在有限空间内,循环播放同一类型的图片.文字等内容. 代码如下: <Carousel Images="@Images" Width=& ...

  9. Codeforces Round 856 (Div2)

    Counting Factorizations 任何一个正整数 \(m\) 都可以被唯一的分解为 \(p_1^{e_1} \cdot p_2^{e_2} \ldots p_k^{e_k}\) 的形式. ...

  10. #oscp#渗透测试 kioptix level 3靶机getshell及提权教程

    声明! 文章所提到的网站以及内容,只做学习交流,其他均与本人以及泷羽sec团队无关,切勿触碰法律底线,否则后果自负!!!! 一.靶机搭建 点击扫描虚拟机 选择靶机使在文件夹即可 二.信息收集 前言 信 ...