hibernate事务,一级缓存,二级缓存

事务的4个基本特性(ACID):
      1. Atomic(原子性):事务中包含的操作被看作一个逻辑单元,这个逻辑单元中的操作要么全部成功,要么全部失败。
      2. Consistency(一致性):只有合法的数据可以被写入数据库,否则事务应该将其回滚到最初状态。
      3. Isolation(隔离性):事务允许多个用户对同一个数据的并发访问,而不破坏数据的正确性和完整性。同时,并行事务的修改必须与其他并行事务的修改相互独立。
      4. Durability(持久性):事务结束后,事务处理的结果必须能够得到固化。
      数据库操作过程中可能出现的3种不确定情况:
      1. 脏读取(Dirty Reads):一个事务读取了另一个并行事务未提交的数据。
      2. 不可重复读取(Non-repeatable Reads):一个事务再次读取之前的数据时,得到的数据不一致,被另一个已提交的事务修改。
      3. 虚读(Phantom Reads):一个事务重新执行一个查询,返回的记录中包含了因为其他最近提交的事务而产生的新记录。

      标准SQL规范中,为了避免上面3种情况的出现,定义了4个事务隔离等级:
      1. Read Uncommitted:最低等级的事务隔离,仅仅保证了读取过程中不会读取到非法数据。上诉3种不确定情况均有可能发生。
      2. Read Committed:大多数主流数据库的默认事务等级,保证了一个事务不会读到另一个并行事务已修改但未提交的数据,避免了“脏读取”。该级别适用于大多数系统。
      3. Repeatable Read:保证了一个事务不会修改已经由另一个事务读取但未提交(回滚)的数据。避免了“脏读取”和“不可重复读取”的情况,但是带来了更多的性能损失。
      4. Serializable:最高等级的事务隔离,上面3种不确定情况都将被规避。这个级别将模拟事务的串行执行。
      Hibernate将事务管理委托给底层的JDBC或者JTA,默认是基于JDBC Transaction的。
      Hibernate支持“悲观锁(Pessimistic Locking)”和“乐观锁(Optimistic Locking)”。
      悲观锁对数据被外界修改持保守态度,因此,在整个数据处理过程中,将数据处于锁定状态。悲观锁的实现,往往依靠数据库提供的锁机制。Hibernate通过使用数据库的for update子句实现了悲观锁机制。Hibernate的加锁模式有:
      1. LockMode.NONE:无锁机制
      2. LockMode.WRITE:Hibernate在Insert和Update记录的时候会自动获取
      3. LockMode.READ:Hibernate在读取记录的时候会自动获取
      4. LockMode.UPGRADE:利用数据库的for update子句加锁
      5. LockMode.UPGRADE_NOWAIT:Oracle的特定实现,利用Oracle的for update nowait子句实现加锁
      乐观锁大多是基于数据版本(Version)记录机制实现。Hibernate在其数据访问引擎中内置了乐观锁实现,可以通过class描述符的 optimistic-lock属性结合version描述符指定。optimistic-lock属性有如下可选取值:
      1. none:无乐观锁
      2. version:通过版本机制实现乐观锁
      3. dirty:通过检查发生变动过的属性实现乐观锁
      4. all:通过检查所有属性实现乐观锁 



通过以上的介绍可以看出hibernate主要从以下几个方面来优化查询性能:

  1,降低访问数据库的频率,减少select语句的数目,实现手段有:使用迫切左外连接或迫切内连接;对延迟检索或立即检索设置批量检索数目;使用查询缓存。

  2,避免加载多余的应用程序不需要访问的数据,实现手段有:使用延迟加载策略;使用集合过滤。

  3,避免报表查询数据占用缓存,实现手段为利用投影查询功能,查询出实体的部分属性。

  4,减少select语句中的字段,从而降低访问数据库的数据量,实现手段为利用Query的iterate()方法。

  Query的iterate()方法首先检索ID字段,然后根据ID字段到hibernate的第一级缓存以及第二级缓存中查找匹配的 Customer对象,如果存在,就直接把它加入到查询结果集中,否则就执行额外的select语句,根据ID字段到数据库中检索该对象。

  

Query query = session.createQuery("from Customer where age<30");

  Iterator result = query.iterate();

  对于经常使用的查询语句,如果启用了查询缓存,当第一次执行查询语句时,hibernate会把查询结果存放在第二级缓存中,以后再次执行该查询语句时,只需从缓存中获得查询结果,从而提高查询性能。如果查询结果中包含实体,第二级缓存只会存放实体的OID,而对于投影查询,第二级缓存会存放所有的数据值。

  查询缓存适用于以下场合:在应用程序运行时经常使用的查询语句;很少对与查询语句关联的数据库数据进行插入,删除,更新操作。

  对查询语句启用查询缓存的步骤如下:

  1,配置第二级缓存。

  2,在hibernate的配置文件中设置查询缓存属性:hibernate.cache.use_query_cache=true

  3,即使设置了缓存,在执行查询语句时仍然不会启用查询缓存,只有在调用query.setCacheable()后才启用缓存:

  

Query query = session.createQuery("from Customer c where c.age > :age");

  query.setInteger("age", age):

  query.setCacheable(true);

  如果希望更加精粒度地控制查询缓存,可以设置缓存区域:query.setCacheRegion("customerQueries");

  hibernate提供了3种和查询相关的缓存区域:

  1,默认的查询缓存区域:net.sf.hibernate.cache.StandardQueryCache。

  2,用户自定义的查询缓存区域:如customerQueries。

  3,时间戳缓存区域:net.sf.hibernate.cache.UpdateTimestampCache。

  默认的查询缓存区域以及用户自定义的查询缓存区域都用于存放查询结果,而时间戳缓存区域存放了对于查询结果相关的表进行插入,更新,删除操作的时间戳。hibernate通过时间戳缓存区域来判断被缓存的查询结果是否过期。当应用进程对数据库的相关数据做了修改,hibernate会自动刷新缓存的查询结果。但是如果其它应用进程对数据库的相关数据做了修改,hibernate无法监测到这一变化,此时必须由应用程序负责监测这一变化(如通过发送和接收事件或消息机制),然后手工刷新查询结果。

  Query.setForceCacheRefresh(true)方法允许手工刷新查询结果,它使得hibernate丢弃查询缓存区域中己有的查询结果,重新到数据库中查询数据,再把查询结果存放在查询缓存区域中。

  一个session可以和多个事务对应:

  

Transaction trans1 = session.beginTransaction();

  ... ...//数据库操作

  trans1.commit();//提交第一个事务

  session.disconnect();//释放数据库连接

  ... ...//执行一些耗时的操作,这段操作不属于任何事务

  session.reconnect();//重新获取数据库连接

  Transaction trans2 = session.beginTransaction();//开始第二个事务

  ... ...//数据库操作

  trans2.commit();//提交第二个事务

  注意:如果在执行session的一个事务时出现了异常,就必须立即关闭这个session,不能再利用这个session来执行其它的事务。

  许多数据库系统都有自动管理锁的功能,它们能根据事务执行的SQL语句,自动在保证事务间的隔离性与保证事务间的并发性之间做出权衡,然后自动为数据库资源加上适当的锁,在运行期间还会自动升级锁的类型,以优化系统的性能。

  对于普通的并发性事务,通过系统的自动锁定管理机制基本可以保证事务之间的隔离性,但如果对数据安全,数据库完整性和一致性有特殊要求,也可以由事务本身来控制对数据资源的锁定和解锁。

  数据库系统能够锁定的资源包括:数据库,表,区域,页面,键值(指带有索引的行数据),行(即表中的单行数据)。在数据库系统中,一般都支持锁升级,以提高性能。

  按照封锁程序,锁可以分为:共享锁,独占锁,更新锁。

  共享锁:用于读数据操作,它是非独占的,允许其它事务同时读取其锁定的资源,但不允许其它事务更新它。

  独占锁:也称排它锁,适用于修改数据的场合,它所销定的资源,其它事务不能读取也不能修改。

  更新锁:在更新操作的初始化阶段用来锁定可能要被修改的资源,这可以避免使用共享锁造成的死锁现象。许多的数据库系统能够自动定期搜索和处理死锁问题,当检测到锁定请求环时,系统将结束死锁优先级最低的事务,并且撤销该事务。


  应用程序中可以采用下面的一些方法尽量避免死锁:

  1,合理安排表访问顺序;

  2,使用短事务;

  3,如果对数据的一致性要求不高,可以允许脏读,脏读不需要对数据资源加锁,可以避免冲突;

  4,如果可能的话,错开多个事务访问相同数据资源的时间,以防止锁冲突。

  5,使用尽可能低的事务隔离级别。

  为了实现短事务,在应用程序中可以考虑使用以下策略:

  1,如果可能的话,尝试把大的事务分解为多个小的事务,然后分别执行,这保证每个小事务都很快完成,不会对数据资源锁定很长时间。

  2,应该在处理事务之前就准备好用户必须提供的数据,不应该在执行事务过程中,停下来长时间等待输入数据。

  数据库系统提供了四种事务隔离级别供用户选择:

  1,Serializable:串行化。

  2,Repeatable Read:可重复读。

  3,Read Commited:读己提交数据。

  4,Read Uncommited:读未提交数据。

  隔离级别越高,越能保证数据的完整性和一致性,但是对并发性能的影响也越大。对于多数应用程序,可以优先把数据库系统的隔离级别设为 ReadCommited,它能够避免脏读,而且具有较好的并发性能,尽管它会导致不可重复读,虚读和第二类丢失更新这些并发问题,在可能出现这类问题的个别场合,可以由应用程序采用悲观锁或乐观锁来控制。

JDBC数据库连接使用数据库系统默认的隔离级别,在hibernate的配置文件中可以显式地设置隔离级别(hibernate.connection.isolation=2),每一种隔离级别都对应一个整数。

  1:Read Uncommitted;

  2:Read Committed;

  4:Repeatable Read;

  8:Serializable

  在受管理的环境中,如果hibernate使用的是数据库连接来自于应用服务器提供的数据源,hibernate不会修改这些连接的事务隔离级别,在这种情况下,应该通过修改应用服务器的数据源配置来修改隔离级别。

  悲观锁:指在应用程序中显式地为数据资源加锁,先锁定资源再进行操作,尽管悲观锁能够防止丢失更新和不可重复读这类并发问题,但是它会影响并发性能,因此应该很谨慎地使用悲观锁。

  乐观锁:完全依靠数据库的隔离级别来自动管理锁的工作,应用程序采用版本控制手段来避免可能出现的并发问题。

  悲观锁有两种实现方式:1,在应用程序中显式指定采用数据库系统的独占锁来锁定数据资源;2,在数据库表中增加一个表明记录状态的LOCK字段,当它取值为Y时,表示该记录己经被某个事务锁定,如果为N,表明该记录处于空闲状态,事务可以访问它。

  以下select语句,指定采用独占锁来锁定查询的记录:select ... for update;执行该查询语句的事务持有这把锁,直到事务结束才会释放锁。

  hibernate可以采用如下方式声明使用悲观锁:Account account = (Account)session.get(Account.class, 1, LockMode.UPGRADE);

  net.sf.hibernate.LockMode类表示锁模式,它的取值如下:

  LockMode.NONE:默认值。先查缓存,缓存没有再去数据库中查。

  LockMode.READ:总是查询数据库,如果映射文件设置了版本元素,就执行版本比较。主要用于对一个游离对象进行版本检查。

  LockMode.UPGRADE:总是查询数据库,如果映射文件设置了版本元素,就执行版本比较。如果数据库支持悲观锁就执行.... for update。否则执行普通查询。

  LockMode.UPGRADE_NOWAIT:和UPGRADE功能一样,此外,对oracle数据库执行... for update nowait; nowait表明,如果不能立即获得悲观锁就抛出异常。

  LockMode.WRITE:当hibernate向数据库保存或更新一个对象时,会自动使用这种模式,它仅供hibernate内部使用,应用程序中不应该使用它。

  如果数据库不支持select ... for update语句,也可以由应用程序来实现悲观锁,这需要要表中增加一个锁字段lock。

  hibernate映射文件中的<version>和<timestamp>元素都具有版本控制功能。< version>利用一个递增的整数来跟踪数据库表中记录的版本,<timestamp>用时间戳来跟踪数据库表中记录的版本。

  version的用法如下:

  配置文件中:<version name="version" column="VERSION"/>必须紧跟在<id>元素的后面。数据库中的version(int)字段与version属性映射。

  应用程序无需为JavaBean的version属性显示赋值,在持久化JavaBean 对象时,hibernate会自动为它赋初始值0,在更新数据时,hibernate会更新自动version属性:update ACCOUNTS set NAME='Tom',BALANCE=1100,VERSION=1 where ID=1 and VERSION=0;

  如果在此过程中有其它程序操作过此记录,那么它的version就会有更新,再次执行update语句时会找不到匹配的记录,此时hibernate会抛出StaleObjectStateException。在应用程序中应该处理这种异常,处理方法有两种:

1,自动撤消事务,通知用户信息己被其它事务修改,需要重新开始事务。

  2,通知用户信息己被其它事务修改,显示最新数据,由用户决定如果继续。

  只有当hibernate通过update语句更新一个对象时,才会修改它的version属性,对于存在关联关系的对象,只更新发生变化的对象,对没有发生变化的关联对象是不会更新的,也就是说version不具有级联特性。

  timestamp用法如下:

  配置文件和表中各加一个属性(表中是timestamp类型):<timestamp name="lastUpdatedTime" column="LAST_UPDATED_TIME" />必须紧跟在<id>元素的后面。

  当持久化一个JavaBean对象时,hibernate会自动用当前的系统时间为lastUpdatedTime属性赋值,更新时也会用系统时间来更新此字段。理论上<version>元素比<timestamp>更安全一些,因为<timestamp>只能精确到秒,不能处理毫秒内的同步。

  因此,建议使用基于整数的<version>元素。

  对游离对象进行版本检查,如果不一致,会抛出StaleObjectStateException()。:

  

Transaction trans = session.beginTransaction();

  session.lock(account, LockMode.READ);//仅仅执行版本检查(与数据库中的最新数据进行比较),而不会保存数据库。

  trans.commit();

  如果数据库中不包含代表版本或时间戳的字段,hibernate提供了其它方法实现乐观锁,把<class>元素的 optimistic-lock属性设为all。把<class>元素的optimistic-lock属性设为all或dirty。必须同时把dynamic-update属性设为true。

  optimistic-lock=true时,hibernate更新时会在where子句中包含JavaBean对象被加载时的所有属性。

  optimistic-lock=dirty时,hibernate更新时会在where子句中仅包含被更新过的属性。

  尽管这种方法也能实现乐观锁,但是这种方法速度很慢,而且只适用于在同一个session中加载了该对象,然后又在同一个session中更新了此对象的场合。如果在不同的session中,会导致第二个session无法知道JavaBean对象被第一个session加载时所有属性的初始值,因此不能在update语句的where子句中包含JavaBean对象的属性的初始值,因此执行以下update语句:update ACCOUNTS set NAME='tom',BALANCE=900 where ID=1;这会导致当前事务覆盖其它事务对这条记录己做的更新。

  hibernate的二级缓存本身的实现很复杂,必须实现并发访问策略以及数据过期策略。SessionFactory的外置缓存是一个可配置的缓存插件,在默认情况下不会启用。

  二级缓存,进程范围或群集范围,会出现并发问题,对二级缓存可以设定以下四种类型的并发访问策略,每一种策略对应一种事务隔离级别。

  1,事务型:仅仅在受管理环境中适用,它提供Repeatable Read事务隔离级别,对于经常读但是很少写的数据,可以采用这种隔离级别,因为它可以防止脏读和不可重复读这类并发问题。

  2,读写型:提供Read Committed事务隔离级别,仅仅在非群集的环境中适用,对于经常读但是很少写的数据,可以采用这种隔离类型,因为它可以防止脏读这类并发问题。

  3,非严格读写型:不保证缓存与数据库中数据的一致性。如果存在两个事务同时访问缓存中相同数据的可能,必须为该数据配置一个很短的数据过期时间,从而尽量避免脏读,对于极少被修改并且允许脏读的数据,可以采用这种并发访问策略。

  4,只读型:对于从来不会写的数据,可以使用这种并发访问策略。

  事务型策略的隔离级别最高,只读型的最低,事务隔离级别越高,并发性能越低,如果二级缓存中存放中的数据会经常被事务修改,就不得不提高缓存的事务隔离级别,但这又会降低并发性能,因此,只有符合以下条件的数据才适合于存放到二级缓存中:

  1,很少被修改;2,不是很重要的数据,允许偶尔出现并发问题;3,不会被并发访问的数据;4,参考数据;

  以下数据不适合于存放到二级缓存中:

  1,经常被修改的数据;2,财务数据,绝对不允许出现并发问题;3,与其它应用共享的数据;

  hibernate还为查询结果提供了一个查询缓存,它依赖于二级缓存。

  Session为应用程序提供了两个管理一缓存的方法:

  evict():从缓存中清除参数指定的持久化对象;如果在映射文件关联关系的cascade为all或all-delete-orphan 时,会级联清除;它适用于不希望session继续按照该对象的状态变化来同步更新数据库;在批量更新或指量删除的场合,当更新或删除一个对象后,及时释放该对象占用的内存;值得注意的是,批量更新或删除的最佳方式是直接通过JDBC API执行相关的SQL语句或者调用相关的存储过程。

  clear():清空缓存中所有持久化对象;

  在多数情况下,不提倡通过evict()和clear()方法来管理一级缓存,因为它们并不能显着地提高应用的性能,管理一级缓存的最有效的方法是采用合理的检索策略和检索方式,如通过延迟加载,集合过滤,投影查询等手段来节省内存开销。

hibernate中直接通过JDBC API来执行更新或删除操作的方法如下:

  

Transaction trans = session.beginTransaction();

  Connection conn = session.connection();

  PreparedStatement statement = conn.prepareStatement("update ACCOUNTS set AGE=AGE+1 where AGE>0");

  statement.executeUpdate();

  trans.commit();

  如果底层数据库支持存储过程,也可以直接调用存储过程来执行指量更新:

  Transaction trans = session.beginTransaction();

  Connection conn = session.connection();

  CallableStatement statement = conn.prepareCall("{call batchUpdateCustomer(?)}");

  statement.setInt(1, 0);//把第1个参数的值设为0

  statement.executeUpdate();

  trans.commit();

  hibernate中session的各种重载的update()方法一次都只能更新一个对象,而delete()方法有些重载形式允许以HQL语句作为参数,如:

  session.delete("from Customer c where c.age>0");

  但是它并不是执行一条delete语句,而是把符合条件的数据先查找出来,再一个个地执行delete操作。

hibernate的二级缓存允许选用以下类型的缓存插件:

  1,EHCache:可作为进程范围内的缓存,存放数据的物理介质可以是内存或硬盘,对hibernate的查询缓存提供了支持。

  2,OpenSymphony OSCache:可作为进程范围内的缓存,存放数据的物理介质可以是内存或硬盘,提供了丰富的缓存数据过期策略,对hibernate的查询缓存提供了支持。

  3,SwarmCache:可作为群集范围内的缓存,但不支持hibernate的查询缓存。

  4,JBossCache:可作为群集范围内的缓存,支持事务型并发访问策略,对hibernate的查询缓存提供了支持。

  下表列出了以上四种类型的缓存插件支持的并发访问策略:

  以面的四种缓存插件都是由第三方提供的。EHCache来自于hibernate开放源代码组织的另一个项目;JBossCache由 JBoss开放源代码组织提供;为了把这些缓存插件集成到hibernate中,hibernate提供了 net.sf.hibernate.cache.CacheProvider接口,它是缓存插件与hibernate之间的适配器。hibernate为以上缓存插件分别提供了内置的CacheProvider实现:

  net.sf.hibernate.cache.EhCacheProvider

  net.sf.hibernate.cache.OSCacheProvider

  net.sf.hibernate.cache.SwarmCacheProvider

  net.sf.hibernate.cache.TreeCacheProvider:JBossCache插件适配器。

  配置进程范围内的二级缓存主要包含以下步骤:

  1,选择需要使用二级缓存的持久化类。设置它的命名缓存的并发访问策略。hibernate既允许在分散的各个映射文件中为持久化类设置二级缓存,还允许在hibernate的配置文件hibernate.cfg.xml中集中设置二级缓存,后一种方式更有利于和缓存相关的配置代码的维护。示例如下:

  

<hibernate-configuration>

  <session-factory>

  <property ... >

  <!-- 设置JBossCache适配器 -->

  <property name="cache.provider_class">net.sf.hibernate.cache.TreeCacheProvider</property>

  <property name="cache.use_minimal_puts">true</property>

  <mapping .../>

  <!-- 设置Category类的二级缓存的并发访问策略 -->

  <class-cache class="mypack.Category" usage="transaction" />

  <!-- 设置Category类的items集合的二级缓存的并发访问策略 -->

  <collection-cache collection="mypack.Category.items" usage="transactional" />

  <!-- 设置Item类的二级缓存的并发访问策略 -->

  <class-cache class="mypack.Item" usage="transactional" />

  </session-factory>

  </hibernate-configuration>

  cache.use_minimal_puts属性为true,表示hibernate会先检查对象是否己经存在于缓存中,只有当对象不在缓存中,才会向缓存加入该对象的散装数据,默认为false。对于群集范围的缓存,如果读缓存的系统开销比写缓存的系统开销小,可以将此属性设为true,从而提高访问缓存的性能,而对于进程范围内的缓存,此属性应该取默认值false。

  2,选择合适的缓存插件,每种插件都有自带的配置文件,因此需要手工编辑该配置文件,EHCache的配置文件为ehcache.xml,而JBossCache的配置文件为treecache.xml。在配置文件中需要为每个命名缓存设置数据过期策略。

  hibernate允许在类和集合的粒度上设置二级缓存,在映射文件中,<class>和<set>元素都有一个 <cache>子元素,这个子元素用来配置二级缓存,例如以下代码把Category实例放入二级缓存中,采用读写并发访问策略:

 

 <class name="mypack.Category" talbe="CATEGORIES">

  <cache usage="read-write"/>
  <id ...>

</id>

  ...

  </class>

  每当应用程序从其它对象导航到Category对象,或者从数据库中加载Category对象时,hibernate就会把这个对象放到第二级缓存中,<class>元素的<cache>子元素表明hibernate会缓存Category对象的简单属性的值,但是它并不会同时缓存Category对象的集合属性,如果希望缓存集合属性中的元素,必须在<set>元素中加入<cache>子元素:

  

<set name="items" inverse="true" lazy="true">

  <cache usage="read-write" />

  <key ...>

  </set>

  当应用程序调用category.getItems().iterate()方法时,hibernate会把item集合中的元素存放到缓存中,此时hibernate仅仅把与Category关联的Item对象的OID存放到缓存中,如果希望把整个Item对象的散装数据存入缓存,应该在 Item.hbm.xml文件的<class>元素中加入<cache>子元素。

  EHCache缓存插件是理想的进程范围内的缓存实现。如果使用这种缓存插件,需要在hibernate的hibernate.properties配置文件中指定EhCacheProvider适配器,代码如下:

  hibernate.cache.provider=net.sf.hibernate.cache.EhCacheProvider

  EHCache缓存有自己的配置文件,名为ehcache.xml,这个文件必须存放于应用的classpath中,下面是一个样例:

  

<ehcache>

  <diskStore path="C:\\temp"/>

  <defaultCache maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="120" timeToLiveSeconds="120" overflowToDisk="true"/>

  <cache name="mypack.Category" maxElementsInMemory="500" eternal="true" timeToIdleSeconds="0" timeToLiveSeconds="0" overflowToDisk="false"/>

  <cache name="mypack.Category.items" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/>

  <cache name="mypack.Item" maxElementsInMemory="10000" eternal="false" timeToIdleSeconds="300" timeToLiveSeconds="600" overflowToDisk="true"/>

  </ehcache>

  hibernate软件包的etc目录下提供了ehcache.xml文件的样例,并且对于它的配置元素做了详细的说明。

  ehcache.xml目录下提供了ehcache.xml文件的样例,并且对它的配置元素做了详细的说明:

  <diskStore>:指定一个文件目录,当EHCache把数据写到硬盘上时,将把数据写到这个文件目录下。

  <defaultCache>:设定缓存的默认数据过期策略。

  <cache>:设定具体的命名缓存的数据过期策略。

  在映射文件中,对每个需要二级缓存的类和集合都做了单独的配置,与此对应,在ehcache.xml文件中通过<cache>元素来为每个需要二级缓存的类和集合设定缓存的数据过期策略。下面解释一下<cache>元素的各个属性的作用:

  name:设置缓存的名字,它的取值为类的完整名字或者类的集合的名字,如果name属性为mypack.Category,表示 Category类的二级缓存;如果name属性为mypack.Category.items,表示Category类的items集合的二级缓存。

  maxInMemory:设置基于内存的缓存可存放的对象的最大数目。

  eternal:如果为true,表示对象永远不会过期,此时会忽略timeToIdleSeconds和timeToLiveSeconds属性。默认为false。

  timeToIdleSeconds:设定允许对象处于空闲状态的最长时间,以秒为单位,当对象从最近一次被访问后,如果处于空闲状态的时间超过了指定的值,这个对象会过期,EHCache将把它从缓存中清除,只有当eternal属性为false,它才有效,值为0表示对象可以无限期地处于空闲状态。

  timeToLiveSeconds:设定对象允许存在于缓存中的最长时间,以秒为单位,当对象自从被放入缓存中后,如果处于缓存中的时间超过了指定的值,这个对象就会过期,EHCache将把它从缓存中清除,只有当eternal属性为false,它才有效,值为0表示对象可以无限期地处于空闲状态。它的值必须大于或等于timeToIdleSeconds的值才有意义。

  overflowToDisk:如果为true,表示当基于内存的缓存中的对象数目达到了maxInMemory界限,会把溢出的对象写到基于硬盘的缓存中。

  每个命名缓存代表一个缓存区域,每个缓存区域有各自的数据过期策略,命名缓存机制使得用户能够在每个类以及类的每个集合的粒度上设置数据过期策略。

  EHCache适用于hibernate应用发布在单个机器中的场合。

在群集环境下,可以用JBossCache作为hibernate的二级缓存,它的配置步骤如下:

  1,在hibernate配置文件中设置JBossCache适配器,并且为需要使用二级缓存的类和集合设置缓存的并发访问策略。

  2,编辑JBossCache自身的配置文件,名为treecache.xml,这个文件必须放在classpath中,对于群集环境中的每个节点,都必须提供单独的treecache.xml文件,假如群集环境中有两个节点node A和node B,node A节点的名字为ClusterA,下面是node A节点的treecache.xml文件的样例:
<?xml version="1.0" encoding="UTF-8"?>

  <server>

  <classpath codebase="./lib" archives="jboss-cache.jar,jgroups.jar"/>

  <!-- 把TreeCache发布为JBoss的一个JMX服务 -->

  <mbean code="org.jboss.cache.TreeCache" name="jboss.cache:service=TreeCache">

  <depends>jboss:service=Naming</depends>

  <depends>jboss:service=TransactionManager</depends>

  <!-- TreeCache运行在群集环境的名为ClusterA的节点上 -->

  <attribute name="ClusterName">ClusterA</attribute>

  <!-- TreeCache采用同步通信机制 -->

  <attribute name="CacheMode">REPL_SYNC</attribute>

  <attribute name="SyncReplTimeout">10000</attribute>

  <attribute name="LockAcquisitionTimeout">15000</attribute>

  <attribute name="FetchStateOnStartup">true</attribute>

  <!-- TreeCache采用内置的数据过期策略:LRUPolicy -->

  <attribute name="EvictionPolicyClass">org.jboss.cache.eviction,LRUPolicy</attribute>

  <attribute name="EvictionPolicyConfig">

  <config>

  <attribute name="wakeUpIntervalSeconds">5</attribute>

  <!-- Cache wide default -->

  <region name="/_default_">

  <attribute name="maxNodes">5000</attribute>

  <attribute name="timeToIdleSeconds">1000</attribute>

  </region>

  <!-- 配置Category类的数据过期策略 -->

  <region name="/mypack/Category">

  <attribute name="maxNodes">500</attribute>

  <attribute name="timeToIdleSeconds">5000</attribute>

  </region>

  <!-- 配置Category类的items集合的数据过期策略 -->

  <region name="/mypack/Category/items">

  <attribute name="maxNodes">5000</attribute>

  <attribute name="timeToIdleSeconds">1800</attribute>

  </region>

  </config>

  </attribute>

  <!-- 配置JGroup -->

  <attribute name="ClusterConfig">

  <config>

  <UDP bind_addr="202.145.1.2" ip_mcast="true" loopback="false" />

  <PING timeout="2000" num_initial_members="3" up_thread="false" down_thread="false" />

  <FD_SOCK/>

  <pbcast.NAKACK gc_lag="50" retransmit_timeout="600,1200,2400,4800" max_xmit_size="8192" up_thread="false" down_thread="false" />

  <UNICAST timeout="600,1200,2400" window_size="100" min_threshold="10" down_thread="false" />

  <pbcast.STABLE desired_avg_gossip="20000" up_thread="false" down_thread="false" />

  <FRAG frag_size="8192" down_thread="false" up_thread="false" />

  <pbcast.GMS join_timeout="5000" join_retry_timeout="2000" shun="true" print_local_addr="true" />

  <pbcast.STATE_TRANSFER up_thread="true" down_thread="true" />

  </config>

  </attribute>

  </mbean>

  </server>

  以上配置文件把JBossCache配置为JBoss的一个JMX服务,此外还配置了JGroup,它是一个通信库。JBoss为 JBossCache提供了几种实现,hibernate采用的是TreeCache实现。treecache.xml文件夹的开头几行是JBoss的 JMX服务的发布描述符,如果TreeCache不运行在JBoss服务器中,这几行会被忽略。

  TreeCache采用内置的org.jboss.cache.eviction.LRUPolicy策略,它是一种控制缓存中的数据过期的策略,由于当一个对象过期后,就会从缓存中清除,因此数据过期策略也叫做数据清除策略。接下来配置了Category类和它的items集合设置了具体的数据过期策略。

  最后配置了JGroup,它包含一系列通信协议,这些通信协议的次序很重要,不能随意修改它们。第一个协议为UDP,它和一个IP地址202.145.1.1绑定,这是当前节点的IP地址,UDP协议使得该节点支持广播通信,如果节点选用的是微软的windows平台,必须把loopback属性设为true。其它的JGroup属性很复杂,它们主要用于管理群集中节点之间的通信,想进一步了解,可以参考JBoss网站上的JGroup文档。

  可以按照同样的方式配置node B节点的treecache.xml文件,只需修改其中UDP协议的IP绑定地址。

  通过以上配置,hibernate将启用群集范围内的事务型缓存,每当一个新的元素加入到一个节点的缓存中时,这个元素就会被复制到其它节点的缓存中,如果缓存中的一个元素被更新,那么它就会过期,并从缓存中清除。

只有在hibernate的配置文件或映射文件中为一个持久化类设置了二级缓存,hibernate在加载这个类的实例时才会启用二级缓存。

  

SessionFactory也提供了evict()方法用于从二级缓存中清除对象的散装数据,如:

  sessionFactory.evict(Category.class, 1);//清除二级缓存中OID为1的Category对象

  sessionFactory.evict("mypack.Category");//清除二级缓存中所有的Category对象

  sessionFactory.evictCollection("mypack.Category.items");//清除二级缓存中Category类的所有对象的items集合


 
Hibernate的事务和并发
 
 

Hibernate的事务和并发控制很容易掌握。Hibernate直接使用JDBC连接和JTA资源,不添加任何附加锁定行为。我们强烈推荐你花点时间了解JDBC编程,ANSI SQL查询语言和你使用的数据库系统的事务隔离规范。Hibernate只添加自动版本管理,而不会锁定内存中的对象,也不会改变数据库事务的隔离级别。基本上,使用Hibernate就好像直接使用JDBC(或者JTA/CMT)来访问你的数据库资源。

除了自动版本管理,针对行级悲观锁定,Hibernate也提供了辅助的API,它使用了SELECT FOR UPDATE的SQL语法。本章后面会讨论这个API。

我们从Configuration层、SessionFactory层, 和Session层开始讨论Hibernate的并行控制、数据库事务和应用程序的长事务。

1.1.Session和事务范围(transaction scopes)

一个SessionFactory对象的创建代价很昂贵,它是线程安全的对象,它被设计成可以为所有的应用程序线程所共享。它只创建一次,通常是在应用程序启动的时候,由一个Configuraion的实例来创建。

一个Session的对象是轻型的,非线程安全的,对于单个业务进程,单个的工作单元而言,它只被使用一次,然后就丢弃。只有在需要的时候,Session才会获取一个JDBC的Connection(或一个Datasource)对象。所以你可以放心的打开和关闭Session,甚至当你并不确定一个特定的请求是否需要数据访问时,你也可以这样做。(一旦你实现下面提到的使用了请求拦截的模式,这就变得很重要了。

此外我们还要考虑数据库事务。数据库事务应该尽可能的短,降低数据库锁定造成的资源争用。数据库长事务会导致你的应用程序无法扩展到高的并发负载。

一个操作单元(Unit of work)的范围是多大?单个的Hibernate Session能跨越多个数据库事务吗?还是一个Session的作用范围对应一个数据库事务的范围?应该何时打开Session,何时关闭Session?,你又如何划分数据库事务的边界呢?

1.1.1.操作单元(Unit of work)

首先,别再用session-per-operation这种反模式了,也就是说,在单个线程中,不要因为一次简单的数据库调用,就打开和关闭一次Session!数据库事务也是如此。应用程序中的数据库调用是按照计划好的次序,分组为原子的操作单元。(注意,这也意味着,应用程序中,在单个的SQL语句发送之后,自动事务提交(auto-commit)模式失效了。这种模式专门为SQL控制台操作设计的。Hibernate禁止立即自动事务提交模式,或者期望应用服务器禁止立即自动事务提交模式。)

在多用户的client/server应用程序中,最常用的模式是每个请求一个会话(session-per-request)。在这种模式下,来自客户端的请求被发送到服务器端(即Hibernate持久化层运行的地方),一个新的Hibernate Session被打开,并且执行这个操作单元中所有的数据库操作。一旦操作完成(同时发送到客户端的响应也准备就绪),session被同步,然后关闭。你也可以使用单个数据库事务来处理客户端请求,在你打开Session之后启动事务,在你关闭Session之前提交事务。会话和请求之间的关系是一对一的关系,这种模式对于大多数应用程序来说是很棒的。

真正的挑战在于如何去实现这种模式:不仅Session和事务必须被正确的开始和结束,而且他们也必须能被数据访问操作访问。用拦截器来实现操作单元的划分,该拦截器在客户端请求达到服务器端的时候开始,在服务器端发送响应(即,ServletFilter)之前结束。我们推荐使用一个ThreadLocal变量,把Session绑定到处理客户端请求的线程上去。这种方式可以让运行在该线程上的所有程序代码轻松的访问Session(就像访问一个静态变量那样)。你也可以在一个ThreadLocal变量中保持事务上下文环境,不过这依赖于你所选择的数据库事务划分机制。这种实现模式被称之为ThreadLocal Session和OpenSession in View。你可以很容易的扩展本文前面章节展示的HibernateUtil辅助类来实现这种模式。当然,你必须找到一种实现拦截器的方法,并且可以把拦截器集成到你的应用环境中。请参考Hibernate网站上面的提示和例子。

1.1.2.应用程序事务(Application transactions)

session-per-request模式不仅仅是一个可以用来设计操作单元的有用概念。很多业务处理流程都需要一系列完整的和用户之间的交互,即用户对数据库的交叉访问。在基于web的应用和企业应用中,跨用户交互的数据库事务是无法接受的。考虑下面的例子:

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

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

从用户的角度来看,我们把这个操作单元称为应用程序长事务(application transaction)。在你的应用程序中,可以有很多种方法来实现它。

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

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

自动版本化-Hibernate能够自动进行乐观并发控制,如果在用户思考的过程中发生并发修改冲突,Hibernate能够自动检测到。

脱管对象(Detached Objects)-如果你决定采用前面已经讨论过的session-per-request模式,所有载入的实例在用户思考的过程中都处于与Session脱离的状态。Hibernate允许你把与Session脱离的对象重新

关联到Session上,并且对修改进行持久化,这种模式被称为session-per-request-with-detached-objects。自动版本化被用来隔离并发修改。

长生命周期的Session(Long Session)-Hibernate的Session可以在数据库事务提交之后和底层的JDBC连接断开,当一个新的客户端请求到来的时候,它又重新连接上底层的JDBC连接。这种模式被称之为session-per-application-transaction,这种情况可能会造成不必要的Session和JDBC连接的重新关联。

自动版本化被用来隔离并发修改。

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

#p#

1.1.3.关注对象标识(Considering object identity)

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

数据库标识

foo.getId().equals( bar.getId() )
JVM 标识
foo==bar

对于那些关联到特定Session(也就是在单个Session的范围内)上的对象来说,这两种标识的语义是等价的,与数据库标识对应的JVM标识是由Hibernate来保证的。不过,当应用程序在两个不同的session中并发访问具有同一持久化标识的业务对象实例的时候,这个业务对象的两个实例事实上是不相同的(从JVM识别来看)。这种冲突可以通过在同步和提交的时候使用自动版本化和乐观锁定方法来解决。

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

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

1.1.4.常见问题

决不要使用反模式session-per-user-session或者session-per-application(当然,这个规定几乎没有例外)。请注意,下述一些问题可能也会出现在我们推荐的模式中,在你作出某个设计决定之前,请务必理解该模式的应用前提。

Session是一个非线程安全的类。如果一个Session实例允许共享的话,那些支持并发运行的东东,例如HTTP request,session beans,或者是Swing workers,将会导致出现资源争用(race condition)。如果在HttpSession中有Hibernate的Session的话(稍后讨论),你应该考虑同步访问你的Httpsession。否则,只要用户足够快的点击浏览器的“刷新”,就会导致两个并发运行线程使用同一个Session。

一个由Hibernate抛出的异常意味着你必须立即回滚数据库事务,并立即关闭Session(稍后会展开讨论)。如果你的Session绑定到一个应用程序上,你必须停止该应用程序。回滚数据库事务并不会把你的业务对象退回到事务启动时候的状态。这意味着数据库状态和业务对象状态不同步。通常情况下,这不是什么问题,因为异常是不可恢复的,你必须在回滚之后重新开始执行。

Session缓存了处于持久化状态的每个对象(Hibernate会监视和检查脏数据)。这意味着,如果你让Session打开很长一段时间,或是仅仅载入了过多的数据,Session占用的内存会一直增长,直到抛出OutOfMemoryException异常。这个问题的一个解决方法是调用clear()和evict()来管理Session的缓存,但是如果你需要大批量数据操作的话,最好考虑使用存储过程。在第14章批量处理(Batchprocessing)中有一些解决方案。在用户会话期间一直保持Session打开也意味着出现脏数据的可能性很高。

1.2.数据库事务声明

数据库(或者系统)事务的声明总是必须的。在数据库事务之外,就无法和数据库通讯(这可能会让那些习惯于自动提交事务模式的开发人员感到迷惑)。永远使用清晰的事务声明,即使只读操作也是如此。进行显式的事务声明并不总是需要的,这取决于你的事务隔离级别和数据库的能力,但不管怎么说,声明事务总归有益无害。

一个Hibernate应用程序可以运行在非托管环境中(也就是独立运行的应用程序,简单Web应用程序,或者Swing图形桌面应用程序),也可以运行在托管的J2EE环境中。在一个非托管环境中,Hibernate通常自己负责管理数据库连接池。应用程序开发人员必须手工设置事务声明,换句话说,就是手工启动,提交,或者回滚数据库事务。一个托管的环境通常提供了容器管理事务,例如事务装配通过可声明的方式定义在EJB session beans的部署描述符中。可编程式事务声明不再需要,即使是Session的同步也可以自动完成。

让持久层具备可移植性是人们的理想。Hibernate提供了一套称为Transaction的封装API,用来把你的部署环境中的本地事务管理系统转换到Hibernate事务上。这个API是可选的,但是我们强烈推荐你使用,除非你用CMT session bean。

通常情况下,结束Session包含了四个不同的阶段:

◆同步session(flush,刷出到磁盘)
◆提交事务
◆关闭session
◆处理异常

session的同步(flush,刷出)前面已经讨论过了,我们现在进一步考察在托管和非托管环境下的事务声明和异常处理。

1.2.1.非托管环境

如果Hibernat持久层运行在一个非托管环境中,数据库连接通常由Hibernate的连接池机制来处理。
  
代码内容
session/transaction处理方式如下所示:

//Non-managed environment idiom Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // do some work ... tx.commit(); } catch (RuntimeException e) { if (tx != null) tx.rollback(); throw e; // or display error message } finally { sess.close(); }

你不需要显式flush() Session-对commit()的调用会自动触发session的同步。

调用close()标志session的结束。close()方法重要的暗示是,session释放了JDBC连接。

这段Java代码是可移植的,可以在非托管环境和JTA环境中运行。

你很可能从未在一个标准的应用程序的业务代码中见过这样的用法;致命的(系统)异常应该总是在应用程序“顶层”被捕获。换句话说,执行Hibernate调用的代码(在持久层)和处理RuntimeException异常的代码(通常只能清理和退出应用程序)应该在不同的应用程序逻辑层。这对于你设计自己的软件系统来说是一个挑战,只要有可能,你就应该使用J2EE/EJB容器服务。异常处理将在本章稍后进行讨论。

请注意,你应该选择org.hibernate.transaction.JDBCTransactionFactory(这是默认选项)。

#p#

1.2.2.使用JTA

如果你的持久层运行在一个应用服务器中(例如,在EJB session beans的后面),Hibernate获取的每个数据源连接将自动成为全局JTA事务的一部分。Hibernate提供了两种策略进行JTA集成。

如果你使用bean管理事务(BMT),可以通过使用Hibernate的Transaction API来告诉应用服务器启动和结束BMT事务。因此,事务管理代码和在非托管环境下是一样的。
  
代码内容

// BMT idiom Session sess = factory.openSession(); Transaction tx = null; try { tx = sess.beginTransaction(); // do some work ... tx.commit(); } catch (RuntimeException e) { if (tx != null) tx.rollback(); throw e; // or display error message } finally { sess.close(); }


在CMT方式下,事务声明是在session bean的部署描述符中,而不需要编程。除非你设置了属性hibernate.transaction.flush_before_completion和hibernate.transaction.auto_close_session为true,否则你必须自己同步和关闭Session。Hibernate可以为你自动同步和关闭Session。你唯一要做的就是当发生异常时进行事务回滚。幸运的是,在一个CMT bean中,事务回滚甚至可以由容器自动进行,因为由session bean方法抛出的未处理的RuntimeException异常可以通知容器设置全局事务回滚。这意味着在CMT中,你完全无需使用Hibernate的Transaction API 。

请注意,当你配置Hibernate事务工厂的时候,在一个BMT session bean中,你应该选择org.hibernate.transaction.JTATransactionFactory,在一个CMT session bean中选择org.hibernate.transaction.CMTTransactionFactory。记住,同时也要设置org.hibernate.transaction.manager_lookup_class。

如果你使用CMT环境,并且让容器自动同步和关闭session,你可能也希望在你代码的不同部分使用同一个session。一般来说,在一个非托管环境中,你可以使用一个ThreadLocal变量来持有这个session,但是单个EJB方法调用可能会在不同的线程中执行(举例来说,一个session bean调用另一个session bean)。如果你不想在应用代码中被传递Session对象实例的问题困扰的话,那么SessionFactory提供的getCurrentSession()方法就很适合你,该方法返回一个绑定到JTA事务上下文环境中的session实例。这也是把Hibernate集成到一个应用程序中的最简单的方法!这个“当前的”session总是可以自动同步和自动关闭(不考虑上述的属性设置)。我们的session/transaction管理代码减少到如下所示:
  
代码内容

// CMT idiom Session sess = factory.getCurrentSession(); // do some work ...

换句话来说,在一个托管环境下,你要做的所有的事情就是调用SessionFactory.getCurrentSession(),然后进行你的数据访问,把其余的工作交给容器来做。事务在你的session bean的部署描述符中以可声明的方式来设置。session的生命周期完全由Hibernate来管理。

对after_statement连接释放方式有一个警告。因为JTA规范的一个很愚蠢的限制,Hibernate不可能自动清理任何未关闭的ScrollableResults或者Iterator,它们是由scroll()或iterate()产生的。你must通过在finally块中,显式调用ScrollableResults.close()或者Hibernate.close(Iterator)方法来释放底层数据库游标。(当然,大部分程序完全可以很容易的避免在CMT代码中出现scroll()或iterate()。)

1.2.3.异常处理

如果Session抛出异常(包括任何SQLException),你应该立即回滚数据库事务,调用Session.close(),丢弃该Session实例。Session的某些方法可能会导致session处于不一致的状态。所有由Hibernate抛出的异常都视为不可以恢复的。确保在finally代码块中调用close()方法,以关闭掉Session。

HibernateException是一个非检查期异常(这不同于Hibernate老的版本),它封装了Hibernate持久层可能出现的大多数错误。我们的观点是,不应该强迫应用程序开发人员在底层捕获无法恢复的异常。在大多数软件系统中,非检查期异常和致命异常都是在相应方法调用的堆栈的顶层被处理的(也就是说,在软件上面的逻辑层),并且提供一个错误信息给应用软件的用户(或者采取其他某些相应的操作)。请注意,Hibernate也有可能抛出其他并不属于HibernateException的非检查期异常。这些异常同样也是无法恢复的,应该采取某些相应的操作去处理。

在和数据库进行交互时,Hibernate把捕获的SQLException封装为Hibernate的JDBCException。事实上,Hibernate尝试把异常转换为更有实际含义的JDBCException异常的子类。底层的SQLException可以通过JDBCException.getCause()来得到。Hibernate通过使用关联到SessionFactory上的SQLExceptionConverter来把SQLException转换为一个对应的JDBCException异常的子类。默认情况下,SQLExceptionConverter可以通过配置dialect选项指定;此外,也可以使用用户自定义的实现类(参考javadocs SQLExceptionConverterFactory类来了解详情)。标准的JDBCException子类型是:

◆JDBCConnectionException-指明底层的JDBC通讯出现错误
◆SQLGrammarException-指明发送的SQL语句的语法或者格式错误
◆ConstraintViolationException-指明某种类型的约束违例错误
◆LockAcquisitionException-指明了在执行请求操作时,获取所需的锁级别时出现的错误。
◆GenericJDBCException-不属于任何其他种类的原生异常

1.3.乐观并发控制(Optimistic concurrency control)

唯一能够同时保持高并发和高可伸缩性的方法就是使用带版本化的乐观并发控制。版本检查使用版本号、或者时间戳来检测更新冲突(并且防止更新丢失)。Hibernate为使用乐观并发控制的代码提供了三种可能的方法,应用程序在编写这些代码时,可以采用它们。我们已经在前面应用程序长事务那部分展示了乐观并发控制的应用场景,此外,在单个数据库事务范围内,版本检查也提供了防止更新丢失的好处。

#p#

1.3.1.应用程序级别的版本检查(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属性使用来映射,如果对象是脏数据,在同步的时候,Hibernate会自动增加版本号。

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

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

1.3.2.长生命周期session和自动版本化

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

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

// foo is an instance loaded earlier by the Session session.reconnect(); // Obtain a new JDBC connection Transaction t = session.beginTransaction(); foo.setProperty("bar"); t.commit(); // End database transaction, flushing the change and checking the version session.disconnect(); // Return JDBC connection


foo对象始终和载入它的Session相关联。Session.reconnect()获取一个新的数据库连接(或者你可以提供一个),并且继续当前的session。Session.disconnect()方法把session与JDBC连接断开,把数据库连接返回到连接池(除非是你自己提供的数据库连接)。在Session重新连接上数据库连接之后,你可以对任何可能被其他事务更新过的对象调用Session.lock(),设置LockMode.READ锁定模式,这样你就可以对那些你不准备更新的数据进行强制版本检查。此外,你并不需要锁定那些你准备更新的数据。假若对disconnect()和reconnect()的显式调用发生得太频繁了,你可以使用hibernate.connection.release_mode来代替。

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

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

1.3.3.脱管对象(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会再一次在同步的时候检查对象实例的版本,如果发生更新冲突,就抛出异常。

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

1.3.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语句。这通常不是个问题,但是数据库里面对onupdate点火的触发器可能在脱管对象没有任何更改的情况下被触发。因此,你可以在的映射中,通过设置select-before-update="true"来定制这一行为,强制Hibernate SELECT这个对象实例,从而保证,在更新记录之前,对象的确是被修改过。

#p#

1.4.悲观锁定(Pessimistic Locking)

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

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

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

◆当Hibernate更新或者插入一行记录的时候,锁定级别自动设置为LockMode.WRITE。
◆当用户显式的使用数据库支持的SQL格式SELECT...FOR UPDATE发送SQL的时候,锁定级别设置为LockMode.UPGRADE。
◆当用户显式的使用Oracle数据库的SQL语句SELECT...FOR UPDATE NOWAIT的时候,锁定级别设置LockMode.UPGRADE_NOWAIT。
◆当Hibernate在“可重复读”或者是“序列化”数据库隔离级别下读取数据的时候,锁定模式自动设置为LockMode.READ。这种模式也可以通过用户显式指定进行设置。
◆LockMode.NONE代表无需锁定。在Transaction结束时,所有的对象都切换到该模式上来。与session相关联的对象通过调用update()或者saveOrUpdate()脱离该模式。

“显式的用户指定”可以通过以下几种方式之一来表示:

◆调用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将使用适当的替代模式(而不是扔出异常)。这一点可以确保应用程序的可移植性。


 


  • 1
    点赞
  • 6
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值