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.
测试#2:1 API调用,在每个服务方法调用中新创建DbContext,并使用与WhenAll的并行线程执行.
结论:
对于那些怀疑结果的人,我在不同的用户负载下多次运行这些测试,每次平均值基本相同.
在我看来,并行处理的性能提升是微不足道的,这并不能证明放弃依赖注入的必要性,这将导致开发开销/维护债务,如果处理错误可能会出现错误,并且不同于微软的官方建议.
还有一点需要注意:正如您所看到的那样,使用WhenAll策略实际上有一些失败的请求,即使确保每次都创建新的上下文.我不确定这个的原因,但我宁愿在10ms的性能提升上没有500错误.
Ger*_*old 19
使用任何context.XyzAsync()方法仅await在被调用方法或返回控制到其范围内没有context的调用线程时才有用.
一个DbContext实例不是线程安全的:你永远也不会用它,在并行线程.这意味着,无论如何,永远不要在多个线程中使用它,即使它们没有并行运行.不要试图解决它.
如果由于某种原因你想要运行并行数据库操作(并且认为你可以避免死锁,并发冲突等),请确保每个操作都有自己的DbContext实例.但请注意,并行化主要用于CPU绑定进程,而不是数据库交互等IO绑定进程.也许您可以从并行独立读取操作中受益,但我绝对不会执行并行写入过程.除了死锁等,它还使得在一个事务中运行所有操作变得更加困难.
在ASP.Net核心中,您通常使用每个请求的上下文模式(ServiceLifetime.Scoped请参见此处),但即使这样也无法阻止您将上下文传输到多个线程.最后,只有程序员才能阻止这种情况.
如果您一直担心创建新上下文的性能成本:不要.创建上下文是一种轻量级操作,因为底层模型(存储模型,概念模型+它们之间的映射)只创建一次,然后存储在应用程序域中.此外,新上下文不会创建与数据库的物理连接.所有ASP.Net数据库操作都通过管理物理连接池的连接池运行.
如果所有这些意味着您必须重新配置DI以符合最佳实践,那就这样吧.如果您当前的设置将上下文传递给多个线程,则过去的设计决策很糟糕.抵制通过解决方案推迟不可避免的重构的诱惑.唯一的解决方法是对代码进行去并行化,因此最终它甚至可能比重新设计DI 和代码以遵守每个线程的上下文要慢.
| 归档时间: |
|
| 查看次数: |
12744 次 |
| 最近记录: |