EF Core 中将并行异步调用与注入的 DbContext 结合使用的最佳实践是什么?

What is the best practice in EF Core for using parallel async calls with an Injected DbContext?

我有一个带有 EF Core 1.1 的 .NET Core 1.1 API 并使用 Microsoft 的原始设置,即使用依赖注入为我的服务提供 DbContext。 (参考:https://docs.microsoft.com/en-us/aspnet/core/data/ef-mvc/intro#register-the-context-with-dependency-injection

现在,我正在研究并行化数据库读取作为使用 WhenAll

的优化

所以代替:

var result1 = await _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);
var result2 = await _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp); 

我使用:

var repositoryTask1 = _dbContext.TableModel1.FirstOrDefaultAsync(x => x.SomeId == AnId);     
var repositoryTask2 = _dbContext.TableModel2.FirstOrDefaultAsync(x => x.SomeOtherProp == AProp);   
(var result1, var result2) = await (repositoryTask1, repositoryTask2 ).WhenAll();

一切都很好,直到我在这些数据库存储库访问之外使用相同的策略 类 并在我的控制器中跨多个服务使用 WhenAll 调用这些相同的方法:

var serviceTask1 = _service1.GetSomethingsFromDb(Id);
var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id);
(var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();

现在,当我从我的控制器调用它时,我会随机出现类似以下的并发错误:

System.InvalidOperationException: ExecuteReader requires an open and available Connection. The connection's current state is closed.

我认为的原因是因为有时这些线程会尝试同时访问相同的表。 I know that this is by design in EF Core and if I wanted to I could create a new dbContext every time, but I am trying to see if there is a workaround. That's when I found this good post by Mehdi El Gueddari: http://mehdi.me/ambient-dbcontext-in-ef6/

他在其中承认了这一限制:

an injected DbContext prevents you from being able to introduce multi-threading or any sort of parallel execution flows in your services.

并提供了 DbContextScope 的自定义解决方法。

但是,他提出了一个警告,即使使用 DbContextScope 也不能并行工作(我在上面尝试做的事情):

if you attempt to start multiple parallel tasks within the context of a DbContextScope (e.g. by creating multiple threads or multiple TPL Task), you will get into big trouble. This is because the ambient DbContextScope will flow through all the threads your parallel tasks are using.

他的最后一点引出了我的问题:

In general, parallelizing database access within a single business transaction has little to no benefits and only adds significant complexity. Any parallel operation performed within the context of a business transaction should not access the database.

在这种情况下,我不应该在我的控制器中使用 WhenAll 并坚持使用 await 一个接一个吗?还是 DbContext 的依赖注入是这里更基本的问题,因此某种工厂每次都应该 created/supplied 一个新的?

只有当您 await 被调用方法或 return 控制调用线程 时,使用任何 context.XyzAsync() 方法才有用 context 在其范围内.

A DbContext 实例不是线程安全的:永远不要在并行线程中使用它。这意味着,可以肯定的是,永远不要在多线程中使用它,即使它们不 运行 并行。不要试图解决它。

如果出于某种原因你想运行并行数据库操作(并且认为你可以avoid deadlocks, concurrency conflicts etc.),确保每个都有自己的DbContext实例。但是请注意,并行化主要用于 CPU 绑定进程,而不是像数据库交互这样的 IO 绑定进程。也许您可以从并行独立 read 操作中受益,但我肯定永远不会执行并行 write 进程。除了死锁等,它还使得 运行 一个事务中的所有操作变得更加困难。

在 ASP.Net 核心中,您通常会使用 context-per-request 模式(ServiceLifetime.Scoped,请参阅 here),但即使那样也不能阻止您转移上下文到多个线程。到头来只有程序员能阻止。

如果您一直担心创建新上下文的性能成本:请不要担心。创建上下文是一个轻量级的操作,因为底层模型(存储模型、概念模型+它们之间的映射)被创建一次,然后存储在应用程序域中。此外,新上下文不会创建到数据库的物理连接。所有 ASP.Net 数据库操作 运行 通过管理物理连接池的连接池。

如果这一切意味着您必须重新配置 DI 以符合最佳实践,那就这样吧。如果您当前的设置将上下文传递给多个线程,那么过去的设计决策很糟糕。抵制通过变通办法推迟不可避免的重构的诱惑。唯一的解决方法是去并行化你的代码,所以最终它甚至可能比你重新设计你的 DI 和代码 以遵守每个线程的上下文更慢。

真正回答争论的唯一方法是做一个 performance/load 测试以获得可比较的、经验的、统计的证据,这样我就可以一劳永逸地解决这个问题。

这是我测试的:

在标准 Azure web 应用程序上使用 VSTS @ 200 个用户进行云负载测试最多 4 分钟。

测试 #1:1 API 调用 DbContext 的依赖注入和每个服务的 async/await。

测试 #1 的结果:

测试 #2:1 API 调用在每个服务方法调用中新创建 DbContext 并使用 WhenAll 并行线程执行。

测试 #2 的结果:

结论:

对于那些怀疑结果的人,我运行这些测试了几次不同的用户负载,每次的平均值基本相同。

在我看来,并行处理带来的性能提升微不足道,这并不能证明有必要放弃依赖注入,这会造成开发 overhead/maintenance 债务,如果处理不当可能会出现错误,并且会偏离微软官方推荐。

还有一点需要注意:正如您所看到的,WhenAll 策略实际上有一些失败的请求,即使确保每次都创建一个新的上下文。我不确定这样做的原因,但我更希望没有 500 个错误,而不是 10 毫秒的性能提升。