消息总线接口解耦
消息总线接口解耦:IMessageBus 设计与实现
基于项目 RabbitMQ 链路重构实践
一、为什么要解耦消息总线
1.1 问题场景
在项目中,RabbitMQ 链路的 Publisher 和 Consumer 直接依赖 RabbitMQ.Client 库:
// ❌ 耦合写法:业务代码直接依赖 RabbitMQ.Client
using RabbitMQ.Client;
public class RabbitMqTelemetryPublisher
{
private IConnection? _connection;
private IChannel? _channel;
public async Task PublishAsync(PlayloadData data)
{
ConnectionFactory factory = new ConnectionFactory
{
HostName = "localhost",
Port = 5672,
UserName = "guest",
Password = "guest"
};
_connection = await factory.CreateConnectionAsync();
_channel = await _connection.CreateChannelAsync();
await _channel.QueueDeclareAsync("telemetry.input", durable: true);
await _channel.BasicPublishAsync("", "telemetry.input", body: body);
}
}
问题:
- 业务逻辑与 RabbitMQ.Client API 绑死
- 换 Kafka/EventHub 需要重写整个 Publisher/Consumer
- 单元测试时无法 Mock 消息总线
- 连接管理、重试、序列化等横切关注点散落在业务代码中
1.2 解耦目标
之前: Publisher/Consumer ──→ RabbitMQ.Client (强耦合)
之后: Publisher/Consumer ──→ IMessageBus ──→ RabbitMqMessageBus ──→ RabbitMQ.Client
↑
将来可替换为 KafkaMessageBus / EventHubMessageBus
二、接口设计:IMessageBus
2.1 核心原则
| 原则 | 说明 |
|---|---|
| 接口隔离 | 只暴露业务需要的方法,不泄露传输层细节 |
| 单一职责 | 只管消息收发,不管业务处理 |
| 可测试性 | 方法签名简单,易于 Mock |
| 传输无关 | 不出现 RabbitMQ/Kafka 等具体类型 |
2.2 接口定义
namespace Admin.NET.Core.TimeSeriesStorage.RabbitMq;
/// <summary>
/// 消息总线抽象(解耦具体 MQ 实现)
/// </summary>
public interface IMessageBus : IDisposable
{
/// <summary>
/// 发布消息到指定队列
/// </summary>
/// <param name="queue">队列名</param>
/// <param name="body">消息体(序列化后的字节数组)</param>
/// <param name="persistent">是否持久化(Broker 重启后保留)</param>
Task PublishAsync(string queue, byte[] body, bool persistent = true);
/// <summary>
/// 订阅指定队列,每收到一条消息触发回调
/// </summary>
/// <param name="queue">队列名</param>
/// <param name="onMessage">回调:(deliveryTag, body) => Task</param>
/// <param name="prefetchCount">预取数量(0=不限)</param>
Task SubscribeAsync(string queue, Func<ulong, ReadOnlyMemory<byte>, Task> onMessage, ushort prefetchCount = 0);
/// <summary>
/// 确认消息(手动 ACK)
/// </summary>
Task AckAsync(ulong deliveryTag);
/// <summary>
/// 拒绝消息(手动 NACK/Reject)
/// </summary>
/// <param name="requeue">是否重新入队</param>
Task RejectAsync(ulong deliveryTag, bool requeue = false);
}
2.3 设计决策解析
为什么用 byte[] 而不是 string 或 object?
- MQ 传输层天然是字节流
- 避免接口内部做序列化(序列化策略由调用方决定)
BasicPublishAsync底层也是byte[]
为什么用 Func<ulong, ReadOnlyMemory<byte>, Task> 而不是自定义事件参数?
ulong deliveryTag是 AMQP 标准概念,几乎所有 MQ 都有ReadOnlyMemory<byte>零拷贝,性能好Task支持异步回调,不阻塞消费线程
为什么 SubscribeAsync 不返回 IDisposable?
- 订阅生命周期由
IHostedService管理(StartAsync/StopAsync) - 避免回调地狱式的资源释放
三、RabbitMQ 实现:RabbitMqMessageBus
3.1 完整实现
using Microsoft.Extensions.Logging;
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
namespace Admin.NET.Core.TimeSeriesStorage.RabbitMq;
public class RabbitMqMessageBus : IMessageBus
{
private readonly ILogger<RabbitMqMessageBus> _logger;
private readonly RabbitMqTelemetryOptions _options;
private readonly SemaphoreSlim _publishGate = new(1, 1);
private IConnection? _connection;
private IChannel? _publishChannel;
private IChannel? _consumeChannel;
public RabbitMqMessageBus(ILogger<RabbitMqMessageBus> logger)
{
_logger = logger;
_options = App.GetConfig<RabbitMqTelemetryOptions>("RabbitMqTelemetry")
?? new RabbitMqTelemetryOptions();
}
// ─── 发布 ───────────────────────────────────────────────
public async Task PublishAsync(string queue, byte[] body, bool persistent = true)
{
IChannel channel = await EnsurePublishChannelAsync(queue);
var props = new BasicProperties { Persistent = persistent };
await channel.BasicPublishAsync("", queue, false, props, body, default);
}
// ─── 订阅 ───────────────────────────────────────────────
public async Task SubscribeAsync(string queue,
Func<ulong, ReadOnlyMemory<byte>, Task> onMessage, ushort prefetchCount = 0)
{
_consumeChannel = await EnsureConsumeChannelAsync(queue, prefetchCount);
var consumer = new AsyncEventingBasicConsumer(_consumeChannel);
consumer.ReceivedAsync += async (_, args) =>
{
try
{
await onMessage(args.DeliveryTag, args.Body);
}
catch (Exception ex)
{
_logger.LogError(ex, "消息处理回调异常: {0}", ex.Message);
}
};
await _consumeChannel.BasicConsumeAsync(
queue, autoAck: false, consumerTag: "", noLocal: false,
exclusive: false, arguments: null, consumer: consumer,
cancellationToken: default);
}
// ─── ACK / Reject ───────────────────────────────────────
public async Task AckAsync(ulong deliveryTag)
{
IChannel? ch = _consumeChannel;
if (ch != null && ch.IsOpen)
await ch.BasicAckAsync(deliveryTag, false, default);
}
public async Task RejectAsync(ulong deliveryTag, bool requeue = false)
{
IChannel? ch = _consumeChannel;
if (ch != null && ch.IsOpen)
await ch.BasicRejectAsync(deliveryTag, requeue, default);
}
// ─── 内部:连接/通道管理 ────────────────────────────────
private async Task<IChannel> EnsurePublishChannelAsync(string queue)
{
if (_publishChannel != null && _connection?.IsOpen == true && _publishChannel.IsOpen)
return _publishChannel;
await EnsureConnectionAsync();
_publishChannel = await _connection!.CreateChannelAsync(default);
await _publishChannel.QueueDeclareAsync(queue, durable: true,
exclusive: false, autoDelete: false, arguments: null);
return _publishChannel;
}
private async Task<IChannel> EnsureConsumeChannelAsync(string queue, ushort prefetchCount)
{
if (_consumeChannel != null && _connection?.IsOpen == true && _consumeChannel.IsOpen)
return _consumeChannel;
await EnsureConnectionAsync();
_consumeChannel = await _connection!.CreateChannelAsync(default);
await _consumeChannel.QueueDeclareAsync(queue, durable: true,
exclusive: false, autoDelete: false, arguments: null);
if (prefetchCount > 0)
await _consumeChannel.BasicQosAsync(0, prefetchCount, false, default);
return _consumeChannel;
}
private async Task EnsureConnectionAsync()
{
if (_connection != null && _connection.IsOpen) return;
ResetConnection();
var factory = new ConnectionFactory
{
HostName = _options.HostName,
Port = _options.Port,
UserName = _options.UserName,
Password = _options.Password,
VirtualHost = string.IsNullOrWhiteSpace(_options.VirtualHost)
? "/" : _options.VirtualHost,
AutomaticRecoveryEnabled = true
};
_connection = await factory.CreateConnectionAsync(default);
}
private void ResetConnection()
{
try { _publishChannel?.Dispose(); } catch { }
try { _consumeChannel?.Dispose(); } catch { }
try { _connection?.Dispose(); } catch { }
_publishChannel = null;
_consumeChannel = null;
_connection = null;
}
public void Dispose()
{
_publishGate.Dispose();
ResetConnection();
}
}
3.2 关键设计点
发布/消费分离的 Channel
private IConnection? _connection; // 共享一个连接
private IChannel? _publishChannel; // 发布专用通道
private IChannel? _consumeChannel; // 消费专用通道
为什么分开?
- AMQP 的 channel 是轻量级的(复用 TCP 连接)
- 发布和消费的 QoS 设置不同(消费需要 prefetch,发布不需要)
- 避免 channel 竞争导致的消息丢失
自动重连
AutomaticRecoveryEnabled = true // RabbitMQ.Client 内置自动重连
配合 EnsureConnectionAsync 中的 null/open 检查,断线后自动重建连接和通道。
线程安全
private readonly SemaphoreSlim _publishGate = new(1, 1);
public async Task PublishAsync(...)
{
await _publishGate.WaitAsync();
try { ... }
finally { _publishGate.Release(); }
}
SemaphoreSlim 保证同一时刻只有一个发布操作在使用 channel,避免并发写入导致 AMQP 协议错误。
四、业务代码重构
4.1 Publisher 重构
Before(耦合):
using RabbitMQ.Client;
public class RabbitMqTelemetryPublisher : ISingleton, IDisposable
{
private IConnection? _connection;
private IChannel? _channel;
public async Task PublishAsync(PlayloadData data)
{
await _gate.WaitAsync();
try
{
IChannel channel = await EnsureChannelAsync();
byte[] body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data));
BasicProperties props = new BasicProperties { Persistent = true };
await channel.BasicPublishAsync("", _options.InputQueue, false, props, body, default);
}
catch (Exception ex) { ResetConnection(); /* ... */ }
finally { _gate.Release(); }
}
private async Task<IChannel> EnsureChannelAsync() { /* 50+ 行连接管理 */ }
private void ResetConnection() { /* 20+ 行资源释放 */ }
}
After(解耦):
// 移除了 using RabbitMQ.Client
public class RabbitMqTelemetryPublisher : ISingleton, IDisposable
{
private readonly IMessageBus _messageBus;
public RabbitMqTelemetryPublisher(ILogger<RabbitMqTelemetryPublisher> logger, IMessageBus messageBus)
{
_messageBus = messageBus;
// ...
}
public async Task PublishAsync(PlayloadData data)
{
if (!_enable || data == null) return;
try
{
byte[] body = Encoding.UTF8.GetBytes(JsonSerializer.Serialize(data));
await _messageBus.PublishAsync(_options.InputQueue, body, persistent: true);
}
catch (Exception ex) { _logger.LogError(ex, "..."); }
}
public void Dispose() { } // 生命周期由 DI 管理
}
对比:
| 指标 | Before | After |
|---|---|---|
| 代码行数 | ~90 行 | ~35 行 |
| RabbitMQ 依赖 | using RabbitMQ.Client |
无 |
| 连接管理 | 手动 50+ 行 | 零(RabbitMqMessageBus 内部处理) |
| 可测试性 | 无法 Mock | 可 Mock IMessageBus |
4.2 Consumer 重构
Before(耦合 + 轮询):
using RabbitMQ.Client;
using RabbitMQ.Client.Events;
public class RabbitMqTelemetryConsumer : IHostedService
{
private IConnection? _connection;
private IChannel? _channel;
private async Task ConsumeLoopAsync()
{
while (!_cts.IsCancellationRequested)
{
await EnsureChannelAsync(); // 手动连接管理
await Task.Delay(flushIntervalMs);
await FlushBatchAsync();
}
}
private async Task EnsureChannelAsync() { /* 60+ 行 */ }
private void ResetConnection() { /* 20+ 行 */ }
// FlushBatchAsync 中直接操作 channel
await ch.BasicAckAsync(tag, default);
await ch.BasicRejectAsync(tag, false, default);
}
After(解耦 + 回调驱动):
// 移除了 using RabbitMQ.Client / RabbitMQ.Client.Events
public class RabbitMqTelemetryConsumer : IHostedService
{
private readonly IMessageBus _messageBus;
public Task StartAsync(CancellationToken ct)
{
// 订阅模式:消息到来时触发回调,不再轮询
_consumeTask = _messageBus.SubscribeAsync(
_options.StoreQueue,
OnMessageReceivedAsync, // 回调
(ushort)Math.Min(_batchSize, 65535));
return Task.CompletedTask;
}
private async Task OnMessageReceivedAsync(ulong deliveryTag, ReadOnlyMemory<byte> body)
{
PlayloadData? data = ParseTelemetry(body);
if (data == null)
{
await _messageBus.RejectAsync(deliveryTag); // 通过接口操作
return;
}
// ... 批量逻辑
}
private async Task FlushBatchAsync()
{
// ...
foreach (var (tag, _) in batch)
{
if (success)
await _messageBus.AckAsync(tag); // 通过接口操作
else
await _messageBus.RejectAsync(tag);
}
}
}
对比:
| 指标 | Before | After |
|---|---|---|
| 消费模式 | 轮询(定时器 + EnsureChannel) | 回调驱动(SubscribeAsync) |
| 连接管理 | 手动 80+ 行 | 零 |
| Channel 操作 | 直接 ch.BasicAckAsync |
_messageBus.AckAsync |
| 代码行数 | ~180 行 | ~120 行 |
五、DI 注册
5.1 注册代码
// Startup.cs
using Admin.NET.Core.TimeSeriesStorage.RabbitMq;
services.AddSingleton<IMessageBus, RabbitMqMessageBus>();
services.AddHostedService<RabbitMqTelemetryConsumer>();
5.2 为什么是 Singleton?
| 生命周期 | 原因 |
|---|---|
IMessageBus → Singleton |
共享一个 TCP 连接 + 两个 channel(publish/consume) |
RabbitMqTelemetryPublisher → Singleton |
ISingleton(Furion 自动注册) |
RabbitMqTelemetryConsumer → HostedService |
IHostedService 框架管理 |
如果注册为 Scoped/Transient 会怎样?
- 每次请求创建新连接 → RabbitMQ 连接数爆炸
- Channel 竞争 → AMQP 协议错误
- 内存泄漏 → IDisposable 不会被正确调用
六、RabbitMQ 核心概念回顾
6.1 Connection vs Channel
Application
│
▼
┌──────────┐
│ Connection │ ← TCP 连接(重量级,创建成本高)
└──────────┘
│
├──▶ Channel 1 (Publish) ← 轻量级,AMQP 复用
├──▶ Channel 2 (Consume)
└──▶ Channel 3 (Admin)
- Connection = TCP 连接,创建/销毁成本高
- Channel = 虚拟连接,轻量级,可大量创建
- 一个 Connection 可承载数千个 Channel
- Channel 之间互不影响(独立 QoS、独立 ACK)
6.2 消息持久化三要素
// 1. 队列声明为 durable
await channel.QueueDeclareAsync("queue", durable: true, ...);
// 2. 消息属性设置 persistent
var props = new BasicProperties { Persistent = true };
// 3. 发布时使用持久化属性
await channel.BasicPublishAsync("", "queue", false, props, body);
缺一不可:
- 只有队列 durable → 队列在,消息丢了
- 只有消息 persistent → 消息在,队列丢了
- 两者都设置 → Broker 重启后消息仍在
6.3 手动 ACK vs 自动 ACK
// 自动 ACK:消息发出即确认(可能丢失)
await channel.BasicConsumeAsync(queue, autoAck: true, ...);
// 手动 ACK:业务处理完再确认(可靠)
await channel.BasicConsumeAsync(queue, autoAck: false, ...);
// 处理成功:
await channel.BasicAckAsync(deliveryTag, false);
// 处理失败:
await channel.BasicRejectAsync(deliveryTag, requeue: true);
| 模式 | 优点 | 缺点 |
|---|---|---|
| autoAck=true | 简单,吞吐高 | 消费失败则消息丢失 |
| autoAck=false | 可靠,失败可重试 | 需手动管理 ACK |
6.4 Prefetch(QoS)
// 一次最多预取 100 条,处理完再给新的
await channel.BasicQosAsync(0, prefetchCount: 100, false);
作用:防止快消费者饿死慢消费者。如果不限制,MQ 会一次性把所有消息推给最快的那个消费者。
七、扩展:将来换 Kafka 怎么做
7.1 新增 Kafka 实现
using Confluent.Kafka;
public class KafkaMessageBus : IMessageBus
{
private readonly IProducer<string, byte[]> _producer;
private readonly IConsumer<string, byte[]> _consumer;
public KafkaMessageBus(KafkaOptions options)
{
var producerConfig = new ProducerConfig
{
BootstrapServers = options.BootstrapServers
};
_producer = new ProducerBuilder<string, byte[]>(producerConfig).Build();
var consumerConfig = new ConsumerConfig
{
BootstrapServers = options.BootstrapServers,
GroupId = options.GroupId,
AutoOffsetReset = AutoOffsetReset.Latest
};
_consumer = new ConsumerBuilder<string, byte[]>(consumerConfig).Build();
}
public async Task PublishAsync(string topic, byte[] body, bool persistent = true)
{
await _producer.ProduceAsync(topic, new Message<string, byte[]>
{
Key = Guid.NewGuid().ToString(),
Value = body
});
}
public Task SubscribeAsync(string topic,
Func<ulong, ReadOnlyMemory<byte>, Task> onMessage, ushort prefetchCount = 0)
{
// Kafka 的订阅模型不同(consumer group + offset)
// 需要适配层将 Kafka offset 映射为 deliveryTag
// ...
}
public Task AckAsync(ulong deliveryTag)
{
// Kafka 通过 commit offset 实现 ACK
// 需要维护 deliveryTag → offset 映射
return Task.CompletedTask;
}
public Task RejectAsync(ulong deliveryTag, bool requeue = false)
{
// Kafka 没有 reject 概念,通过 offset 管理实现
return Task.CompletedTask;
}
}
7.2 切换只需改注册
// 切换到 Kafka:
services.AddSingleton<IMessageBus, KafkaMessageBus>();
// 切换到 RabbitMQ:
services.AddSingleton<IMessageBus, RabbitMqMessageBus>();
Publisher 和 Consumer 零改动。
八、单元测试示例
8.1 Mock IMessageBus
public class MockMessageBus : IMessageBus
{
public List<(string Queue, byte[] Body)> Published { get; } = new();
public Func<ulong, ReadOnlyMemory<byte>, Task>? OnReceived { get; set; }
public Task PublishAsync(string queue, byte[] body, bool persistent = true)
{
Published.Add((queue, body));
return Task.CompletedTask;
}
public Task SubscribeAsync(string queue,
Func<ulong, ReadOnlyMemory<byte>, Task> onMessage, ushort prefetchCount = 0)
{
OnReceived = onMessage;
return Task.CompletedTask;
}
public Task AckAsync(ulong deliveryTag) => Task.CompletedTask;
public Task RejectAsync(ulong deliveryTag, bool requeue = false) => Task.CompletedTask;
public void Dispose() { }
}
8.2 测试 Publisher
[Fact]
public async Task PublishAsync_ShouldSendToInputQueue()
{
var mockBus = new MockMessageBus();
var publisher = new RabbitMqTelemetryPublisher(
NullLogger<RabbitMqTelemetryPublisher>.Instance, mockBus);
var data = new PlayloadData { DeviceId = Guid.NewGuid(), MacNo = "M001" };
await publisher.PublishAsync(data);
Assert.Single(mockBus.Published);
Assert.Equal("telemetry.nodered.input", mockBus.Published[0].Queue);
}
8.3 测试 Consumer
[Fact]
public async Task Consumer_FlushBatch_ShouldAckOnSuccess()
{
var mockBus = new MockMessageBus();
var consumer = new RabbitMqTelemetryConsumer(
NullLogger<RabbitMqTelemetryConsumer>.Instance, mockBus);
// 模拟收到消息
var body = JsonSerializer.SerializeToUtf8Bytes(new PlayloadData
{
DeviceId = Guid.NewGuid(),
MsgBody = new Dictionary<string, object> { { "temp", 25.5 } }
});
await mockBus.OnReceived!.Invoke(1UL, body);
// ... 触发 flush
}
九、设计模式总结
| 模式 | 在本项目中的应用 |
|---|---|
| 策略模式 | IMessageBus 定义策略,RabbitMqMessageBus / KafkaMessageBus 是具体策略 |
| 依赖倒置 | 高层模块(Publisher/Consumer)不依赖低层模块(RabbitMQ.Client),都依赖抽象(IMessageBus) |
| 接口隔离 | IMessageBus 只有 4 个方法,不暴露 ConnectionFactory/Channel 等实现细节 |
| 单一职责 | RabbitMqMessageBus 只管连接+收发,Publisher 只管序列化+发布,Consumer 只管解析+批量入库 |
| 工厂模式 | EnsureConnectionAsync / EnsurePublishChannelAsync 按需创建资源 |
十、常见问题
Q1: 为什么不用抽象工厂创建 Channel?
A: 本场景 Channel 生命周期跟 MessageBus 一致(Singleton),不需要工厂。如果 Channel 需要按请求创建(如每个 HTTP 请求独立 Channel),才需要工厂模式。
Q2: Publish 和 Subscribe 共享一个 Channel 行不行?
A: 技术上可以,但不推荐。不同操作的 QoS 设置会互相影响,且一个 channel 异常会导致所有操作中断。分开更安全。
Q3: IMessageBus 是否需要支持 Request/Reply 模式?
A: 当前场景是 Fire-and-Forget(发布即忘),不需要。如果将来需要 RPC 模式,可以在接口中扩展 Task<T> RequestAsync<T>(string queue, byte[] body, TimeSpan timeout) 方法。
Q4: 如何处理消息序列化?
A: IMessageBus 传输 byte[],序列化由调用方决定。Publisher 用 System.Text.Json,Consumer 用 System.Text.Json + 自定义解析。如果需要切换序列化框架(如 MessagePack),改调用方即可,不改 MessageBus。

浙公网安备 33010602011771号