您的位置:首页 > 数据库

hibernate3.6文档关于事务及并发策略的笔记

2011-12-04 22:26 393 查看
hibernate3.6文档关于事务及并发策略的笔记

Hibernate session可能的生命周期:1.operation(即一个sql语句的执行)2.request 3.open session in view(由request引出) 4.conversion

工作单元:多个operation组成

session-per-operation这种反模式(operation就是一条sql语句用一个session执行) 这也意味着,应用程序中,在单个的SQL语句发送之后,自动事务提交(auto-commit)模式失效了。数据库事务绝不是可有可无的,任何与数据库之间的通讯都必须在某个事务中进行,不管你是在读还是在写数据。对读数据而言,应该避免auto-commit行为,因为很多小的事务比一个清晰定义的工作单元性能差。session-per-request 模式是用来设计操作单元的有用概念。

在多用户的 client/server 应用程序中,最常用的模式是 每个请求一个会话(session-per-request)。(整个请求过程中一个session执行多个sql语句。) 在这种模式下,来自客户端的请求被发送到服务器端(即 Hibernate 持久化层运行的地方),一个新的 Hibernate Session 被打开,并且执行这个操作单元中所有的数据库操作。一旦操作完成(同时对客户端的响应也准备就绪),session 被同步,然后关闭。

你也可以使用单个数据库事务来处理客户端请求(一个session,一个事务执行多个sql语句),在你打开 Session 之后启动事务,在你关闭 Session 之前提交事务。会话和请求之间的关系是一对一的关系,这种模式对 于大多数应用程序来说是很棒的。

如何实现session-per-request:ServletFilter,proxy/interception 容器

由session-per-request引出话题:open session in view

将 Session 和数据库事务的边界延伸到"展示层被渲染后"会带来便利。假若你实现你自己的拦截器,把事务边界延伸到展示层渲染结束后非常容易。然而,假若你依赖[[[有容器管理事务]]]的 EJB,这就不太容易了,因为事务会在 EJB 方法返回后结束,而那是在任何展示层渲染开始之前。

在业务事务的场景里,一个conversion由多个request组成。每个request对应一个事务。hibernate可以让整个conversion的过程中,只使用一个session,这种方式又被称为Extended (or Long) Session

Hibernate 的 Session 可以在数据库事务提交之后和底层的 JDBC 连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的 JDBC 连接。这种模式被称之为session-per-conversation,这种情况可 能会造成不必要的 Session 和 JDBC 连接的重新关联。自动版本化被用来隔离并发修改,Session 通常不允许自动 flush,而是显性地 flush。

典型的长会话(业务事务)场景:

*

在界面的第一屏,打开对话框,用户所看到的数据是被一个特定的 Session 和数据 库事务载入(load)的。用户可以随意修改对话框中的数据对象。

*

5 分钟后,用户点击“保存”,期望所做出的修改被持久化;同时他[也期望自己是唯一修改这个信息的人,不会出现修改冲突]。

从用户的角度来看,我们把这个操作单元称为长时间运行的对话(conversation),或者应用事务(application transaction)

针对应用事务和系统事务,并发更新就可能出现在二种环境里:1.针对一个事务周期的(也可以说是一个request周期),2:跨多个事务周期的(可以理解为一个会话周期(conversation),它包括了用户思考的时间。

以前没有理解事务隔离的本质,认为只有在长会话的场景才是cmm中并发修改的场景。其实ccm并不涉及到长会话。它只是针对一个事务周期中,其它并发事务修改相同的数据导致的问题。

对于1,采取的方案是乐观锁或悲观锁。具体看锁的成本与业务的成本比较。

对于2,采用的方案是乐观离线锁与悲观离线锁。

Hibernate针对业务事务的解决办法:

头一个幼稚的做法是,在用户思考的过程中,保持 Session 和数据库事务是打开的,保持数据库锁定,以阻止并发修改,从而保证数据库事务隔离级别和原子操作。这种方式当然是一个反模式,因为锁争用会导致应用程序无法扩展并发用户的数目。(看来hibernate极力反对这种long transaction)

很明显,我们必须使用多个数据库事务来实现这个对话。在这个例子中,维护业务处理的事务隔离变成了应用程序层的部分责任。一个对话通常跨越多个数据库事务。如果仅仅只有一个数据库事务(最后的那个事务)保存更新过的数据,而所有其他事务只是单纯的读取数据(例如在一个跨越多个请求/响应周期的向导风格的对话框中),那么应用程序事务将保证其原子性。这种方式比听起来还要容易实现,特别是当你使用了 Hibernate 的下述特性的时候:

*

自动版本化:Hibernate 能够自动进行乐观并发控制,如果在用户思考的过程中发生并发修改,Hibernate 能够自动检测到。一般我们只在对话结束时才检查(也就是业务事务包含的系统事务集的最后一个事务提交时检查。检查的原理还version=version+1 where version=?,只不过sql的构造交给hibernate自动生成,版本化更新语句是乐观离线锁实现的核心)。

*

脱管对象(Detached Objects):如果你决定采用前面已经讨论过的 session-per-request 模式,所有载入的实例在用户思考的过程中都处于与 Session 脱离的状态。Hibernate 允许你把与 Session 脱离的对象重新关联到 Session 上,并且对修改进行持久化,这种模式被称为 session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。(乐观离线锁的实现方式之一就是update(detach object),生成语句应该用到了上面的自动版本化特征。如果用的是JDBC,则是在http
session里面保存版本号来实现,这种方式也是比较常用的,detach object通常保存在http sesson里,这样用户最后一个更新请求达到的时候,可以方便的取出来update)

*

Extended (or Long) Session:Hibernate 的 Session 可以在数据库事务提交之后和底层的 JDBC 连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的 JDBC 连接。这种模式被称之为session-per-conversation,这种情况可 能会造成不必要的 Session 和 JDBC 连接的重新关联。自动版本化被用来隔离并发修改,Session 通常不允许自动 flush,而是显性地 flush。 (个人理解:这种方式应该不会每次请求就关闭session,detach
object也就不用象session-per-request方式那样需要将detach object复制到http session里以供下一次请求对应的session使用)

session-per-request-with-detached-objects 和 session-per-conversation 各有优缺点,我们在本章后面乐观并发控制那部分再进行讨论。

对象标识

应用程序可能在两个不同的 Session 中并发访问同一持久化状态,但是,一个持久化类的实例无法在两个 Session 中共享。因此有两种不同的标识语义:

数据库标识

foo.getId().equals( bar.getId() )

JVM 标识

foo==bar

对于那些关联到 特定 Session(也就是在单个 Session 的范围内)上的对象来说,这两种标识的语义是等价的,与数据库标识对应的 JVM 标识是由[[[ Hibernate 来保证]]]的(:估计hibernate会认为如果foo,bar在同一个session里,使用foo.getId().equals( bar.getId() )来判断是不是等价)。不过,当应用程序在两个不同的 session 中并发访问具有同一持久化标识的业务对象实例的时候,这个业务对象的两个实例事实上是不相同的(从 JVM 识别来看)。这种冲突可以通过在同步和提交的时候使用自动版本化和乐观锁定方法来解决。(:不同session分别持有foo,bar时,它们应该是二个不同的new
instance.站在jvm的角度,当然不等价。但是它们的目的地都是数据库里的记录。当同时提交的时候,就有可能出现并发更新的问题。这也是为了描述乐观离线锁做准备。)

这种方式把关于并发的头疼问题留给了 Hibernate 和数据库;由于在单个线程内,操作单元中的对象识别不 需要代价昂贵的锁定或其他意义上的同步,因此它同时可以提供最好的可伸缩性。只要在单个线程只持有一个 Session,应用程序就不需要同步任何业务对象。在 Session 的范围内,应用程序可以放心的使用 == 进行对象比较。(:同一个session里面foo,bar的情况)

不过,应用程序在 Session 的外面使用 == 进行对象比较可能会 导致无法预期的结果。(:sessionA持有foo,sessionB持有bar,当分别把它们放入一个set的时候,其实它们对应的是一个记录,但被放入二次,因为foo!=bar,所以,需要实现自定义的equals/hashcode方法 )在一些无法预料的场合,例如,如果你把两个脱管对象实例放进同一个 Set 的时候,就可能发生。这两个对象实例可能有同一个数据库标识(也就是说, 他们代表了表的同一行数据),从 JVM 标识的定义上来说,对脱管的对象而言,Hibernate
无法保证他们 的的 JVM 标识一致。开发人员必须覆盖持久化类的 equals() 方法和 hashCode() 方法,从而实现自定义的对象相等语义。警告:不要使用数据库标识来实现对象相等,应该使用业务键值,由唯一的,通常不变的属性组成。当一个瞬时对象被持久化的时候,它的数据库标识会发生改变。如果一个瞬时对象(通常也包括脱管对象实例)被放入一个 Set,改变它的 hashcode 会导致与这个 Set 的关系中断。虽 然业务键值的属性不象数据库主键那样稳定不变,但是你只需要保证在同一个 Set 中的对象属性的稳定性就足够了。请到
Hibernate 网站去寻求这个问题更多的详细的讨论。请注意,这不是一个有关 Hibernate 的问题,而仅仅是一个关于 Java 对象标识和判等行为如何实现的问题。

13.2讲了hibernate中的事务的一些知识。

-----------------------------------------------

hibernate实现乐观离线锁

使用hibernate,但是没有利用它的reattach特性或扩展会话实现,而是使用的传统的乐观离线锁的实现方式,这是使用hiberate时,不推荐的方式,如果使用的是jdbc,则只能使用传统的实现方式了:

传统的实现方式--应用程序级别的版本检查(Application version checking)

未能充分利用 Hibernate 功能的实现代码中,每次和数据库交互都需要一个新的 Session,而且开发人员必须在显示数据之前从数据库中重新载入所有的持久化对象实例。这种方式迫使应用程序自己实现版本检查来确保对话事务的隔离,从数据访问的角度来说是最低效的。这种使用方式和 entity EJB 最相似。

// foo is an instance loaded by a previous Session

session = factory.openSession();

Transaction t = session.beginTransaction();

int oldVersion = foo.getVersion();

session.load( foo, foo.getKey() ); // load the current state

if ( oldVersion != foo.getVersion() ) throw new StaleObjectStateException();

foo.setProperty("bar");

t.commit();

session.close();

version 属性使用 <version> 来映射,如果对象是脏数据,在同步的时候,Hibernate 会自动增加版本号。

当然,如果你的应用是在一个低数据并发环境下,并不需要版本检查的话,你照样可以使用这种方式,只不过跳过版本检查就是了。在这种情况下,最晚提交生效 (last commit wins)就是你的长对话的默认处理策略。请记住这种策略可能会让应用软件的用户感到困惑,因为他们有可能会碰上更新丢失掉却没有出错信息,或者需要合并更改冲突的情况。

很明显,手工进行版本检查只适合于某些软件规模非常小的应用场景,对于大多数软件应用场景来说并不现实。通常情况下,不仅是单个对象实例需要进行版本检查,整个被修改过的关联对象图也都需要进行版本检查。作为标准设计范例,Hibernate 使用扩展周期的 Session 的方式,或者脱管对象实例的方式来提供自动版本检查。

使用Hibernate的前提下,推荐的方式一: 扩展周期的 session 和自动版本化

单个 Session 实例和它所关联的所有持久化对象实例都被用于整个对话,这被称为 session-per-conversation。Hibernate 在同步的时候进行对象实例的版本检查,如果检测到并发修改则抛出异常。由开发人员来决定是否需要捕获和处理这个异常(通常的抉择是给用户 提供一个合并更改,或者在无脏数据情况下重新进行业务对话的机会)。

在等待用户交互的时候, Session 断开底层的 JDBC 连接。这种方式以数据库访问的角度来说是最高效的方式。应用程序不需要关心版本检查或脱管对象实例的重新关联(:checkConcurrent和upate(detachObject)),在每个数据库事务中,应用程序也不需要载入读取对象实例(:不需要checkConcurrent当然也就不需要reload instance)。

// foo is an instance loaded earlier by the old session

Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction

foo.setProperty("bar");

session.flush(); // Only for last transaction in conversation

t.commit(); // Also return JDBC connection

session.close(); // Only for last transaction in conversation

foo 对象知道它是在哪个 Session 中被装入的。在一个旧 session 中开启一个新的数据库事务,会导致 session 获取一个新的连接,并恢复 session 的功能。将数据库事务提交,使得 session 从 JDBC 连接断开,并将此连接交还给连接池(:业务事务中的多个系统事务,每个系统事务在提交之后,都会将连接返回连接池)。

1.GYB:如何进行强制更新检查:

在重新连接之后,要强制对你没有更新的数据进行一次版本检查,你可以对[[[所有可能被其他事务修改过的对象,使用参数 LockMode.READ 来调用 Session.lock()]]]。

2.如何确保最后一次事务才把“整个会话发生的修改”发送到数据库,这是我一直没解决掉的难题,我将所有的业务事务都简化成二个系统事务,第一个load,第二个修改。

你不用 lock 任何你正在更新的数据。一般你会在扩展的 Session 上设置 FlushMode.NEVER,因此[[[只有最后一个数据库事务循环才会真正的把整个对话中发生的修改发送到数据库]]]。因此,只有这最后一次数据库事务才会包含 flush() 操作,然后在整个对话结束后,还要 close() 这个 session。

GYB:

我的疑惑在于通常事务在commit的时候,会flush。而第一个系统事务中如果不仅包含了load操作,也包含了更新的操作,会不会随着第一个系统事务提交而导致flush呢? 答案是通过跟踪源码发现commit方法会执行flush操作除非sessoin处于FlushMode.NEVER。所以,要明确不是所有的commit都会做flush操作。

3.Session与HtppSession一样,应该尽可能的小。

如果在用户思考的过程中,Session 因为太大了而不能保存,那么这种模式是有问题的。举例来说,一个 HttpSession 应该尽可能的小。

由于 Session 是一级缓存,并且保持了所有被载入过的对象,因此我们只应该在那些少量的 request/response 情况下使用这种策略。你应该只把一个 Session 用于单个对话(避免出现多个会话使用同一个session),因为它很快就会出现脏数据。

使用Hibernate的前提下,推荐的方式二: 脱管对象(deatched object)和自动版本化

这种方式下,与持久化存储的每次交互都发生在一个新的 Session 中。然而,同一持久化对象实例可以在多次与数据库的交互中重用。应用程序操纵脱管对象实例 的状态,这个脱管对象实例最初是在另一个 Session 中载入的,然后调用 Session.update(),Session.saveOrUpdate(),或者 Session.merge() 来重新关联该对象实例。

// foo is an instance loaded by a previous Session

foo.setProperty("bar");

session = factory.openSession();

Transaction t = session.beginTransaction();

session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already

t.commit();

session.close();

Hibernate 会再一次在同步的时候检查对象实例的版本,如果发生更新冲突,就抛出异常。

如果你确信对象没有被修改过(比如没有上面的foo.setProperty("bar"),只是在第二个系统事务里做reattach的操作时,利用update做版本检查就显得很怪,此时可以利用另一种reattach的方式lock),你也可以调用 lock() 来设置 LockMode.READ(绕过所有的缓存,执行版本检查),从而取代 update() 操作。

GYB:上面就是hibernate做为持久层时,乐观离线锁的二种实现方式。在使用hibernate的前提下,不推荐在应用程序层使用传统的乐观离线锁方式(checkConcurrent+带版本更新的sql--hibernate在session的FLUSHMODE!=NEVER的时候,只需要commit()就能将对象的更改同步到数据库,即构造成带版本更新的sql语句并执行)

对于遗留系统乐观离线锁的使用:

由于遗留系统中的表可能不包含version列,但同时又不能修改表时,可以使用定制自动版本化行为:

对于特定的属性和集合,通过为它们设置映射属性 optimistic-lock 的值为 false,来禁止 Hibernate 的版本自动增加。这样的话,如果该属性脏数据,Hibernate 将不再增加版本号。

遗留系统的数据库 Schema 通常是静态的,不可修改的。或者,其他应用程序也可能访问同一数据库,根本无法得知如何处理版本号,甚至时间戳。在以上的所有场景中,实现版本化不能依靠数据库表的某个特定列。在 <class> 的映射中设置 optimistic-lock="all" 可以在没有版本或者时间戳属性映射的情况下实现版本检查,此时 Hibernate 将比较一行记录的每个字段的状态。请注意,只有当 Hibernate 能够比较新旧状态的情况下,这种方式才能生效,也就是说,你必须使用单个长生命周期 Session
模式,而不能使用 session-per-request-with-detached-objects 模式。

有些情况下,只要更改不发生交错,并发修改也是允许的。当你在 <class> 的映射中设置 optimistic-lock="dirty",Hibernate 在同步的时候将只比较有脏数据的字段。

在以上所有场景中,不管是专门设置一个版本/时间戳列,还是进行全部字段/脏数据字段比较,Hibernate 都会针对每个实体对象发送一条 UPDATE(带有相应的 WHERE 语句 )的 SQL 语句来执行版本检查和数据更新。如果你对关联实体 设置级联关系使用传播性持久化(transitive persistence),那么 Hibernate 可能会执行不必 要的update语句。这通常不是个问题,但是数据库里面对 on update 点火 的触发器可能在脱管对象没有任何更改的情况下被触发。因此,你可以在
<class> 的映射中,通过设置select-before-update="true" 来定制这一行为,强制 Hibernate SELECT 这个对象实例,从而保证,在更新记录之前,对象的确是被修改过。

-----------------------------------------------

悲观锁定(Pessimistic Locking)

用户其实并不需要花很多精力去担心锁定策略的问题。通常情况下,只要为 JDBC 连接指定一下隔离级别,然后让数据库去搞定一切就够了。然而,高级用户有时候希望进行一个排它的悲观锁定,或者在一个新的事务启动的时候,重新进行锁定。

Hibernate 总是使用数据库的锁定机制,从不在内存中锁定对象。

类 LockMode 定义了 Hibernate 所需的不同的锁定级别。一个锁定可以通过以下的机制来设置:

*

当 Hibernate 更新或者插入一行记录的时候,锁定级别自动设置为 LockMode.WRITE。

*

当用户显式的使用数据库支持的 SQL 格式 SELECT ... FOR UPDATE 发送 SQL 的时候,锁定级别设置为 LockMode.UPGRADE。 (GYB:当用户想要达到手写sql:select *** for update的效果,在hibernate中只需要设置锁定方式为LockMode.UPGRADE即可。hibernate会自动翻译成相应的sql.

*

当用户显式的使用 Oracle 数据库的 SQL 语句 SELECT ... FOR UPDATE NOWAIT 的时候,锁定级别设置 LockMode.UPGRADE_NOWAIT。GYB:同上理解

*

当 Hibernate 在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式自动设置为 LockMode.READ。这种模式也可以通过用户显式指定进行设置。GYB:同上理解

*

LockMode.NONE 代表无需锁定。在 Transaction 结束时, 所有的对象都切换到该模式上来。与 session 相关联的对象通过调用 update() 或者 saveOrUpdate() 脱离该模式。

"显式的用户指定"(所以“显式的指定”并不是说应用程序直接显式的使用sql *** for update,而是通过调用hibernate提供的API实现,hibernate会做自动翻译成对应的sql的工作)可以通过以下几种方式之一来表示:

*

调用 Session.load() 的时候指定锁定模式(LockMode)

*

调用 Session.lock()。

*

调用 Query.setLockMode()。

如果在 UPGRADE 或者 UPGRADE_NOWAIT 锁定模式下调用 Session.load(),并且要读取的对象尚未被 session 载入过,那么对象通过 SELECT ... FOR UPDATE 这样的 SQL 语句被载入。如果为一个对象调用 load() 方法时,该对象已经在另一个较少限制的锁定模式下被载入了,那么 Hibernate 就对该对象调用 lock() 方法。

如果指定的锁定模式是 READ,UPGRADE 或 UPGRADE_NOWAIT,那么 Session.lock() 就执行版本号检查。(在 UPGRADE 或者 UPGRADE_NOWAIT 锁定模式下,执行 SELECT ... FOR UPDATE这样的SQL语句。)

如果数据库不支持用户设置的锁定模式,Hibernate 将使用适当的替代模式(而不是扔出异常)。这一点可以确保应用程序的可移植性。

GYB:上面都是讲hibernate如何实现悲观锁,利用JPA/hibernate实现悲观离线锁的一个参考:http://fangtianying.javaeye.com/blog/317044

-----------------------------------------------

连接释放

Hibernate 关于 JDBC 连接管理的旧(2.x)行为是,Session 在第一次需要的时候获取一个连接,在 session 关闭之前一直会持有这个连接。Hibernate 引入了连接释放的概念,来告诉 session 如何处理它的 JDBC 连接。注意,下面的讨论只适用于采用配置 ConnectionProvider 来提供连接的情况,用户自己提供的连接与这里的讨论无关。通过 org.hibernate.ConnectionReleaseMode 的不同枚举值来使用不用的释放模式:

*

ON_CLOSE:基本上就是上面提到的老式行为。Hibernate session 在第一次需要进行 JDBC 操作的时候获取连接,然后持有它,直到 session 关闭。

*

AFTER_TRANSACTION:在 org.hibernate.Transaction 结束后释放连接。(hibernate实现的乐观离线锁中的extend session就需要这样的模式)

*

AFTER_STATEMENT(也被称做积极释放):在每一条语句被执行后就释放连接。但假若语句留下了与 session 相关的资源,那就不会被释放。目前唯一的这种情形就是使用 org.hibernate.ScrollableResults。

hibernate.connection.release_mode 配置参数用来指定使用哪一种释放模式。可能的值有:

*

auto(默认):这一选择把释放模式委派给 org.hibernate.transaction.TransactionFactory.getDefaultReleaseMode() 方法。对 JTATransactionFactory 来说,它会返回 ConnectionReleaseMode.AFTER_STATEMENT;对 JDBCTransactionFactory 来说,则是 ConnectionReleaseMode.AFTER_TRANSACTION。很少需要修改这一默认行为,因为假若设置不当,就会带来
bug,或者给用户代码带来误导。

*

on_close:使用 ConnectionReleaseMode.ON_CLOSE。这种方式是为了向下兼容的,但是已经完全不被鼓励使用了。

*

after_transaction:使用 ConnectionReleaseMode.AFTER_TRANSACTION。这一设置不应该在 JTA 环境下使用。也要注意,使用 ConnectionReleaseMode.AFTER_TRANSACTION 的时候,假若session 处于 auto-commit 状态,连接会像 AFTER_STATEMENT 那样被释放。

*

after_statement:使用 ConnectionReleaseMode.AFTER_STATEMENT。除此之外,会查询配置的 ConnectionProvider,是否它支持这一设置(supportsAggressiveRelease())。假若不支持,释放模式会被设置为 ConnectionReleaseMode.AFTER_TRANSACTION。只有在你每次调用 ConnectionProvider.getConnection() 获取底层 JDBC 连接的时候,都可以确信获得同一个连接的时候,这一设置才是安全的;或者在
auto-commit 环境中,你可以不管是否每次都获得同一个连接的时候,这才是安全的。 

本文转载自:技术生活杂烩-迟到的博客的博客

内容来自用户分享和网络整理,不保证内容的准确性,如有侵权内容,可联系管理员处理 点击这里给我发消息