一、问题背景:微服务中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%用合适的模式优雅解决。这比在一个错误的边界上反复修补要省力得多。
评论
围绕“EF Core在微服务中按聚合拆分DbContext:边界划分错误导致跨上下文事务补偿难题”参与讨论