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

标签 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 对象有一个包含用户详细信息的简单接口(interface),从依赖项解析器注入(inject)。我已验证 UserId 存在且有效,一切都非常简单。当我查看这两个请求何时挂起的转储时,我收到一条警告,指出多个线程正在枚举一个集合。当我检查转储中的两个线程时,我看到了几乎相同的堆栈跟踪。 (我已经重命名了堆栈跟踪中的一些 namespace 详细信息,但其他方面没有改变)。两个请求中的 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) 进行依赖注入(inject),与我的数据持久化相关的行如下。

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 依赖注入(inject)提供的服务(发布的最新版本 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 是单例 ISession 是 HybridHttpOrThreadLocalScoped ISecurityService 和 IRepository 都保留默认的 Transient

角色被缓存,如果没有找到,那么系统会调用安全服务上的 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 之后还有更多内容,但据我所知,它与问题并没有真正的关系?

最佳答案

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

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

Sometimes our IIS process starting to use 100% CPU. In a memory dump we see that lots of threads is in the Dictionary FindEntry method, that is called from the ColumnNameCache.GetIndexForColumnName.

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

问题是通用 Dictionary当基础集合被修改时进入无限循环,即两个线程正在尝试读取值,一个正在写入。

来自 documentation :

A Dictionary can support multiple readers concurrently, as long as the collection is not modified. Even so, enumerating through a collection is intrinsically not a thread-safe procedure. In the rare case where an enumeration contends with write accesses, the collection must be locked during the entire enumeration. To allow the collection to be accessed by multiple threads for reading and writing, you must implement your own synchronization.

线程不安全版本: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

关于c# - nHibernate 在多个线程上枚举相同的集合,我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/26536361/

相关文章:

c# - 有没有办法从静态方法访问缓存或 session ?

asp.net-mvc - 在自定义授权属性中覆盖 AuthorizeCore 导致 "no suitable method found to override"错误

javascript - d3.js scaleTime 返回未定义

asp.net - 使用 JSON.Parse 优雅降级

c# - numpy array[0, :] *= 1. 23 的 MathNet 等价物是什么

c# Parallel.For 和 UI 更新?

asp.net-mvc - Ajax.ActionLink 与 mvc 核心的替代方案

c# - 在razor View 中,如何将模型属性传递给JavaScript函数并显示返回值?

c# - 字典 : Will calling . 键的顺序是否与 .Values 一致?

c# - KnockoutJS 的购物车逻辑(?)问题