在 Brighter V10 中使用 MongoDB

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

评论
成就一亿技术人!
拼手气红包6.0元
还能输入1000个字符
 
红包 添加红包
表情包 插入表情
 条评论被折叠 查看
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值