Hibernate如何提升数据库查询的性能

数据库查询性能的提升也是涉及到开发中的各个阶段,在开发中选用正确的查询方法无疑是最基础也最简单的。

SQL语句的优化

使用正确的SQL语句可以在很大程度上提高系统的查询性能。获得同样数据而采用不同方式的SQL语句在性能上的差距可能是十分巨大的。

由于Hibernate是对JDBC的封装,SQL语句的产生都是动态由Hibernate自动完成的。Hibernate产生SQL语句的方式有两种:一种是通过开发人员编写的HQL语句来生成,另一种是依据开发人员对关联对象的访问来自动生成相应的SQL语句。

至于使用什么样的SQL语句可以获得更好的性能要依据数据库的结构以及所要获取数据的具体情况来进行处理。在确定了所要执行的SQL语句后,可以通过以下三个方面来影响Hibernate所生成的SQL语句:

●   HQL语句的书写方法。
●   查询时所使用的查询方法。
●   对象关联时所使用的抓取策略。
使用正确的查询方法

在前面已经介绍过,执行数据查询功能的基本方法有两种:一种是得到单个持久化对象的get()方法和load()方法,另一种是Query对象的list()方法和iterator()方法。在开发中应该依据不同的情况选用正确的方法。

get()方法和load()方法的区别在于对二级缓存的使用上。load()方法会使用二级缓存,而get()方法在一级缓存没有找到的情况下会直接查询数据库,不会去二级缓存中查找。在使用中,对使用了二级缓存的对象进行查询时最好使用load()方法,以充分利用二级缓存来提高检索的效率。

list()方法和iterator()方法之间的区别可以从以下几个方面来进行比较。

● 执行的查询不同

list()方法在执行时,是直接运行查询结果所需要的查询语句,而iterator()方法则是先执行得到对象ID的查询,然后再根据每个ID值去取得所要查询的对象。因此,对于list()方式的查询通常只会执行一个SQL语句,而对于iterator()方法的查询则可能需要执行N+1条SQL语句(N为结果集中的记录数)。

iterator()方法只是可能执行N+1条数据,具体执行SQL语句的数量取决于缓存的情况以及对结果集的访问情况。

● 缓存的使用

list()方法只能使用二级缓存中的查询缓存,而无法使用二级缓存对单个对象的缓存(但是会把查询出的对象放入二级缓存中)。所以,除非重复执行相同的查询操作,否则无法利用缓存的机制来提高查询的效率。

iterator()方法则可以充分利用二级缓存,在根据ID检索对象的时候会首先到缓存中查找,只有在找不到的情况下才会执行相应的查询语句。所以,缓存中对象的存在与否会影响到SQL语句的执行数量。

● 对于结果集的处理方法不同

list()方法会一次获得所有的结果集对象,而且它会依据查询的结果初始化所有的结果集对象。这在结果集非常大的时候必然会占据非常多的内存,甚至会造成内存溢出情况的发生。

iterator()方法在执行时不会一次初始化所有的对象,而是根据对结果集的访问情况来初始化对象。因此在访问中可以控制缓存中对象的数量,以避免占用过多缓存,导致内存溢出情况的发生。使用iterator()方法的另外一个好处是,如果只需要结果集中的部分记录,那么没有被用到的结果对象根本不会被初始化。所以,对结果集的访问情况也是调用iterator()方法时执行数据库SQL语句多少的一个因素。

所以,在使用Query对象执行数据查询时应该从以上几个方面去考虑使用何种方法来执行数据库的查询操作。

使用正确的抓取策略

所谓抓取策略(fetching strategy)是指当应用程序需要利用关联关系进行对象获取的时候,Hibernate获取关联对象的策略。抓取策略可以在O/R映射的元数据中声明,也可以在特定的HQL或条件查询中声明。

Hibernate 3定义了以下几种抓取策略。

● 连接抓取(Join fetching)

连接抓取是指Hibernate在获得关联对象时会在SELECT语句中使用外连接的方式来获得关联对象。

● 查询抓取(Select fetching)

查询抓取是指Hibernate通过另外一条SELECT语句来抓取当前对象的关联对象的方式。这也是通过外键的方式来执行数据库的查询。与连接抓取的区别在于,通常情况下这个SELECT语句不是立即执行的,而是在访问到关联对象的时候才会执行。

● 子查询抓取(Subselect fetching)

子查询抓取也是指Hibernate通过另外一条SELECT语句来抓取当前对象的关联对象的方式。与查询抓取的区别在于它所采用的SELECT语句的方式为子查询,而不是通过外连接。

● 批量抓取(Batch fetching)

批量抓取是对查询抓取的优化,它会依据主键或者外键的列表来通过单条SELECT语句实现管理对象的批量抓取。

以上介绍的是Hibernate 3所提供的抓取策略,也就是抓取关联对象的手段。为了提升系统的性能,在抓取关联对象的时机上,还有以下一些选择。

● 立即抓取(Immediate fetching)

立即抓取是指宿主对象被加载时,它所关联的对象也会被立即加载。

● 延迟集合抓取(Lazy collection fetching)

延迟集合抓取是指在加载宿主对象时,并不立即加载它所关联的对象,而是到应用程序访问关联对象的时候才抓取关联对象。这是集合关联对象的默认行为。

● 延迟代理抓取(Lazy proxy fetching)

延迟代理抓取是指在返回单值关联对象的情况下,并不在对其进行get操作时抓取,而是直到调用其某个方法的时候才会抓取这个对象。

● 延迟属性加载(Lazy attribute fetching)

延迟属性加载是指在关联对象被访问的时候才进行关联对象的抓取。

介绍了Hibernate所提供的关联对象的抓取方法和抓取时机,这两个方面的因素都会影响Hibernate的抓取行为,最重要的是要清楚这两方面的影响是不同的,不要将这两个因素混淆,在开发中要结合实际情况选用正确的抓取策略和合适的抓取时机。

抓取时机的选择

在Hibernate 3中,对于集合类型的关联在默认情况下会使用延迟集合加载的抓取时机,而对于返回单值类型的关联在默认情况下会使用延迟代理抓取的抓取时机。

对于立即抓取在开发中很少被用到,因为这很可能会造成不必要的数据库操作,从而影响系统的性能。当宿主对象和关联对象总是被同时访问的时候才有可能会用到这种抓取时机。另外,使用立即连接抓取可以通过外连接来减少查询SQL语句的数量,所以,也会在某些特殊的情况下使用。

然而,延迟加载又会面临另外一个问题,如果在Session关闭前关联对象没有被实例化,那么在访问关联对象的时候就会抛出异常。处理的方法就是在事务提交之前就完成对关联对象的访问。

所以,在通常情况下都会使用延迟的方式来抓取关联的对象。因为每个立即抓取都会导致关联对象的立即实例化,太多的立即抓取关联会导致大量的对象被实例化,从而占用过多的内存资源。

抓取策略的选取

对于抓取策略的选取将影响到抓取关联对象的方式,也就是抓取关联对象时所执行的SQL语句。这就要根据实际的业务需求、数据的数量以及数据库的结构来进行选择了。

在这里需要注意的是,通常情况下都会在执行查询的时候针对每个查询来指定对其合适的抓取策略。指定抓取策略的方法如下所示:

User user = (User) session.createCriteria(User.class)

.setFetchMode("permissions", FetchMode.JOIN)

.add( Restrictions.idEq(userId) )

.uniqueResult();

本文介绍了查询性能提升的方法,关键是如何通过优化SQL语句来提升系统的查询性能。查询方法和抓取策略的影响也是通过执行查询方式和SQL语句的多少来改变系统的性能的。这些都属于开发人员所应该掌握的基本技能,避免由于开发不当而导致系统性能的低下。

在性能调整中,除了前面介绍的执行SQL语句的因素外,对于缓存的使用也会影响系统的性能。通常来说,缓存的使用会增加系统查询的性能,而降低系统增加、修改和删除操作的性能(因为要进行缓存的同步处理)。所以,开发人员应该能够正确地使用有效的缓存来提高数据查询的性能,而要避免滥用缓存而导致的系统性能变低。在采用缓存的时候也应该注意调整自己的检索策略和查询方法,这三者配合起来才可以达到最优的性能。

另外,事务的使用策略也会影响到系统的性能。选取正确的事务隔离级别以及使用正确的锁机制来控制数据的并发访问都会影响到系统的性能。

Hibernate的性能优化

Hibernate是对JDBC的轻量级封装,因此在很多情况下Hibernate的性能比直接使用JDBC存取数据库要低。然而,通过正确的方法和策略,在使用Hibernate的时候还是可以非常接近直接使用JDBC时的效率的,并且,在有些情况下还有可能高于使用JDBC时的执行效率。

在进行Hibernate性能优化时,需要从以下几个方面进行考虑:

●   数据库设计调整。

●   HQL优化。

●   API的正确使用(如根据不同的业务类型选用不同的集合及查询API)。

●   主配置参数(日志、查询缓存、fetch_size、batch_size等)。

●   映射文件优化(ID生成策略、二级缓存、延迟加载、关联优化)。

●   一级缓存的管理。

●   针对二级缓存,还有许多特有的策略。

●   事务控制策略。

数据的查询性能往往是影响一个应用系统性能的主要因素。对查询性能的影响会涉及到系统软件开发的各个阶段,例如,良好的设计、正确的查询方法、适当的缓存都有利于系统性能的提升。

系统性能的提升设计到系统中的各个方面,是一个相互平衡的过程,需要在应用的各个阶段都要考虑。并且在开发、运行的过程中要不断地调整和优化才能逐步提升系统的性能。

Hibernate查询方法与缓存的关系

在前面介绍了Hibernate的缓存技术以及基本的用法,在这里就具体的Hibernate所提供的查询方法与Hibernate缓存之间的关系做一个简单的总结。

在开发中,通常是通过两种方式来执行对数据库的查询操作的。一种方式是通过ID来获得单独的Java对象,另一种方式是通过HQL语句来执行对数据库的查询操作。下面就分别结合这两种查询方式来说明一下缓存的作用。

通过ID来获得Java对象可以直接使用Session对象的load()或者get()方法,这两种方式的区别就在于对缓存的使用上。

● load()方法

在使用了二级缓存的情况下,使用load()方法会在二级缓存中查找指定的对象是否存在。

在执行load()方法时,Hibernate首先从当前Session的一级缓存中获取ID对应的值,在获取不到的情况下,将根据该对象是否配置了二级缓存来做相应的处理。

如配置了二级缓存,则从二级缓存中获取ID对应的值,如仍然获取不到则还需要根据是否配置了延迟加载来决定如何执行,如未配置延迟加载则从数据库中直接获取。在从数据库获取到数据的情况下,Hibernate会相应地填充一级缓存和二级缓存,如配置了延迟加载则直接返回一个代理类,只有在触发代理类的调用时才进行数据库的查询操作。

在Session一直打开的情况下,并在该对象具有单向关联维护的时候,需要使用类似Session.clear(),Session.evict()的方法来强制刷新一级缓存。

● get()方法

get()方法与load()方法的区别就在于不会查找二级缓存。在当前Session的一级缓存中获取不到指定的对象时,会直接执行查询语句从数据库中获得所需要的数据。

在Hibernate中,可以通过HQL来执行对数据库的查询操作。具体的查询是由Query对象的list()和iterator()方法来执行的。这两个方法在执行查询时的处理方法存在着一定的差别,在开发中应该依据具体的情况来选择合适的方法。

● list()方法

在执行Query的list()方法时,Hibernate的做法是首先检查是否配置了查询缓存,如配置了则从查询缓存中寻找是否已经对该查询进行了缓存,如获取不到则从数据库中进行获取。从数据库中获取到后,Hibernate将会相应地填充一级、二级和查询缓存。如获取到的为直接的结果集,则直接返回,如获取到的为一些ID的值,则再根据ID获取相应的值(Session.load()),最后形成结果集返回。可以看到,在这样的情况下,list()方法也是有可能造成N次查询的。

查询缓存在数据发生任何变化的情况下都会被自动清空。

● iterator()方法

Query的iterator()方法处理查询的方式与list()方法是不同的,它首先会使用查询语句得到ID值的列表,然后再使用Session的load()方法得到所需要的对象的值。

在获取数据的时候,应该依据这4种获取数据方式的特点来选择合适的方法。在开发中可以通过设置show_sql选项来输出Hibernate所执行的SQL语句,以此来了解Hibernate是如何操作数据库的。

Hibernate查询缓存
查询缓存

查询缓存是专门针对各种查询操作进行缓存。查询缓存会在整个SessionFactory的生命周期中起作用,存储的方式也是采用key-value的形式来进行存储的。

查询缓存中的key是根据查询的语句、查询的条件、查询的参数和查询的页数等信息组成的。而数据的存储则会使用两种方式,使用SELECT语句只查询实体对象的某些列或者某些实体对象列的组合时,会直接缓存整个结果集。而对于查询结果为某个实体对象集合的情况则只会缓存实体对象的ID值,以达到缓存空间可以共用,节省空间的目的。

在使用查询缓存时,除了需要设置hibernate.cache.provider_class参数来启动二级缓存外,还需要通过hibernate.cache.use_query_cache参数来启动对查询缓存的支持。

另外需要注意的是,查询缓存是在执行查询语句的时候指定缓存的方式以及是否需要对查询的结果进行缓存。

下面就来了解一下查询缓存的使用方法及作用。

修改Hibernate配置文件

首先需要修改Hibernate的配置文件,增加hibernate.cache.use_query_cache参数的配置。配置方法如下:

<property name="hibernate.cache.use_query_cache">true</property>

Hibernate配置文件的详细内容请参考配套光盘中的hibernate\src\cn\hxex\ hibernate\cache\hibernate.cfg.xml文件。

编写主测试程序

由于这是在前面二级缓存例子的基础上来开发的,所以,对于EHCache的配置以及视图对象的开发和映射文件的配置工作就都不需要再重新进行了。下面就来看一下主测试程序的实现方法,如清单14.11所示。

清单14.11 主程序的实现

……

public void run() {

       SessionFactory sf = QueryCacheMain.getSessionFactory();

       Session session = sf.getCurrentSession();

       session.beginTransaction();

       Query query = session.createQuery( "from User" );

       Iterator it = query.setCacheable( true ).list().iterator();

       while( it.hasNext() ) {

              System.out.println( it.next() );

       }

       User user = (User)session.get( User.class, "1" );

       System.out.println( user );

       session.getTransaction().commit();

}

   public static void main(String[] args) {

          QueryCacheMain main1 = new QueryCacheMain();

          main1.start();

          try {

                 Thread.sleep( 2000 );

          } catch (InterruptedException e) {

                 e.printStackTrace();

          }

          QueryCacheMain main2 = new QueryCacheMain();

          main2.start();

   }

}

主程序在实现的时候采用了多线程的方式来运行。首先将“from User”查询结果进行缓存,然后再通过ID取得对象来检查是否对对象进行了缓存。另外,多个线程的执行可以看出对于进行了缓存的查询是不会执行第二次的。

运行测试主程序

接着就来运行测试主程序,其输出结果应该如下所示:

Hibernate: select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_ from USERINFO user0_

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

ID: 1

Namge:   galaxy

Age:       32

通过上面的执行结果可以看到,在两个线程执行中,只执行了一个SQL查询语句。这是因为根据ID所要获取的对象在前面的查询中已经得到了,并进行了缓存,所以没有再次执行查询语句。

Hibernate二级缓存
二级缓存

与Session相对的是,SessionFactory也提供了相应的缓存机制。SessionFactory缓存可以依据功能和目的的不同而划分为内置缓存和外置缓存。

SessionFactory的内置缓存中存放了映射元数据和预定义SQL语句,映射元数据是映射文件中数据的副本,而预定义SQL语句是在Hibernate初始化阶段根据映射元数据推导出来的。SessionFactory的内置缓存是只读的,应用程序不能修改缓存中的映射元数据和预定义SQL语句,因此SessionFactory不需要进行内置缓存与映射文件的同步。

SessionFactory的外置缓存是一个可配置的插件。在默认情况下,SessionFactory不会启用这个插件。外置缓存的数据是数据库数据的副本,外置缓存的介质可以是内存或者硬盘。SessionFactory的外置缓存也被称为Hibernate的二级缓存。

Hibernate的二级缓存的实现原理与一级缓存是一样的,也是通过以ID为key的Map来实现对对象的缓存。

由于Hibernate的二级缓存是作用在SessionFactory范围内的,因而它比一级缓存的范围更广,可以被所有的Session对象所共享。

二级缓存的工作内容

Hibernate的二级缓存同一级缓存一样,也是针对对象ID来进行缓存。所以说,二级缓存的作用范围是针对根据ID获得对象的查询。

二级缓存的工作可以概括为以下几个部分:

● 在执行各种条件查询时,如果所获得的结果集为实体对象的集合,那么就会把所有的数据对象根据ID放入到二级缓存中。

● 当Hibernate根据ID访问数据对象的时候,首先会从Session一级缓存中查找,如果查不到并且配置了二级缓存,那么会从二级缓存中查找,如果还查不到,就会查询数据库,把结果按照ID放入到缓存中。

● 删除、更新、增加数据的时候,同时更新缓存。

二级缓存的适用范围
Hibernate的二级缓存作为一个可插入的组件在使用的时候也是可以进行配置的,但并不是所有的对象都适合放在二级缓存中。

在通常情况下会将具有以下特征的数据放入到二级缓存中:
●   很少被修改的数据。

●   不是很重要的数据,允许出现偶尔并发的数据。

●   不会被并发访问的数据。

●   参考数据。
而对于具有以下特征的数据则不适合放在二级缓存中:
●   经常被修改的数据。

●   财务数据,绝对不允许出现并发。

●   与其他应用共享的数据。

在这里特别要注意的是对放入缓存中的数据不能有第三方的应用对数据进行更改(其中也包括在自己程序中使用其他方式进行数据的修改,例如,JDBC),因为那样Hibernate将不会知道数据已经被修改,也就无法保证缓存中的数据与数据库中数据的一致性。

二级缓存组件

在默认情况下,Hibernate会使用EHCache作为二级缓存组件。但是,可以通过设置hibernate.cache.provider_class属性,指定其他的缓存策略,该缓存策略必须实现org.hibernate.cache.CacheProvider接口。

通过实现org.hibernate.cache.CacheProvider接口可以提供对不同二级缓存组件的支持。

Hibernate内置支持的二级缓存组件如表14.1所示。

表14.1 Hibernate所支持的二级缓存组件

Hibernate一级缓存

大家都知道,Hibernate是以JDBC为基础实现的持久层组件,因而其性能肯定会低于直接使用JDBC来访问数据库。因此,为了提高Hibernate的性能,在Hibernate组件中提供了完善的缓存机制来提高数据库访问的性能。

什么是缓存

缓存是介于应用程序和物理数据之间的,其作用是为了降低应用程序对物理数据访问的频次从而提高应用系统的性能。缓存思想的提出主要是因为对物理数据的访问效率要远远低于对内存的访问速度,因而采用了将部分物理数据存放于内存当中,这样可以有效地减少对物理数据的访问次数,从而提高系统的性能。

缓存广泛地存在于我们所接触的各种应用系统中,例如数据库系统、Windows操作系统等,在进行物理数据的访问时无一例外地都使用了缓存机制来提高操作的性能。

缓存内的数据是对物理数据的复制,因此一个缓存系统所应该包括的最基本的功能是数据的缓存和读取,同时在使用缓存的时候还要考虑缓存中的数据与物理数据的同步,也就是要保持两者是一致的。

缓存要求对数据的读写速度很高,因此,一般情况下会选用内存作为存储的介质。但如果内存有限,并且缓存中存放的数据量非常大时,也会用硬盘作为缓存介质。缓存的实现不仅仅要考虑存储的介质,还要考虑到管理缓存的并发访问和缓存数据的生命周期。

为了提高系统的性能,Hibernate也使用了缓存的机制。在Hibernate框架中,主要包括以下两个方面的缓存:一级缓存和二级缓存(包含查询缓存)。Hibernate中缓存的作用主要表现在以下两个方面:

●   通过主键(ID)加载数据的时候

●   延迟加载中
一级缓存

Hibernate的一级缓存是由Session提供的,因此它只存在于Session的生命周期中,也就是当Session关闭的时候该Session所管理的一级缓存也会立即被清除。

Hibernate的一级缓存是Session所内置的,不能被卸载,也不能进行任何配置。

一级缓存采用的是key-value的Map方式来实现的,在缓存实体对象时,对象的主关键字ID是Map的key,实体对象就是对应的值。所以说,一级缓存是以实体对象为单位进行存储的,在访问的时候使用的是主关键字ID。

虽然,Hibernate对一级缓存使用的是自动维护的功能,没有提供任何配置功能,但是可以通过Session中所提供的方法来对一级缓存的管理进行手工干预。Session中所提供的干预方法包括以下两种。

● evict() :用于将某个对象从Session的一级缓存中清除。

● clear() :用于将一级缓存中的对象全部清除。

在进行大批量数据一次性更新的时候,会占用非常多的内存来缓存被更新的对象。这时就应该阶段性地调用clear()方法来清空一级缓存中的对象,控制一级缓存的大小,以避免产生内存溢出的情况。具体的实现方法如清单14.8所示。

Session session = sessionFactory.openSession();

Transaction tx = session.beginTransaction();

  

for ( int i=0; i<100000; i++ ) {

    Customer customer = new Customer(……);

    session.save(customer);

    if ( i % 20 == 0 ) {

        //将本批插入的对象立即写入数据库并释放内存

        session.flush();

        session.clear();

    }

}

  

tx.commit();

session.close();
并发控制、悲观锁、乐观锁
并发控制

当数据库系统采用Read Committed隔离级别时,会导致不可重复读取和两次更新丢失的并发问题,可以在应用程序中采用锁机制来避免这类问题的产生。

从应用程序的角度上看,锁可以分为乐观锁和悲观锁两大类。

悲观锁

在多个客户端可能读取同一笔数据或同时更新一笔数据的情况下,必须要有访问控制的手段,防止同一个数据被修改而造成混乱,最简单的手段就是对数据进行锁定。在自己进行数据读取或更新等动作时,锁定其他客户端不能对同一笔数据进行任何的动作。

悲观锁(Pessimistic Locking),如其名称所示,悲观地认定每次资料存取时,其他的客户端也会存取同一笔数据,因此将会锁住该笔数据,直到自己操作完成后再解除锁。

悲观锁假定任何时刻存取数据时,都可能有另一个客户也正在存取同一笔数据,因而对数据采取了数据库层次的锁定状态,在锁定的时间内其他的客户不能对数据进行存取。对于单机或小系统而言,这并不成问题,然而如果是在网络上的系统,同时间会有许多访问的机器,如果每一次读取数据都造成锁定,其后继的存取就必须等待,这将造成效能上的问题,造成后继使用者的长时间等待。

悲观锁通常透过系统或数据库本身的功能来实现,依赖系统或数据库本身提供的锁机制。Hibernate即是如此,可以利用Query或Criteria的setLockMode()方法来设定要锁定的表或列及其锁模式,可设定的锁模式有以下几个。

LockMode.UPGRADE:利用数据库的for update子句进行锁定。

LockMode.UPGRADE_NOWAIT:使用for update nowait子句进行锁定,在Oracle数据库中使用。

下面来实现一个简单的例子,测试一下采用悲观锁时数据库是如何进行操作的。

首先来完成一个实体对象——User,该对象包含了id,name和age三个属性,实现的方法如清单14.1所示。

清单14.1 User对象的实现

package cn.hxex.hibernate.lock;

public class User {

       private String id;

       private String name;

       private Integer age;

      

       // 省略了getter和setter方法

       ……

}

接下来就是映射文件的配置,由于该映射文件没有涉及到任何与其他对象的关联配置,所以实现的方法也非常简单,代码如清单14.2所示。

清单14.2 User映射文件的实现

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

       "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

       "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="cn.hxex.hibernate.lock">
       <class name="User" table="USERINFO">
              <id name="id" column="userId">
                 <generator class="uuid.hex"/>
           </id>
              <property name="name" column="name" type="java.lang.String"/>

              <property name="age" column="age" type="java.lang.Integer"/>
       </class>
</hibernate-mapping>

另外一件重要的工作就是Hibernate的配置文件了,在这个配置文件中包含了连接数据库的参数以及其他一些重要的参数,实现的方法如清单14.3所示。

清单14.3 Hibernate配置文件的实现

<?xml version='1.0' encoding='UTF-8'?>

<!DOCTYPE hibernate-configuration PUBLIC

          "-//Hibernate/Hibernate Configuration DTD 3.0//EN"

          "http://hibernate.sourceforge.net/hibernate-configuration-3.0.dtd">

<hibernate-configuration>

       <session-factory>

              <!-- 数据库的URL -->

              <!-- property name="hibernate.connection.url">

              jdbc:oracle:thin:@192.168.10.121:1521:HiFinance</property-->

              <property name="hibernate.connection.url">

       jdbc:mysql://localhost:3306/lockdb?useUnicode=true&amp;characterEncoding=utf8&amp;autoReconnect=true&amp;autoReconnectForPools=true

        </property>

             

              <!-- 数据库的驱动程序 -->

              <!-- property name="hibernate.connection.driver_class">

              oracle.jdbc.driver.OracleDriver</property-->

              <property name="hibernate.connection.driver_class">

           com.mysql.jdbc.Driver

        </property>

              <!-- 数据库的用户名 -->

              <property name="hibernate.connection.username">lockdb</property>

              <!-- 数据库的密码 -->

              <property name="hibernate.connection.password">lockdb</property>

              <!-- 数据库的Dialect -->

              <!-- property name="hibernate.dialect">

              org.hibernate.dialect.Oracle9Dialect</property -->

              <property name="hibernate.dialect">

              org.hibernate.dialect.MySQLDialect</property>

              <!-- 输出执行的SQL语句 -->

              <property name="hibernate.show_sql">true</property>

             

              <property name="hibernate.current_session_context_class">thread</property>

             

              <property name="hibernate.hbm2ddl.auto">update</property>

              <!-- HBM文件列表 -->

              <mapping resource="cn/hxex/hibernate/lock/User.hbm.xml" />

       </session-factory>

</hibernate-configuration>

最后要实现的就是测试主程序了,在测试主程序中包含了Hibernate的初始化代码以及悲观锁的测试方法。测试主程序的实现方法如清单14.4所示。

清单14.4 测试主程序的实现

package cn.hxex.hibernate.lock;

import java.net.URL;

import java.util.List;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.hibernate.LockMode;

import org.hibernate.Query;

import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.hibernate.cfg.Configuration;

public class LockMain {

    private static Log log = LogFactory.getLog( LockMain.class );

    // 静态Configuration和SessionFactory对象的实例(全局唯一的)

    private static Configuration configuration;

    private static SessionFactory sessionFactory;

    static

    {

        // 从默认的配置文件创建SessionFactory

        try

        {

                          URL configURL = ClassLoader.getSystemResource(

                               "cn/hxex/hibernate/lock/hibernate.cfg.xml" );

                          // 创建默认的Configuration对象的实例

            configuration = new Configuration();

            // 读取hibernate.properties或者hibernate.cfg.xml文件

            configuration.configure( configURL );

            // 使用静态变量来保持SessioFactory对象的实例

            sessionFactory = configuration.buildSessionFactory();

        }

        catch (Throwable ex)

        {

            // 输出异常信息

            log.error("Building SessionFactory failed.", ex);

            ex.printStackTrace();

            throw new ExceptionInInitializerError(ex);

        }

    }

      

    public static SessionFactory getSessionFactory() {

                  return sessionFactory;

    }

   

    public void testPessimisticLock() {

              SessionFactory sf = LockMain.getSessionFactory();

              Session session = sf.getCurrentSession();

              session.beginTransaction();

             

              Query query = session.createQuery("from User user");

              query.setLockMode("user", LockMode.UPGRADE);

              List users = query.list();

              for( int i=0; i<users.size(); i++ ) {

                     System.out.println( users.get( i ) );

              }

              session.getTransaction().commit();

    }

   

       public static void main(String[] args) {

             

              LockMain main = new LockMain();

              main.testPessimisticLock();

       }

}

在上面的清单中,testPessimisticLock()方法就是测试悲观锁的方法,该方法在执行查询之前通过Query对象的setLockMode()方法设置了访问User对象的模式,这样,这个程序在执行的时候就会使用以下的SQL语句:

select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_

       from USERINFO user0_ for update

除了Query对象外,也可以在使用Session的load()或是lock()时指定锁模式。

除了前面所提及的两种锁模式外,还有三种Hibernate内部自动对数据进行加锁的模式,但它的处理是与数据库无关的。

LockMode.WRITE:在insert或update时进行锁定,Hibernate会在调用save()方法时自动获得锁。

LockMode.READ:在读取记录时Hibernate会自动获得锁。

LockMode.NONE:没有锁。

如果数据库不支持所指定的锁模式,Hibernate会选择一个合适的锁替换,而不是抛出一个异常。

乐观锁

乐观锁(Optimistic Locking)认为资料的存取很少发生同时存取的问题,因而不做数据库层次上的锁定。为了维护正确的数据,乐观锁是使用应用程序上的逻辑来实现版本控制的。

在使用乐观锁策略的情况下,数据不一致的情况一旦发生,有几个解决方法,一种是先更新为主,一种是后更新为主,比较复杂的就是检查发生变动的数据来实现,或是检查所有属性来实现乐观锁。

Hibernate中通过检查版本号来判断数据是否已经被其他人所改动,这也是Hibernate所推荐的方式。在数据库中加入一个version字段记录,在读取数据时连同版本号一同读取,并在更新数据时比较版本号与数据库中的版本号,如果等于数据库中的版本号则予以更新,并递增版本号,如果小于数据库中的版本号就抛出异常。

下面就来在前面例子的基础上进行Hibernate乐观锁的测试。

首先需要修改前面所实现的业务对象,在其中增加一个version属性,用来记录该对象所包含数据的版本信息,修改后的User对象如清单14.5所示。

清单14.5 修改后的User对象

package cn.hxex.hibernate.lock;

public class User {

       private String id;

       private Integer version; // 增加版本属性

       private String name;

       private Integer age;

      

       // 省略了getter和setter方法

       ……

}

然后是修改映射文件,增加version属性的配置。在这里需要注意的是,这里的version属性应该使用专门的元素来进行配置,这样才能使其发挥乐观锁的作用。如果还使用元素来进行配置,那么Hibernate只会将其作为一个普通的属性来进行处理。

修改后的映射文件如清单14.6所示。

清单14.6 修改后的映射文件

<?xml version="1.0"?>

<!DOCTYPE hibernate-mapping PUBLIC

       "-//Hibernate/Hibernate Mapping DTD 3.0//EN"

       "http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">

<hibernate-mapping package="cn.hxex.hibernate.lock">

       <class name="User" table="USERINFO" optimistic-lock="version">

              <id name="id" column="userId">

                 <generator class="uuid.hex"/>

           </id>

             

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

             

              <property name="name" column="name" type="java.lang.String"/>

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

       </class>

</hibernate-mapping>

接下来还要进行测试主程序的修改。由于需要模拟两个人同时修改同一个记录的情况,所以在这里需要将主程序修改为是可以多线程执行的,然后在run()方法中,调用对User对象的修改程序。

实现后的主测试程序如清单14.7所示。

清单14.7 修改后的测试主程序

package cn.hxex.hibernate.lock;

import java.net.URL;

import java.util.List;

import org.apache.commons.logging.Log;

import org.apache.commons.logging.LogFactory;

import org.hibernate.LockMode;

import org.hibernate.Query;

import org.hibernate.Session;

import org.hibernate.SessionFactory;

import org.hibernate.Transaction;

import org.hibernate.cfg.Configuration;

public class LockMain extends Thread{

……

    public void testOptimisticLock() {

           SessionFactory sf = LockMain.getSessionFactory();

           Session session = sf.openSession();

           Transaction tx = session.beginTransaction();

          

           User userV1 = (User)session.load( User.class, "1" );

          

           // 等第二个进程执行

           try {

                     sleep( 3000 );

              } catch (InterruptedException e) {

                     e.printStackTrace();

              }

          

              userV1.setAge(new Integer(32));

              tx.commit();

              session.close();

    }

   

    public void run() {

                  testOptimisticLock();

    }

   

       public static void main(String[] args) {

             

              // LockMain main = new LockMain();

              // main.testPessimisticLock();

             

              LockMain main1 = new LockMain();

              main1.start();

              LockMain main2 = new LockMain();

              main2.start();

       }

}

最后,执行测试主程序,在控制台中应该看到类似下面的输出:

Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=?

Hibernate: select user0_.userId as userId0_0_, user0_.version as version0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=?

Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=?

Hibernate: update USERINFO set version=?, name=?, age=? where userId=? and version=?

2006-10-3 21:27:20 org.hibernate.event.def.AbstractFlushingEventListener performExecutions

严重: Could not synchronize database state with session

org.hibernate.StaleObjectStateException: Row was updated or deleted by another transaction (or unsaved-value mapping was incorrect): [cn.hxex.hibernate.lock.User#1]

……

在Hibernate所执行的UPDATE语句中可以看到,version字段是作为更新的条件来执行的。对于第二个进程来说,由于数据库中的记录已经被第一个进程更新(更新的同时会导致version自动增加),就必然会导致第二个进程操作的失败。Hibernate正是利用这种机制来避免两次更新问题的出现。

Hibernate中的事务处理

在现在的B/S体系结构的软件开发中,对于数据库事务处理中最常使用的方式是每个用户请求一个事务。也就是说,当服务器端接收到一个用户请求后,会开始一个新的事务,直到对用户请求的所有处理都进行完毕并且完成了响应用户请求的所有输出之后才会关闭这个事务。

对于使用Hibernate实现持久化功能的系统来说,事务的处理是这样的:服务器端在接收到用户的请求后,会创建一个新的Hibernate Session对象,然后通过该Session对象开始一个新的事务并且之后所有对数据库的操作都通过该Session对象来进行。最后,完成将响应页面发送到客户端的工作后再提交事务并且关闭Session。

Session的对象是轻型的,非线程安全的,所以在每次用户请求时创建,请求处理完毕后丢弃。

那么,该如何实现这种方式的事务处理呢?处理的难点在于如何在业务处理之前创建Session并开始事务以及在业务处理之后提交事务并关闭Session。对于现在的Web应用来说,通常情况下是通过ServletFilter来完成事务处理的操作。这样,就可以轻松地实现在用户请求到达服务器端的时候创建Session并开始事务,而服务器端响应处理结束之前提交事务并关闭Session。

另外一个问题是,在ServletFilter中创建的Session是如何传递给业务处理方法中的呢?处理的方法是通过一个ThreadLocal变量来把创建的Session对象绑定到处理用户请求的线程上去,这样就可以使任何的业务处理方法可以轻松得到Session对象。

Hibernate中事务处理的具体方法可以参照前面的网络博客的实例。

但是这种事务处理的方式还是会遇到一些问题,其中最突出的就是更新冲突的问题。例如,某个操作人员进入了用户信息的修改页面,在经过一段时间的对用户信息的修改后,进行提交操作,而与此同时可能会有另外一个操作人员也进行了相同的操作,这样在处理提交的时候就会产生冲突。

产生这个冲突的原因在于在开发中需要使用多个数据库事务来实现一个应用事务。也就是说,在应用程序层,应该将读取用户信息、显示修改页面以及用户提交工作来作为一个事务进行处理,在处理的过程中应该避免其他操作人员进行类似的操作。

回想前面的介绍,我们对于数据库事务所采取的策略是每个用户请求一个事务,而上面的业务处理则至少需要两个请求才能完成。这样,两者之间就存在着一定的矛盾,这也就导致了不可重复读取和两次更新问题的发生。

为了解决并发中数据访问的问题,通常会采用锁的机制来实现数据访问的排他性,从而避免两次更新问题的发生。

事务的隔离级别

为了避免上面出现的几种情况,在标准SQL规范中,定义了4个事务隔离级别,不同的隔离级别对事务的处理不同。

● 未授权读取(Read Uncommitted):允许脏读取,但不允许更新丢失。如果一个事务已经开始写数据,则另外一个数据则不允许同时进行写操作,但允许其他事务读此行数据。该隔离级别可以通过“排他写锁”实现。

● 授权读取(Read Committed):允许不可重复读取,但不允许脏读取。这可以通过“瞬间共享读锁”和“排他写锁”实现。读取数据的事务允许其他事务继续访问该行数据,但是未提交的写事务将会禁止其他事务访问该行。

● 可重复读取(Repeatable Read):禁止不可重复读取和脏读取,但是有时可能出现幻影数据。这可以通过“共享读锁”和“排他写锁”实现。读取数据的事务将会禁止写事务(但允许读事务),写事务则禁止任何其他事务。

● 序列化(Serializable):提供严格的事务隔离。它要求事务序列化执行,事务只能一个接着一个地执行,但不能并发执行。如果仅仅通过“行级锁”是无法实现事务序列化的,必须通过其他机制保证新插入的数据不会被刚执行查询操作的事务访问到。

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

通过前面的介绍已经知道,通过选用不同的隔离等级就可以在不同程度上避免前面所提及的在事务处理中所面临的各种问题。所以,数据库隔离级别的选取就显得尤为重要,在选取数据库的隔离级别时,应该注意以下几个处理的原则:

首先,必须排除“未授权读取”,因为在多个事务之间使用它将会是非常危险的。事务的回滚操作或失败将会影响到其他并发事务。第一个事务的回滚将会完全将其他事务的操作清除,甚至使数据库处在一个不一致的状态。很可能一个已回滚为结束的事务对数据的修改最后却修改提交了,因为“未授权读取”允许其他事务读取数据,最后整个错误状态在其他事务之间传播开来。

其次,绝大部分应用都无须使用“序列化”隔离(一般来说,读取幻影数据并不是一个问题),此隔离级别也难以测量。目前使用序列化隔离的应用中,一般都使用悲观锁,这样强行使所有事务都序列化执行。

剩下的也就是在“授权读取”和“可重复读取”之间选择了。我们先考虑可重复读取。如果所有的数据访问都是在统一的原子数据库事务中,此隔离级别将消除一个事务在另外一个并发事务过程中覆盖数据的可能性(第二个事务更新丢失问题)。这是一个非常重要的问题,但是使用可重复读取并不是解决问题的唯一途径。

假设使用了“版本数据”,Hibernate会自动使用版本数据。Hibernate的一级Session缓存和版本数据已经为你提供了“可重复读取隔离”绝大部分的特性。特别是,版本数据可以防止二次更新丢失的问题,一级Session缓存可以保证持久载入数据的状态与其他事务对数据的修改隔离开来,因此如果使用对所有的数据库事务采用授权读取隔离和版本数据是行得通的。

“可重复读取”为数据库查询提供了更好的效率(仅对那些长时间的数据库事务),但是由于幻影读取依然存在,因此没必要使用它(对于Web应用来说,一般也很少在一个数据库事务中对同一个表查询两次)。

也可以同时考虑选择使用Hibernate的二级缓存,它可以如同底层的数据库事务一样提供相同的事务隔离,但是它可能弱化隔离。假如在二级缓存大量使用缓存并发策略,它并不提供重复读取语义(例如,后面章节中将要讨论的读写,特别是非严格读写),很容易可以选择默认的隔离级别:因为无论如何都无法实现“可重复读取”,因此就更没有必要拖慢数据库了。另一方面,可能对关键类不采用二级缓存,或者采用一个完全的事务缓存,提供“可重复读取隔离”。那么在业务中需要使用到“可重复读取”吗?如果你喜欢,当然可以那样做,但更多的时候并没有必要花费这个代价。

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值