關鍵字: hibernate二級緩存
原文來源:http://www.devx.com/dbzone/Article/29685/1954?pf=true
作者簡介:John Ferguson Smart,參與過很多企業和政府大型的的J2EE項目,他的專長包括J2EE的架構,開發和IT項目管理。他也有很多的在JAVA的開源技術方面的經驗。這是他技術blog的鏈接www.jroller.com/page/wakaleo
原文翻譯如下:
通過二級緩存來加快你的hibernate應用程序
新的hibernate開發人員有時並不知道hibernate的緩存而只是簡單的把它作爲一種結果,儘管如此,當我們正確使用緩存的時候,它能夠變爲加速hibernate應用程序最有力的武器之一。
在web應用程序中和大數據量的數據庫交互經常導致性能問題。hibernate是一種高性能的,提供對象/關係持久化和查詢的服務,但是如果沒 有幫助就不會解決所有性能上的問題。在很多情況下,二級緩存就是hibernate潛在的需要來實現所有的性能上的處理。這篇文章將研究 hibernate的緩存功能並且展現怎麼使用才能明顯的提升應用程序的性能。
緩存介紹
緩存被廣泛用於數據庫應用領域。緩存的設計就是爲了通過存儲已經從數據庫讀取的數據來減少應用程序和數據庫之間的數據流量,而數據庫的訪問只在檢 索的數據不在當前緩存的時候才需要。如果數據庫以一些方式進行更新和修改的話,那麼應用程序可能需要每隔一段時間清空緩存,因爲它沒有方法知道緩存裏的數 據是不是最新的。
Hibernate緩存
從對象來說,hibernate用了兩種緩存的方法:一級緩存和二級緩存。一級緩存和Session對象關聯,二級緩存和Session Factory對象關聯。默認情況下,hibernate使用一級緩存來作爲一次事務預處理的基礎。hibernate使用它主要是爲了減少在一次給定的 事務中需要被生成的SQL查詢語句。例如,一個對象在同一個事務中被修改好幾次,hibernate會在事務結束的時候只生成一條SQL更新語句,它包含 了所有修改內容。這篇文章主要介紹二級緩存。爲了減少和數據庫的交互,二級緩存保存已經讀出的對象在各個事務之間的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是一種快速的,輕量級的,容易上手的緩存。它支持只讀和讀寫緩存,基於內存和硬盤的數據儲存。但是,它不支持簇。
OSCache是另一個開源的緩存解決方案,它是一個也爲jsp頁面和任意對象提供緩存功能的大的開發包的一部分。它本身也是一個強大和靈活的開 發包,像EHCache一樣,也支持只讀和讀寫緩存,基於內存和硬盤的數據儲存。它通過JavaGroups或者JMS也提供了對簇的基本支持。
SwarmCache是一種基於簇的解決方案,它本身也基於集羣服務實體間通信的通信協議。它提供了只讀和沒有限制的讀寫緩存(下一部分將解釋這個名詞)。這種類型的緩存對那些典型的讀操作比寫操作多的多的應用程序是適合的。
JBoss TreeCache是一種強大的兩重性(同步的或異步的)和事務性的緩存。使用此實現如果你確實需要一種事務能力的緩存架構。
另一種值得提及的緩存實現是商業化的Tangosol Coherence cache。
緩存策略
一旦你選擇了你的緩存實現,你需要指定你的緩存策略。以下是四種緩存策略:
只讀:這種策略對那種數據被頻繁讀取但是不更新是有效的,這是到目前爲止最簡單,表現最好的緩存策略。
讀寫:讀寫緩存對假設你的數據需要更新是必要的。但是讀寫需要比只讀緩存花費更多的資源。在非JTA的事務環境中,每一個事務需要完成在Session.close() 或Session.disconnect()被調用的時候。
沒有限制的讀寫:這個策略不能保證2個事務不會同時的修改相同的數據。因此,它可能對那些數據經常被讀取但只會偶爾進行寫操作的最適合。
事務性:這是個完全事務性的緩存,它可能只能被用在JTA環境中。
對每一個緩存實現來說,支持策略不是唯一的。圖一展現了對
緩存實現可供的選擇。
文章餘下的部分用來展示一個使用EHCache單一的JVM緩存。
緩存配置
爲了啓用二級緩存,我們需要在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-configuration>
- <session-factory>
- ...
- <property name="hibernate.cache.provider_class" >
- org.hibernate.cache.EHCacheProvider
- </property>
- ...
- </session-factory>
- </hibernate-configuration>
<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。
一個實際應用
這個例子演示的程序包含四張簡單的表:國家列表,機場列表,員工列表,語言列表。每個員工被分配了一個國家,並且能說很多語言。每個國家能有任意數量的機場。
圖一展現了UML類圖
圖二展現了數據庫架構
這個例子的源代碼(http://assets.devx.com/sourcecode/14239.tgz )包括了以下的SQL腳本,你需要用它創建和實例化數據庫。
* src/sql/create.sql: 創建數據庫的SQL腳本
* src/sql/init.sql: 測試數據
安裝Maven2
在寫的時候,Maven2安裝目錄看來好像缺少了一些jars。爲了解決這個問題,在應用程序源碼的根目錄裏找到那些缺少的jars。把它們安裝到Maven2的文件裏,到應用程序的目錄下,執行以下的命令
$ 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
建立一個只讀緩存
從簡單的開始,下面是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>
- <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>
<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>
假設你要顯示國家列表。你可以通過CountryDAO類中一個簡單的方法實現,如下
- public class CountryDAO {
- ...
- public List getCountries() {
- return SessionManager.currentSession()
- .createQuery(
- "from Country as c order by c.name" )
- .list();
- }
- }
- public class CountryDAO {
- ...
- public List getCountries() {
- return SessionManager.currentSession()
- .createQuery(
- "from Country as c order by c.name" )
- .list();
- }
- }
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 );
- }
- }
- 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 );
- }
- }
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的命令行(演示程序提供了2個Maven2的工程文件)。這個演示程序通過本地的mysql來測試。當你運行這個測試的時候,應該得到類似以下的一些信息:
$mvn test -Dtest=CountryDAOTest
...
testGetCountries: 521 ms.
testGetCountries: 357 ms.
testGetCountries: 249 ms.
testGetCountries: 257 ms.
testGetCountries: 355 ms.
[surefire] Running com.wakaleo.articles.caching.dao.CountryDAOTest
[surefire] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 3,504 sec
可以看出每次調用大概花費半秒的時間,對大多數標準來說還是有點遲緩的。國家的列表很可能不是經常的改變,所以這個類可以作爲只讀緩存一個好的候選。所以加上去
你可以啓用二級緩存用以下兩種方法的任意一種
1.你可以在*.hbm.xml裏啓用它,使用cache的屬性
- <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>
- <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>
<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文件中,使用class-cache屬性
- <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>
- <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>
<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>
下一步,你需要爲這個類設置緩存規則,這些規則決定了緩存怎麼表現的細節。這個例子的演示是使用EHCache,但是記住每一種緩存實現是不一樣的。
EHCache需要一個配置文件(通常叫做ehcache.xml)在類的根目錄。EHCache配置文件的詳細文檔可以看這裏(http://ehcache.sourceforge.net/documentation )。基本上,你要爲每個需要緩存的類定義規則,以及一個defaultCache在你沒有明確指明任何規則給一個類的時候使用。
對第一個例子來說,你可以使用下面簡單的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>
- <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>
<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>
這個文件爲countries類建立一個基於內存最多300單位的緩存(countries類包含了229個國家)。注意緩存不會過期('eternal=true'屬性)。現在通過返回的結果看下緩存的表現
$mvn test -Dtest=CompanyDAOTest
...
testGetCountries: 412 ms.
testGetCountries: 98 ms.
testGetCountries: 92 ms.
testGetCountries: 82 ms.
testGetCountries: 93 ms.
[surefire] Running com.wakaleo.articles.caching.dao.CountryDAOTest
[surefire] Tests run: 1, Failures: 0, Errors: 0, Time elapsed: 2,823 sec
正如你期盼的那樣,第一次查詢沒有改變因爲需要加載數據。但是,隨後的幾次查詢就快多了。
後臺
在我們繼續之前,看下後臺發生了什麼非常有用。一件事情你需要知道的是hibernate緩存不儲存對象實例,代替的是它儲存對象“脫水”的形式(hibernate的術語),也就是作爲一系列屬性值。以下是一個countries緩存例子的內容
{
30 => [bw,Botswana,30],
214 => [uy,Uruguay,214],
158 => [pa,Panama,158],
31 => [by,Belarus,31]
95 => [in,India,95]
...
}
注意每個ID是怎麼樣映射到擁有屬性值的數組的。你可能也注意到了只有主要的屬性被儲存了,而沒有airports屬性,這是因爲airports屬性只是一個關聯:對其他持久化對象一系列的引用。
默認情況下,hibernate不緩存關聯。而由你決定來緩存哪個關聯,哪個關聯需要被重載當緩存對象從二級緩存獲得的時候。
關聯緩存是一個非常強大的功能。下一部分我們將介紹更多的內容。
和關聯緩存一起工作
假設你需要顯示一個給定的國家的所有的員工(包括員工的名字,使用的語言等)。以下是employee類的hibernate映射
- <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>
- <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>
<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="first