EF Core - 多个上下文和事务

7
我们将大型DbContext拆分为更小的上下文,每个上下文都负责一个小的领域边界上下文。这些上下文的保存操作由工作单元进行编排,如下所示。
该领域有两个边界上下文:合作伙伴和员工。工作单元管理两个DbContext:PartnerContextEmployeeContext。我们在事务中运行所有保存操作,以确保操作是原子的。
简化版本的问题可以在github上找到。
public class UnitOfWork {

  public Task SaveChanges(){
      // EmployeeContext begins a transaction and shares it with other contexts
      var strategy = employeeContext.Database.CreateExecutionStrategy();
      return strategy.ExecuteAsync(async () =>
      {
        await using var transaction = await employeeContext.Database.BeginTransactionAsync();
        await partnerContext.Database.UseTransactionAsync(transaction.GetDbTransaction());
        await partnerContext.SaveChangesAsync();
        await employeeContext.SaveChangesAsync();
        await transaction.CommitAsync();
      });
  }
}

以下代码可以正常工作。所有更改都在单个事务中执行。
   var unitOfWork = new unitOfWork();
   ... perform updates to both contexts
   await unitOfWork.SaveChanges();

然而,当尝试第二次保存更改时,以下代码会抛出异常。
   var unitOfWork = new unitOfWork();
   ... perform updates to both contexts
   await unitOfWork.SaveChanges(); <-- Work fine
   ... doing a bit more work
   await unitOfWork.SaveChanges(); <-- Crashes

上述代码行出现了错误信息:连接已经处于一个事务中,无法参与另一个事务。

结果产生的SQL日志为:

**** The first save operation logs start here:
    SET NOCOUNT ON;
    INSERT INTO [Person] ([Discriminator], [ManagerId], [Name])
    VALUES (@p0, @p1, @p2);
    SELECT [Id]
    FROM [Person]
    WHERE @@ROWCOUNT = 1 AND [Id] = scope_identity();
    Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Committing transaction.
    Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Disposing transaction.

**** The second save operation logs start here:
    
    Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opening connection to database 'EF_DDD' on server 'localhost'.
    Microsoft.EntityFrameworkCore.Database.Connection: Debug: Opened connection to database 'EF_DDD' on server 'localhost'.
    Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Beginning transaction with isolation level 'Unspecified'.
    Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Began transaction with isolation level 'ReadCommitted'.
    Microsoft.EntityFrameworkCore.Database.Transaction: Debug: Disposing transaction.

请问有人知道第二个 unitOfWork.SaveChanges() 为什么会抱怨有一个未关闭的事务吗?虽然第一个事务已经提交并且被销毁了(正如您在上面的日志中所看到的)。

更新

我删除了所有异步代码和执行策略(重试),以缩小问题范围,现在代码如下:

    static void Main(string[] args)
    {
        var employeeContext = new EmployeeContext(ConnectionString);
        var partnersContext = new PartnersContext(employeeContext.Database.GetDbConnection());
        var unitOfWork = new UnitOfWork();
        unitOfWork.Update(employeeContext, partnersContext, 1);
        unitOfWork.Update(employeeContext, partnersContext, 2);

    }

    public class UnitOfWork
    {
        public void Update(EmployeeContext employeeContext, PartnersContext partnerContext, int count)
        {
            partnerContext.Partners.Add(new Partner($"John Smith {count}"));
            employeeContext.Persons.Add(new Person() { Name = $"Richard Keno {count}" });

            using var trans = employeeContext.Database.BeginTransaction();
            partnerContext.Database.UseTransaction(trans.GetDbTransaction());
            partnerContext.SaveChanges();
            employeeContext.SaveChanges();
            trans.Commit();
        }
    }

第一次调用过了,数据库也被更新了,但第二次调用却失败了,出现以下错误。

enter image description here

更新2

使用TransactionScope而不是BeginTransaction似乎可行。以下代码能够工作并相应地更新数据库。

        var strategy = employeeContext.Database.CreateExecutionStrategy();
        await strategy.ExecuteAsync(async () =>
        {
            using var scope = new TransactionScope(TransactionScopeAsyncFlowOption.Enabled);
            partnerContext.Partners.Add(new Partner($"John Smith {count}"));
            employeeContext.Persons.Add(new Person() { Name = $"Richard Keno {count}" });
            await partnerContext.SaveChangesAsync();
            await employeeContext.SaveChangesAsync();
            scope.Complete();
        });

2
这可能与您的异步处理有关。在 await strategy.ExecuteAsync 中的 await 将控制权返回给调用者,并允许在主线程上继续执行。因此,当第一次到达 unitOfWork.SaveChanges(); 时,它将触发事务,然后立即尝试执行您的 "... doing a bit more work",然后再次立即调用 unitOfWork.SaveChanges(); - Ibrennan208
2
通过单步调试并不能完全掌控异步代码。此外,你的 strategy.ExecuteAsync 也会出现上述情况,因为你在其中使用了 await。你可能需要捕获正在保存的任务,并确保在下一次保存之前该任务已经完成。 - Ibrennan208
1
你的方法必须是public async Task SaveChangesAsync()而不是public void SaveChanges() - Alexander Petrov
1
@GETah 请查看有关异步代码执行流程的资源:https://learn.microsoft.com/zh-cn/dotnet/csharp/programming-guide/concepts/async/task-asynchronous-programming-model#BKMK_WhatHappensUnderstandinganAsyncMethod - Ibrennan208
1
这两个DbContext是否连接到同一个数据库?如果是,如果您只是在两个DbContext之间共享DbConnection,您将获得更好的性能,并且不会受限于Windows。如果不是,则需要真正的分布式事务,这几乎是一种反模式。 - David Browne - Microsoft
显示剩余15条评论
1个回答

4
看起来你遇到了一些内部 EF Core 3 实现缺陷/bug,这在后续版本中已经修复,因为该问题在最新的 EF Core 6.0 中无法重现(但在 EF Core 3.1 中可以重现)。
问题出在共享底层数据库连接和数据库事务的清理上。可以通过处理由 UseTransaction{Async} 调用返回的 EF Core 事务包装器 (IDbContextTransaction) 来解决此问题(这也有助于未来的 EF Core 版本)。
using var trans2 = await partnerContext.Database.UseTransactionAsync(transaction.GetDbTransaction());

或者

using var trans2 = partnerContext.Database.UseTransaction(trans.GetDbTransaction());

非常感谢您的帮助。我尝试强制处理事务的释放 trans.Dispose(),但没有成功。 - GETah
更新:我刚刚尝试使用TransactionScope(请参见更新2),它可以正常工作。代码按预期工作(我还更新了github存储库)。 - GETah
1
使用原始的 DbTransaction,您需要处理由 UseDbTransaction(trans) 返回的 trans2,而不是 BeginTransasction 返回的 trans 变量。这是因为 trans 拥有底层的 DbTransaction,而 trans2 并没有 - 它只需要适当的清理,EF Core 包装对象的 Dispose 就可以了。对于这种使用场景(具有共享 DbConnectionDbContext),使用分布式事务 (TransactionScope) 看起来过于复杂。 - Ivan Stoev
非常感谢。成功了!处理trans2的方法非常好用!我编辑了我的Github存储库,并添加了一个单独的方法“UpdateFixed”来突出修复。 - GETah

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接