读书人

透过二级缓存提升Hibernate应用的性能

发布时间: 2012-09-27 11:11:17 作者: rapoo

通过二级缓存提升Hibernate应用的性能
通过二级缓存提升Hibernate应用的性能



刚刚接触Hibernate的开发工程师有时不理解Hibernate的缓存,并合理地使用。如果能够很好地使用,二级缓存会成为提高应用性能的最有效的方式。
作者 John Ferguson Smart 译者 张立明(baccc@sina.com)





大量的数据库通讯流量是影响Web应用性能的最常见原因。Hibernate是一个高性能、对象-关系型持久和查询服务的框架,但是,如果不做一些调整,它不能解决你的性能问题。本文剖析了Hibernate的缓存功能并告诉你如何显著提高应用程序的性能。

缓存介绍

缓存技术在优化数据库应用中被广泛地使用。缓存通过在应用程序中保存从数据库读取的数据的机制来减少数据库和应用程序之间的通讯流量。只有在读取缓存中没有的数据的时候,才有必要从数据库读取。因为应用程序的缓存无法知道缓存中的数据是否为最新的数据,所以应用程序在数据可能被更新的情况下,需要定时不断地清空缓存。

Hibernate缓存

Hibernate使用两种不同的缓存来保存对象:一级缓存和二级缓存。一级缓存是会话(Session)级的缓存,而二级缓存是会话工厂(Session Factory)级的缓存。默认情况下,Hibernate针对每个事务使用一级缓存。通过一级缓存,Hibernate减少了一个事务期间需要生成的SQL查询。例如,如果对象在一个事务期间被修改了几次,Hibernate会仅仅在事务结束之前生成一个SQL UPDATE语句,这个语句包含了这几次修改的内容。本文重点讲述二级缓存。为了减少数据库的访问压力,二级缓存保存跨多个事务的、会话工厂(Session Factory)级的载入的对象。这些对象可以被整个应用所使用,而不是仅仅执行这个查询的用户。一旦一个查询返回一个对象,如果这个对象已经在缓存中,一个或多个潜在的事务可以免去。

此外,如果你需要缓存查询的结果,而不是持久化的对象,你可以使用查询级的缓存。

缓存的实现

缓存是软件中的一个复杂领域,市面上有多个可选的方案(开源的或商业的)。Hibernate支持下面的开源缓存实现方案:

?EHCache (org.hibernate.cache.EhCacheProvider)
?OSCache (org.hibernate.cache.OSCacheProvider)
?SwarmCache (org.hibernate.cache.SwarmCacheProvider)
?JBoss TreeCache (org.hibernate.cache.TreeCacheProvider)

每一个缓存方案在性能、内存使用、可配置能力等方面都是不同的:

?EHCache是一个快速的、轻量级的、易于使用的、进程内的缓存。它支持read-only和read/write缓存,内存和磁盘缓存。但是不支持集群(Clustering)。
?OSCache是另外一个开源的缓存方案。它同时还支持JSP页面或任意对象的缓存。OSCache功能强大、灵活,和EHCache一样支持read-only和read/write缓存、支持内存和磁盘缓存。同时,它还提供通过JGroups或JMS进行集群的基本支持。
?SwarmCache 是一个简单的、基于JavaGroups提供集群的缓存方案。支持read-only和nonstrict read/write缓存(下一节解释这个概念)。这种缓存适用于读操作远远高于写操作频率的应用。
?JBoss TreeCache is a powerful replicated (synchronous or asynchronous) and transactional cache. Use this solution if you really need a true transaction-capable caching architecture. 是一个强大的、可复制(同步或异步)和支持事务的缓存。如果你需要一个真正的支持事务的缓存架构,使用这个方案吧。

另外一个值得提及的商业缓存方案是 Tangosol Coherence cache.

缓存策略

一旦你选定了你的缓存方案,你需要指定你的缓存策略。下面是四个可选的缓存策略:

?Read-only: 这种策略对于从来不修改、只需要频繁读取的数据是非常有用的,也是最简单、性能最好的缓存策略。
?Read/write: 如果数据需要被更新,Read/write缓存策略可能是比较合适的。这种策略比read-only消耗更多的资源。 在非JTA环境中,每个事务必须在Session.close()或Session.disconnect()调用之前结束。
?Nonstrict read/write: 这种策略不保证两个事务不会同时改变同一个数据。因此,更适用于偶尔修改数据、频繁读取数据的场合。
?Transactional: 这是一个完全事务支持的策略,可以在JTA环境中使用。

每个缓存方案支持的缓存策略是不同的。表1列出了每个缓存方案可用的缓存策略:


缓存方案

Read-only

Nonstrict Read/write

Read/write

Transactional


EHCache

Yes

Yes

Yes

No


OSCache

Yes

Yes

Yes

No


SwarmCache

Yes

Yes

No

No


JBoss TreeCache

Yes

No

No

Yes






Table 1. Supported Caching Strategies for Hibernate Out-of-the-Box Cache Implementations



下面介绍在单个JVM中应用EHCache。









缓存配置

要使用二级缓存,你需要按照下面的步骤在hibernate.cfg.xml中定义hibernate.cache.provider_class属性。




<hibernate-configuration>


<session-factory>


...


<property name="hibernate.cache.provider_class">


org.hibernate.cache.EHCacheProvider


</property>


...


</session-factory>


</hibernate-configuration>

为了在Hibernate3中进行测试,你需要使用hibernate.cache.use_second_level_cache属性。这个属性允许你启用(和禁用)二级缓存。默认情况下,二级缓存是启用的,并且使用EHCache。

示例应用

下面的示例程序包含四个简单的数据库表:Country、Ariport、Employee、Spoken Language。每个Employee有一个Country、可以说多个Spoken Language。每个Country有任意多个Airport。图1是UML类图,图2是数据库模型。本示例的源代码中包含下面的用来初始化数据库的SQL脚本。

?src/sql/create.sql: 初始化数据库的SQL脚本.

0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" border=0>





图 1. Employee UML类图




?src/sql/init.sql: 测试数据

安装Maven2提示

到撰写本文时,Maven2 Repository似乎缺少一些jar文件。为了解决这些问题,可以在本示例程序的源代码根目录中找到缺失的jar。为了安装Maven2 repository,到app文件夹并执行下面的命令:




$ mvn install:install-file -DgroupId=javax.security -DartifactId=jacc -Dversion=1.0




-Dpackaging=jar -Dfile=jacc-1.0.jar


$ mvn install:install-file -DgroupId=javax.transaction -DartifactId=jta -Dversion=1.0.1B






-Dpackaging=jar -Dfile=jta-1.0.1B.jar



0 && image.height>0){if(image.width>=700){this.width=700;this.height=image.height*700/image.width;}}" border=0>





Figure 2. The Database Schema





设置 Read-Only 缓存策略

从简单开始。下面是Hibernate 中Country类的映射




<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">


<class name="Country" table="COUNTRY" dynamic-update="true">


<meta attribute="implement-equals">true</meta>


<cache usage="read-only"/>





<id name="id" type="long" unsaved-value="null" >


<column name="cn_id" not-null="true"/>


<generator name="code" type="string"/>


<property column="cn_name" name="name" type="string"/>





<set name="airports">


<key column="cn_id"/>


<one-to-many table="COUNTRY" dynamic-update="true">


5.

<meta attribute="implement-equals">true</meta>


6.

<cache usage="read-only"/>


7.

...


8.

</class>


9.

</hibernate-mapping>


10.你可以在hibernate.cfg.xml中使用class-cache属性来存储所有class的缓存设置:

11.




12.

<hibernate-configuration>


13.

<session-factory>


14.

...


15.

<property name="hibernate.cache.provider_class">


16.

org.hibernate.cache.EHCacheProvider


17.

</property>


18.

...


19.

<class-cache


20.

table="EMPLOYEE" dynamic-update="true">


<meta attribute="implement-equals">true</meta>





<id name="id" type="long" unsaved-value="null" >


<column name="emp_id" not-null="true"/>


<generator name="surname" type="string"/>


<property column="emp_firstname" name="firstname" type="string"/>





<many-to-one name="country"


column="cn_id"


/>





<!-- Lazy-loading is deactivated to demonstrate caching behavior -->


<set name="languages" table="EMPLOYEE_SPEAKS_LANGUAGE" lazy="false">


<key column="emp_id"/>


<many-to-many column="lan_id" order by e.surname, e.firstname")


.setParameter("country",country)


.list();


}


}

接着,我们写一个单元测试看看性能如何。正如上一个例子,你应该看到在连续调用情况下的性能情况:




public class EmployeeDAOTest extends TestCase {





CountryDAO countryDao = new CountryDAO();


EmployeeDAO employeeDao = new EmployeeDAO();





/**


* Ensure that the Hibernate session is available


* to avoid the Hibernate initialisation interfering with


* the benchmarks


*/


protected void setUp() throws Exception {


super.setUp();


SessionManager.getSession();


}





public void testGetNZEmployees() {


TestTimer timer = new TestTimer("testGetNZEmployees");


Transaction tx = SessionManager.getSession().beginTransaction();


Country nz = countryDao.findCountryByCode("nz");


List kiwis = employeeDao.getEmployeesByCountry(nz);


tx.commit();


SessionManager.closeSession();


timer.done();


}





public void testGetAUEmployees() {


TestTimer timer = new TestTimer("testGetAUEmployees");


Transaction tx = SessionManager.getSession().beginTransaction();


Country au = countryDao.findCountryByCode("au");


List aussis = employeeDao.getEmployeesByCountry(au);


tx.commit();


SessionManager.closeSession();


timer.done();


}





public void testRepeatedGetEmployees() {


testGetNZEmployees();


testGetAUEmployees();


testGetNZEmployees();


testGetAUEmployees();


}


}

如果在上面的配置下运行这个测试,应该可以看到下面的结果:




$mvn test -Dtest=EmployeeDAOTest


...





testGetNZEmployees

: 1227 ms.


testGetAUEmployees

: 883 ms.


testGetNZEmployees

: 907 ms.


testGetAUEmployees

: 873 ms.


testGetNZEmployees

: 987 ms.


testGetAUEmployees

: 916 ms.


[surefire] Running com.wakaleo.articles.caching.dao.EmployeeDAOTest


[surefire] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 3,684 sec

可以看出,加载一个国家的50个左右的Employee,耗时大概1秒。这太慢了。这是一个典型的N+1查询问题。如果你启用SQL日志,会发现一个对Employee表的查询后面跟着上百个对Language表的查询:每次Hibernate从缓存中获取一个Employee对象,都会从数据库加载所有的Language。怎么能改进这一点呢?首先要启用Employee对象的read/write缓存:




<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">


<class name="Employee" table="EMPLOYEE" dynamic-update="true">


<meta attribute="implement-equals">true</meta>


<cache usage="read-write"/>


...


</class>


</hibernate-mapping>


还需要启用Language对象的缓存,Read-only缓存即可:


<class name="Language" table="SPOKEN_LANGUAGE" dynamic-update="true">


<meta attribute="implement-equals">true</meta>


<cache usage="read-only"/>


...


</class>


</hibernate-mapping>

接下来,你应该配置缓存规则--增加下面的内容到ehcache.xml文件:




<cache name="com.wakaleo.articles.caching.businessobjects.Employee"


maxElementsInMemory="5000"


eternal="false"


overflowToDisk="false"


timeToIdleSeconds="300"


timeToLiveSeconds="600"


/>


<cache name="com.wakaleo.articles.caching.businessobjects.Language"


maxElementsInMemory="100"


eternal="true"


overflowToDisk="false"


/>

似乎已经可以了,但是这仍然不能解决N+1查询问题:当你加载一个Employee时,大约50左右的额外查询仍然执行。这种情况下你需要在Employee.hbm.xml中启用对Language引用的缓存。




<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">


<class name="Employee" table="EMPLOYEE" dynamic-update="true">


<meta attribute="implement-equals">true</meta>





<id name="id" type="long" unsaved-value="null" >


<column name="emp_id" not-null="true"/>


<generator name="surname" type="string"/>


<property column="emp_firstname" name="firstname" type="string"/>





<many-to-one name="country"


column="cn_id"


/>





<!-- Lazy-loading is deactivated to demonstrate caching behavior -->


<set name="languages" table="EMPLOYEE_SPEAKS_LANGUAGE" lazy="false">


<cache usage="read-write"/>


<key column="emp_id"/>


<many-to-many column="lan_id" class="Language"/>


</set>


</class>


</hibernate-mapping>

通过这样配置,你得到了优化的性能。




$mvn test -Dtest=EmployeeDAOTest


...


testGetNZEmployees

: 1477 ms.


testGetAUEmployees

: 940 ms.


testGetNZEmployees

: 65 ms.


testGetAUEmployees

: 65 ms.


testGetNZEmployees

: 76 ms.


testGetAUEmployees

: 52 ms.


[surefire] Running com.wakaleo.articles.caching.dao.EmployeeDAOTest


[surefire] Tests run: 3, Failures: 0, Errors: 0, Time elapsed: 0,228 sec











使用查询缓存

在某些情况下,缓存整个查询结果,而不是特定的对象,是非常有必要的。例如,getCountries()方法可能在每次调用的时候都返回相同的结果。因此,除了缓存Country类之外,你应该同时缓存查询结果集。

要这么做,你需要设置hibernate.cfg.xml 文件中的hibernate.cache.use_query_cache属性:




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

然后,你在你希望缓存的查询中使用setCacheable()方法:




public class CountryDAO {





public List getCountries() {


return SessionManager.currentSession()


.createQuery("from Country as c order by c.name")


.setCacheable(true)(查询缓存)

.list();


}


}

为了保证缓存中的查询结果的有效性,Hibernate在缓存的数据被应用修改的时候,把查询结果设置为失效,但是如果有其他的应用直接访问数据库,这种保证就失去意义了。因此如果你的数据必须在任何时候都是最新的,你就不要用任何的二级缓存(或者配置class缓存和collection缓存很短的时间过期)。

合理的Hibernate缓存

缓存是有效的技术,Hibernate提供了一个高效的、灵活的、优雅的实现方式。即使是默认的配置也能显著提高简单场景的性能。然而,和许多其他强大的工具一样,Hibernate需要一些思考和细致调整以达到最优的结果。和其他优化技术一样,缓存应该通过一种增量的、测试驱动的方式来实现。如果能正确地使用,少量的缓存能把系统的性能提升到最大化。

读书人网 >开源软件

热点推荐