Hibernate 性能优化

jopen 12年前

1、针对Mysql数据库而言主要是物理硬件的优化磁盘的寻道能力(磁盘的IO)Mysql的自身优化(Mysql.cnf)文件的优化

2、针对Oracle数据库而言,Fetch Size 是设定JDBC的 Statement读取数据的时候每次从数据库中取出的记录条数,一般设置为30、50、100.Oracle数据库的JDBC驱动默认的 Fetch Size=15,设置Fetch Size设置为:30、50,性能会有明显提升,如果继续增大,超出100,性能提升不明显,反而会消耗内存。 
即在Hibernate配制文件中进行配制:


<property name="hibernateProperties">
<props>
<prop key="hibernate.dialect">org.hibernate.dialect.Oracle9Dialect</prop>
<prop key="hibernate.show_sql">false</prop>
<!-- Create/update the database tables automatically when the JVM starts up
<prop key="hibernate.hbm2ddl.auto">update</prop> -->
<!-- Turn batching off for better error messages under PostgreSQL 
<prop key="hibernate.jdbc.batch_size">100</prop> -->
<prop key="hibernate.jdbc.batch_size">50</prop>
</props>
</property> 
2、如果是超大的系统,建议生成htm文件。加快页面提升速度。

3、不要把所有的责任推在hibernate上,对代码进行重构,减少对数据库的操作,尽量避免在数据库查询时使用in操作,以及避免递归查询操作,代码质量、系统设计的合理性决定系统性能的高低。

4、 对大数据量查询时,慎用list()或者iterator()返回查询结果,

(1)。 使用List()返回结果时,Hibernate会所有查询结果初始化为持久化对象,结果集较大时,会占用很多的处理时间。

(2)。 而使用iterator()返回结果时,在每次调用 iterator.next()返回对象并使用对象时,Hibernate才调用查询将对应的对象初始化,对于大数据量时,每调用一次查询都会花费较多的时间。当结果集较大,但是含有较大量相同的数据,或者结果集不是全部都会使用时,使用iterator()才有优势。

5、在一对多、多对一的关系中,使用延迟加载机制,会使不少的对象在使用时方会初始化,这样可使得节省内存空间以及减少数据库的负荷,而且若PO中的集合没有被使用时,就可减少互数据库的交互从而减少处理时间。

6、对含有关联的PO(持久化对象)时,若default-cascade="all"或者 “save-update”,新增PO时,请注意对PO中的集合的赋值操作,因为有可能使得多执行一次update操作。

7、 对于大数据量新增、修改、删除操作或者是对大数据量的查询,与数据库的交互次数是决定处理时间的最重要因素,减少交互的次数是提升效率的最好途径,所以在开发过程中,请将show_sql设置为true,深入了解Hibernate 的处理过程,尝试不同的方式,可以使得效率提升。尽可能对每个页面的显示,对数据库的操作减少到100——150条以内。越少越好。

以上是在进行Struts+hibernate+spring进行项目开发中,对hibernate性能优化的几点心得。


本文依照HIBERNATE帮助文件,一些网络书籍及项目经验整理而成,只提供要点和思路,具体做法能留言探讨,或是找一些更周详更有针对性的资料。

  初用HIBERNATE的人也许都遇见过性能问题,实现同一功能,用HIBERNATE和用JDBC性能相差十几倍非常正常,如果不及早调整,非常可能影响整个项目的进度。

  大体上,对于HIBERNATE性能调优的主要考虑点如下:

  数据库设计调整

  HQL优化

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

  主设置参数(日志,查询缓存,fetch_size, batch_size等)

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

  一级缓存的管理

  针对二级缓存,更有许多特有的策略

  事务控制策略。

  1、 数据库设计

  a) 降低关联的复杂性

  b) 尽量不使用联合主键

  c) ID的生成机制,不同的数据库所提供的机制并不完全相同

  d) 适当的冗余数据,不过分追求高范式

  2、 HQL优化

  HQL如果抛开他同HIBERNATE本身一些缓存机制的关联,HQL的优化技巧同普通的SQL优化技巧相同,能非常容易在网上找到一些经验之谈。

  3、 主设置

  a) 查询缓存,同下面讲的缓存不太相同,他是针对HQL语句的缓存,即完全相同的语句再次执行时能利用缓存数据。不过,查询缓存在一个交易系统(数据变更频繁,查询条件相同的机率并不大)中可能会起反作用:他会白白耗费大量的系统资源但却难以派上用场。

  b) fetch_size,同JDBC的相关参数作用类似,参数并不是越大越好,而应根据业务特征去设置

  c) batch_size同上。

  d) 生产系统中,切记要关掉SQL语句打印。

  4、 缓存

  a) 数据库级缓存:这级缓存是最高效和安全的,但不同的数据库可管理的层次并不相同,比如,在ORACLE中,能在建表时指定将整个表置于缓存当中。

  b) SESSION缓存:在一个HIBERNATE SESSION有效,这级缓存的可干预性不强,大多于HIBERNATE自动管理,但他提供清除缓存的方法,这在大批量增加/更新操作是有效的。比如,同时增加十万条记录,按常规方式进行,非常可能会发现OutofMemeroy的异常,这时可能需要手动清除这一级缓存:Session.evict及Session.clear

  c) 应用缓存:在一个SESSIONFACTORY中有效,因此也是优化的重中之重,因此,各类策略也考虑的较多,在将数据放入这一级缓存之前,需要考虑一些前提条件:

  i. 数据不会被第三方修改(比如,是否有另一个应用也在修改这些数据?)

  ii. 数据不会太大

  iii. 数据不会频繁更新(否则使用CACHE可能适得其反)

  iv. 数据会被频繁查询

  v. 数据不是关键数据(如涉及钱,安全等方面的问题)。

  缓存有几种形式,能在映射文件中设置:read-only(只读,适用于非常少变更的静态数据/历史数据),nonstrict-read-write,read-write(比较普遍的形式,效率一般),transactional(JTA中,且支持的缓存产品较少)

  d) 分布式缓存:同c)的设置相同,只是缓存产品的选用不同,在目前的 HIBERNATE中可供选择的不多,oscache, jboss cache,目前的大多数项目,对他们的用于集群的使用(特别是关键交易系统)都持保守态度。在集群环境中,只利用数据库级的缓存是最安全的。

  5、 延迟加载

  a) 实体延迟加载:通过使用动态代理实现

  b) 集合延迟加载:通过实现自有的SET/LIST,HIBERNATE提供了这方面的支持

  c) 属性延迟加载:

  6、 方法选用

  a) 完成同样一件事,HIBERNATE提供了可供选择的一些方式,但具体使用什么方式,可能用性能/代码都会有影响。显示,一次返回十万条记录(List/Set/Bag/Map等)进行处理,非常可能导致内存不够的问题,而如果用基于游标(ScrollableResults)或Iterator的结果集,则不存在这样的问题。

  b) Session的load/get方法,前者会使用二级缓存,而后者则不使用。

  c) Query和list/iterator,如果去仔细研究一下他们,你可能会发现非常多有意思的情况,二者主要差别(如果使用了Spring,在HibernateTemplate中对应find,iterator方法):

  i. list只能利用查询缓存(但在交易系统中查询缓存作用不大),无法利用二级缓存中的单个实体,但list查出的对象会写入二级缓存,但他一般只生成较少的执行SQL语句,非常多情况就是一条(无关联)。

  ii. iterator则能利用二级缓存,对于一条查询语句,他会先从数据库中找出所有符合条件的记录的ID,再通过ID去缓存找,对于缓存中没有的记录,再构造语句从数据库中查出,因此非常容易知道,如果缓存中没有所有符合条件的记录,使用iterator会产生N+1条SQL语句(N为符合条件的记录数)

  iii. 通过iterator,配合缓存管理API,在海量数据查询中能非常好的解决内存问题,如:

  while(it.hasNext()){

  YouObject object = (YouObject)it.next();

  session.evict(youObject);

  sessionFactory.evice(YouObject.class, youObject.getId());

  }

  如果用list方法,非常可能就出OutofMemory错误了。

  iv. 通过上面的说明,我想你应该知道怎么去使用这两个方法了。

  7、 集合的选用

  在HIBERNATE 3.1文件的“19.5. Understanding Collection performance”中有周详的说明。

  8、 事务控制

  事务方面对性能有影响的主要包括:事务方式的选用,事务隔离级别及锁的选用

  a) 事务方式选用:如果不涉及多个事务管理器事务的话,不必使用JTA,只有JDBC的事务控制就能。

  b) 事务隔离级别:参见标准的SQL事务隔离级别

  c) 锁的选用:悲观锁(一般由具体的事务管理器实现),对于长事务效率低,但安全。乐观锁(一般在应用级别实现),如在HIBERNATE中能定义VERSION字段,显然,如果有多个应用操作数据,且这些应用不是用同一种乐观锁机制,则乐观锁会失效。因此,针对不同的数据应有不同的策略,同前面许多情况相同,非常多时候我们是在效率和安全/准确性上找一个平衡点,无论怎么,优化都不是个纯技术的问题,你应该对你的应用和业务特征有足够的了解。

  9、 批量操作

  即使是使用JDBC,在进行大批数据更新时,BATCH和不使用BATCH有效率上也有非常大的差别。我们能通过设置batch_size来让其支持批量操作。

  举个例子,要批量删除某表中的对象,如“delete Account”,打出来的语句,会发现HIBERNATE找出了所有ACCOUNT的ID,再进行删除,这主要是为了维护二级缓存,这样效率肯定高不了,在后续的版本中增加了 bulk delete/update,但这也无法解决缓存的维护问题。也就是说,由于有了二级缓存的维护问题,HIBERNATE的批量操作效率并不尽如人意!

  从前面许多要点能看出,非常多时候我们是在效率和安全/准确性上找一个平衡点,无论怎么,优化都不是个纯技术的问题,你应该对你的应用和业务特征有足够的了解,一般的,优化方案应在架构设计期就基本确定,否则可能导致没必要的返工,致使项目延期,而作为架构师和项目经理,还要面对研发人员可能的抱怨,必竟,我们对用户需求更改的控制力不大,但技术/架构风险是应该在初期意识到并制定好相关的对策。

  更有一点要注意,应用层的缓存只是锦上添花,永远不要把他当救命稻草,应用的根基(数据库设计,算法,高效的操作语句,恰当API的选择等)才是最重要的。

Hibernate优化方法一:批量修改和删除 

在Hibernate 2中,如果需要对任何数据进行修改和删除操作,都需要先执行查询操作,在得到要修改或者删除的数据后,再对该数据进行相应的操作处理。在数据量少的情况下采用这种处理方式没有问题,但需要处理大量数据的时候就可能存在以下的问题: 

◆占用大量的内存。 

◆需要多次执行update/delete语句,而每次执行只能处理一条数据。 

以上两个问题的出现会严重影响系统的性能。因此,在Hibernate 3中引入了用于批量更新或者删除数据的HQL语句。这样,开发人员就可以一次更新或者删除多条记录,而不用每次都一个一个地修改或者删除记录了。 

如果要删除所有的User对象(也就是User对象所对应表中的记录),则可以直接使用下面的HQL语句: 

delete User 

而在执行这个HQL语句时,需要调用Query对象的executeUpdate()方法,具体的实例如下所示: 

String HQL="delete User"; 

Query query=session.createQuery(HQL); 

int size=query.executeUpdate(); 

采用这种方式进行数据的修改和删除时与直接使用JDBC的方式在性能上相差无几,是推荐使用的正确方法。 

如果不能采用HQL语句进行大量数据的修改,也就是说只能使用取出再修改的方式时,也会遇到批量插入时的内存溢出问题,所以也要采用上面所提供的处理方法来进行类似的处理。 

Hibernate优化方法二:使用SQL执行批量操作 

在进行批量插入、修改和删除操作时,直接使用JDBC来执行原生态的SQL语句无疑会获得最佳的性能,这是因为在处理的过程中省略或者简化了以下处理内容: 

● HQL语句到SQL语句的转换。 

● Java对象的初始化。 

● Java对象的缓存处理。 

但是在直接使用JDBC执行SQL语句时,有一个最重要的问题就是要处理缓存中的Java对象。因为通过这种底层方式对数据的修改将不能通知缓存去进行相应的更新操作,以保证缓存中的对象与数据库中的数据是一致的。 

Hibernate优化方法三:提升数据库查询的性能 

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

1 、SQL语句的优化 

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

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

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

◆HQL语句的书写方法。 

◆查询时所使用的查询方法。 

◆对象关联时所使用的抓取策略。 

2 、使用正确的查询方法 

在前面已经介绍过,执行数据查询功能的基本方法有两种:一种是得到单个持久化对象的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对象执行数据查询时应该从以上几个方面去考虑使用何种方法来执行数据库的查询操作。 

Hibernate优化方法四:使用正确的抓取策略 

所谓抓取策略(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(); 

Hibernate优化方法五:查询性能提升小结 

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

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

另外,事务的使用策略也会影响到系统的性能。选取正确的事务隔离级别以及使用。 

在使用Hibernate进行查询的时候大家都会用到Hibernate缓存,其中 Session缓存即一块内存空间,存放了相互关联的Java对象,这些位于Session缓存中的对象就是持久化对象,Session根据持久化对象的状态变化来同步更新数据库。这个Session缓存是Hibernate的一级缓存。此外,SessionFactory有一个内置缓存和一个外置缓存,即Hibernate的第二级缓存。而Hibernate正是由于这些缓存的存在,才使得其数据库操作效率提高,就是说,在提供了方便易操作的操作数据库数据的方式的同时保证了工作效率,但是不能因此而免去后顾之忧,需要在设计业务逻辑层的时候考虑使用最优的架构,节省有效的系统资源。在查询方面,Hibernate主要从以下几个方面来优化查询性能:

1.降低访问数据库的频率,减少select语句的数目。实现手段包括:

    使用迫切左外连接或迫切内连接检索策略。
    对延迟检索或立即检索策略设置批量检索数目。
    使用查询缓存。

2.避免多余加载程序不需要访问的数据。实现手段包括:

    使用延迟检索策略。
    使用集合过滤。

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

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

    在插入和更新数据时,要控制insert和update语句,合理设置映射属性来保证插入更新的性能,例如,当表中包含许多字段时,建议把dynamic-update属性和dynamic-update属性都设为true,这样在insert和update语句中就只包含需要插入或更新的字段,这可以节省数据库执行SQL语句的时间,从而提高应用的运行性能。