Hibernate 的事务与并发

1.数据库并发会发生的一些问题

1.1 丢失或覆盖更新

 多个事务同时读取数据,并且都要对其进行修改,结果必定最后一个执行完毕的事务将其他事务的更新覆盖掉了。

1.2 脏读

某个事务在其他事务的处理过程中读取数据,并且要进一步处理但是它读早了。

1.3不可重复读

当第二个事务多次访问同一行而且每次读取不同的数据时,会发生不一致的分析问题。不一致的分析涉及多次读取同一行,而且每次信息都是有其他事务更改,因而改行被非重复读取。

1.4 虚读

当对某行执行插入或删除操作,而该行属于某个事务正在读取的行的范围时,会发生虚读问题。事务第一次读的行范围显示出其中一行已不复存与第二次读或后续读中,因为该行已经被其他事务删除了。


2.数据库锁与封锁协议

从数据库并发所产生的数据库不一致的种种问题可以看出,只有事务控制是不够的,必须引入“锁”的机制。

数据库中的锁是一种软件机制,用来指示某个用户已经占用了某种资源,从而防止其他用户做出影响本用户数据的修改或破坏数据库完整性和一致性。数据库中可锁定的资源包括:数据库、表、区域、页面、键值、行标志。假设某用户只对表中的部分行数据进行操作,系统可能只会添加几个行锁(RID)或页面锁,这样可以尽可能多地支持多用户的并发操作。

数据库中锁按照锁的程度主要分为:排他锁、共享锁、修改锁三类。

共享锁:

共享锁是非独占的,允许多个并发事务读取其锁定的资源。用于所有的只读数据操作,默认情况下,数据被读取后,数据库立即释放共享锁。例如:执行 “select * from Table”,首先会锁定第一页,读取后,释放第一页的锁,然后锁定第二页。需要指出的是,这种锁定是不允许其他事务进行写操作的。

排他锁:

排他锁也叫独占锁,他是为修改数据而保留的。它所占有的资源不能读取也不能修改。

修改锁:

修改锁在操作的初始化阶段用来锁定可能要被修改的资源,这样可以避免使用共享锁造成的死锁现象。因为使用共享锁时,操作分为两步,首先获得一个共享锁,读取数据,然后将共享锁升级为排他锁,然后再执行修改的操作。


悲观锁:

悲观锁是指对数据被外界修改持保守的态度。假定任何时候存取数据时,都有可能另一个客户也在存取同一笔数据,为了保持数据操作的一致性,于是对数据采取数据库层次的锁定状态,依靠数据库提供的锁机制。

乐观锁:

乐观锁则乐观的认为数据的存取很少发生同时存取的问题,因而不在数据库层次上的锁定,为了维护正确的数据,乐观锁定采用应用程序上的逻辑实现版本控制。


数据库的事务隔离级别:

1.串行化

可串行化级别是最严格的事务隔离。这个级别模拟串行的事务执行,就好像事务是一个接一个那样串行的,而不是并行执行。不过,使用这个级别的应用必须准备在串行化失败的时候重新启动事务。该级别与事务有关,对全表加锁。在数据表上放置排他锁。

2.可重复读

当一个事务运行在这个隔离级别时,一个事务在执行过程中可以看到其他事务已经提交新插入的记录,但是不能看到其他事务对已有记录的更新。该级别与事务有关,对扫描过的地方加锁。例如:“select * from table”,系统从第一行开始扫描,扫描到第五行的时候,1到5行都处于锁定状态,直到commit,这些锁才会解开。假设A事务对读取的所有数据Data放置了锁,以阻止其他事务对Data的更改,在A没有提交之前,新的并发事务读取到的数据如果存在于Data中,那么该数据的状态和A事务提交的数据是一致的,从而避免了不可重复读取。但在A事务没有结束之前,B事务可以插入新记录到Data所在的表中,那么其他事务再次用相同的where子句查询时,得到的结果数可能与上一次的不一致。

3.读已提交

当一个事务运行在这个隔离级别的时候,一个select查询只能看到查询开始之前提交的数据而永远看不到未提交的数据或者是在查询执行时其他并行的事务提交做的改变。该级别与事务无关,只对正在取数的行加锁,取完数马上开锁,也就是说,begin tran 然后 select * from table 即使没有commit,锁也会自动打开。假设A事务对正在读取数据Data放置共享锁,那么Data不能被其他事务改写,所以当B事务对Data进行读取时总和A事务读取的Data数据是一致的。

4.读未提交的数据

当一个事务处于这个事务隔离级别的时候,一个事务在执行的过程中可以看到其他事务没有提交的新插入的记录,而且能看到其他事务没有提交的对已有记录的更新。该级别与事务无关,并且不加锁。


HIbernate中的事务:

在处理异常时,HIbernate抛出的异常,注意在抛出的异常之前要执行事务的回滚,HIbernateException封装了HIbernate持久层可能出现的大多数错误,只要是HIbernate抛出的异常都是不可恢复的,对于一般的SQLException,HIbernate已经将其封装成JDBCException,至于真正的底层SQLException,可以通过JDBCException.getCause()获得。当然,用户也可以定义自己的异常类。


Hibernate乐观并发控制(Optimistic concurrency control)唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的Hibernate乐观并发控制。版本检查使用版本号、 或者时间戳来检测更新冲突(并且防止更新丢失)。

Hibernate乐观并发控制的代码提供了三种可能的方法,应用程序在编写这些代码时,可以采用它们。我们已经在前面应用程序对话那部分展示了乐观并发控制的应用场景,此外,在单个数据库事务范围内,版本检查也提供了防止更新丢失的好处。

1. 应用程序级别的版本检查(Application version checking)

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

 
 
  1. // foo is an instance loaded by a previous Session  
  2. session = factory.openSession();  
  3. Transaction t = session.beginTransaction();  
  4.  
  5. int oldVersion = foo.getVersion();  
  6. session.load( foo, foo.getKey() ); // load the current state  
  7. if ( oldVersion!=foo.getVersion ) throw new StaleObjectStateException();  
  8. foo.setProperty("bar");  
  9.  
  10. t.commit();  
  11. session.close(); 

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

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

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

2. 扩展周期的session和自动版本化

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

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

 
 
  1. // foo is an instance loaded earlier by the old session  
  2. Transaction t = session.beginTransaction(); // Obtain a new JDBC connection, start transaction  
  3.  
  4. foo.setProperty("bar");  
  5.  
  6. session.flush();    // Only for last transaction in conversation  
  7. t.commit();         // Also return JDBC connection  
  8. session.close();    // Only for last transaction in conversation  

foo对象知道它是在哪个Session中被装入的。在一个旧session中开启一个新的数据库事务,会导致session获取一个新的连接,并恢复session的功能。将数据库事务提交,使得session从JDBC连接断开,并将此连接交还给连接池。在重新连接之后,要强制对你没有更新的数据进行一次版本检查,你可以对所有可能被其他事务修改过的对象,使用参数LockMode.READ来调用Session.lock()。你不用lock任何你正在更新的数据。

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

如果在用户思考的过程中,Session因为太大了而不能保存,那么这种模式是有 问题的。举例来说,一个HttpSession应该尽可能的小。由于 Session是一级缓存,并且保持了所有被载入过的对象,因此 我们只应该在那些少量的request/response情况下使用这种策略。你应该只把一个Session用于单个对话,因为它很快就会出现脏数据。

(注意,早期的Hibernate版本需要明确的对Session进行disconnec和reconnect。这些方法现在已经过时了,打开事务和关闭事务会起到同样的效果。)

此外,也请注意,你应该让与数据库连接断开的Session对持久层保持 关闭状态。换句话说,在三层环境中,使用有状态的EJB session bean来持有Session, 而不要把它传递到web层(甚至把它序列化到一个单独的层),保存在HttpSession中。

扩展session模式,或者被称为每次对话一个session(session-per-conversation), 在与自动管理当前session上下文联用的时候会更困难。你需要提供你自己的CurrentSessionContext实现。请参阅Hibernate Wiki以获得示例。

3. 脱管对象(deatched object)和自动版本化

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

 
 
  1. // foo is an instance loaded by a previous Session  
  2. foo.setProperty("bar");  
  3. session = factory.openSession();  
  4. Transaction t = session.beginTransaction();  
  5. session.saveOrUpdate(foo); // Use merge() if "foo" might have been loaded already  
  6. t.commit();  
  7. session.close(); 

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

如果你确信对象没有被修改过,你也可以调用lock() 来设置 LockMode.READ(绕过所有的缓存,执行版本检查),从而取 代 update()操作。

4. 定制自动版本化行为

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

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

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

在以上所有场景中,不管是专门设置一个版本/时间戳列,还是进行全部字段/脏数据字段比较, Hibernate都会针对每个实体对象发送一条UPDATE(带有相应的 WHERE语句 )的SQL语句来执行版本检查和数据更新。如果你对关联实体 设置级联关系使用传播性持久化(transitive persistence),那么Hibernate可能会执行不必 要的update语句。这通常不是个问题,但是数据库里面对on update点火 的触发器可能在脱管对象没有任何更改的情况下被触发。

因此,你可以在 的映射中,通过设置select-before-update="true" 来定制这一行为,强制Hibernate SELECT这个对象实例,从而保证, 在更新记录之前,对象的确是被修改过。

设置optimistic-lock="version" 然后添加一个Version属性描述符

<version column="version" name="version" type="java.lang.Integer"/>

一、使用乐观锁解决事务并发问题
 
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本(Version)记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个"version"字段来实现。
  乐观锁的工作原理(这里和SVN版本管理有点相似):读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。

Hibernate为乐观锁提供了3中实现:

1.基于version

2.基于timestamp

3.为遗留项目添加添加乐观锁 

A、配置基于version的乐观锁:

<?xml version="1.0"encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
   <class name="com.suxiaolei.hibernate.pojos.People"table="people">
       <id name="id" type="string">
           <columnname="id"></column>
           <generatorclass="uuid"></generator>
       </id>
       
       <!--version标签用于指定表示版本号的字段信息 -->
       <version name="version" type="integer"></version>


       <property name="name" column="name"type="string"></property>
       
   </class>
</hibernate-mapping>

#####:注意<version>元素一定要紧挨在id元素之后,并且在实体类中也添加属性version

B、配置基于timestamp的乐观锁:

<?xml version="1.0"encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
   <class name="com.suxiaolei.hibernate.pojos.People"table="people">
       <id name="id" type="string">
           <columnname="id"></column>
           <generatorclass="uuid"></generator>
       </id>
       
       <!--timestamp标签用于指定表示版本号的字段信息 -->
       <timestamp name="updateDate"></timestamp>


       <property name="name" column="name"type="string"></property>
       
   </class>
</hibernate-mapping>

#####:注意<timestamp>元素一定要紧挨在id元素之后,并且在实体类中也添加属性updateDate


C、遗留项目,由于各种原因无法为原有的数据库添加"version"或"timestamp"字段,这时不可以使用上面两种方式配置乐观锁,Hibernate为这种情况提供了一个"optimisitic-lock"属性,它位于<class>标签上:

<?xml version="1.0"encoding="utf-8"?>
<!DOCTYPE hibernate-mapping PUBLIC"-//Hibernate/Hibernate Mapping DTD 3.0//EN""http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping>
   <class name="com.suxiaolei.hibernate.pojos.People"table="people"optimistic-lock="all">
       <id name="id" type="string">
           <columnname="id"></column>
           <generatorclass="uuid"></generator>
       </id>

       <property name="name" column="name"type="string"></property>
   </class>
</hibernate-mapping>

将该属性的值设置为all,让该记录所有的字段都为版本控制信息。

二、使用悲观锁解决事务并发问题

  悲观锁,悲观锁由数据库来实现,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。

  一个典型的依赖数据库的悲观锁调用:select * from account where name=”Erica” forupdate这条 sql 语句锁定了 account 表中所有符合检索条件( name=”Erica”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。悲观锁,也是基于数据库的锁机制实现。

在Hibernate使用悲观锁十分容易,但实际应用中悲观锁是很少被使用的,因为它大大限制了并发性:

Hibernate悲观锁、乐观锁解决事务并发

图为Hibernate3.6的帮助文档Session文档的get方法截图,可以看到get方法第三个参数"lockMode"或"lockOptions",注意在Hibernate3.6以上的版本中"LockMode"已经不建议使用。方法的第三个参数就是用来设置悲观锁的,使用第三个参数之后,我们每次发送的SQL语句都会加上"for update"用于告诉数据库锁定相关数据。

Hibernate悲观锁、乐观锁解决事务并发

LockMode参数选择该选项,就会开启悲观锁。

Hibernate悲观锁、乐观锁解决事务并发

  T1,T2时刻取款事务和转账事务分别开启,T3事务查询ACCOUNTS表的数据并用悲观锁锁定,T4转账事务也要查询同一条数据,数据库发现该记录已经被前一个事务使用悲观锁锁定了,然后让转账事务等待直到取款事务提交。T6时刻取款事务提交,T7时刻转账事务获取数据。

悲观锁 ( Pessimistic Locking ) 
悲观锁,正如其名,它指的是对数据被外界(包括本系统当前的其他事务,以及来自外部系统的事务处理)修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制(也只有数据库层提供的锁机制才能真正保证数据访问的排他性,否则,即使在本系统中实现了加锁机制,也无法保证外部系统不会修改数据)。 
一个典型的倚赖数据库的悲观锁调用: 
select * from account where name=”Erica” for update
这条 sql 语句锁定了 account 表中所有符合检索条件(name=”Erica”)的记录。本次事务提交之前(事务提交时会释放事务过程中的锁),外界无法修改这些记录。Hibernate 的悲观锁,也是基于数据库的锁机制实现。
下面的代码实现了对查询记录的加锁:
String hqlStr = "from TUser as user where user.name=‘Erica‘" ;
Query query = session.createQuery(hqlStr);
query.setLockMode( "user" ,LockMode.UPGRADE); // 加锁
List userList = query.list(); // 执行查询,获取数据

  query.setLockMode 对查询语句中,特定别名所对应的记录进行加锁(我们为TUser 类指定了一个别名 “user” ),这里也就是对返回的所有 user 记录进行加锁。
观察运行期 Hibernate 生成的 SQL 语句:

<span style= "font-size:13px;" >select tuser0_.id as id, tuser0_.name as name, tuser0_.group_id
as group_id, tuser0_.user_type as user_type, tuser0_.sex as sex
from t_user tuser0_ where (tuser0_.name=‘Erica‘ ) for update</span>

  这里 Hibernate 通过使用数据库的 for update 子句实现了悲观锁机制。 
Hibernate 的加锁模式有: 
LockMode.NONE : 无锁机制。 
LockMode.WRITE : Hibernate 在 Insert 和 Update 记录的时候会自动获取。 
LockMode.READ : Hibernate 在读取记录的时候会自动获取。
以上这三种锁机制一般由 Hibernate 内部使用,如 Hibernate 为了保证 Update过程中对象不会被外界修改,会在 save 方法实现中自动为目标对象加上 WRITE 锁。
LockMode.UPGRADE :利用数据库的 for update 子句加锁。 
LockMode. UPGRADE_NOWAIT : Oracle 的特定实现,利用 Oracle 的 for update nowait 子句实现加锁。 
上面这两种锁机制是我们在应用层较为常用的,加锁一般通过以下方法实现: 
Criteria.setLockMode
Query.setLockMode
Session.lock

注意,只有在查询开始之前(也就是 Hiberate 生成 SQL 之前)设定加锁,才会真正通过数据库的锁机制进行加锁处理,否则,数据已经通过不包含 for update 子句的 Select SQL 加载进来,所谓数据库加锁也就无从谈起。

乐观锁( Optimistic Locking ) 
相对悲观锁而言,乐观锁机制采取了更加宽松的加锁机制。悲观锁大多数情况下依靠数据库的锁机制实现,以保证操作最大程度的独占性。但随之而来的就是数据库性能的大量开销,特别是对长事务而言,这样的开销往往无法承受。 
如一个金融系统,当某个操作员读取用户的数据,并在读出的用户数据的基础上进行修改时如更改用户帐户余额),如果采用悲观锁机制,也就意味着整个操作过程中(从操作员读出数、开始修改直至提交修改结果的全过程,甚至还包括操作员中途去煮咖啡的时间),数据库记录始终处于加锁状态,可以想见,如果面对几百上千个并发,这样的情况将导致怎样的后果。乐观锁机制在一定程度上解决了这个问题。乐观锁,大多是基于数据版本( Version )记录机制实现。何谓数据版本?即为数据增加一个版本标识,在基于数据库表的版本解决方案中,一般是通过为数据库表增加一个 “version” 字段来实现。 
读取出数据时,将此版本号一同读出,之后更新时,对此版本号加一。此时,将提交数据的版本数据与数据库表对应记录的当前版本信息进行比对,如果提交的数据版本号大于数据库表当前版本号,则予以更新,否则认为是过期数据。 
对于上面修改用户帐户信息的例子而言,假设数据库中帐户信息表中有一个version 字段,当前值为 1 ;而当前帐户余额字段(balance)为 $100 。 
1 操作员 A 此时将其读出(version=1),并从其帐户余额中扣除 $50($100-$50)。 
2 在操作员 A 操作的过程中,操作员 B 也读入此用户信息(version=1),并从其帐户余额中扣除 $20 ($100-$20)。 
3 操作员 A 完成了修改工作,将数据版本号加一(version=2),连同帐户扣除后余额(balance=$50),提交至数据库更新,此时由于提交数据版本大于数据库记录当前版本,数据被更新,数据库记录 version 更新为 2 。 
4 操作员 B 完成了操作,也将版本号加一(version=2)试图向数据库提交数据(balance=$80),但此时比对数据库记录版本时发现,操作员 B 提交的数据版本号为 2 ,数据库记录当前版本也为 2 ,不满足“ 提交版本必须大于记录当前版本才能执行更新“ 的乐观锁策略,因此,操作员 B 的提交被驳回。这样,就避免了操作员 B 用基于 version=1 的旧数据修改的结果覆盖操作员 A 的操作结果的可能。 
从上面的例子可以看出,乐观锁机制避免了长事务中的数据库加锁开销(操作员 A 和操作员 B 操作过程中,都没有对数据库数据加锁),大大提升了大并发量下的系统整体性能表现。需要注意的是,乐观锁机制往往基于系统中的数据存储逻辑,因此也具备一定的局限性,如在上例中,由于乐观锁机制是在我们的系统中实现,来自外部系统的用户余额更新操作不受我们系统的控制,因此可能会造成脏数据被更新到数据库中。在系统设计阶段,我们应该充分考虑到这些情况出现的可能性,并进行相应调整(如将乐观锁策略在数据库存储过程中实现,对外只开放基于此存储过程的数据更新途径,而不是将数据库表直接对外公开)。 
Hibernate 在其数据访问引擎中内置了乐观锁实现。如果不用考虑外部系统对数据库的更新操作,利用 Hibernate 提供的透明化乐观锁实现,将大大提升我们的生产力。 
Hibernate 中可以通过 class 描述符的 optimistic-lock 属性结合 version描述符指定。

现在,我们为之前示例中的 TUser 加上乐观锁机制。
1 . 首先为 TUser 的 class 描述符添加 optimistic-lock 属性:

<span style= "font-size:13px;" ><hibernate-mapping>
< class name= "org.hibernate.sample.TUser" table= "t_user" dynamic-update= "true"
dynamic-insert= "true" optimistic-lock= "version" >
……
</ class >
</hibernate-mapping></span>

  optimistic-lock 属性有如下可选取值: 
none:无乐观锁 
version:通过版本机制实现乐观锁 
dirty:通过检查发生变动过的属性实现乐观锁 
all:通过检查所有属性实现乐观锁 
其中通过 version 实现的乐观锁机制是 Hibernate 官方推荐的乐观锁实现,同时也是 Hibernate 中,目前唯一在数据对象脱离 Session 发生修改的情况下依然有效的锁机制。因此,一般情况下,我们都选择 version 方式作为 Hibernate 乐观锁实现机制。 
2 . 添加一个 Version 属性描述符

<span style= "font-size:13px;" ><hibernate-mapping>
< class name= "org.hibernate.sample.TUser" table= "t_user" dynamic-update= "true" dynamic-insert= "true"
<span style= "color:#ff0000;" >optimistic-lock= "version" ></span>
<id name= "id" column= "id" type= "java.lang.Integer" >
<generator class = "native" >
</generator>
</id>
<version column= "version" name= "version" type= "java.lang.Integer" />
……
</ class ></span>
</hibernate-mapping>

  注意 version 节点必须出现在 ID 节点之后。这里我们声明了一个 version 属性,用于存放用户的版本信息,保存在 TUser 表的version 字段中。 
此时如果我们尝试编写一段代码,更新 TUser 表中记录数据,如: 
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
List userList = criteria.list();
TUser user =(TUser)userList.get(0);
Transaction tx = session.beginTransaction();
user.setUserType(1); // 更新 UserType 字段 
tx.commit();
每次对 TUser 进行更新的时候,我们可以发现,数据库中的 version 都在递增。而如果我们尝试在 tx.commit 之前,启动另外一个 Session ,对名为 Erica 的用户进行操作,以模拟并发更新时的情形: 
Session session= getSession();
Criteria criteria = session.createCriteria(TUser.class);
criteria.add(Expression.eq("name","Erica"));
Session session2 = getSession();
Criteria criteria2 = session2.createCriteria(TUser.class);
criteria2.add(Expression.eq("name","Erica"));
List userList = criteria.list();
List userList2 = criteria2.list();TUser user =(TUser)userList.get(0);
TUser user2 =(TUser)userList2.get(0);
Transaction tx = session.beginTransaction();
Transaction tx2 = session2.beginTransaction();
user2.setUserType(99);
tx2.commit();
user.setUserType(1);
tx.commit();
执行以上代码,代码将在 tx.commit() 处抛出 StaleObjectStateException 异常,并指出版本检查失败,当前事务正在试图提交一个过期数据。通过捕捉这个异常,我们就可以在乐观锁校验失败时进行相应处理。


  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值