12

我已经使用 NHibernate 一段时间了,并且不时发现如果我尝试同时请求两个页面(或尽可能接近),它偶尔会出错。所以我认为这是因为我的会话管理不是线程安全的。

我以为这是我的课,所以我尝试使用与这篇博文不同的方法http://pwigle.wordpress.com/2008/11/21/nhibernate-session-handling-in-aspnet-the-easy-way/但是我仍然遇到同样的问题。我得到的实际错误是:

Server Error in '/AvvioCMS' Application.
failed to lazily initialize a collection, no session or session was closed
Description: An unhandled exception occurred during the execution of the current web request. Please review the stack trace for more information about the error and where it originated in the code.

Exception Details: NHibernate.LazyInitializationException: failed to lazily initialize a collection, no session or session was closed

要么打开要么没有打开数据读取器,但这是罪魁祸首。

我把我的会话管理课程放在下面,有人能发现我为什么会遇到这些问题吗?

public interface IUnitOfWorkDataStore
{
    object this[string key] { get; set; }
}


    public static Configuration Init(IUnitOfWorkDataStore storage, Assembly[] assemblies)
    {
        if (storage == null)
            throw new Exception("storage mechanism was null but must be provided");

        Configuration cfg = ConfigureNHibernate(string.Empty);
        foreach (Assembly assembly in assemblies)
        {
            cfg.AddMappingsFromAssembly(assembly);
        }

        SessionFactory = cfg.BuildSessionFactory();
        ContextDataStore = storage;

        return cfg;
    }

    public static ISessionFactory SessionFactory { get; set; }
    public static ISession StoredSession
    {
        get
        {
            return (ISession)ContextDataStore[NHibernateSession.CDS_NHibernateSession];
        }
        set
        {
            ContextDataStore[NHibernateSession.CDS_NHibernateSession] = value;
        }
    }

    public const string CDS_NHibernateSession = "NHibernateSession";
    public const string CDS_IDbConnection = "IDbConnection";

    public static IUnitOfWorkDataStore ContextDataStore { get; set; }

    private static object locker = new object();
    public static ISession Current 
    {
        get 
        {
            ISession session = StoredSession;
            
            if (session == null) 
            {
                lock (locker)
                {
                    if (DBConnection != null)
                        session = SessionFactory.OpenSession(DBConnection);
                    else
                        session = SessionFactory.OpenSession();

                    StoredSession = session;
                }
            }

            return session;
        }
        set
        {
            StoredSession = value;
        }
    }

    public static IDbConnection DBConnection
    {
        get
        {
            return (IDbConnection)ContextDataStore[NHibernateSession.CDS_IDbConnection];
        }
        set
        {
            ContextDataStore[NHibernateSession.CDS_IDbConnection] = value;
        }
    }

}

我正在使用的实际商店是这样的:

public class HttpContextDataStore : IUnitOfWorkDataStore
{
    public object this[string key]
    {
        get { return HttpContext.Current.Items[key]; }
        set { HttpContext.Current.Items[key] = value; }
    }
}

我在 Application_Start 上初始化 SessionFactory:

NHibernateSession.Init(new HttpContextDataStore(), new Assembly[] { 
                typeof(MappedClass).Assembly});

更新

谢谢你的建议。我尝试了一些不同的方法来尝试简化代码,但我仍然遇到相同的问题,我可能知道原因。

我在需要时为每个请求创建会话,但在我的 global.asax 中,我正在处理 Application_EndRequest 上的会话。但是,当我在加载页面结束时进行调试时,我发现 Application_EndRequest 被多次触发。我认为该事件只假设在请求结束时触发一次,但如果不是,并且其他一些项目正在尝试使用 Session (这是错误所抱怨的),无论出于什么奇怪的原因可能是我的问题,会话仍然是线程安全的,它只是被提前处理掉了。

有人有什么想法吗?我做了一个谷歌,看到 VS 开发服务器确实会导致这样的问题,但我是通过 IIS 运行它。

4

3 回答 3

24

虽然我还没有看到你的整个代码库或你试图解决的问题,但重新思考你如何使用 NHibernate 可能是正确的。从文档中:

在创建 NHibernate 会话时,您应该遵守以下做法:

  • 切勿为每个数据库连接创建多个并发 ISession 或 ITransaction 实例。

  • 在为每个数据库的每个事务创建多个 ISession 时要格外小心。ISession 本身会跟踪对加载对象所做的更新,因此不同的 ISession 可能会看到陈旧的数据。

  • ISession不是线程安全的!永远不要在两个并发线程中访问同一个 ISession。ISession 通常只是一个单一的工作单元

最后一点与我所说的最相关(在多线程环境的情况下也很重要)。一个 ISession 应该被用于一个小的原子操作,然后被释放。同样来自文档:

ISessionFactory 是一个创建成本高、线程安全的对象,旨在由所有应用程序线程共享。ISession 是一个廉价的、非线程安全的对象,应该为单个业务流程使用一次,然后丢弃。

结合这两个想法,而不是存储 ISession 本身,存储会话工厂,因为那是“大”对象。然后,您可以使用类似 SessionManager.GetSession() 作为包装器从会话存储中检索工厂并实例化会话并将其用于一项操作。

该问题在 ASP.NET 应用程序的上下文中也不太明显。您正在静态地确定 ISession 对象的范围,这意味着它在 AppDomain 中共享。如果在该 AppDomain 的生命周期内创建了两个不同的页面请求并同时执行,那么您现在有两个页面(不同的线程)接触同一个 ISession,这是不安全的。

基本上,与其尝试尽可能长时间地保持会话,不如尝试尽快摆脱​​它们,看看是否有更好的结果。

编辑:

好的,我可以看到你想用这个去哪里。听起来您正在尝试实现 Open Session In View 模式,并且您可以采取几种不同的路线:

如果添加另一个框架不是问题,请查看类似Spring.NET的内容。它是模块化的,所以你不必使用整个东西,你可以只使用 NHibernate 辅助模块。它支持视图模式中的打开会话。此处的文档(标题 21.2.10。“Web 会话管理”)。

如果您想自己动手,请查看 Bill McCafferty 发布的这个代码项目:“NHibernate Best Practices”。最后,他描述了通过自定义 IHttpModule 实现该模式。我还在 Internet 上看到了关于在没有 IHttpModule 的情况下实现该模式的帖子,但这可能是您一直在尝试的。

我通常的模式(也许你已经在这里跳过了)首先使用框架。它消除了很多头痛。如果它太慢或不符合我的需求,那么我会尝试调整配置或自定义它。只有在那之后我才尝试推出自己的,但 YMMV。:)

于 2009-03-10T19:37:44.460 回答
2

我不能确定(因为我是一个 Java Hibernate 人)在 NHibernate 中,但在 hibernate 中,Session 对象在设计上不是线程安全的。您应该打开和关闭一个会话,并且永远不要让它超出当前线程的范围。

我确信诸如“打开会话视图”之类的模式已经在.Net 的某个地方实现了。

另一个有趣的问题是当您在会话中放置一个休眠实体时。这里的问题是它所附加的会话将在请求完成时关闭(或应该关闭)。如果您希望导航任何未加载的关联,则必须将实体重新附加到新的(休眠)会话。如果两个请求同时尝试执行此操作,这本身会导致一个新问题,因为如果您尝试将实体附加到两个会话,则会发生某些事情。

希望这可以帮助。加雷斯

于 2009-03-10T13:28:42.023 回答
1

问题最终是我的控制反转库没有正确管理在 HTTP 上下文中创建的对象,因此我获得了对该上下文不可用的对象的引用。这是使用 Ninject 1.0,一旦我更新到 Ninject 2.0(测试版),问题就解决了。

于 2009-07-20T10:15:21.783 回答