使用hibernate二级缓存优化你的应用

原文:http://www.devx.com/dbzone/article/29685/1954

作者:John Ferguson Smart

翻译及加工: 魏超

 

 

因为对hibernate缓存的不了解,新接触hiberante开发的人往往无法很好的使用它。然而,合理的使用缓存将成为加速hibernate程序的最有效途径。


 

 

频繁的数据库读写会影响web项目的性能表现。作为一个高性能的对象/关系持久化查询技术,单纯的使用hibernate可能还不能解决你所有的性能问题。很多时候,开启二级缓存将会很好的改变这种境况。下面的文章会让你对缓存有个初步的了解,同时告诉你怎么用缓存来提升性能。

 

 

什么是缓存(Cache)?

 

缓存被广泛应用的用于优化数据库。当一些数据被从数据库中读取出来的时候,我们可以把它们放到缓存里.这样,在再次用到这些数据的时候,我们就可以直接从缓存把他们取出来,而不是去连接数据库。当然,当数据库的记录被修改更新的时候,我们就需要把缓存清空掉。因为我们无从得知在数据库记录更新时,缓存中的记录是否还和数据库里的相同

 

 

Hibernate的缓存

Hibernate有一级和二级两种缓存对象。一级缓存关联session对象,而二级缓存关联着session工厂对象。默认情况下,一级缓存是在单个事务中使用的。举例来说,在同一个事务中,当一个对象在事务提交前,被修改了很多次,那么同过一级缓存,在事务提交的时候,我们就会把所有这些修改写在同一条SQL语句中传递给数据库,而不是每次修改都有一条语句。当然,我们这篇文章关注的是二级缓存。可以这么说,相比一级缓存,二级缓存是跨事务的,它将一个事务中产生的查询对象保存下来,在其他事务执行相同的查询的时候,这些被保存的对象就可以被直接拿出来使用,这样就能最大化的减少数据库操作。因此,对于同一个服务,只要有一个用户执行了某个查询,那么其他将要执行相同查询的用户都将从二级缓存中受益。

 

此外,相对于上面说的缓存持久化对象,你可以使用query-level的缓存来存储真正的查询结果集

 

 

缓存的实现

市场上提供了相当多的缓存技术的选择,既有开源的也有收费的。 Hibernate支持下面的开源缓存:

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

以下是不同的缓存产品的特点:

  • EHCache  是一个快速,轻巧,易于使用的线程内的高速缓存。它支持只读和读/写缓存,内存和基于磁盘的缓存。但是,它不支持群集。
  • OSCache 是另一个开源缓存解决方案,也提供对JSP页面和任意对象的缓存功能。它是一个强大和灵活的方案,如同EHCache,支持只读和读/写缓存,内存和基于磁盘的缓存。它还提供对JavaGroup或JMS集群的基础支持。
  • SwarmCache 是一个基于JavaGroup的简单集群缓存解决方案。它支持只读或不严格的读/写缓存(接下来的部分解释这个词)。此缓存的适用于读操作远多于写操作的数据库应用。
  • JBoss TreeCache的 是一个强大的复制(同步或异步)事务缓存。如果你需要一个真正有事务能力的缓存架构,选择这个缓存。

另一个值得一提的是商业缓存Tangosol: Tangosol的高速缓存的一致性.

 

 

缓存策略

一旦选择了缓存实现,你还需要指定你的访问策略。以下四个缓存策略可供选择:

  • 只读:用于只读取而不写入的访问策略。这是迄今为止最简单,效果最好的缓存策略。
  • 读/写:如果你有数据需要写入(更新),可以用这个缓存。当然,读/写缓存的开销要比只读缓存大。在非JTA的环境中,每次操作应在Session.close()或者Session.disconnect()被结束。
  • 非严格的读/写:这种策略并不能保证两个操作修改同一个数据的安全。因此,它适用于经常查询,而只是偶尔的修改的数据。
  • 事务性:这是一个完全的事务缓存,可用于JTA环境。

下表显示了可供选择的不同的缓存实现:

 

缓存只读非严格的读/写读/写事务
EHCache是的是的是的没有
OSCache是的是的是的没有
SwarmCache是的是的没有没有
JBoss TreeCache是的没有没有是的


下面的部分将展示在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>

 

 

当你Hibernate版本是3以上时,你可能还需要使用hibernate.cache.use_second_level_cache属性。这个属性让你可以激活(或停止)二级缓存。默认情况下,二级缓存使用的是EHCache并且已经激活。

 

一个例子

这个例子由几个简单的表组成:Airport, Employee, Language,Country. 每个employee属于一个country,会说多国的language. 而每个country有许多的airport. 下面2幅图分别是这4个类的类图和数据库表图。

 




 
 

设置一个只读缓存

首先设置国家(Country)的Hibernate映射类:

 

<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 class="increment"/>
        </id>

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

	  <set name="airports">
	   <key column="cn_id"/>
	   <one-to-many class="Airport"/>
	  </set>
    </class>
</hibernate-mapping>

 

 

如果这时候你要取所有的country列表,你可以在CountryDao中添加如下方法:

 

public class CountryDAO {
	...	
	public List getCountries() {
		return SessionManager.currentSession()
					   .createQuery(
					      "from Country as c order by c.name")
					   .list();
	}
}

 

 

 

因为上面的方法可能会被频繁的调用,所以我们需要了解它在压力下的性能表现。这里我们写一个单元测试来模拟5次成功的调用:

 

public void testGetCountries() {
		CountryDAO dao = new CountryDAO();
		for(int i = 1; i <= 5; i++) {
  		    Transaction tx = SessionManager.getSession().beginTransaction();
		    TestTimer timer = new TestTimer("testGetCountries");
		    List countries = dao.getCountries();
		    tx.commit();
		    SessionManager.closeSession();
		    timer.done();
		    assertNotNull(countries);
		    assertEquals(countries.size(),229);
		}
	}

 

 

你可以用你熟悉的IDE或者用Maven2的命令行来运行上面的代码。在这里我们连接了一个本地的MySQL数据库。当这段代码被成功的运行,你应该会看到下面的输出:

 

testGetCountries: 521 ms.
testGetCountries: 357 ms.
testGetCountries: 249 ms.
testGetCountries: 257 ms.
testGetCountries: 355 ms.

 

 

每次操作基本上要用4分之1秒。在多数情况下,上面我们取得的国家列表不会被频繁的修改。因此这是一个很好的例子来引入只读缓存(read-only Cache).

 

有2种方法可以激活二级缓存的类:

 

1. 在要使用二级缓存的那个类对应的hbm.xml文件中添加下面的属性:

<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"/>
            ...			        
        </class>
    </hibernate-mapping>

2. 或者把所有的缓存信息都记录在hibernate.cfg.xml文件中:

 

<hibernate-configuration>
	<session-factory>
		...
		<property name="hibernate.cache.provider_class">
			org.hibernate.cache.EHCacheProvider
		</property>
		...
		<class-cache 
class="com.wakaleo.articles.caching.businessobjects.Country"
usage="read-only"
		/>
	</session-factory>
</hibernate-configuration>

 

接下来,你需要配置这个类的缓存规则。这些规则会决定缓存的一些细节。我们在这个例子中用的是EHChe缓存。当然,在其他不同的缓存中,配置规则的方式是不一样的。

EHCache需要一个配置文件(在类路径的根目录,一般称为ehcache.xml)。在下面这个站点有这个文件的模板: EHCache模板 。基本上,你要为每一个你想做二级缓存的类在这个文件中配置规则。如果你没有配置,程序将调用一个默认的规则。

对于这个例子,我们可以使用下面的简单EHCache配置文件:

<ehcache>

    <diskStore path="java.io.tmpdir"/>

    <defaultCache
        maxElementsInMemory="10000"
        eternal="false"
        timeToIdleSeconds="120"
        timeToLiveSeconds="120"
        overflowToDisk="true"
        diskPersistent="false"
        diskExpiryThreadIntervalSeconds="120"
        memoryStoreEvictionPolicy="LRU"
        />
        
    <cache name="com.wakaleo.articles.caching.businessobjects.Country"
        maxElementsInMemory="300"
        eternal="true"
        overflowToDisk="false"
        />

</ehcache>

 

这个文件基本上建立了一个国家基于内存的缓存,最多300个元素(国家清单包含229个国家)。请注意,缓存永不过期(即'永恒=真正的财产)。

这个配置建立了一个基于内存的country缓存。里面最多可以保存300个元素(在我们这个例子中,查询出的结果集包含229个country).(注:这个缓存被配置为永远不过期。eternal=true)

现在,重新运行测试,结果如下:

 

testGetCountries: 412 ms.
testGetCountries: 98 ms.
testGetCountries: 92 ms.
testGetCountries: 82 ms.
testGetCountries: 93 ms.

 

可以明显的看出,操作时间提高到了10分之1秒左右(第一次用了400多ms是因为第一次真正和数据库交互了)

 

缓存内部的存储

在继续下去之前,我们有必要先来了解在上面的代码背后发生了什么。值得注意的是,hibernate缓存并不存储对象的实例(Instances).实际上它存储的是对象的“dehydrated”格式(hibernate术语),这种格式其实就是一系列的属性值。下面是country缓存中内容的一个例子:

 

{ 
  30  => [bw,Botswana,30], 
  214 => [uy,Uruguay,214], 
  158 => [pa,Panama,158],
  31  => [by,Belarus,31]
  95  => [in,India,95]
  ...
}

 注意,每一个Id映射到一系列的属性上。这里你可能会发现,只有原始的属性被缓存下来了,具体来说,airport属性没有被缓存。实际上,这是因为airport属性是一个association:是一系列指向其他持久化对象的引用。

 

默认情况下,hibernate不会对associations进行缓存。当然,你可以设置是否缓存association,同时可以设置当二级缓存中的对象被重新加载出来的时候哪些associations要被检索。

 

Association缓存是一个非常强大的功能。下面我们将对它进行更详细的研究。

 

 

使用Associations缓存

假设你需要获得一个country对应的所有employees的列表(包括employee名字, 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 class="increment"/>
       </id>

	 <property column="emp_surname" name="surname" type="string"/>
	 <property column="emp_firstname" name="firstname" type="string"/>
	   
	 <many-to-one name="country"
 	              column="cn_id"
	              class="com.wakaleo.articles.caching.businessobjects.Country"  
			  not-null="true" />
			    
	 <!-- 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" class="Language"/>
	 </set>				        
    </class>
</hibernate-mapping>

 假如你真的需要在每次取得employee对象的时候都加载其对应的language信息,你需要把language属性的lazy设置为false.(不延迟加载)(这里只是为了举例的需要,在真实情况下,不建议取消延迟加载,因为这会影响性能)。同时,你需要添加下面的方法来获得employee:

 

public class EmployeeDAO {

	public List getEmployeesByCountry(Country country) {
		return SessionManager.currentSession()
		 .createQuery(
		      "from Employee as e where e.country = :country "
                + " 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();
	}
}

 

运行这个单元测试,可以看到下面的结果:

 

testGetNZEmployees: 1227 ms.
testGetAUEmployees: 883 ms.
testGetNZEmployees: 907 ms.
testGetAUEmployees: 873 ms.
testGetNZEmployees: 987 ms.
testGetAUEmployees: 916 ms.

可以看出,每次为一个country取得50几个employee都需要差不多1秒钟 。这样的速度太慢了。这是一个传统的"N+1"问题。如果启用SQL日志,我们可以看到,每次执行一个employee的查询语句后面,都跟着数百条language表的查询。每次我们从缓存中加载employee对象,其关联的全部language都会被重新检索。我们应该怎么改善这里的性能呢?首先,激活employee类的读写缓存(read/write cache):

 

<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 cache):

 

<class name="Language" table="SPOKEN_LANGUAGE" dynamic-update="true">
		<meta attribute="implement-equals">true</meta>    
		<cache usage="read-only"/>
            ...			        
        </class>
    </hibernate-mapping>

 之后,在ehcache.xml文件中添加下面2个缓存规则:

 

<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的association.

 

<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 class="increment"/>
      </id>

	<property column="emp_surname" name="surname" type="string"/>
	<property column="emp_firstname" name="firstname" type="string"/>
	   
	<many-to-one name="country"
 	  		 column="cn_id"
 		       class="com.wakaleo.articles.caching.businessobjects.Country"  
			not-null="true" />
			    
	<!-- 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>

 这样配置之后,我们可以看到下面的结果(速度提高了10倍左右):

 

testGetNZEmployees: 1477 ms.
testGetAUEmployees: 940 ms.
testGetNZEmployees: 65 ms.
testGetAUEmployees: 65 ms.
testGetNZEmployees: 76 ms.
testGetAUEmployees: 52 ms.

 

 

使用查询缓存(Query Caches)

在某些情况下,我们需要缓存的是确切的查询结果集,而不是某些对象。例如,每次调用getCountries()方法的时候,我们都会得到相同的country列表。因此,除了缓存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中就过期了(hibernate会重新刷新这部分缓存)。然而,hibernate却无法获悉本身应用之外的,其他应用直接去修改数据库数据的操作。因此,如果你使用的数据会频繁的处于提交更新的状态下,你就不应该使用任何的二级缓存。如果非要使用的话,至少,你应该将二级缓存的超时时间设置的足够短。  

 

正确的Hibernate缓存

缓存是一个强大的技术,Hibernate提供了一个有效,灵活且过度缓和的方式来实现它。即使是默认的配置也可以在许多简单的应用中有效的提高性能。然而,如同其他强大的工具一样,hibernate需要更深入的思考和微调,来取得最佳的效果。而缓存——如同其他的优化技术——应该遵循增量的,测试驱动的方法。当合理使用的时候,少量的缓存就可以使你程序发挥出最大的效能。

 

 

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

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

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

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值