Brighter V10 支持的新提供程序之一就是 MongoDB。本文将探讨如何结合收件箱(Inbox)和发件箱(Outbox)模式使用它。
MongoDB
MongoDB 是一种流行的 NoSQL 文档数据库,采用灵活的类 JSON 文档存储数据。其可扩展性和高性能使其成为高吞吐量应用的理想选择,其文档模型能自然映射到分布式系统中常用的消息结构。
项目概览
我们将构建一个 .NET 8+ 服务,该服务:
- 从 Kafka 主题消费/生产消息
- 通过 Brighter 处理消息
- 使用 MongoDB 作为持久化收件箱和发件箱,确保所有消息可靠投递
- 配置分布式锁,避免在多节点环境下重复发布消息
必需的 NuGet 包
您可任选以下两种配置风格:
1. 纯 Brighter 包:
- Paramore.Brighter.Inbox.MongoDb
- Paramore.Brighter.Outbox.MongoDb
- Paramore.Brighter.Locking.MongoDb
- Paramore.Brighter.MessagingGateway.Kafka
- Paramore.Brighter.ServiceActivator.Extensions.Hosting
- Paramore.Brighter.Extensions.DependencyInjection
2. Fluent Brighter 封装包:
- Fluent.Brighter.Kafka
- Fluent.Brighter.MongoDb
- Paramore.Brighter.ServiceActivator.Extensions.Hosting
Brighter 核心概念快速回顾
深入配置前,先快速回顾 Brighter 的核心概念:
请求:命令与事件
首要概念是 IRequest 接口,通常由 Command(命令)或 Event(事件)实现。Brighter 的 IAmACommandProcessor 通过这些对象:
- Send(期望恰好一个处理器)
- Publish(期望零或多个处理器)
- Post(发送至外部消息代理)
命令表示待执行的操作(如 CreateOrder),事件表示已发生的事实(如 OrderCreated)。
// 待执行的命令
public class CreateOrder() : Command(Id.Random());
// 已发生的事件
public class OrderCreated() : Event(Id.Random());
// 也可直接实现 IRequest
public class CustomRequest : IRequest { ... }
消息映射器(Message Mapper)
当通过 Post 发送或消费外部代理消息时,需要消息映射器。它负责在 IRequest 对象与 Brighter Message 对象(含代理所需的头部和正文)之间进行双向转换。
Brighter v10+ 默认使用 JSON 映射器,但您可通过实现 IAmAMessageMapper 或 IAmAMessageMapperAsync 提供自定义映射逻辑。
请求处理器(Request Handler)
最后是 RequestHandler(或 RequestHandlerAsync),它包含处理特定 IRequest 的核心业务逻辑。Brighter 的管道模型允许您:
- 链接多个处理器
- 通过特性(Attribute)添加重试、日志等横切关注点
- 应用收件箱模式等机制
同步处理器示例:
public class GreetingHandler : RequestHandler<Greeting>
{
public override Greeting Handle(Greeting @event)
{
Console.WriteLine("===== 你好,{0}", @event.Name);
return base.Handle(@event);
}
}
public class GreetingHandlerAsync : RequestHandlerAsync<GreetingAsync>
{
public override Task<Greeting> HandleAsync(GreetingAsync @event, CancellationToken ct = default)
{
Console.WriteLine("===== 你好,{0}", @event.Name);
return base.Handle(@event);
}
}
配置 Brighter 与 Kafka 集成
以下是连接 Kafka 代理的核心配置(提供两种风格):
使用 Fluent Brighter 风格
services
.AddHostedService<ServiceActivatorHostedService>()
.AddFluentBrighter(opt => opt
.UsingKafka(kf => kf
.SetConnection(c => c
.SetName("sample")
.SetBootstrapServers("localhost:9092")
.SetSecurityProtocol(SecurityProtocol.Plaintext)
.SetSaslMechanisms(SaslMechanism.Plain))
.UseSubscriptions(s => s
.AddSubscription<OrderPlaced>(sb => sb
.SetTopic("order-placed-topic")
.SetConsumerGroupId("order-placed-topic-1")
.SetRequeueCount(3)
.CreateInfrastructureIfMissing()
.UseReactorMode())
.AddSubscription<OrderPaid>(sb => sb
.SetTopic("order-paid-topic")
.SetConsumerGroupId("order-paid-topic-1")
.CreateInfrastructureIfMissing()
.UseReactorMode()))
.UsePublications(p => p
.AddPublication<OrderPaid>(kp => kp
.SetTopic("order-paid-topic")
.CreateTopicIfMissing())
.AddPublication<OrderPlaced>(kp => kp
.SetTopic("order-placed-topic")
.CreateTopicIfMissing()))));
使用纯 Brighter 风格
var connection = new KafkaMessagingGatewayConfiguration
{
Name = "sample",
BootStrapServers = ["localhost:9092"],
SecurityProtocol = SecurityProtocol.Plaintext,
SaslMechanisms = SaslMechanism.Plain,
};
services
.AddHostedService<ServiceActivatorHostedService>()
.AddConsumers(opt =>
{
opt.Subscriptions =
[
new KafkaSubscription<OrderPlaced>(
new SubscriptionName("subscription-orderplaced"),
new ChannelName("order-placed-queue"),
new RoutingKey("order-placed-topic"),
makeChannels: OnMissingChannel.Create,
messagePumpType: MessagePumpType.Reactor,
groupId: "test-1"),
new KafkaSubscription<OrderPaid>(
new SubscriptionName("subscription-orderpaid"),
new ChannelName("order-paid-queue"),
new RoutingKey("order-paid-topic"),
makeChannels: OnMissingChannel.Create,
messagePumpType: MessagePumpType.Reactor,
groupId: "test-2"),
];
opt.DefaultChannelFactory = new ChannelFactory(new KafkaMessageConsumerFactory(connection));
})
.AutoFromAssemblies()
.AddProducers(opt =>
{
opt.ProducerRegistry = new KafkaProducerRegistryFactory(
connection,
[
new KafkaPublication<OrderPaid>
{
MakeChannels = OnMissingChannel.Create,
Topic = new RoutingKey("order-paid-topic"),
},
new KafkaPublication<OrderPlaced>
{
MakeChannels = OnMissingChannel.Create,
Topic = new RoutingKey("order-placed-topic"),
}
]).Create();
});
配置 MongoDB 收件箱/发件箱
为本项目配置 MongoDB 作为收件箱、发件箱和分布式锁存储:
使用 Fluent Brighter 风格
services
.AddFluentBrighter(opt => opt
.UsingMongoDb(pg => pg
.SetConnection(db => db
.SetConnectionString(connectionString)
.SetDatabaseName("brightertests")
.SetInbox("inbox")
.SetOutbox("outbox")
.SetLocking("locking")))
.UseDistributedLock()
.UseInbox()
.UseOutbox())
// ... 其他配置(如 Kafka)放在这里
);
使用纯 Brighter 风格
var configuration = new MongoDbConfiguration(connectionString, "brighter")
{
Inbox = new MongoDbCollectionConfiguration { Name = "inbox" },
Outbox = new MongoDbCollectionConfiguration { Name = "outbox" },
Locking = new MongoDbCollectionConfiguration { Name = "locking" },
};
services
.AddHostedService<ServiceActivatorHostedService>()
.AddConsumers(opt =>
{
opt.InboxConfiguration = new InboxConfiguration(new MongoDbInbox(configuration));
})
.AddProducers(opt =>
{
opt.Outbox = new MongoDbOutbox(configuration);
opt.DistributedLock = new MongoDbLockingProvider(configuration);
opt.ConnectionProvider = typeof(MongoDbConnectionProvider);
opt.TransactionProvider = typeof(MongoDbUnitOfWork);
});
示例:消息与请求处理器
消息定义
public class CreateNewOrder() : Command(Id.Random())
{
public decimal Value { get; set; }
}
public class OrderPlaced() : Event(Id.Random())
{
public string OrderId { get; set; } = string.Empty;
public decimal Value { get; set; }
}
public class OrderPaid() : Event(Id.Random())
{
public string OrderId { get; set; } = string.Empty;
}
请求处理器实现
CreateNewOrderHandler:
- 通过 Send 接收 CreateNewOrder 命令
- 将 OrderPlaced 和 OrderPaid 事件存入发件箱(DepositPost)
- 由后台清理进程异步发布到 Kafka
public class CreateNewOrderHandler(IAmACommandProcessor commandProcessor,
ILogger<CreateNewOrderHandler> logger) : RequestHandler<CreateNewOrder>
{
public override CreateNewOrder Handle(CreateNewOrder command)
{
try
{
var id = Uuid.NewAsString();
logger.LogInformation("正在创建新订单: {OrderId}", id);
// 消息持久化到发件箱,确保可靠投递
commandProcessor.DepositPost(new OrderPlaced { OrderId = id, Value = command.Value });
commandProcessor.DepositPost(new OrderPaid { OrderId = id });
return base.Handle(command);
}
catch (Exception ex)
{
logger.LogError(ex, "数据无效");
throw;
}
}
}
OrderPaidHandler:
- 消费 Kafka 中的 OrderPaid 事件
- 仅记录日志
public class OrderPaidHandler(ILogger<OrderPaidHandler> logger) : RequestHandler<OrderPaid>
{
public override OrderPaid Handle(OrderPaid command)
{
logger.LogInformation("订单 {OrderId} 已支付", command.OrderId);
return base.Handle(command);
}
}
OrderPlaceHandler:
- 模拟失败场景(当订单金额能被 3 整除时抛出异常)
- 通过 [UseResiliencePipeline] 应用重试策略(如 "kafka-policy" 配置重试 3 次)
public class OrderPlaceHandler(ILogger<OrderPlaceHandler> logger) : RequestHandler<OrderPlaced>
{
[UseResiliencePipeline("kafka-policy", 1)] // 应用重试策略
public override OrderPlaced Handle(OrderPlaced command)
{
logger.LogInformation("订单 {OrderId} 已下单,金额 {OrderValue}", command.OrderId, command.Value);
if (command.Value % 3 == 0)
{
logger.LogError("为订单 {OrderId}(金额 {OrderValue})模拟错误", command.OrderId, command.Value);
throw new InvalidOperationException("无效错误");
}
return base.Handle(command);
}
}
此处理器展示了关键机制的协同工作:
- 弹性重试:[UseResiliencePipeline] 特性在处理器内部实现重试逻辑
- 收件箱模式:处理器失败时,消息不会存入收件箱。重置 Kafka 偏移量后,仅未处理的消息会被重新消费,避免重复
- 发件箱模式:DepositPost 将消息持久化到本地存储,即使代理暂时不可用也不会丢失消息
结论
收件箱模式对构建弹性分布式系统至关重要。Paramore.Brighter 结合 MongoDB 收件箱和 Kafka 集成,为 .NET 应用提供了简洁健壮的实现方案。
此架构确保服务在故障时优雅降级,同时保障分布式系统中的数据一致性。
完整示例代码: https://github.com/lillo42/brighter-sample/tree/v10-mongodb
844

被折叠的 条评论
为什么被折叠?



