我有一个带有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);
我用:
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();
这一切都很好,直到我在这些DB Repository访问类之外使用相同的策略,并在我的控制器中使用WhenAll在多个服务中调用这些相同的方法:
var serviceTask1 = _service1.GetSomethingsFromDb(Id);
var serviceTask2 = _service2.GetSomeMoreThingsFromDb(Id);
(var dataForController1, var dataForController2) = await (serviceTask1, serviceTask2).WhenAll();
现在当我从我的控制器中调用它时,我会随机得到并发错误,如:
System.InvalidOperationException:ExecuteReader需要一个开放且可用的连接。连接的当前状态已关闭。
我相信的原因是因为有时这些线程会尝试同时访问相同的表。 I know that this is by design in EF Core如果我想,我每次都可以创建一个新的dbContext,但我想看看是否有解决方法。就在那时,Mehdi El Gueddari发现了这篇好文章:http://mehdi.me/ambient-dbcontext-in-ef6/
他承认这一限制:
注入的DbContext会阻止您在服务中引入多线程或任何类型的并行执行流。
并提供DbContextScope
的自定义解决方法。
然而,即使使用DbContextScope,他也会提出警告,因为它不能并行工作(我上面要做的是):
如果您尝试在上下文中启动多个并行任务 DbContextScope(例如,通过创建多个线程或多个TPL 任务),你将遇到大麻烦。这是因为环境 DbContextScope将遍历您的并行任务的所有线程 正在使用。
他在这里的最后一点引出了我的问题:
通常,在单个业务事务中并行化数据库访问几乎没有任何好处,只会增加显着的复杂性。在业务事务的上下文中执行的任何并行操作都不应访问数据库。
在我的控制器中,我是否应该在这种情况下不使用WhenAll并坚持使用等待一个接一个?或者DbContext的依赖注入是这里更基本的问题,因此每次都应该由某种工厂创建/提供一个新的?
答案 0 :(得分:17)
实际上回答辩论的唯一方法是进行性能/负载测试以获得可比较的,经验性的统计证据,以便我能够一劳永逸地解决这个问题。
以下是我测试的内容:
在标准Azure网络应用上使用VSTS @ 200用户进行最多4分钟的云负载测试。
使用DbContext的依赖注入和每个服务的async / await测试#1:1 API调用。
测试#2:在每个服务方法调用中使用新创建的DbContext进行API调用,并使用与WhenAll的并行线程执行。
结论:
对于那些怀疑结果的人,我在不同的用户负载下多次运行这些测试,每次平均值基本相同。
在我看来,并行处理的性能提升是微不足道的,这并不能证明放弃依赖注入的必要性,这将导致开发开销/维护债务,如果处理错误可能会出现错误,并且偏离微软的官方建议。
还有一点需要注意:正如您所看到的那样,使用WhenAll策略实际上有一些失败的请求,即使确保每次都创建新的上下文。我不确定这个的原因,但我宁愿在10ms的性能增益上没有500错误。
答案 1 :(得分:15)
使用任何context.XyzAsync()
方法仅在await
被调用方法或将控制返回到调用线程且其范围内没有context
时才有用
DbContext
实例不是线程安全的:你永远不应该在并行线程中使用它。这意味着,无论如何,永远不要在多个线程中使用它,即使它们没有并行运行。不要试图解决它。
如果由于某种原因你想要运行并行数据库操作(并认为你可以avoid deadlocks, concurrency conflicts etc.),请确保每个操作都有自己的DbContext
实例。但请注意,并行化主要用于CPU绑定进程,而不是数据库交互等IO绑定进程。也许您可以从并行独立的读取操作中受益,但我绝对不会执行并行 write 进程。除了死锁等,它还使得在一次交易中运行所有操作变得更加困难。
在ASP.Net核心中,您通常使用每个请求的上下文模式(ServiceLifetime.Scoped
,请参阅here),但即使这样也无法阻止您将上下文转移到多个线程。最后,只有程序员可以阻止它。
如果您一直担心创建新上下文的性能成本:不要。创建上下文是一种轻量级操作,因为底层模型(存储模型,概念模型+它们之间的映射)只创建一次,然后存储在应用程序域中。此外,新上下文不会创建与数据库的物理连接。所有ASP.Net数据库操作都通过管理物理连接池的连接池运行。
如果所有这些意味着你必须重新配置你的DI以符合最佳实践,那就这样吧。如果您当前的设置将上下文传递给多个线程,则过去的设计决策很糟糕。抵制通过解决方案推迟不可避免的重构的诱惑。唯一的解决方法是对代码进行去并行化,因此最终它甚至可能比重新设计DI 和代码以遵守每个线程的上下文更慢。