NHibernate官方文档中文版——事务和并发(Transactions And Concurrency)

NHibernate本身并不是一个数据库。它是一个轻量级的对象-关系映射工具。因此,它的事务管理代理给对应的数据库连接。如果这个连接代理了一个分布式的事务,ISession管理的操作就会自动成为整个分布式事务的一部分。NHibernate可以被当作是一个简单的ADO.NET的配适器,再加上一些面向对象的语法。

配置,session和工厂

ISessionFactory是一个耗资源的线程安全对象,因此被所有应用程序线程共享。一个ISession是一个一次性的耗资源的线程不安全对象,对应一个单独的业务过程,业务结束之后就被抛弃。例如,当在ASP.NET应用程序中使用NHibernate的时候,页面可以获得一个ISessionFactory,通过以下方式:

ISessionFactory sf = Global.SessionFactory;

每次调用这个方法都会创造一个ISession,你可以Flush() 它, Commit()它的事务, Close() 它会最终抛弃它。(ISessionFactory会被存放在一个静态的单利帮助变量中)。

我们像之前讨论的那样来使用NHibernate的ITransaction API,一个NHibernate ITransaction 的Commit()方法将会flush整个状态,然后将对应的数据库连接中的操作提交(在分布式事务中会有特殊处理)。

要确保你理解Flush()的含义。flushing操作将内存中的改变同步到数据库中,但是不会反过来将数据库的改变同步到内存里。要注意的是,对于所有的NHibernate ADO.NET 连接/事务,对这个连接的事务独立等级会应用到NHibernate执行的所有操作中。

下面的几个小节中,我们会讨论其他通过使用版本来保证事务的原子性的方法。这些方法都是比较先进的方法,但是使用的时候需要小心。

线程和连接

你在产生NHibernate session的时候应当研究下面这些东西:

  • 永远不要为一个数据库连接创建多余一个并发的ISession或者ITransaction
  • 在为一个数据库的一个事务创建一个Isession的时候一定要非常小心。ISession自身会最总加载到内存中的数据的变更情况,因此不同的ISession中可能会看到旧数据。
  • ISession不是线程安全的。在两个并发线程中不要同时操作同一个ISession。一个ISession通常仅仅是一个工作单元。

考虑模型一致

应用程序可能在两个不同的工作单元中同时获取同一个持久化状态。然而,一个持久化类的实例永远不能再两个ISession实例中共享。因此,有两个不同的标识符概念:

The application may concurrently access the same persistent state in two different units-of-work. However, an instance of a persistent class is never shared between two ISession instances. Hence there are two different notions of identity:

数据库标识符

         foo.Id.Equals( bar.Id )

CLR标识符

                 foo == bar

对于关联某个特定Session的对象们来说,这两个概念是一致的。然而,应用程序可能在两个不同的session中并发地获取“相同的”(持久化身份)业务实体,这两个实例实际上是不同的(CLR身份)。
这种方法为NHibernate和数据库带来并发的问题。应用程序永远不需要同步其他的业务对象,如果它严格地使每个线程对应每个ISession或者对象身份(在一个ISession中,应用程序可以安全地使用==来比较对象)。

乐观并发控制

很多业务过程需要一个用户与数据库的各种交互。然而,在web或者企业应用程序中,it is not acceptable for a database transaction to span a user interaction。

保证业务过程的独立性成为了应用程序的一个责任,因此我们把这种保证业务过程独立性的过程叫做长时间运行的应用程序事务。一个单独的应用程序事务通常会横跨几个数据库事务。然而,只有在一个数据库事务(并且是最后一个)保存更新后的数据,而其他所有数据库只进行读数据操作的时候,它才是原子的。

针对这种高并发和高伸缩性系统的系统,唯一满足的设计方法就是通过版本来实现乐观并发控制。NHibernate提供了三个有效的方法。

自动生成版本信息的长session方式

整个应用程序事务中,只有一个ISession实例和他的持久化实例。

ISession使用带版本的乐观锁来保证许多应用程序中的数据库事务是一个单一的逻辑应用事务。ISession在等待用户交互的时候断开相应的ADO.NET连接。这种数据库连接的方式最有效。应用程序不需要关心它自身的数据版本检查或者是否重新关联游离的实例。

foo对象仍然知道哪一个ISession加载了它。只要这个ISession保持ADO.NET的连接,我们就可以把对象的改变提交给数据库。

// foo is an instance loaded earlier by the Session session.Reconnect(); transaction = session.BeginTransaction(); foo.Property = "bar"
;
session.Flush();
transaction.Commit();
session.Disconnect();

但是,如果我们的ISession在用户思考的时候过于庞大以至于不适合存储在内存里,例如一个HttpSession应该尽可能地小,这种模式就会产生问题。因为ISession同时也是(强制的)一级缓存并且包含各种已经加载到内存里面的对象的时候,我们可能只能在一些非常少的request/response周期中使用这种方式。这个是一个推荐的方法,虽然ISession也会有可能产生过期的数据。

自动生成版本信息的多session方式

每一个和数据库交互都在一个新的ISession中发生。然而,同样的持久化实例在每一个和数据库交互中重复使用。应用程序控制在其他ISession加载的游离实例的状态,然后通过使用ISession.Update() 或者 ISession.SaveOrUpdate()来重新关联它们。

// foo is an instance loaded by a previous Session foo.Property = "bar"; session = factory.OpenSession(); transaction =
 session.BeginTransaction();
session.SaveOrUpdate(foo);
session.Flush();
transaction.Commit();
session.Close();

如果你确定实体没有经过任何的修改,你也可以调用Lock() 而不是 Update() 方法,然后使用LockMode.Read 方法(忽视所有的缓存执行版本检查)。

自定义版本信息生成方式

对一些属性和集合,你可以通过将optimistic-lock 的mapping特性设置成 false来关闭NHibernate的自增长版本信息功能。这样NHibernate就不会为脏属性增长版本信息了。

数据库表空间通常不能修改。或者其他应用程序可能也连接到了相同的数据库,然而不知道如何处理版本信息或者是时间戳。在这两种情况下,版本不能仅仅依赖表中的特定字段。为了强制进行版本的校验,尽管并不存在版本或者时间戳属性的mapping,通过比较一条数据中的所有字段的状态,在<class> mapping中将optimistic-lock设置成"all" 。需要注意的是,这个方式理论上只在NHibernate能够分辨新和旧的状态的时候有效,例如,如果你使用一个长时间运行的ISession并且没有针对每一个游离对象请求对应一个session。

在一些情况下,只要修改的东西没有重叠,并发的修改操作都是可以被允许的。如果你在<class> mapping中将optimistic-lock设置成"all,NHibernate在进行flush操作的时候就仅仅比较脏字段。

无论哪种情况,通过精细的版本/时间戳字段或者通过完整/脏字段的比较,NHibernate通过针对每一个实体使用一个UPDATE声明(包含相应的where限定语句)来执行版本检查和字段更新。如果你使用级联操作的持久化方式来重新关联相关的实体,NHibernate就可能会执行一些不必要的更新。这个通常不是一个问题,但是on update会导致数据库执行一些操作,哪怕对于瞬时态的对象并没有任何修改操作。你可以通过在<class> mapping中将select-before-update设置成"true"来自定义这个行为,这样能让NHibernate先select这个实例来查看是否真的在更新操作之前发生了修改行为。

应用程序版本检查

每一个与数据库的交互都在一个新的ISession中产生,这个Isession在操作这些实例之前都会重新加载所有的持久化实例。这个操作迫使应用程序来进行自身的版本检查来保证应用程序事务的独立(当然,NHibernate会为你更新版本信息)。这个方法是数据库交互方式中效率最低的方法。

// foo is an instance loaded by a previous Session session = factory.OpenSession(); transaction = session.BeginTransaction(); int oldVersion = foo.Version; session.Load( foo, foo.Key ); if ( oldVersion != foo.Version ) throw new StaleObjectStateException(); foo.Property = "bar"
;
session.Flush();
transaction.Commit();
session.close();

当然,如果你在一个低并发的环境下病情不需要版本检查,你可以通过使用这个办法来跳过版本检查步骤。

断开session连接

上面介绍的第一个方法是在用户思考的时候(例如,一个servlet(java?!!)可能会在用户的httpsession中存放一个ISession)对整个业务过程维护一个单一的ISession。为了保证性能,你需要

  • 执行这个ITransaction然后
  • 关闭ISession的ADO.NET连接
  • 在等待用户响应之前。ISession.Disconnect() 方法会断开session中的ADO.NET连接然后把连接放回连接池中(除非是你提供的连接而不是连接池)。

ISession.Reconnect() 会获得一个新的连接然后重新开启session。重新连接之后,为了强制进行版本检查,你不能进行更新操作,你可以在任何可能已经在其他事务中更新对象上调用ISession.Lock() 。你不需要锁住任何你正在更新的数据。

这里是一个例子

 

ISessionFactory sessions; IList<Foo> fooList; Bar bar; .... ISession s = sessions.OpenSession(); ITransaction tx = null; try

{
    tx 
= s.BeginTransaction()) fooList = s.CreateQuery( "select foo from Eg.Foo foo where foo.Date = current date" // uses db2 date function ).List<Foo>(); bar = new Bar(); s.Save(bar); tx.Commit(); } catch (Exception) { if (tx != null) tx.Rollback(); s.Close(); throw
;
}
s.Disconnect();

然后

s.Reconnect(); try

{
    tx 
= s.BeginTransaction(); bar.FooTable = new HashMap(); foreach (Foo foo in fooList) { s.Lock(foo, LockMode.Read); //check that foo isn't stale bar.FooTable.Put( foo.Name, foo ); } tx.Commit(); } catch (Exception) { if (tx != null) tx.Rollback(); throw; } finally

{
    s.Close();
}

你可以从这里看到,ITransactions 和ISessions 是many-to-one关系。一个ISession代表应用程序和数据库的会话。ITransaction 将这个会话在数据库级别上拆成了原子工作单元。

乐观锁

我们并不希望用户花过多的时间在担心锁的问题上。通常指定一个ADO.NET的隔离等级然后让数据库去做其他的工作就行了。然而,高级用户可能在一些时候想要获得排他的乐观锁,或者在一个新的事务开始的时候重新获得锁。

NHibernate将会一直使用数据库的锁机制,而不会在内存中锁住对象。

LockMode类定义了NHibernate可能获得的不同所得等级。一个锁会通过下面的这些机制获得:

  • LockMode.Write 会在NHibernate更新或者插入一个数据的时候自动获得
  • LockMode.Upgrade 可能会在用户在支持“SELECT ... FOR UPDATE ”语法的数据库中直接使用SELECT ... FOR UPDATE 的时候获得。
  • LockMode.UpgradeNoWait 可能会在用户在oracle中使用SELECT ... FOR UPDATE NOWAIT
  • LockMode.Read 在NHibernate在Repeatable Read or Serializable 隔离等级下读数据的时候自动获得。可能在用户的一些直接请求的时候再次获得。
  • LockMode.None 表示没有锁。所有对象在一个ITransaction结束的时候转换到这个锁模式。对象通过调用Update() 或者 SaveOrUpdate() 方法来产生关联也会以这种模式开始。
  • 所谓的“用户的直接请求”是下面几种方式之一:

  • 调用ISession.Load(),指定一个LockMode
  • 调用ISession.Lock()
  • 调用IQuery.SetLockMode()

如果ISession.Load()在Upgrade or UpgradeNoWait模式下调用,并且被请求的对象在session中还没有被加载,这个对象就是使用SELECT ... FOR UPDATE语句来加载的。如果为一个已经加载的对象调用Load() 方法,并且这个对象比请求对象锁的限定性更小,NHibernate就会为这个对象调用Lock()方法。

ISession.Lock()提供了一个版本数字来检查特定的锁模式是Read, Upgrade 还是 UpgradeNoWait(在Upgrade 或者 UpgradeNoWait情况下,使用SELECT ... FOR UPDATE

如果数据库不支持请求的锁模式,NHibernate就会使用其他合适的模式(而不是抛出异常)。这样保证了应用程序的轻便性。

释放连接模式

关于1.0版本之后的NHibernate对于ADO.NET连接管理方式,在首次需要一个ISession的时候,ISession会获得一个连接,然后保持这个连接直到session关闭。Nhibernate引入了释放连接模式来告诉session如何管理它的ADO.NET连接。需要注意的是,下面的讨论仅仅是关于通过配置好的IConnectionProvider提供的连接;用户提供的连接不在讨论范围内。不同的模式配置是根据NHibernate.ConnectionReleaseMode这个枚举来区分的:

  • OnClose -这种模式在前文介绍的管理方式中十分重要。NHibernate的session在它初次被创建的时候获得一个连接来实现一些数据库连接,并且保持这个连接直到这个session被关闭。

  • AfterTransaction - 事务完成后释放连接。

配置参数hibernate.connection.release_mode 主要用来指定用来释放连接的模式,可能的值有:

  • auto (the default) - 在当前的释放模式中相当于after_transaction 。通常来说,改变这个默认行为不是一个好主意,因为改变这个值可能会造成一些bug。

  • on_close - says to use ConnectionReleaseMode.OnClose. This setting is left for backwards compatibility, but its use is highly discouraged.相当于使用ConnectionReleaseMode.OnClose。这个泪痣主要是为了向后兼容,十分不建议使用。

  • after_transaction - says to use ConnectionReleaseMode.AfterTransaction. Note that with ConnectionReleaseMode.AfterTransaction, if a session is considered to be in auto-commit mode (i.e. no transaction was started) connections will be released after every operation.相当于使用ConnectionReleaseMode.AfterTransaction。需要注意的是,在这个模式下,如果一个session被认为是在自动提交模式下(例如,没有开始任何事务),连接会在每次操作之后被释放。

关于NHibernate,如果你的应用程序通过使用.NET API,例如System.Transactions 库,来管理事务,ConnectionReleaseMode.AfterTransaction 模式可能会使NHibernate在一个事务中开启和关闭多个连接,这会导致一些不必要的开销和事务从本地事务向分布式事务的升级。将模式制定成ConnectionReleaseMode.OnClose 就能够阻止这种错误。

 

posted @ 2017-01-15 00:00  balavatasky  阅读(2407)  评论(0编辑  收藏  举报