EF Core中使用带有Injected DbContext的并行异步调用的最佳做法是什么?

sta*_*uxe 21 asynchronous entity-framework dbcontext asp.net-core-webapi

我有一个带有EF Core 1.1的.NET Core 1.1 API,并使用Microsoft的vanilla设置使用依赖注入为我的服务提供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); 
Run Code Online (Sandbox Code Playgroud)

我用:

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();
Run Code Online (Sandbox Code Playgroud)

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

var serviceTask1 = _service1.GetSomethingsFromDb(Id);
var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id);
(var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();
Run Code Online (Sandbox Code Playgroud)

现在当我从我的控制器调用它时,随机我将得到并发错误,如:

System.InvalidOperationException:ExecuteReader需要一个开放且可用的连接.连接的当前状态已关闭.

我相信的原因是因为有时这些线程会尝试同时访问相同的表.我知道这是EF Core中的设计,如果我想,我每次都可以创建一个新的dbContext,但我想看看是否有解决方法.就在那时我发现了Mehdi El Gueddari的这篇好文章:http://mehdi.me/ambient-dbcontext-in-ef6/

他承认这一限制:

注入的DbContext会阻止您在服务中引入多线程或任何类型的并行执行流.

并提供自定义解决方法DbContextScope.

然而,即使使用DbContextScope,他也会提出警告,因为它不能并行工作(我上面要做的是):

如果您尝试在DbContextScope的上下文中启动多个并行任务(例如,通过创建多个线程或多个TPL任务),您将遇到大麻烦.这是因为环境DbContextScope将流经并行任务正在使用的所有线程.

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

通常,在单个业务事务中并行化数据库访问几乎没有任何好处,只会增加显着的复杂性.在业务事务的上下文中执行的任何并行操作都不应访问数据库.

在这种情况下我是不是应该在我的控制器中使用WhenAll并坚持使用等待一个接一个?或者DbContext的依赖注入是这里更基本的问题,因此每次应该由某种工厂创建/提供一个新的?

sta*_*uxe 20

它真正回答辩论的唯一方法是进行性能/负载测试以获得可比较的,经验性的统计证据,以便我能够一劳永逸地解决这个问题.

这是我测试的:

在标准Azure Web应用上使用VSTS @ 200用户进行云负载测试最多4分钟.

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

测试#1的结果:在此输入图像描述

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

测试#2的结果:在此输入图像描述

结论:

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

在我看来,并行处理的性能提升是微不足道的,这并不能证明放弃依赖注入的必要性,这将导致开发开销/维护债务,如果处理错误可能会出现错误,并且不同于微软的官方建议.

还有一点需要注意:正如您所看到的那样,使用WhenAll策略实际上有一些失败的请求,即使确保每次都创建新的上下文.我不确定这个的原因,但我宁愿在10ms的性能提升上没有500错误.

  • 这是你的结果,不应该是答案。其他人可能会得到其他结果。某些数据库类型针对高度分布的查询进行了优化,并且延迟较高。延迟或重试、数据库锁定、数据库过载、大型查询等网络条件都可能导致延迟。当用户每天使用该应用程序时,您可能希望该应用程序尽可能快速且响应灵敏。 (6认同)
  • 抱歉,这是我公司内部的机密信息……我可以告诉您,它正在访问多个服务中的多个表,并执行各种复杂的查询和结果聚合。有关我正在使用的语法示例,请参阅我的问题。 (2认同)

Ger*_*old 19

使用任何context.XyzAsync()方法仅await在被调用方法或返回控制到其范围内没有context的调用线程时才有用.

一个DbContext实例不是线程安全的:你永远也不会用它,在并行线程.这意味着,无论如何,永远不要在多个线程中使用它,即使它们没有并行运行.不要试图解决它.

如果由于某种原因你想要运行并行数据库操作(并且认为你可以避免死锁,并发冲突等),请确保每个操作都有自己的DbContext实例.但请注意,并行化主要用于CPU绑定进程,而不是数据库交互等IO绑定进程.也许您可以从并行独立读取操作中受益,但我绝对不会执行并行写入过程.除了死锁等,它还使得在一个事务中运行所有操作变得更加困难.

在ASP.Net核心中,您通常使用每个请求的上下文模式(ServiceLifetime.Scoped请参见此处),但即使这样也无法阻止您将上下文传输到多个线程.最后,只有程序员才能阻止这种情况.

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

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

  • 谢谢。不幸的是,您提供的所有这些信息都是我已经知道和/或在我的问题中已经说明的内容。具体来说,当您说“如果出于某种原因想要运行并行数据库操作”时,我想知道是什么原因(如果有的话)会这样做(这似乎与 Microsoft 推荐的示例背道而驰)。这是因为我试图避免每次都创建一个新的上下文,就像我在我的问题中所说的那样,因为它不仅会导致可能的内存成本,还会导致依赖注入代码的重新设计。 (2认同)
  • *我想知道什么原因*好吧,您提出来,所以我想您有理由。我试图详细说明一些,但是恐怕并没有太大变化。 (2认同)
  • 不,我的意思是,设计为并行运行的解并行化代码可能会变得比所需的速度慢,因为在无害的情况下它还消除了并行化的可能性。至于“唯一正确的方法”,那是很荒谬的。众所周知,并行化IO绑定进程的效果是有限的。它很简单:如果体系结构允许上下文由多个线程共享,则设计存在缺陷,应进行重构。 (2认同)