EF、ASP MVC + 依赖注入。多个并发请求和数据库连接的问题

Posted

技术标签:

【中文标题】EF、ASP MVC + 依赖注入。多个并发请求和数据库连接的问题【英文标题】:EF, ASP MVC + dependency injection. Issues with multiple concurrent requests and DB connectivity 【发布时间】:2015-01-09 06:29:57 【问题描述】:

我正在开发一个基于 NopCommerce 的项目,该项目使用 ASP MVC、Autofac 和实体框架。我在从 MVC 路由内部调用服务上的方法时发生异常,这将使用 EF 调用数据库。

在开发期间,一切正常 - 但是在负载测试期间,当有并发用户时,1 或 2 个请求将崩溃,并且将以下错误之一记录到 ELMAH。

System.InvalidOperationException ExecuteReader 需要一个打开且可用的连接。连接的当前状态为打开。

System.InvalidOperationException ExecuteReader 需要一个打开且可用的连接。连接的当前状态为关闭。

System.InvalidOperationException 连接未关闭。连接的当前状态为正在连接。

System.ObjectDisposedException ObjectContext 实例已被释放,不能再用于需要连接的操作。

System.ObjectDisposedException 操作无法完成,因为 DbContext 已被释放。

System.ArgumentException 已添加具有相同键的项。

我通过打开网站上的许多链接,然后使用 Chrome 插件刷新所有选项卡来对此进行测试,模拟了大约 25 个同时访问网站的请求。

该服务有 2 个从路由内部调用的方法,然后这些相同的方法之一可以从控制器操作中被调用 50 多次。有时异常是从 Route 内部触发的,有时它来自控制器内部。这意味着路由的 GetRouteData 已经完成,将“流”传递给控制器​​,然后在那里失败。但是,大多数时候,异常发生在 Route 中。当异常确实从控制器内部发生时,它位于发生异常的不同行。

有时一种方法会失败,而另一种方法会运行良好,然后调用堆栈中的下一个方法会失败。每次都不同,但这些方法调用使用一种通用方法从数据库中检索。

在此之前注册的其他 2 个路由映射到 *url 执行传入 URL 的数据库查找,并且该异常永远不会发生。因此,这条路线并不是执行任何数据库工作的第一个操作。

服务是依赖注册的:-

builder.RegisterControllers(typeFinder.GetAssemblies().ToArray());

builder.Register<IDbContext>(c => new NopObjectContext(DataSettings.DataConnectionString)).InstancePerLifetimeScope();

builder.RegisterGeneric(typeof(EfRepository<>)).As(typeof(IRepository<>)).InstancePerLifetimeScope();

builder.RegisterType<MemoryCacheManager>().As<ICacheManager>().Named<ICacheManager>("nop_cache_static").SingleInstance();

builder.RegisterType<MyService>().As<IMySerivce>()    
               .WithParameter(ResolvedParameter.ForNamed<ICacheManager>("nop_cache_static"))    
               .InstancePerLifetimeScope(); 

Controller通过构造函数注入接收服务:-

protected readonly IMyService _myService;

public MyController(IMyService myService)

    _myService = myService;

并且路由解析服务如下:

public override RouteData GetRouteData(HttpContextBase httpContext)

    var _myService = EngineContext.Current.Resolve<IMyService>();
    myService.databaseOperation(); <--- falls over here w/ concurrency
 

任何想法为什么会发生这些错误?以及如何解决它们?

根据我的理解,我们的 DBContext 似乎在 2 个请求之间共享,但是在我的依赖项注册中,我告诉它解决为生命周期分数 - 这对于每个请求应该是唯一的。我已经阅读了很多有关此异常的原因以及依赖注入框架如何控制依赖项的生命周期并管理其资源的处置 - 似乎在这里事情正在下降。

就服务本身而言,它与应用程序中的所有其他服务一样工作 - 没有什么突出和不同之处。

我已经粘贴了完整的异常堆栈跟踪http://pastebin.com/XYEwRQsv 以及有问题的代码行。

编辑:我在连接字符串中使用 MultipleActiveResultSets=True。该服务处理的实体是独立的,即它与其他实体没有关系,因此一旦执行查询,就不应该有子实体的多次迭代,正如关于这些异常的其他答案所指出的那样。

编辑:这是引发异常的行。

public string GetFriendlyUrlString(string originalUrl)

    var friendly =  _cacheManager.Get(originalUrl.ToLower(), () =>
            (from f in _friendlyUrlRepository.ReadOnlyTable      <--------- Here
             where f.OriginalUrl == originalUrl
             select f.Url).ToList().SingleOrDefault());

    return friendly ?? originalUrl;

例外是

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

这太奇怪了。在我的路线中,有 4 个地方可以进行数据库调用。我的例外情况是这 4 次调用中的一次出现了 95% 的情况——通常第一次失败,但有时第一次 DB 调用会正常,而其他调用会失​​败。我很少看到异常来自该路由 uh...路由到的控制器内部。同样,由于该控制器异常,实际的数据库连接问题发生在 5 行代码之一上 - 再次表明它进行了 x 多次数据库调用,然后失败了。

【问题讨论】:

我似乎在 nopCommerce 中找不到 ReadOnlyTable 的定义。这让我认为您在存储库定义中有一些自定义代码的问题可能会导致此问题。你能分享那个代码吗? 顺便说一句,我已经更新了您的问题,以添加您在 pastebin 共享的代码,因为它与此处最相关。 Marco - 这是我添加的允许使用“NoTracking”查询的内容 - 基本上是“this.Entities.AsNoTracking();”。干杯 我还研究了将无跟踪 DbContext 加入到跟踪 DbContext 中,看看这是否可能导致它:***.com/questions/18231039 但答案表明这是一件可以做的事情。我仍然不确定这是依赖注入问题还是依赖于 EF - 由于上下文不同,我倾向于 DI,这让我认为每个 HTTP 请求的依赖注入在某个地方失败了? 您是否尝试过在 DependencyResolver.Current.GetService&lt;IMyService&gt;() 中的路由中使用 MVC 依赖解析器?我想这不会有任何区别,因为您可能正在使用NopDependencyResolver,它在内部与您正在做的事情相同。 (除非您在 global.asax 上手动配置了不同的依赖解析器?) 【参考方案1】:

您确定您的 IDbContext 实现被注入了正确的范围吗?我自己不使用 Autofac,但快速查看他们的网站文档表明 InstancePerRequest 将更适合您的需求:

InstancePerRequest

您的连接字符串中的 MARS 是否设置为 true?

Multiple Active Result Sets

【讨论】:

nopCommerce 的最新版本(3.30 和 3.40)已删除 InstancePerRequest 依赖项,并将其替换为 InstancePerLifetimeScope。【参考方案2】:

您的代码基本上是正确的。您显示的依赖项来自 nopCommerce 本身(IDbContext 完全相同,但我想您已将其剥离一点以在此处显示)并且您的服务注册是标准的。在 nopcommerce 中有更多的数据库访问路径没有失败;如果是这样的话,会有很多错误报告..

这让我觉得问题应该出在其他地方,所以我做了和你一样的测试,没有发现任何问题。

我自己从未使用过 Glimpse,但我在您的堆栈跟踪中看到了它。您是否在没有 Glimpse 的情况下进行了一些测试?我看到它使用 Castle 来生成代理,这可能会弄乱您的依赖项...问题必须在 nopCommerce 代码之外的其他地方,可能在您的代码或工具中。

顺便说一句。我看到您使用的是 nopcommerce 3.30 或 3.40。由于不再需要 v3.40 MARS 导致性能提升......

【讨论】:

对 Glimpse 的思考很棒 - 我删除了它并进行了测试(在我的站点中打开 30 个标签并刷新所有标签),但不幸的是,在几次重新加载后,错误仍然出现了大约 5 次。不幸的是,我确实有一个“voilla”!我删除它的那一刻。我剥离了 nop 的数据层以摆脱该 DataSettingsManager 类或它们拥有的任何东西,并将其替换为从 web.config 中提取连接字符串的静态属性。 同样禁用 MARS 会经常触发此异常:已经有一个打开的 DataReader 与此命令关联,必须先关闭。 您的意思是您删除了参数并且它有效?你确定是这个原因吗?很抱歉不同意,但这没有意义。它只是一个参数。您确定将站点完全清除应用程序域后它不起作用吗? (我以前见过这种情况)。如果您只是更改连接字符串源,则不会影响依赖项实例化。 不抱歉,我禁用了 Glimpse,但错误仍然像以前一样随机发生。我还说我剥离了 Nop 数据层的架构,以删除以前存在的 DataSettingsManager 类,以便从 settings.txt 中提取连接字符串,并替换为更直接的类。 是的,但是,基本上,您是说原因是连接字符串或某个在依赖项中不起任何作用的静态类...【参考方案3】:

Autofac 的 InstancePerLifetimeScope 不保证每个 http 请求的唯一范围。它保证的是在它被解析的生命周期范围内只有一个组件实例。 因此,例如,如果您从根容器解析 InstancePerLifetimeScope 组件,则该组件将在应用程序过程中基本上充当单例。

如果您在单独注册的服务(例如,全局操作过滤器)中解决您的依赖关系,那么您的依赖关系(dbContext 或其他)将不会在每个请求上处理 - 它只会保持打开状态,泄漏内存和出血连接,直到它被处理或关闭您的应用程序。

假设您正在使用 Autofac Mvc 集成,作为一个实验,也许尝试暂时将您的 dbContext 注册为 InstancePerMatchingLifetimeScope("AutofacWebRequest") - 这实际上等同于完全按照 Will Appleby 所说的那样做,但与比您正在使用的 Autofac 新版本。

如果这能解决您的问题,那么基本上,@Will Appleby 是对的。您需要将所有 InstancePerLifetimeScope 组件注册为 InstancePerRequest(自 3.4.0 版起新添加到 Autofac 核心)或 InstancePerHttpRequest(在 3.4.0 之前的 Autofac MVC 集成中可用)。

否则,如果我猜对了,你可能会得到如下异常:

DependencyResolutionException: No scope with a Tag matching 'AutofacWebRequest' is visible from the scope in which the instance was requested.

如果发生这种情况,您仍然希望将注册更改为 InstancePerRequest/InstancePerHttpRequest,但现在您手头有更深层次的任务。您可能有一个单例(或其他比 httpRequest 寿命更长的对象)持有您的 dbContext 人质。您需要识别那个寿命更长的组件,并弄清楚如何释放这种强制依赖——这可能需要重构组件以更好地符合 Autofac 的作用域行为。

您需要在此处参考故障排除部分: http://autofac.readthedocs.org/en/latest/faq/per-request-scope.html#troubleshooting-per-request-dependencies

这里有一段摘录:

造成这种情况的常见原因包括:

应用注册正在跨应用类型共享。 单元测试在实际应用注册的情况下运行,但没有模拟每个请求的生命周期。 您有一个组件的生命周期比一个请求长,但它需要一个仅针对一个请求的依赖项。例如,一个单例组件接受注册为每个请求的服务。 在应用程序启动期间(例如,在 ASP.NET Global.asax 中)运行的代码在没有活动请求时使用依赖项解析。 代码在“后台线程”(没有请求语义)中运行,但正在尝试调用 ASP.NET MVC DependencyResolver 来进行服务定位。

【讨论】:

您好,感谢您的详细回复。 RE:“如果您在单独注册的服务(例如,全局操作过滤器)中解决您的依赖关系,那么您的依赖关系将不会在每个请求上得到处理” - 这适用于路由吗? > 90% 的时间是发生异常的地方 - 我已经玩过 Route 将在哪里解决依赖关系 - 通过构造函数接收它(通过从 global.asax 中解析它传递)而不是 Route 解析它本身。 我还检查了所有注册为 SingleInstance 的服务,这些接口的实现没有任何依赖于任何非单例实现(我们没有在这里添加任何新内容 - 所有默认的 nopCommerce 东西) 其实这很可能适用于路由。你能发布你的路线配置吗?另外,您发布的GetRouteData方法属于哪个类? James:这是路由的设置方式 - pastebin.com/abTHk30X 我也尝试过使用这种方式传递路由服务 pastebin.com/u27hC3vx 另外值得注意的是:在 FeaturedRangeRoute 和 MapCmsPageRoute 中,在“CatalogRoute”之前(也映射到 *url,它们都有类似的代码来解析服务、执行数据库调用和传递是否流向控制器,取决于 URL 是否匹配。因此,当 CatalogRoute 在整个地方抛出异常时,到那时至少已经进行了 2 个 DB 调用。有时异常发生在 CatalogRoute 发送的控制器中到...但 90% 的时间是在路线上。非常令人沮丧!【参考方案4】:

最后的解决办法是在我的 Route 类中有一个私有变量。我没有意识到 Route 对象并不是在每次请求时都实例化的,而且我每次都在“GetRouteData”中初始化这个私有变量。因此,在对站点的 2 个并发请求期间,请求 A 会实例化变量并继续执行其方法,而请求 B 进来时,会重新设置私有变量,这会弄乱请求 A 的一切。

我现在已经在本地限定了这个变量并将它传递给需要它的任何其他方法,我的所有问题都已经消失了。所以,它并不是真正的 Autofac,也不是 Entity Framework——我没有考虑过 MVC 如何处理它的路由。

感谢所有人的帮助,并推动问题所在的正确方向。

【讨论】:

我无法正确使用您的解决方案,您能否提供代码 sn-p 提前谢谢 谢谢我得到了同样的解决方案,我也声明了私有变量

以上是关于EF、ASP MVC + 依赖注入。多个并发请求和数据库连接的问题的主要内容,如果未能解决你的问题,请参考以下文章

构建ASP.NET MVC5+EF6+EasyUI 1.5+Unity4.x注入的后台管理系统-前言与目录(持续更新中...)

构建ASP.NET MVC5+EF6+EasyUI 1.4.3+Unity4.x注入的后台管理系统(61)-如何使用框架来开发?

构建ASP.NET MVC5+EF6+EasyUI 1.4.3+Unity4.x注入的后台管理系统--工作流演示截图

构建ASP.NET MVC5+EF6+EasyUI 1.4.3+Unity4.x注入的后台管理系统(57)-插件---ueditor使用

解读ASP.NET 5 & MVC6系列:依赖注入

构建ASP.NET MVC5+EF6+EasyUI 1.4.3+Unity4.x注入的后台管理系统(63)-Excel导入和导出