nHibernate在多个线程上枚举相同的集合

我有一个生产应用程序(IIS8,MVC5,nHibernate DAL),我注意到最近的高CPU使用率。 循环应用程序池修复它但在从服务器进行一些诊断和内存转储以分析问题后,我注意到多个线程的一致模式试图枚举相同的集合。 最常见的一点是应用程序检查用户角色的位置。 我怀疑这可能更多的是这个代码是为每个validation权限的请求运行的,所以它更可能是它被卡住的集合?

public IList GetRoles(string username) { var login = GetLoginForUser(username); return !login.Groups.Any() ? new List() : login.Groups.SelectMany(x => x.Roles).OrderBy(x => x.DisplayName).ToList(); } 

我的CurrentUser对象有一个简单的接口,包含从依赖项解析器注入的用户的详细信息。 我已经validation了UserId存在且有效,这一切都非常简单。 当我看到这两个请求被挂起的转储时,我得到一个警告,多个线程正在枚举一个集合。 当我检查转储中的两个线程时,我看到几乎相同的堆栈跟踪。 (我已经在堆栈跟踪中重命名了一些命名空间细节,但它没有改变)。 两个请求中的userId(和结果配置文件)是相同的,因此它似乎是由于两个单独的线程试图在几乎同时从数据库加载相同的对象。

堆栈跟踪在下面,但我不知道从这里去哪里以解决这个问题。

 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Nullable`1[[System.Int32, mscorlib]], mscorlib]].FindEntry(System.__Canon)+129 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Nullable`1[[System.Int32, mscorlib]], mscorlib]].TryGetValue(System.__Canon, System.Nullable`1 ByRef)+12 NHibernate.AdoNet.ColumnNameCache.GetIndexForColumnName(System.String, NHibernate.AdoNet.ResultSetWrapper)+25 NHibernate.AdoNet.ColumnNameCache.GetIndexForColumnName(System.String, NHibernate.AdoNet.ResultSetWrapper)+25 NHibernate.AdoNet.ResultSetWrapper.GetOrdinal(System.String)+e NHibernate.AdoNet.ResultSetWrapper.GetOrdinal(System.String)+e NHibernate.Type.NullableType.NullSafeGet(System.Data.IDataReader, System.String)+29 NHibernate.Type.NullableType.NullSafeGet(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor, System.Object)+16 NHibernate.Type.NullableType.NullSafeGet(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor, System.Object)+16 NHibernate.Persister.Collection.AbstractCollectionPersister.ReadKey(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor)+14 NHibernate.Persister.Collection.AbstractCollectionPersister.ReadKey(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor)+14 NHibernate.Loader.Loader.ReadCollectionElement(System.Object, System.Object, NHibernate.Persister.Collection.ICollectionPersister, NHibernate.Loader.ICollectionAliases, System.Data.IDataReader, NHibernate.Engine.ISessionImplementor)+34 NHibernate.Loader.Loader.ReadCollectionElement(System.Object, System.Object, NHibernate.Persister.Collection.ICollectionPersister, NHibernate.Loader.ICollectionAliases, System.Data.IDataReader, NHibernate.Engine.ISessionImplementor)+34 NHibernate.Loader.Loader.ReadCollectionElements(System.Object[], System.Data.IDataReader, NHibernate.Engine.ISessionImplementor)+d2 NHibernate.Loader.Loader.ReadCollectionElements(System.Object[], System.Data.IDataReader, NHibernate.Engine.ISessionImplementor)+d2 NHibernate.Loader.Loader.GetRowFromResultSet(System.Data.IDataReader, NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, NHibernate.LockMode[], NHibernate.Engine.EntityKey, System.Collections.IList, NHibernate.Engine.EntityKey[], Bo+ab NHibernate.Loader.Loader.GetRowFromResultSet(System.Data.IDataReader, NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, NHibernate.LockMode[], NHibernate.Engine.EntityKey, System.Collections.IList, NHibernate.Engine.EntityKey[], Bo+ab NHibernate.Loader.Loader.DoQuery(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+1e7 NHibernate.Loader.Loader.DoQuery(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+1e7 NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+7f NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+7f NHibernate.Loader.Loader.LoadCollection(NHibernate.Engine.ISessionImplementor, System.Object, NHibernate.Type.IType)+de NHibernate.Loader.Loader.LoadCollection(NHibernate.Engine.ISessionImplementor, System.Object, NHibernate.Type.IType)+de NHibernate.Loader.Collection.CollectionLoader.Initialize(System.Object, NHibernate.Engine.ISessionImplementor)+1c NHibernate.Loader.Collection.CollectionLoader.Initialize(System.Object, NHibernate.Engine.ISessionImplementor)+1c NHibernate.Persister.Collection.AbstractCollectionPersister.Initialize(System.Object, NHibernate.Engine.ISessionImplementor)+1e NHibernate.Persister.Collection.AbstractCollectionPersister.Initialize(System.Object, NHibernate.Engine.ISessionImplementor)+1e NHibernate.Event.Default.DefaultInitializeCollectionEventListener.OnInitializeCollection(NHibernate.Event.InitializeCollectionEvent)+16d NHibernate.Impl.SessionImpl.InitializeCollection(NHibernate.Collection.IPersistentCollection, Boolean)+1fa NHibernate.Collection.AbstractPersistentCollection.Initialize(Boolean)+2f NHibernate.Collection.AbstractPersistentCollection.Read()+d NHibernate.Collection.Generic.PersistentGenericBag`1[[System.__Canon, mscorlib]].System.Collections.Generic.IEnumerable.GetEnumerator()+11 System_Core_ni!System.Linq.Enumerable+d__14`2[[System.__Canon, mscorlib],[System.__Canon, mscorlib]].MoveNext()+10c System_Core_ni!System.Linq.Buffer`1[[System.__Canon, mscorlib]]..ctor(System.Collections.Generic.IEnumerable`1)+d9 System_Core_ni!System.Linq.OrderedEnumerable`1+d__0[[System.__Canon, mscorlib]].MoveNext()+6f System_Core_ni!System.Linq.OrderedEnumerable`1+d__0[[System.__Canon, mscorlib]].MoveNext()+6f mscorlib_ni!System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor(System.Collections.Generic.IEnumerable`1)+17e System_Core_ni!System.Linq.Enumerable.ToList[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1)+3b Company.ApplicationServices.SecurityService.GetRoles(System.String)+ef 

我正在ActionFilter中打开我的数据库事务,当OnActionExecuting()发生时打开事务,然后在OnActionExecuted()发生时提交/回滚事务。

我正在使用StructureMap(v2.6.4.1)进行dependency injection,而我的数据持久性的相关行如下所示。

 var cfg = Fluently.Configure() .Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("DatabaseConnectionString")) .CurrentSessionContext() // ... etc etc.... .Cache(c => c.ProviderClass() .UseQueryCache() .UseSecondLevelCache() .UseMinimalPuts()); For().Singleton().Use(cfg); For().Singleton() .Use(ctx => { try { var config = ctx.GetInstance(); return config.BuildSessionFactory(); } catch (SqlException ex) { ctx.GetInstance().Error(ex); throw; } }); For().HybridHttpOrThreadLocalScoped() .Use(ctx => ctx.GetInstance().OpenSession()); 

更新:我还在处理这个问题,如果这是nhibernate的问题,或者我如何配置它,我会喜欢一些提示? 由于19个独立的线程试图枚举相同的集合,我的应用程序锁定到了我们今天必须重新启动到服务器的程度。

下面提到它可能是SecurityService的终身范围问题,我同意这是一种可能性。 目前我通过Structuremap通过dependency injection提供服务(2.6的最新版本发布,尚未更新到3.x)。 我在下面简要介绍了我希望简洁但仍然相关的细节。

 public class SecurityService : ISecurityService { private readonly IRepository loginRepository; public IList GetCurrentUserRoles() { var login = GetLoginForCurrentUser(); return GetRoles(login.Name); } public Login GetLoginForCurrentUser() { //Some logic to derive the current UserId {guid} via some resources injected into this service class. return loginRepository.GetReference(loginId); } } public class NHibernateRepository : IRepository where T : class { protected ISession Session { get; set; } public NHibernateRepository(ISession session) { Session = session; } public T GetReference(object id) { return Session.Get(id); } // Other methods typical of a repository class, nothing special } 

我的依赖解析器设置….

 For().Use(); For(typeof (IRepository)).Use(typeof (NHibernateRepository)); //And then the ISession is commented above. 

nHibernate配置了WebSessionContext的内部上下文ISessionFactory是Singleton ISession是HybridHttpOrThreadLocalScoped ISecurityService和IRepository都是默认的瞬态

这些角色是缓存的,如果没有找到,那么系统会调用安全服务上的GetRoles方法,我想我可能会遇到一个问题,它调用GetRoles的次数比它需要的多,但这超出了范围。我现在有多个并发枚举问题。

更新:所以我很困惑,我今天得到同样的问题来调用GetReference。 18个单独的线程枚举枚举相同的集合,但这个是nHibernate内部的。

 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Nullable`1[[System.Int32, mscorlib]], mscorlib]].FindEntry(System.__Canon)+129 System.Collections.Generic.Dictionary`2[[System.__Canon, mscorlib],[System.Nullable`1[[System.Int32, mscorlib]], mscorlib]].TryGetValue(System.__Canon, System.Nullable`1 ByRef)+12 NHibernate.AdoNet.ColumnNameCache.GetIndexForColumnName(System.String, NHibernate.AdoNet.ResultSetWrapper)+25 NHibernate.AdoNet.ResultSetWrapper.GetOrdinal(System.String)+e NHibernate.Type.NullableType.NullSafeGet(System.Data.IDataReader, System.String)+29 NHibernate.Type.NullableType.NullSafeGet(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor, System.Object)+16 NHibernate.Type.AbstractType.Hydrate(System.Data.IDataReader, System.String[], NHibernate.Engine.ISessionImplementor, System.Object)+14 NHibernate.Persister.Entity.AbstractEntityPersister.Hydrate(System.Data.IDataReader, System.Object, System.Object, NHibernate.Persister.Entity.ILoadable, System.String[][], Boolean, NHibernate.Engine.ISessionImplementor)+3ce NHibernate.Loader.Loader.LoadFromResultSet(System.Data.IDataReader, Int32, System.Object, System.String, NHibernate.Engine.EntityKey, System.String, NHibernate.LockMode, NHibernate.Persister.Entity.ILoadable, NHibernate.Engine.ISessionImplementor)+118 NHibernate.Loader.Loader.InstanceNotYetLoaded(System.Data.IDataReader, Int32, NHibernate.Persister.Entity.ILoadable, NHibernate.Engine.EntityKey, NHibernate.LockMode, System.String, NHibernate.Engine.EntityKey, System.Object, System.Collections.IList, NHi+8c NHibernate.Loader.Loader.GetRow(System.Data.IDataReader, NHibernate.Persister.Entity.ILoadable[], NHibernate.Engine.EntityKey[], System.Object, NHibernate.Engine.EntityKey, NHibernate.LockMode[], System.Collections.IList, NHibernate.Engine.ISessionImpleme+129 NHibernate.Loader.Loader.GetRowFromResultSet(System.Data.IDataReader, NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, NHibernate.LockMode[], NHibernate.Engine.EntityKey, System.Collections.IList, NHibernate.Engine.EntityKey[], Bo+97 NHibernate.Loader.Loader.DoQuery(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+1e7 NHibernate.Loader.Loader.DoQueryAndInitializeNonLazyCollections(NHibernate.Engine.ISessionImplementor, NHibernate.Engine.QueryParameters, Boolean)+7f NHibernate.Loader.Loader.LoadEntity(NHibernate.Engine.ISessionImplementor, System.Object, NHibernate.Type.IType, System.Object, System.String, System.Object, NHibernate.Persister.Entity.IEntityPersister)+f3 NHibernate.Loader.Entity.AbstractEntityLoader.Load(NHibernate.Engine.ISessionImplementor, System.Object, System.Object, System.Object)+22 NHibernate.Loader.Entity.AbstractEntityLoader.Load(System.Object, System.Object, NHibernate.Engine.ISessionImplementor)+12 NHibernate.Persister.Entity.AbstractEntityPersister.Load(System.Object, System.Object, NHibernate.LockMode, NHibernate.Engine.ISessionImplementor)+69 NHibernate.Event.Default.DefaultLoadEventListener.LoadFromDatasource(NHibernate.Event.LoadEvent, NHibernate.Persister.Entity.IEntityPersister, NHibernate.Engine.EntityKey, NHibernate.Event.LoadType)+84 NHibernate.Event.Default.DefaultLoadEventListener.DoLoad(NHibernate.Event.LoadEvent, NHibernate.Persister.Entity.IEntityPersister, NHibernate.Engine.EntityKey, NHibernate.Event.LoadType)+1d7 NHibernate.Event.Default.DefaultLoadEventListener.Load(NHibernate.Event.LoadEvent, NHibernate.Persister.Entity.IEntityPersister, NHibernate.Engine.EntityKey, NHibernate.Event.LoadType)+5e NHibernate.Event.Default.DefaultLoadEventListener.ReturnNarrowedProxy(NHibernate.Event.LoadEvent, NHibernate.Persister.Entity.IEntityPersister, NHibernate.Engine.EntityKey, NHibernate.Event.LoadType, NHibernate.Engine.IPersistenceContext, System.Object)+73 NHibernate.Event.Default.DefaultLoadEventListener.ProxyOrLoad(NHibernate.Event.LoadEvent, NHibernate.Persister.Entity.IEntityPersister, NHibernate.Engine.EntityKey, NHibernate.Event.LoadType)+cb NHibernate.Event.Default.DefaultLoadEventListener.OnLoad(NHibernate.Event.LoadEvent, NHibernate.Event.LoadType)+120 NHibernate.Impl.SessionImpl.FireLoad(NHibernate.Event.LoadEvent, NHibernate.Event.LoadType)+140 NHibernate.Impl.SessionImpl.Get(System.String, System.Object)+148 NHibernate.Impl.SessionImpl.Get(System.Type, System.Object)+121 NHibernate.Impl.SessionImpl.Get[[System.__Canon, mscorlib]](System.Object)+143 Intellitive.Data.Repositories.NHibernateRepository`1[[System.__Canon, mscorlib]].GetReference(System.Object)+38 

在调用GetReference之后还有更多,但它与我所知道的问题无关?

对我来说,看起来你正在使用早于4。0的NHibernate(2014年8月17日发布)。 如果您使用的是较新版本,请忽略此答案。

NHibernate存在并发问题 – 请参见此处 :

有时我们的IIS进程开始使用100%的CPU。 在内存转储中,我们看到许multithreading位于Dictionary FindEntry方法中,该方法是从ColumnNameCache.GetIndexForColumnName调用的。

此问题已得到解决,但补丁仅合并到版本4.0.0。

问题是当底层集合被修改时,generics字典进入无限循环,即两个线程试图读取值而另一个正在写入。

从文档 :

只要不修改集合,Dictionary就可以同时支持多个读者。 即便如此,枚举通过集合本质上不是一个线程安全的过程。 在枚举与写访问争用的极少数情况下,必须在整个枚举期间锁定该集合。 要允许多个线程访问集合以进行读取和写入,您必须实现自己的同步。

线程不安全版本: https : //github.com/nhibernate/nhibernate-core/blob/3.4.x/src/NHibernate/AdoNet/ColumnNameCache.cs

应用补丁相同: https : //github.com/nhibernate/nhibernate-core/blob/master/src/NHibernate/AdoNet/ColumnNameCache.cs

更长的解释为什么Dictionary不是线程安全的,以及IIS停止提供请求的原因:

  1. http://blogs.msdn.com/b/tess/archive/2009/12/21/high-cpu-in-net-app-using-a-static-generic-dictionary.aspx
  2. http://improve.dk/debugging-in-production-part-2-latent-race-condition-bugs/
  3. ASP.NET Hang – Generic Dictionary并发问题导致GC死锁

这个问题将有一个罪魁祸首: Company.ApplicationServices.SecurityService ,其生命周期将超过WebRequest / ISession生命周期。

一些假设

SecurityService方法正在做some magical (在上面的问题中没有显示)调用ISession ,收到Login对象。

如果Login包含Groups(第一次迭代),它会继续迭代它并加载许多角色。

 public IList GetRoles(string username) { // the instance of login is loaded, still referencing some ISession var login = GetLoginForUser(username); return !login.Groups.Any() // first iteration over Groups ? new List() // second iteration : login .Groups .SelectMany(x => x.Roles) // other iterations .OrderBy(x => x.DisplayName) .ToList(); } 

从资源消耗的角度来看,这种呼叫显然非常昂贵。 所以必须在某处进行一些缓存(至少保留每个线程的角色,但可能更长)

最有可能的SecurityService是一个单例。 但这意味着,它有自己的ISession 。 这样的ISession几乎不依赖于Web请求。 这意味着,它可以持续一段时间。

它可能发生,它返回相同的 login实例两个不同的线程(许多Web请求由不同的线程处理)

类似的故事:

我建议看看这个Q&A及其讨论: 视图exception后的Nhibernate Lazy Loadexception 。 问题,问题原因是不同的,但解决方案应该是相同的。

建议:

上述假设即使只是部分正确,也应有助于理解下面的这一建议。 简而言之,我们应该避免从一个ISession共享任何对象…与其他线程/请求…

我们可以看到, 迭代对象来自一个 ISession,在其他线程内部的问题。 我喜欢的解决方案:使用Prototype模式。 (模式处理问题昂贵的对象创建/加载)

  • 让我们一次性按用户名加载角色
  • 将它们(原型)克隆到我们需要的水平
  • 缓存它们,稍后返回缓存的克隆
  • 介绍一些刷新(2分钟后,sql缓存依赖,文件依赖),以便将安全对象保留在缓存中仅短暂/合理的时间

甚至可能有其他方法(例如使用NHibernateUtil.Initialize()初始化所有属性并跳过克隆) …但我可以确认,克隆对我来说效果很好。

快速概述:

 class Group : ICloneable { ... public override object Clone() { var entity = base.Clone() as Group; entity.Roles = new List(); foreach(var r in Roles) { entity.Roles.Add(r.Clone() as Role); } ... return entity; } } class Login: ICloneable { ... public override object Clone() { var entity = base.Clone() as Login; entity.Groups = new List(); foreach(var g in Group) { entity.Groups.Add(r.Clone() as Group); } ... return entity; } } 

不错的是,上面的这个草案掌握在我们手中。 我们可以根据需要调整克隆部分。 最后,我们可以拥有一个Clone,加载一次,独立于任何会话,只具有安全性所需的属性……准备好进行缓存

EXTEND:基于扩展问题中的更多信息

我想说,这与我上面的猜测相关,有罪魁祸首(至少可疑)

角色被缓存……

但这些角色与会话有关。 他们并没有超脱。 他们刚刚通过Linq在ISession返回的Login实例上收到:

 login.Groups.SelectMany(x => x.Roles).OrderBy(x => x.DisplayName).ToList() 

这些对象(登录,组,角色)中的每一个仍然附加到它已经诞生的会话中。

与此同时,不同的网络请求即将到来。 针对相同登录的不同Web请求(部分视图,Web API调用) 。 因此,在multithreading环境中,更多Web请求可以触及缓存的角色并使用它们。 这些仍然与现有的,已打开的ISession相关,但是在不同的线程上

很可能你的框架正在密集地使用这些来决定要显示什么和隐藏什么,要编辑什么……

因此,在许多Web请求中,存在与另一个ISession相关的共享对象(对象集)。

我的建议:分离这些对象。 而我发现更准确的方式是Protype模式。

所以,我相信这应该让你了解这些问题是如何发生的,但我想强调的是解决方案。 不要在更多请求/线程之间共享与一个会话相关的对象。 克隆只是一种方式。 但校长是关键。