原文翻譯如下:
通過(guò)二級緩存來(lái)加快你的hibernate應用程序
新的hibernate開(kāi)發(fā)人員有時(shí)并不知道hibernate的緩存而只是簡(jiǎn)單的把它作為一種結果,盡管如此,當我們正確使用緩存的時(shí)候,它能夠變?yōu)榧铀賖ibernate應用程序最有力的武器之一。
在web應用程序中和大數據量的數據庫交互經(jīng)常導致性能問(wèn)題。hibernate是一種高性能的,提供對象/關(guān)系持久化和查詢(xún)的服務(wù),但是如果沒(méi)有幫助就不會(huì )解決所有性能上的問(wèn)題。在很多情況下,二級緩存就是hibernate潛在的需要來(lái)實(shí)現所有的性能上的處理。這篇文章將研究hibernate的緩存功能并且展現怎么使用才能明顯的提升應用程序的性能。
緩存介紹
緩存被廣泛用于數據庫應用領(lǐng)域。緩存的設計就是為了通過(guò)存儲已經(jīng)從數據庫讀取的數據來(lái)減少應用程序和數據庫之間的數據流量,而數據庫的訪(fǎng)問(wèn)只在檢索的數據不在當前緩存的時(shí)候才需要。如果數據庫以一些方式進(jìn)行更新和修改的話(huà),那么應用程序可能需要每隔一段時(shí)間清空緩存,因為它沒(méi)有方法知道緩存里的數據是不是最新的。
Hibernate緩存
從對象來(lái)說(shuō),hibernate用了兩種緩存的方法:一級緩存和二級緩存。一級緩存和Session對象關(guān)聯(lián),二級緩存和Session Factory對象關(guān)聯(lián)。默認情況下,hibernate使用一級緩存來(lái)作為一次事務(wù)預處理的基礎。hibernate使用它主要是為了減少在一次給定的事務(wù)中需要被生成的SQL查詢(xún)語(yǔ)句。例如,一個(gè)對象在同一個(gè)事務(wù)中被修改好幾次,hibernate會(huì )在事務(wù)結束的時(shí)候只生成一條SQL更新語(yǔ)句,它包含了所有修改內容。這篇文章主要介紹二級緩存。為了減少和數據庫的交互,二級緩存保存已經(jīng)讀出的對象在各個(gè)事務(wù)之間的Session Factory級別。這些對象在整個(gè)程序中都可以得到,而不僅是用戶(hù)運行查詢(xún)的時(shí)候。這種方式,每次查詢(xún)返回的對象都已經(jīng)被載入了緩存,一次或多次潛在的數據庫事務(wù)可以避免。
除此之外,你可以使用查詢(xún)級別緩存如果你需要緩存的實(shí)際的查詢(xún)結果,而不僅僅是持久化對象。
緩存的實(shí)現
緩存是軟件中復雜的部分,市場(chǎng)上也提供了相當數量的選擇,包括基于開(kāi)源的和商業(yè)的。hibernate提供了以下開(kāi)源的緩存實(shí)現,如下:
* EHCache (org.hibernate.cache.EhCacheProvider)
* OSCache (org.hibernate.cache.OSCacheProvider)
* SwarmCache (org.hibernate.cache.SwarmCacheProvider)
* JBoss TreeCache (org.hibernate.cache.TreeCacheProvider)
不同的緩存提供了在性能,內存使用和配置的可擴展性方面不同的能力
EHCache是一種快速的,輕量級的,容易上手的緩存。它支持只讀和讀寫(xiě)緩存,基于內存和硬盤(pán)的數據儲存。但是,它不支持簇。
OSCache是另一個(gè)開(kāi)源的緩存解決方案,它是一個(gè)也為jsp頁(yè)面和任意對象提供緩存功能的大的開(kāi)發(fā)包的一部分。它本身也是一個(gè)強大和靈活的開(kāi)發(fā)包,像EHCache一樣,也支持只讀和讀寫(xiě)緩存,基于內存和硬盤(pán)的數據儲存。它通過(guò)JavaGroups或者JMS也提供了對簇的基本支持。
SwarmCache是一種基于簇的解決方案,它本身也基于集群服務(wù)實(shí)體間通信的通信協(xié)議。它提供了只讀和沒(méi)有限制的讀寫(xiě)緩存(下一部分將解釋這個(gè)名詞)。這種類(lèi)型的緩存對那些典型的讀操作比寫(xiě)操作多的多的應用程序是適合的。
JBoss TreeCache是一種強大的兩重性(同步的或異步的)和事務(wù)性的緩存。使用此實(shí)現如果你確實(shí)需要一種事務(wù)能力的緩存架構。
另一種值得提及的緩存實(shí)現是商業(yè)化的Tangosol Coherence cache。
緩存策略
一旦你選擇了你的緩存實(shí)現,你需要指定你的緩存策略。以下是四種緩存策略:
只讀:這種策略對那種數據被頻繁讀取但是不更新是有效的,這是到目前為止最簡(jiǎn)單,表現最好的緩存策略。
讀寫(xiě):讀寫(xiě)緩存對假設你的數據需要更新是必要的。但是讀寫(xiě)需要比只讀緩存花費更多的資源。在非JTA的事務(wù)環(huán)境中,每一個(gè)事務(wù)需要完成在Session.close() 或Session.disconnect()被調用的時(shí)候。
沒(méi)有限制的讀寫(xiě):這個(gè)策略不能保證2個(gè)事務(wù)不會(huì )同時(shí)的修改相同的數據。因此,它可能對那些數據經(jīng)常被讀取但只會(huì )偶爾進(jìn)行寫(xiě)操作的最適合。
事務(wù)性:這是個(gè)完全事務(wù)性的緩存,它可能只能被用在JTA環(huán)境中。
對每一個(gè)緩存實(shí)現來(lái)說(shuō),支持策略不是唯一的。圖一展現了對
緩存實(shí)現可供的選擇。
文章余下的部分用來(lái)展示一個(gè)使用EHCache單一的JVM緩存。
緩存配置
為了啟用二級緩存,我們需要在hibernate.cfg.xml配置文件中定義hibernate.cache.provider_class屬性,如下
Java代碼 1.<hibernate-configuration>
2. <session-factory>
3. ...
4. <property name="hibernate.cache.provider_class"> 5. org.hibernate.cache.EHCacheProvider
6. </property>
7. ...
8. </session-factory>
9.</hibernate-configuration>
為了在hibernate3中測試的目的,你還要使用hibernate.cache.use_second_level_cache屬性,它可以讓你啟用(和關(guān)閉)二級緩存。默認情況下,二級緩存是啟用的同時(shí)使用EHCache。
一個(gè)實(shí)際應用
這個(gè)例子演示的程序包含四張簡(jiǎn)單的表:國家列表,機場(chǎng)列表,員工列表,語(yǔ)言列表。每個(gè)員工被分配了一個(gè)國家,并且能說(shuō)很多語(yǔ)言。每個(gè)國家能有任意數量的機場(chǎng)。
圖一展現了UML類(lèi)圖
圖二展現了數據庫架構
這個(gè)例子的源代碼(
http://assets.devx.com/sourcecode/14239.tgz)包括了以下的SQL腳本,你需要用它創(chuàng )建和實(shí)例化數據庫。
* src/sql/create.sql: 創(chuàng )建數據庫的SQL腳本
* src/sql/init.sql: 測試數據
安裝Maven2
在寫(xiě)的時(shí)候,Maven2安裝目錄看來(lái)好像缺少了一些jars。為了解決這個(gè)問(wèn)題,在應用程序源碼的根目錄里找到那些缺少的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
建立一個(gè)只讀緩存
從簡(jiǎn)單的開(kāi)始,下面是country類(lèi)的hibernate映射。
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Country" table="COUNTRY" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4. <cache usage="read-only"/> 5.
6. <id name="id" type="long" unsaved-value="null" > 7. <column name="cn_id" not-null="true"/> 8. <generator class="increment"/> 9. </id>
10.
11. <property column="cn_code" name="code" type="string"/> 12. <property column="cn_name" name="name" type="string"/> 13.
14. <set name="airports"> 15. <key column="cn_id"/> 16. <one-to-many class="Airport"/> 17. </set>
18. </class> 19.</hibernate-mapping>
20.
假設你要顯示國家列表。你可以通過(guò)CountryDAO類(lèi)中一個(gè)簡(jiǎn)單的方法實(shí)現,如下
Java代碼 1.public class CountryDAO {
2. ...
3. public List getCountries() { 4. return SessionManager.currentSession() 5. .createQuery(
6. "from Country as c order by c.name") 7. .list();
8. }
9.}
因為這個(gè)方法經(jīng)常被調用,所以你要知道它在壓力下的行為。寫(xiě)一個(gè)簡(jiǎn)單的單元測試來(lái)模擬5次連續的調用。
Java代碼
1.public void testGetCountries() {
2. CountryDAO dao = new CountryDAO();
3. for(int i = 1; i <= 5; i++) {
4. Transaction tx = SessionManager.getSession().beginTransaction();
5. TestTimer timer = new TestTimer("testGetCountries");
6. List countries = dao.getCountries();
7. tx.commit();
8. SessionManager.closeSession();
9. timer.done();
10. assertNotNull(countries);
11. assertEquals(countries.size(),229);
12. }
13.}
你能夠運行這個(gè)測試通過(guò)自己喜歡的IDE或者M(jìn)aven2的命令行(演示程序提供了2個(gè)Maven2的工程文件)。這個(gè)演示程序通過(guò)本地的mysql來(lái)測試。當你運行這個(gè)測試的時(shí)候,應該得到類(lèi)似以下的一些信息:
$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
可以看出每次調用大概花費半秒的時(shí)間,對大多數標準來(lái)說(shuō)還是有點(diǎn)遲緩的。國家的列表很可能不是經(jīng)常的改變,所以這個(gè)類(lèi)可以作為只讀緩存一個(gè)好的候選。所以加上去
你可以啟用二級緩存用以下兩種方法的任意一種
1.你可以在*.hbm.xml里啟用它,使用cache的屬性
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Country" table="COUNTRY" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4. <cache usage="read-only"/> 5. ...
6. </class> 7. </hibernate-mapping>
2.你可以存儲所有緩存信息在hibernate.cfg.xml文件中,使用class-cache屬性
Java代碼 1.<hibernate-configuration>
2. <session-factory>
3. ...
4. <property name="hibernate.cache.provider_class"> 5. org.hibernate.cache.EHCacheProvider
6. </property>
7. ...
8. <class-cache 9.class="com.wakaleo.articles.caching.businessobjects.Country" 10.usage="read-only" 11. />
12. </session-factory>
13.</hibernate-configuration>
下一步,你需要為這個(gè)類(lèi)設置緩存規則,這些規則決定了緩存怎么表現的細節。這個(gè)例子的演示是使用EHCache,但是記住每一種緩存實(shí)現是不一樣的。
EHCache需要一個(gè)配置文件(通常叫做ehcache.xml)在類(lèi)的根目錄。EHCache配置文件的詳細文檔可以看這里(
http://ehcache.sourceforge.net/documentation)?;旧?,你要為每個(gè)需要緩存的類(lèi)定義規則,以及一個(gè)defaultCache在你沒(méi)有明確指明任何規則給一個(gè)類(lèi)的時(shí)候使用。
對第一個(gè)例子來(lái)說(shuō),你可以使用下面簡(jiǎn)單的EHCache配置文件
Java代碼 1.<ehcache>
2.
3. <diskStore path="java.io.tmpdir"/> 4.
5. <defaultCache
6. maxElementsInMemory="10000" 7. eternal="false" 8. timeToIdleSeconds="120" 9. timeToLiveSeconds="120" 10. overflowToDisk="true" 11. diskPersistent="false" 12. diskExpiryThreadIntervalSeconds="120" 13. memoryStoreEvictionPolicy="LRU" 14. />
15.
16. <cache name="com.wakaleo.articles.caching.businessobjects.Country" 17. maxElementsInMemory="300" 18. eternal="true" 19. overflowToDisk="false" 20. />
21.
22.</ehcache>
這個(gè)文件為countries類(lèi)建立一個(gè)基于內存最多300單位的緩存(countries類(lèi)包含了229個(gè)國家)。注意緩存不會(huì )過(guò)期('eternal=true'屬性)?,F在通過(guò)返回的結果看下緩存的表現
$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
正如你期盼的那樣,第一次查詢(xún)沒(méi)有改變因為需要加載數據。但是,隨后的幾次查詢(xún)就快多了。
后臺
在我們繼續之前,看下后臺發(fā)生了什么非常有用。一件事情你需要知道的是hibernate緩存不儲存對象實(shí)例,代替的是它儲存對象“脫水”的形式(hibernate的術(shù)語(yǔ)),也就是作為一系列屬性值。以下是一個(gè)countries緩存例子的內容
{
30 => [bw,Botswana,30],
214 => [uy,Uruguay,214],
158 => [pa,Panama,158],
31 => [by,Belarus,31]
95 => [in,India,95]
...
}
注意每個(gè)ID是怎么樣映射到擁有屬性值的數組的。你可能也注意到了只有主要的屬性被儲存了,而沒(méi)有airports屬性,這是因為airports屬性只是一個(gè)關(guān)聯(lián):對其他持久化對象一系列的引用。
默認情況下,hibernate不緩存關(guān)聯(lián)。而由你決定來(lái)緩存哪個(gè)關(guān)聯(lián),哪個(gè)關(guān)聯(lián)需要被重載當緩存對象從二級緩存獲得的時(shí)候。
關(guān)聯(lián)緩存是一個(gè)非常強大的功能。下一部分我們將介紹更多的內容。
和關(guān)聯(lián)緩存一起工作
假設你需要顯示一個(gè)給定的國家的所有的員工(包括員工的名字,使用的語(yǔ)言等)。以下是employee類(lèi)的hibernate映射
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Employee" table="EMPLOYEE" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4.
5. <id name="id" type="long" unsaved-value="null" > 6. <column name="emp_id" not-null="true"/> 7. <generator class="increment"/> 8. </id>
9.
10. <property column="emp_surname" name="surname" type="string"/> 11. <property column="emp_firstname" name="firstname" type="string"/> 12.
13. <many-to-one name="country" 14. column="cn_id" 15. class="com.wakaleo.articles.caching.businessobjects.Country" 16. not-null="true" /> 17.
18. <!-- Lazy-loading is deactivated to demonstrate caching behavior -->
19. <set name="languages" table="EMPLOYEE_SPEAKS_LANGUAGE" lazy="false"> 20. <key column="emp_id"/> 21. <many-to-many column="lan_id" class="Language"/> 22. </set>
23. </class> 24.</hibernate-mapping>
假設你每次使用employee對象的時(shí)候需要得到一個(gè)員工會(huì )說(shuō)的語(yǔ)言。強逼hibernate自動(dòng)載入關(guān)聯(lián)的languages集合,你設置了lazy屬性為false()。你還需要一個(gè)DAO類(lèi)來(lái)得到所有employee,以下的代碼來(lái)幫你實(shí)現
Java代碼 1.public class EmployeeDAO {
2.
3. public List getEmployeesByCountry(Country country) { 4. return SessionManager.currentSession() 5. .createQuery(
6. "from Employee as e where e.country = :country " 7. + " order by e.surname, e.firstname") 8. .setParameter("country",country) 9. .list();
10. }
11.}
下一步,寫(xiě)一些簡(jiǎn)單的單元測試來(lái)看它怎么表現。正如前面的例子一樣,你需要知道它被重復調用時(shí)候的性能
Java代碼 1.public class EmployeeDAOTest extends TestCase {
2.
3. CountryDAO countryDao = new CountryDAO(); 4. EmployeeDAO employeeDao = new EmployeeDAO(); 5.
6. /** 7. * Ensure that the Hibernate session is available
8. * to avoid the Hibernate initialisation interfering with
9. * the benchmarks
10. */
11. protected void setUp() throws Exception { 12. super.setUp(); 13. SessionManager.getSession();
14. }
15.
16. public void testGetNZEmployees() { 17. TestTimer timer = new TestTimer("testGetNZEmployees"); 18. Transaction tx = SessionManager.getSession().beginTransaction();
19. Country nz = countryDao.findCountryByCode("nz"); 20. List kiwis = employeeDao.getEmployeesByCountry(nz);
21. tx.commit();
22. SessionManager.closeSession();
23. timer.done();
24. }
25.
26. public void testGetAUEmployees() { 27. TestTimer timer = new TestTimer("testGetAUEmployees"); 28. Transaction tx = SessionManager.getSession().beginTransaction();
29. Country au = countryDao.findCountryByCode("au"); 30. List aussis = employeeDao.getEmployeesByCountry(au);
31. tx.commit();
32. SessionManager.closeSession();
33. timer.done();
34. }
35.
36. public void testRepeatedGetEmployees() { 37. testGetNZEmployees();
38. testGetAUEmployees();
39. testGetNZEmployees();
40. testGetAUEmployees();
41. }
42.}
如果你運行上面的代碼,你會(huì )得到類(lèi)似以下的一些數據
$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
所以對一個(gè)國家載入大約50個(gè)員工需要花費大約一秒的時(shí)間。這種方法顯然太慢了。這是典型的N+1的查詢(xún)問(wèn)題。如果你啟用SQL日志,你會(huì )發(fā)現對employee表的一次查詢(xún),緊跟著(zhù)對language表幾百次的查詢(xún),無(wú)論什么時(shí)候hibernate從緩存里得到一個(gè)employee對象,它都會(huì )重載所有關(guān)聯(lián)的language。那怎么提升它的性能呢?第一件要做的事就是對employee啟用讀寫(xiě)緩存,如下
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Employee" table="EMPLOYEE" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4. <cache usage="read-write"/> 5. ...
6. </class> 7.</hibernate-mapping>
你還應該對language類(lèi)啟用緩存。只讀緩存如下
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Language" table="SPOKEN_LANGUAGE" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4. <cache usage="read-only"/> 5. ...
6. </class> 7.</hibernate-mapping>
然后你需要配置緩存的規則通過(guò)加入以下的內容到ehcache.xml文件中
Java代碼
1.<cache name="com.wakaleo.articles.caching.businessobjects.Employee"
2. maxElementsInMemory="5000"
3. eternal="false"
4. overflowToDisk="false"
5. timeToIdleSeconds="300"
6. timeToLiveSeconds="600"
7./>
8.<cache name="com.wakaleo.articles.caching.businessobjects.Language"
9. maxElementsInMemory="100"
10. eternal="true"
11. overflowToDisk="false"
12./>
但是還是沒(méi)有解決N+1的查詢(xún)問(wèn)題:當你載入一個(gè)employee對象的時(shí)候大約50次的額外查詢(xún)還是會(huì )執行。這里你就需要在Employee.hbm.xml映射文件里關(guān)聯(lián)的language啟用緩存,如下
Java代碼 1.<hibernate-mapping package="com.wakaleo.articles.caching.businessobjects">
2. <class name="Employee" table="EMPLOYEE" dynamic-update="true"> 3. <meta attribute="implement-equals">true</meta> 4.
5. <id name="id" type="long" unsaved-value="null" > 6. <column name="emp_id" not-null="true"/> 7. <generator class="increment"/> 8. </id>
9.
10. <property column="emp_surname" name="surname" type="string"/> 11. <property column="emp_firstname" name="firstname" type="string"/> 12.
13. <many-to-one name="country" 14. column="cn_id" 15. class="com.wakaleo.articles.caching.businessobjects.Country" 16. not-null="true" /> 17.
18. <!-- Lazy-loading is deactivated to demonstrate caching behavior -->
19. <set name="languages" table="EMPLOYEE_SPEAKS_LANGUAGE" lazy="false"> 20. <cache usage="read-write"/> 21. <key column="emp_id"/> 22. <many-to-many column="lan_id" class="Language"/> 23. </set>
24. </class> 25.</hibernate-mapping>
通過(guò)這個(gè)配置,你就能得到幾近最優(yōu)的性能
$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
查詢(xún)緩存
在確定的情況下,緩存一次查詢(xún)的正確結果是非常有用的,不僅是只是個(gè)確定的對象。例如,getCountries()方法在每次調用的時(shí)候或許會(huì )返回相同的國家列表。所以,除了緩存country類(lèi)之外,你也應該緩存查詢(xún)結果本身。
為了實(shí)現這個(gè)目標,你需要在hibernate.cfg.xml文件中設置hibernate.cache.use_query_cache屬性為true,如下
<property name="hibernate.cache.use_query_cache">true</property>
然后需要在對任何查詢(xún)緩存的時(shí)候使用setCacheable()方法,如下
Java代碼 1.public class CountryDAO {
2.
3. public List getCountries() { 4. return SessionManager.currentSession() 5. .createQuery("from Country as c order by c.name") 6. .setCacheable(true) 7. .list();
8. }
9.}
但是,它不能預知到其他程序對數據庫任何的改變。所以你不應該使用任何二級緩存(或為類(lèi)和集合的緩存設置簡(jiǎn)短的過(guò)期時(shí)間)如果你的數據總是要保證最新的狀態(tài)。
正確的使用hibernate緩存
緩存是一種強大的技術(shù),hibernate提供了一種強大的,靈活的,不顯眼的方式來(lái)實(shí)現它。即使對很多簡(jiǎn)單的例子來(lái)說(shuō)默認的設置能使實(shí)際的性能得到提升。但是,像很多強大工具一樣,hibernate還是需要一些思考和微調來(lái)得到最優(yōu)的結果,而緩存像其他的優(yōu)化技術(shù)一樣,應該使用一種可擴展,測試驅動(dòng)的方法所實(shí)現。當正確使用的時(shí)候,少量的緩存實(shí)現,能最大程度的提高你的程序運行。