一、问题背景:微服务中DbContext的聚合拆分

在微服务架构中,数据隔离是核心原则之一。每个微服务都应该拥有自己独立的数据存储,互不干涉。而在.NET生态中,EF Core作为最主流的对象关系映射框架,其DbContext是数据访问的核心入口。当我们将单体应用拆分为微服务时,一个常见的做法就是按照业务聚合来拆分DbContext,每个聚合对应一个独立的DbContext。

听起来似乎很简单,但在实际落地过程中,边界划分错误会成为最常见的陷阱之一。边界划错了,就会导致一个聚合的数据被分散到多个DbContext中,进而引发跨上下文事务补偿的难题。这就像你把一个家庭的户口本信息分到了两个派出所,当需要修改这个家庭的信息时,就必须同时去两个地方办理手续,任何一个地方失败,整个流程就要回滚。

1.1 聚合与DbContext的正确对应关系

在领域驱动设计中,聚合是一组被当作一个整体来操作的对象集合。聚合根是这个集合的入口,外部对象只能通过聚合根来访问内部的实体。一个健康的设计中,一个聚合应该对应一个DbContext,也就是说,这个聚合内的所有数据都应该在同一个数据库事务中被读写。

举个例子,假设我们有一个订单系统。一个订单包含订单头和订单明细,订单头记录了客户信息、下单时间等,订单明细记录了每样商品的数量和价格。订单头和订单明细就构成了一个完整的订单聚合。当我们创建订单时,订单头和订单明细必须在同一个事务中写入数据库,要么都成功,要么都失败。如果用两个不同的DbContext分别管理订单头和订单明细,那么在订单头写入成功后、订单明细写入失败的情况下,就会出现脏数据。

1.2 边界划分错误的常见表现

边界划分错误通常表现为以下几种情况。第一种情况是将同一个聚合的实体拆分到了多个DbContext中,比如把订单头放到了订单服务里,把订单明细放到了商品服务里。第二种情况是没有识别出真正的聚合边界,把不同聚合的数据混在了同一个DbContext中,导致服务之间的耦合度仍然很高。第三种情况是聚合边界模糊,开发者不确定某个实体应该归属于哪个聚合,最终做出了错误的划分。

二、典型错误案例:跨上下文事务难题

为了让大家更直观地理解这个问题,我们来看一个具体的错误案例。假设我们有一个电商系统,需要处理下单和库存扣减两个业务操作。

技术栈:C# / .NET 6 / EF Core / ASP.NET Core

// ===== 错误的做法:一个聚合被拆分到多个DbContext中 =====

// 订单聚合根 —— 本应是一个完整的聚合
public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public List<OrderItem> Items { get; set; } = new(); // 订单明细是订单聚合的一部分
}

// 订单明细 —— 应该和Order在同一个聚合中
public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
}

// 错误的DbContext拆分:订单头和明细被分离
public class OrderHeadDbContext : DbContext
{
    // 只管理订单头,没有订单明细
    public DbSet<Order> Orders { get; set; } = null!;

    public OrderHeadDbContext(DbContextOptions<OrderHeadDbContext> options)
        : base(options) { }
}

public class OrderItemDbContext : DbContext
{
    // 只管理订单明细,没有订单头的完整关系
    public DbSet<OrderItem> OrderItems { get; set; } = null!;

    public OrderItemDbContext(DbContextOptions<OrderItemDbContext> options)
        : base(options) { }
}

上面这段代码展示了一个典型的边界划分错误。订单头和订单明细明明属于同一个聚合,却被拆分到了两个不同的DbContext中。接下来看看在服务层调用时会发生什么。

// 使用错误设计的DbContext来创建订单 —— 会出现跨上下文事务问题
public class OrderService
{
    private readonly OrderHeadDbContext _orderHeadContext;
    private readonly OrderItemDbContext _orderItemContext;

    public OrderService(OrderHeadDbContext orderHeadContext,
        OrderItemDbContext orderItemContext)
    {
        _orderHeadContext = orderHeadContext;
        _orderItemContext = orderItemContext;
    }

    // 创建订单时需要在两个DbContext中操作
    public async Task<int> CreateOrderAsync(string customerName,
        List<(string ProductName, int Quantity, decimal UnitPrice)> items)
    {
        // 步骤一:在第一个DbContext中保存订单头
        var order = new Order
        {
            CustomerName = customerName,
            CreatedAt = DateTime.UtcNow
        };
        _orderHeadContext.Orders.Add(order);
        await _orderHeadContext.SaveChangesAsync(); // 第一次数据库写入

        // 步骤二:在第二个DbContext中保存订单明细
        foreach (var (productName, quantity, unitPrice) in items)
        {
            var item = new OrderItem
            {
                OrderId = order.Id,
                ProductName = productName,
                Quantity = quantity,
                UnitPrice = unitPrice
            };
            _orderItemContext.OrderItems.Add(item);
        }
        await _orderItemContext.SaveChangesAsync(); // 第二次数据库写入

        return order.Id;
    }
}

这段代码的问题非常明显。创建订单被分成了两步,分别通过两个不同的DbContext执行两次数据库写入操作。如果第一步成功了但第二步失败了,数据库中就会留下一个没有明细的订单头,这就是数据不一致。更糟糕的是,因为使用了两个不同的DbContext,EF Core无法将它们放入同一个数据库事务中。

// 尝试用分布式事务来补救 —— 仍然有问题
public class OrderServiceWithDistributedTransaction
{
    private readonly OrderHeadDbContext _orderHeadContext;
    private readonly OrderItemDbContext _orderItemContext;

    public OrderServiceWithDistributedTransaction(
        OrderHeadDbContext orderHeadContext,
        OrderItemDbContext orderItemContext)
    {
        _orderHeadContext = orderHeadContext;
        _orderItemContext = orderItemContext;
    }

    public async Task<int> CreateOrderWithDTCAssync(string customerName,
        List<(string ProductName, int Quantity, decimal UnitPrice)> items)
    {
        using var transactionScope = new System.Transactions.TransactionScope(
            System.Transactions.TransactionScopeAsyncFlowOption.Enabled);

        try
        {
            var order = new Order
            {
                CustomerName = customerName,
                CreatedAt = DateTime.UtcNow
            };
            _orderHeadContext.Orders.Add(order);
            await _orderHeadContext.SaveChangesAsync();

            foreach (var (productName, quantity, unitPrice) in items)
            {
                var item = new OrderItem
                {
                    OrderId = order.Id,
                    ProductName = productName,
                    Quantity = quantity,
                    UnitPrice = unitPrice
                };
                _orderItemContext.OrderItems.Add(item);
            }
            await _orderItemContext.SaveChangesAsync();

            await transactionScope.CompleteAsync(); // 标记事务完成
            return order.Id;
        }
        catch
        {
            // 异常时TransactionScope会自动回滚
            throw;
        }
    }
}

即使使用了分布式事务(TransactionScope),在微服务架构中也存在严重的问题。首先,不同微服务可能使用不同的数据库实例,TransactionScope依赖的DTC(分布式事务协调器)在现代云原生环境中很难部署和扩展。其次,分布式事务的性能开销很大,会显著降低系统的吞吐量。最后,当网络出现短暂故障时,分布式事务很容易超时,导致整个系统阻塞。

三、正确的边界划分策略

明确了问题之后,我们来看正确的做法。核心原则很简单:一个聚合对应一个DbContext,一个聚合内的所有数据操作都在同一个事务中完成。

3.1 聚合根与DbContext的一一对应

正确的设计中,订单聚合(包括订单头和订单明细)应该被同一个DbContext管理。这样,所有的数据操作都在同一个数据库连接和同一个事务中执行,EF Core能确保原子性。

// ===== 正确的做法:一个聚合对应一个DbContext =====

// 订单聚合根 —— 保持不变
public class Order
{
    public int Id { get; set; }
    public string CustomerName { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; }
    public List<OrderItem> Items { get; set; } = new();
}

// 订单明细 —— 仍然是聚合内的一部分
public class OrderItem
{
    public int Id { get; set; }
    public int OrderId { get; set; }
    public string ProductName { get; set; } = string.Empty;
    public int Quantity { get; set; }
    public decimal UnitPrice { get; set; }
    public Order? Order { get; set; } // 导航属性:指向所属订单
}

// 正确的DbContext设计:一个聚合一个DbContext
public class OrderDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; } = null!;
    public DbSet<OrderItem> OrderItems { get; set; } = null!;

    public OrderDbContext(DbContextOptions<OrderDbContext> options)
        : base(options) { }

    // 配置实体关系和表映射
    protected override void OnModelCreating(ModelBuilder modelBuilder)
    {
        base.OnModelCreating(modelBuilder);

        // 配置订单和订单明细的一对多关系
        modelBuilder.Entity<Order>(entity =>
        {
            entity.HasKey(e => e.Id);
            entity.HasMany(e => e.Items)
                .WithOne(i => i.Order)
                .HasForeignKey(i => i.OrderId);
        });

        modelBuilder.Entity<OrderItem>(entity =>
        {
            entity.HasKey(e => e.Id);
        });
    }
}

现在来看看在正确的设计下,服务层的代码变得多么简洁。

// 使用正确设计的DbContext来创建订单 —— 事务天然保证
public class OrderServiceCorrect
{
    private readonly OrderDbContext _context;

    public OrderServiceCorrect(OrderDbContext context)
    {
        _context = context;
    }

    // 创建订单:所有操作在同一个DbContext中完成
    public async Task<int> CreateOrderAsync(string customerName,
        List<(string ProductName, int Quantity, decimal UnitPrice)> items)
    {
        // 构建完整的订单聚合
        var order = new Order
        {
            CustomerName = customerName,
            CreatedAt = DateTime.UtcNow,
            Items = items.Select(item => new OrderItem
            {
                ProductName = item.ProductName,
                Quantity = item.Quantity,
                UnitPrice = item.UnitPrice
            }).ToList()
        };

        // 只需要一次数据库写入
        _context.Orders.Add(order);
        await _context.SaveChangesAsync(); // EF Core自动处理级联保存

        return order.Id;
    }
}

注意上面代码中的关键变化。我们不再需要分别保存订单头和订单明细,EF Core会自动识别导航属性关系,在一次SaveChanges调用中完成整个聚合的持久化。这就是聚合边界划分正确的力量。

3.2 微服务间的聚合拆分

在微服务架构中,我们按业务边界拆分聚合,每个微服务拥有自己独立的DbContext。以下是一个更完整的微服务场景示例。

// ===== 微服务架构下的多聚合多DbContext设计 =====

// —— 订单微服务 ——

// 订单微服务的DbContext:管理订单聚合
public class OrderServiceDbContext : DbContext
{
    public DbSet<Order> Orders { get; set; } = null!;
    public DbSet<OrderItem> OrderItems { get; set; } = null!;

    public OrderServiceDbContext(DbContextOptions<OrderServiceDbContext> options)
        : base(options) { }
}

// —— 库存微服务 ——

public class Product
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public decimal Price { get; set; }
    public int AvailableStock { get; set; }
}

// 库存微服务的DbContext:管理商品聚合
public class ProductServiceDbContext : DbContext
{
    public DbSet<Product> Products { get; set; } = null!;

    public ProductServiceDbContext(DbContextOptions<ProductServiceDbContext> options)
        : base(options) { }
}

// —— 用户微服务 ——

public class Customer
{
    public int Id { get; set; }
    public string Name { get; set; } = string.Empty;
    public string Email { get; set; } = string.Empty;
    public int CreditScore { get; set; }
}

// 用户微服务的DbContext:管理用户聚合
public class CustomerServiceDbContext : DbContext
{
    public DbSet<Customer> Customers { get; set; } = null!;

    public CustomerServiceDbContext(DbContextOptions<CustomerServiceDbContext> options)
        : base(options) { }
}

在上面的设计中,每个微服务都有自己独立的DbContext,管理着各自的聚合。订单微服务管理订单聚合,库存微服务管理商品聚合,用户微服务管理用户聚合。每个聚合内部的数据一致性由各自的DbContext和数据库事务来保证。

四、跨上下文事务补偿方案

当边界划分正确后,聚合内部的事务问题解决了。但微服务之间仍然需要协作,这就引出了跨服务的一致性保证问题。当创建订单时,我们需要同时调用库存服务扣减库存,如果订单创建成功了但库存扣减失败了怎么办?

4.1 Saga模式:分布式事务的替代方案

Saga是一种补偿事务模式,它将一个长事务拆分成多个小事务,每个小事务都有对应的补偿操作。如果某个步骤失败了,就执行前面步骤的补偿操作来回滚。

// ===== Saga模式实现:订单创建的多步骤事务 =====

// 定义一个Saga编排器
public class CreateOrderSaga
{
    private readonly OrderServiceDbContext _orderContext;
    private readonly HttpClient _httpClient; // 用于调用其他微服务

    public CreateOrderSaga(OrderServiceDbContext orderContext,
        HttpClient httpClient)
    {
        _orderContext = orderContext;
        _httpClient = httpClient;
    }

    // 执行创建订单的完整流程
    public async Task<bool> ExecuteAsync(string customerName,
        List<(string ProductName, int Quantity, decimal UnitPrice)> items)
    {
        string? createdOrderId = null; // 用于补偿时引用
        var reservedStocks = new List<string>(); // 记录已预留的库存

        try
        {
            // 步骤1:预留库存 —— 调用库存微服务
            foreach (var (productName, quantity, unitPrice) in items)
            {
                var response = await _httpClient.PostAsJsonAsync(
                    "/api/stock/reserve",
                    new { productName, quantity });

                if (!response.IsSuccessStatusCode)
                {
                    throw new InvalidOperationException(
                        $"库存预留失败:{productName}");
                }

                var result = await response.Content
                    .ReadFromJsonAsync<StockReservationResult>();
                reservedStocks.Add(result!.ReservationId);
            }

            // 步骤2:创建订单 —— 本地操作
            var order = new Order
            {
                CustomerName = customerName,
                CreatedAt = DateTime.UtcNow,
                Items = items.Select(item => new OrderItem
                {
                    ProductName = item.ProductName,
                    Quantity = item.Quantity,
                    UnitPrice = item.UnitPrice
                }).ToList()
            };

            _orderContext.Orders.Add(order);
            await _orderContext.SaveChangesAsync();
            createdOrderId = order.Id.ToString();

            return true; // 全部成功
        }
        catch (Exception)
        {
            // 补偿:按反顺序回滚已执行的步骤
            await CompensateAsync(createdOrderId, reservedStocks);
            throw;
        }
    }

    // 补偿逻辑:逆序执行回滚操作
    private async Task CompensateAsync(string? orderId,
        List<string> reservationIds)
    {
        // 补偿步骤2:删除已创建的订单
        if (orderId != null)
        {
            var order = await _orderContext.Orders
                .FindAsync(int.Parse(orderId));
            if (order != null)
            {
                _orderContext.Orders.Remove(order);
                await _orderContext.SaveChangesAsync();
                Console.WriteLine($"已补偿:删除订单 {orderId}");
            }
        }

        // 补偿步骤1:释放已预留的库存
        foreach (var reservationId in reservationIds.AsEnumerable().Reverse())
        {
            await _httpClient.PostAsync(
                $"/api/stock/release/{reservationId}", null);
            Console.WriteLine($"已补偿:释放库存预留 {reservationId}");
        }
    }
}

// 库存预留响应
public class StockReservationResult
{
    public string ReservationId { get; set; } = string.Empty;
    public string ProductName { get; set; } = string.Empty;
    public int Quantity { get; set; }
}

这段代码展示了Saga模式的核心思想。创建订单被拆成了两个步骤:预留库存和创建订单记录。如果任何一个步骤失败,就执行补偿操作来回滚。补偿操作是按照反顺序执行的,先删除订单,再释放库存预留。

4.2 Outbox模式:保证事件发布的事务一致性

在实际项目中,另一个常见的跨服务协作场景是事件发布。当我们创建订单后,需要发布一个订单创建事件,通知其他服务做出响应。但如果订单保存成功了,事件发布失败了怎么办?Outbox模式可以解决这个问题。

// ===== Outbox模式:保证事件发布与数据写入的一致性 =====

// Outbox记录表 —— 待发布的事件
public class OutboxMessage
{
    public Guid Id { get; set; } = Guid.NewGuid();
    public string EventType { get; set; } = string.Empty;
    public string Payload { get; set; } = string.Empty;
    public DateTime CreatedAt { get; set; } = DateTime.UtcNow;
    public DateTime? ProcessedAt { get; set; } // 已处理时间
    public string? Error { get; set; } // 错误信息
    public int RetryCount { get; set; } = 0; // 重试次数
}

// 扩展订单DbContext,增加Outbox表
public class OrderDbContextWithOutbox : OrderServiceDbContext
{
    public DbSet<OutboxMessage> OutboxMessages { get; set; } = null!;

    public OrderDbContextWithOutbox(
        DbContextOptions<OrderDbContextWithOutbox> options)
        : base(options) { }
}

// 订单创建事件
public class OrderCreatedEvent
{
    public Guid EventId { get; set; }
    public int OrderId { get; set; }
    public string CustomerName { get; set; } = string.Empty;
    public DateTime OrderDate { get; set; }
}

// 使用Outbox模式的订单服务
public class OrderServiceWithOutbox
{
    private readonly OrderDbContextWithOutbox _context;
    private readonly IEventPublisher _eventPublisher;

    public OrderServiceWithOutbox(OrderDbContextWithOutbox context,
        IEventPublisher eventPublisher)
    {
        _context = context;
        _eventPublisher = eventPublisher;
    }

    public async Task<int> CreateOrderAsync(string customerName,
        List<(string ProductName, int Quantity, decimal UnitPrice)> items)
    {
        // 在同一个事务中:保存订单 + 写入Outbox消息
        using var transaction = await _context.Database
            .BeginTransactionAsync();

        try
        {
            // 保存订单数据
            var order = new Order
            {
                CustomerName = customerName,
                CreatedAt = DateTime.UtcNow,
                Items = items.Select(item => new OrderItem
                {
                    ProductName = item.ProductName,
                    Quantity = item.Quantity,
                    UnitPrice = item.UnitPrice
                }).ToList()
            };
            _context.Orders.Add(order);

            // 构建要发布的事件
            var orderEvent = new OrderCreatedEvent
            {
                EventId = Guid.NewGuid(),
                OrderId = order.Id,
                CustomerName = customerName,
                OrderDate = order.CreatedAt
            };

            // 将事件写入Outbox表(而不是直接发布)
            var outboxMessage = new OutboxMessage
            {
                EventType = typeof(OrderCreatedEvent).Name,
                Payload = JsonSerializer.Serialize(orderEvent)
            };
            _context.OutboxMessages.Add(outboxMessage);

            // 一次提交:订单和事件消息同时落库
            await _context.SaveChangesAsync();
            await transaction.CommitAsync();

            return order.Id;
        }
        catch
        {
            await transaction.RollbackAsync();
            throw;
        }
    }

    // 后台任务:将Outbox消息发布到消息队列
    public async Task ProcessOutboxMessagesAsync()
    {
        // 获取待处理的消息
        var pendingMessages = await _context.OutboxMessages
            .Where(m => m.ProcessedAt == null && m.RetryCount < 3)
            .OrderBy(m => m.CreatedAt)
            .Take(100)
            .ToListAsync();

        foreach (var message in pendingMessages)
        {
            try
            {
                // 发布到消息队列(如RabbitMQ、Azure Service Bus等)
                await _eventPublisher.PublishAsync(message.EventType,
                    message.Payload);

                // 标记为已处理
                message.ProcessedAt = DateTime.UtcNow;
                await _context.SaveChangesAsync();
            }
            catch (Exception ex)
            {
                // 记录错误并增加重试计数
                message.Error = ex.Message;
                message.RetryCount++;
                await _context.SaveChangesAsync();
            }
        }
    }
}

// 事件发布接口
public interface IEventPublisher
{
    Task PublishAsync(string eventType, string payload);
}

Outbox模式的关键在于将事件消息和业务数据放在同一个事务中提交。这样就能保证:只要订单保存成功了,事件消息一定也写入了数据库。后台有一个定时任务不断检查Outbox表,将待处理的消息发布到消息队列中。即使发布失败,消息还在数据库里,可以不断重试。

五、技术优缺点分析

了解了各种方案之后,我们需要客观地评估它们的优缺点。

正确的聚合边界划分(一个聚合一个DbContext)的优点非常明显。首先,数据一致性得到了天然保证,因为所有操作都在同一个事务中完成,不需要额外的补偿机制。其次,代码结构简单清晰,开发者不需要关心复杂的分布式事务逻辑。第三,性能表现优秀,单次数据库调用就完成了所有操作,没有网络往返开销。

但这种方式也有局限性。当业务确实需要跨聚合操作时,比如创建订单同时要扣减库存,就无法在一个事务内完成。这时候就需要引入Saga或Outbox等模式来协调。

Saga模式的优点在于它不依赖分布式事务基础设施,适合云原生环境。每个步骤都可以独立部署和扩展。缺点是系统复杂度增加,需要编写补偿逻辑,而且补偿操作本身也可能失败,需要处理补偿失败的场景。

Outbox模式的优点是实现简单,事务保证可靠,消息不会丢失。缺点是引入了额外的存储开销,需要额外的后台任务来消费消息,而且消息的有序性和幂等性需要额外处理。

六、注意事项与最佳实践

在实际项目中应用这些模式时,有几个关键问题需要特别注意。

第一,聚合边界的划分需要业务专家和技术人员共同讨论。不要仅从技术角度出发划分聚合,而要深入理解业务规则。一个聚合内部的数据变更必须保持原子性,这是划分聚合的根本依据。

第二,不要在聚合之间使用外键约束。微服务各自拥有独立的数据库,外键约束会引入不必要的耦合。跨服务的数据关联应该通过事件驱动来实现。

第三,Saga中的补偿操作必须是幂等的。因为补偿操作可能因为网络故障而重试,如果补偿操作本身不是幂等的,多次执行就会产生错误的结果。比如释放库存预留时,应该检查该预留是否仍然存在,不存在就忽略。

第四,Outbox消息的发布要保证幂等性。消息可能会被重复投递,消费者需要做幂等处理,通常可以通过事件ID去重来实现。

第五,定期清理已经处理完毕的Outbox消息,避免表数据无限增长。可以设置保留策略,比如处理完成后保留7天然后自动删除。

第六,对于高并发场景,EF Core的DbContext是线程不安全的,每个请求应该使用独立的DbContext实例。可以通过DI容器的Scoped生命周期来保证这一点。

// ===== 正确的DbContext生命周期配置 =====

// 在Program.cs或Startup.cs中配置DI
var builder = WebApplication.CreateBuilder(args);

// DbContext配置为Scoped生命周期 —— 每个HTTP请求一个实例
builder.Services.AddDbContext<OrderDbContextWithOutbox>(options =>
    options.UseSqlServer(builder.Configuration
        .GetConnectionString("OrderDb")));

// HttpClient配置为IHttpClientFactory管理
builder.Services.AddHttpClient<IEventPublisher, EventPublisherImpl>(client =>
{
    client.BaseAddress = new Uri("http://message-queue-service");
    client.Timeout = TimeSpan.FromSeconds(30);
});

// 后台服务:处理Outbox消息
builder.Services.AddHostedService<OutboxMessageProcessor>();

var app = builder.Build();
app.Run();

// Outbox消息处理后台服务
public class OutboxMessageProcessor : BackgroundService
{
    private readonly IServiceScopeFactory _scopeFactory;

    public OutboxMessageProcessor(IServiceScopeFactory scopeFactory)
    {
        _scopeFactory = scopeFactory;
    }

    protected override async Task ExecuteAsync(CancellationToken stoppingToken)
    {
        while (!stoppingToken.IsCancellationRequested)
        {
            try
            {
                // 每个循环创建新的作用域,确保DbContext是新的实例
                using var scope = _scopeFactory.CreateScope();
                var dbContext = scope.ServiceProvider
                    .GetRequiredService<OrderDbContextWithOutbox>();

                var pendingMessages = await dbContext.OutboxMessages
                    .Where(m => m.ProcessedAt == null && m.RetryCount < 5)
                    .OrderBy(m => m.CreatedAt)
                    .Take(50)
                    .ToListAsync(stoppingToken);

                if (pendingMessages.Count > 0)
                {
                    var publisher = scope.ServiceProvider
                        .GetRequiredService<IEventPublisher>();

                    foreach (var message in pendingMessages)
                    {
                        try
                        {
                            await publisher.PublishAsync(
                                message.EventType, message.Payload,
                                stoppingToken);
                            message.ProcessedAt = DateTime.UtcNow;
                        }
                        catch (Exception ex)
                        {
                            message.Error = ex.Message;
                            message.RetryCount++;
                        }
                    }

                    await dbContext.SaveChangesAsync(stoppingToken);
                }
            }
            catch (Exception ex)
            {
                // 记录错误,不影响下一次循环
                Console.WriteLine($"Outbox处理异常:{ex.Message}");
            }

            await Task.Delay(TimeSpan.FromSeconds(5), stoppingToken);
        }
    }
}

七、文章总结

按聚合拆分DbContext是微服务架构中的一个基础但关键的设计决策。边界划分正确,事务管理就简单明了;边界划分错误,就会陷入跨上下文事务补偿的泥潭。

正确的做法是一个聚合对应一个DbContext,确保聚合内部的数据操作具有原子性。跨聚合的协作不要使用分布式事务,而是采用Saga模式配合补偿操作,或者使用Outbox模式保证事件发布的可靠性。

在实际项目中,建议先从正确划分聚合边界做起,这是所有后续设计的基础。在这个基础上,再根据业务需要引入Saga或Outbox等模式来处理跨服务的协作。不要一开始就过度设计,也不要因为赶进度而忽视了聚合边界的正确性。

记住一句话:好的聚合设计让80%的事务问题自然消失,剩下的20%用合适的模式优雅解决。这比在一个错误的边界上反复修补要省力得多。