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

时间:2014-10-23 19:58:34

标签: c# asp.net asp.net-mvc multithreading nhibernate

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

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

我的CurrentUser对象有一个简单的界面,包含从依赖项解析器注入的用户的详细信息。我已经验证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<Int32> 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<T>.GetEnumerator()+11 
System_Core_ni!System.Linq.Enumerable+<SelectManyIterator>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<System.__Canon>)+d9 
System_Core_ni!System.Linq.OrderedEnumerable`1+<GetEnumerator>d__0[[System.__Canon, mscorlib]].MoveNext()+6f 
System_Core_ni!System.Linq.OrderedEnumerable`1+<GetEnumerator>d__0[[System.__Canon, mscorlib]].MoveNext()+6f 
mscorlib_ni!System.Collections.Generic.List`1[[System.__Canon, mscorlib]]..ctor(System.Collections.Generic.IEnumerable`1<System.__Canon>)+17e 
System_Core_ni!System.Linq.Enumerable.ToList[[System.__Canon, mscorlib]](System.Collections.Generic.IEnumerable`1<System.__Canon>)+3b 
Company.ApplicationServices.SecurityService.GetRoles(System.String)+ef 

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

我使用StructureMap(v2.6.4.1)进行依赖注入,我的数据持久性的相关行如下所示。

var cfg = Fluently.Configure()
    .Database(MsSqlConfiguration.MsSql2008.ConnectionString(c => c.FromConnectionStringWithKey("DatabaseConnectionString"))
    .CurrentSessionContext<WebSessionContext>()
    // ... etc etc....
    .Cache(c => c.ProviderClass<NHibernate.Caches.SysCache2.SysCacheProvider>()
                .UseQueryCache()
                .UseSecondLevelCache()
                .UseMinimalPuts());

For<NHibernate.Cfg.Configuration>().Singleton().Use(cfg);
For<NHibernate.ISessionFactory>().Singleton()
    .Use(ctx =>
        {
            try
            {
                var config = ctx.GetInstance<NHibernate.Cfg.Configuration>();
                return config.BuildSessionFactory();
            }
            catch (SqlException ex)
            {
                ctx.GetInstance<IExceptionLogger>().Error(ex);
                throw;
            }
        });
For<NHibernate.ISession>().HybridHttpOrThreadLocalScoped()
    .Use(ctx => ctx.GetInstance<NHibernate.ISessionFactory>().OpenSession());

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

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

public class SecurityService : ISecurityService
{
    private readonly IRepository<Login> loginRepository;

    public IList<Role> 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<T> : IRepository<T> where T : class
{
    protected ISession Session { get; set; }

    public NHibernateRepository(ISession session)
    {
        Session = session;
    }

    public T GetReference(object id)
    {
        return Session.Get<T>(id);
    }

    // Other methods typical of a repository class, nothing special
}

我的依赖解析器设置....

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

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

角色被缓存,如果没有找到,那么系统会调用安全服务上的GetRoles方法,我想我可能会遇到一个问题,它调用GetRoles的频率超过了它的需要,但是那个&#39;在我现在拥有的多个并发枚举问题的范围之外。

更新 所以我感到困惑,今天我遇到了同样的问题,因为我打电话给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之后还有更多内容,但它与我能说出来的问题并没有真正相关?

2 个答案:

答案 0 :(得分:4)

对我而言,您似乎使用的是早于4。0的NHibernate(2014年8月17日发布)。如果您使用的是较新版本,请忽略此答案。

NHibernate存在并发问题 - 请参阅here

  

有时我们的IIS进程开始使用100%的CPU。在内存转储中   我们看到很多线程都在Dictionary FindEntry方法中,   从ColumnNameCache.GetIndexForColumnName调用。

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

问题在于,当修改基础集合时,泛型Dictionary进入无限循环,即两个线程正在尝试读取值而另一个正在写入。

来自documentation

  

字典可以同时支持多个读者,   只要集合没有被修改。即便如此,列举   通过集合本质上不是一个线程安全的过程。在   枚举与写访问争用的罕见情况   必须在整个枚举期间锁定集合。允许的   多个线程可以访问的集合,用于读写   你必须实现自己的同步。

线程不安全版本: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 concurrency issues causes GC deadlock

答案 1 :(得分:1)

这个问题有一个罪魁祸首: Company.ApplicationServices.SecurityService 生命周期会超越 WebRequest / ISession 生命周期

一些假设

SecurityService方法正在执行some magical (未在上面的问题中显示)调用 ISession ,接收Login个对象。

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

public IList<Role> 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<Role>() 
         // second iteration 
         : login
            .Groups
            .SelectMany(x => x.Roles) // other iterations
            .OrderBy(x => x.DisplayName)
            .ToList();
}

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

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

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

类似的故事:

我建议你看看这个Q&amp; A及其讨论:Nhibernate Lazy Load exception after a view exception。问题的原因是不同的,但解决方案应该是相同的。

建议:

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

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

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

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

快速概述:

class Group : ICloneable
{
    ...
    public override object Clone()
    {
        var entity = base.Clone() as Group;
        entity.Roles = new List<Role>();
        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<Group>();
        foreach(var g in Group)
        {
           entity.Groups.Add(r.Clone() as Group);
        }
        ...
        return entity;
    }
}

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

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

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

  

角色已缓存...

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

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

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

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

您的框架很可能会密集地使用这些来决定要显示的内容和隐藏的内容,要编辑的内容......

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

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

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