Hibernate的一級緩存是由Session提供的,因此它只存在於Session的生命週期中,也就是當Session關閉的時候該Session所管理的一級緩存也會立即被清除。
一級緩存採用的是key-value的Map方式來實現的,在緩存實體對象時,對象的主關鍵字ID是Map的key,實體對象就是對應的值。所以說,一級緩存是以實體對象爲單位進行存儲的,在訪問的時候使用的是主關鍵字ID。
雖然,Hibernate對一級緩存使用的是自動維護的功能,沒有提供任何配置功能,但是可以通過Session中所提供的方法來對一級緩存的管理進行手工干預。Session中所提供的干預方法包括以下兩種。
在進行大批量數據一次性更新的時候,會佔用非常多的內存來緩存被更新的對象。這時就應該階段性地調用clear()方法來清空一級緩存中的對象,控制一級緩存的大小,以避免產生內存溢出的情況。具體的實現方法如清單14.8所示。
14.2.3 二級緩存
與Session相對的是,SessionFactory也提供了相應的緩存機制。SessionFactory緩存可以依據功能和目的的不同而劃分爲內置緩存和外置緩存。
SessionFactory的內置緩存中存放了映射元數據和預定義SQL語句,映射元數據是映射文件中數據的副本,而預定義SQL語句是在Hibernate初始化階段根據映射元數據推導出來的。SessionFactory的內置緩存是隻讀的,應用程序不能修改緩存中的映射元數據和預定義SQL語句,因此SessionFactory不需要進行內置緩存與映射文件的同步。
SessionFactory的外置緩存是一個可配置的插件。在默認情況下,SessionFactory不會啓用這個插件。外置緩存的數據是數據庫數據的副本,外置緩存的介質可以是內存或者硬盤。SessionFactory的外置緩存也被稱爲Hibernate的二級緩存。
Hibernate的二級緩存的實現原理與一級緩存是一樣的,也是通過以ID爲key的Map來實現對對象的緩存。
由於Hibernate的二級緩存是作用在SessionFactory範圍內的,因而它比一級緩存的範圍更廣,可以被所有的Session對象所共享。
14.2.3.1 二級緩存的工作內容
Hibernate的二級緩存同一級緩存一樣,也是針對對象ID來進行緩存。所以說,二級緩存的作用範圍是針對根據ID獲得對象的查詢。
二級緩存的工作可以概括爲以下幾個部分:
● 在執行各種條件查詢時,如果所獲得的結果集爲實體對象的集合,那麼就會把所有的數據對象根據ID放入到二級緩存中。
● 當Hibernate根據ID訪問數據對象的時候,首先會從Session一級緩存中查找,如果查不到並且配置了二級緩存,那麼會從二級緩存中查找,如果還查不到,就會查詢數據庫,把結果按照ID放入到緩存中。
● 刪除、更新、增加數據的時候,同時更新緩存。
14.2.3.2 二級緩存的適用範圍
Hibernate的二級緩存作爲一個可插入的組件在使用的時候也是可以進行配置的,但並不是所有的對象都適合放在二級緩存中。
在通常情況下會將具有以下特徵的數據放入到二級緩存中:
● 很少被修改的數據。
● 不是很重要的數據,允許出現偶爾併發的數據。
● 不會被併發訪問的數據。
● 參考數據。
而對於具有以下特徵的數據則不適合放在二級緩存中:
● 經常被修改的數據。
● 財務數據,絕對不允許出現併發。
● 與其他應用共享的數據。
在這裏特別要注意的是對放入緩存中的數據不能有第三方的應用對數據進行更改(其中也包括在自己程序中使用其他方式進行數據的修改,例如,JDBC),因爲那樣Hibernate將不會知道數據已經被修改,也就無法保證緩存中的數據與數據庫中數據的一致性。
14.2.3.3 二級緩存組件
在默認情況下,Hibernate會使用EHCache作爲二級緩存組件。但是,可以通過設置hibernate.cache.provider_class屬性,指定其他的緩存策略,該緩存策略必須實現org.hibernate.cache.CacheProvider接口。
通過實現org.hibernate.cache.CacheProvider接口可以提供對不同二級緩存組件的支持。
Hibernate內置支持的二級緩存組件如表14.1所示。
表14.1 Hibernate所支持的二級緩存組件
組件 |
Provider類 |
類型 |
集羣 |
查詢緩存 |
Hashtable |
org.hibernate.cache.HashtableCacheProvider |
內存 |
不支持 |
支持 |
EHCache |
org.hibernate.cache.EhCacheProvider |
內存,硬盤 |
不支持 |
支持 |
OSCache |
org.hibernate.cache.OSCacheProvider |
內存,硬盤 |
不支持 |
支持 |
SwarmCache |
org.hibernate.cache.SwarmCacheProvider |
集羣 |
支持 |
不支持 |
JBoss TreeCache |
org.hibernate.cache.TreeCacheProvider |
集羣 |
支持 |
支持 |
Hibernate已經不再提供對JCS(Java Caching System)組件的支持了。
14.2.3.4 二級緩存的配置
在使用Hibernate的二級緩存時,對於每個需要使用二級緩存的對象都需要進行相應的配置工作。也就是說,只有配置了使用二級緩存的對象纔會被放置在二級緩存中。二級緩存是通過<cache>元素來進行配置的。
<cache>元素的屬性定義說明如下所示:
<cache
usage="transactional|read-write|nonstrict-read-write|read-only" (1)
region="RegionName" (2)
include="all|non-lazy" (3)
/>
<cache>元素的屬性說明如表14.2所示。
表14.2 <cache>元素的屬性說明
序號 |
屬性 |
含義和作用 |
必須 |
默認值 |
(1) |
usage |
指定緩存策略,可選的策略包括:transactional,read-write,nonstrict-read-write或read-only |
Y |
(2) |
region |
指定二級緩存區域名 |
N |
(3) |
include |
指定是否緩存延遲加載的對象。all,表示緩存所有對象;non-lazy,表示不緩存延遲加載的對象 |
N |
all |
14.2.3.5 二級緩存的策略
當多個併發的事務同時訪問持久化層的緩存中的相同數據時,會引起併發問題,必須採用必要的事務隔離措施。
在進程範圍或集羣範圍的緩存,即第二級緩存,會出現併發問題。因此可以設定以下4種類型的併發訪問策略,每一種策略對應一種事務隔離級別。
● 只讀緩存(read-only)
如果應用程序需要讀取一個持久化類的實例,但是並不打算修改它們,可以使用read-only緩存。這是最簡單,也是實用性最好的策略。
對於從來不會修改的數據,如參考數據,可以使用這種併發訪問策略。
● 讀/寫緩存(read-write)
如果應用程序需要更新數據,可能read-write緩存比較合適。如果需要序列化事務隔離級別,那麼就不能使用這種緩存策略。
對於經常被讀但很少修改的數據,可以採用這種隔離類型,因爲它可以防止髒讀這類的併發問題。
● 不嚴格的讀/寫緩存(nonstrict-read-write)
如果程序偶爾需要更新數據(也就是說,出現兩個事務同時更新同一個條目的現象很不常見),也不需要十分嚴格的事務隔離,可能適用nonstrict-read-write緩存。
對於極少被修改,並且允許偶爾髒讀的數據,可以採用這種併發訪問策略。
● 事務緩存(transactional)
transactional緩存策略提供了對全事務的緩存,僅僅在受管理環境中使用。它提供了Repeatable Read事務隔離級別。對於經常被讀但很少修改的數據,可以採用這種隔離類型,因爲它可以防止髒讀和不可重複讀這類的併發問題。
在上面所介紹的隔離級別中,事務型併發訪問策略的隔離級別最高,然後依次是讀/寫型和不嚴格讀寫型,只讀型的隔離級別最低。事務的隔離級別越高,併發性能就越低。
14.2.3.6 在開發中使用二級緩存
在這一部分中,將細緻地介紹如何在Hibernate中使用二級緩存。在這裏所使用的二級緩存組件爲EHCache。
關於EHCache的詳細信息請參考http://ehcache.sourceforge.net上的內容。
在Hibernate中使用二級緩存需要經歷以下步驟:
● 在Hibernate配置文件(通常爲hibernate.cfg.xml)中,設置二級緩存的提供者類。
● 配置EHCache的基本參數。
● 在需要進行緩存的實體對象的映射文件中配置緩存的策略。
下面就來逐步演示一下如何在開發中使用Hibernate的二級緩存。
修改Hibernate的配置文件
在使用Hibernate的二級緩存時,需要在Hibernate的配置文件中指定緩存提供者對象,以便於Hibernate可以通過其實現對數據的緩存處理。
在這裏需要設置的參數是hibernate.cache.provider_class,在使用EHCache時,需要將其值設置爲org.hibernate.cache.EhCacheProvider。具體要增加的配置如下所示:
<property name="hibernate.cache.provider_class">
org.hibernate.cache.EhCacheProvider</property>
Hibernate配置文件的詳細內容請參考配套光盤中的hibernate/src/cn/hxex/ hibernate/cache/hibernate.cfg.xml文件。
增加EHCache配置參數
在默認情況下,EHCache會到classpath所指定的路徑中尋找ehcache.xml文件來作爲EHCache的配置文件。
在配置文件中,包含了EHCache進行緩存管理時的一些基本的參數。具體的配置方法如清單14.9所示。
清單14.9 EHCache的配置
<?xml version="1.0" encoding="UTF-8"?>
<ehcache xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<diskStore path="java.io.tmpdir" />
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
diskPersistent="false"
diskExpiryThreadIntervalSeconds="120"
memoryStoreEvictionPolicy="LRU" />
</ehcache>
在這裏只是使用EHCache所提供的默認配置文件進行了EHCache的基本配置,對於這些參數的詳細含義請參考其官方網站(http://ehcache.sourceforge.net/)中的資料。在實際的開發中,應該依據自己的具體情況來設置這些參數的值。
開發實體對象
這裏所使用的是一個非常簡單的User對象,它只包含了ID,name和age三個屬性,具體的實現方法請參見配套光盤中的hibernate/src/cn/hxex/cache/User.java文件。
配置映射文件
映射文件的配置與不使用二級緩存的Java對象的區別就在於需要增加前面所介紹的<cache>元素來配置此對象的緩存策略。在這裏所使用的緩存策略爲“read-write”。所以,應該在映射文件中增加如下的配置:
<cache usage="read-write"/>
映射文件的詳細配置請參考配套光盤中的hibernate/src/cn/hxex/cache/User.hbm.xml文件。
測試主程序
在這裏的測試主程序採用了多線程的運行方式,以模擬在不同Session的情況下是否真的可以避免查詢的重複進行。
在這個測試程序中,所做的工作就是依據ID來得到對應的實體對象,並將其輸出。然後通過多次運行此程序,來檢查後面的運行是否進行了數據庫的操作。
測試主程序的主要測試方法的實現如清單14.10所示。
清單14.10 測試主程序的實現
……
public void run() {
SessionFactory sf = CacheMain.getSessionFactory();
Session session = sf.getCurrentSession();
session.beginTransaction();
User user = (User)session.get( User.class, "1" );
System.out.println( user );
session.getTransaction().commit();
}
public static void main(String[] args) {
CacheMain main1 = new CacheMain();
main1.start();
CacheMain main2 = new CacheMain();
main2.start();
}
}
運行測試程序
在運行測試程序之前,需要先手動地向數據庫中增加一條記錄。該記錄的ID值爲1,可以採用下面的SQL語句。
INSERT INTO userinfo(userId, name, age) VALUES( '1', 'galaxy', 32 );
接下來在運行測試主程序時,應該看到類似下面的輸出:
Hibernate: select user0_.userId as userId0_0_, user0_.name as name0_0_, user0_.age as age0_0_ from USERINFO user0_ where user0_.userId=?
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
通過上面的結果可以看到,每個運行的進程都輸出了User對象的信息,但在運行中只進行了一次數據庫讀取操作,這說明第二次User對象的獲得是從緩存中抓取的,而沒有進行數據庫的查詢操作。
14.2.3.7 查詢緩存
查詢緩存是專門針對各種查詢操作進行緩存。查詢緩存會在整個SessionFactory的生命週期中起作用,存儲的方式也是採用key-value的形式來進行存儲的。
查詢緩存中的key是根據查詢的語句、查詢的條件、查詢的參數和查詢的頁數等信息組成的。而數據的存儲則會使用兩種方式,使用SELECT語句只查詢實體對象的某些列或者某些實體對象列的組合時,會直接緩存整個結果集。而對於查詢結果爲某個實體對象集合的情況則只會緩存實體對象的ID值,以達到緩存空間可以共用,節省空間的目的。
在使用查詢緩存時,除了需要設置hibernate.cache.provider_class參數來啓動二級緩存外,還需要通過hibernate.cache.use_query_cache參數來啓動對查詢緩存的支持。
另外需要注意的是,查詢緩存是在執行查詢語句的時候指定緩存的方式以及是否需要對查詢的結果進行緩存。
下面就來了解一下查詢緩存的使用方法及作用。
修改Hibernate配置文件
首先需要修改Hibernate的配置文件,增加hibernate.cache.use_query_cache參數的配置。配置方法如下:
<property name="hibernate.cache.use_query_cache">true</property>
Hibernate配置文件的詳細內容請參考配套光盤中的hibernate/src/cn/hxex/ hibernate/cache/hibernate.cfg.xml文件。
編寫主測試程序
由於這是在前面二級緩存例子的基礎上來開發的,所以,對於EHCache的配置以及視圖對象的開發和映射文件的配置工作就都不需要再重新進行了。下面就來看一下主測試程序的實現方法,如清單14.11所示。
清單14.11 主程序的實現
……
public void run() {
SessionFactory sf = QueryCacheMain.getSessionFactory();
Session session = sf.getCurrentSession();
session.beginTransaction();
Query query = session.createQuery( "from User" );
Iterator it = query.setCacheable( true ).list().iterator();
while( it.hasNext() ) {
System.out.println( it.next() );
}
User user = (User)session.get( User.class, "1" );
System.out.println( user );
session.getTransaction().commit();
}
public static void main(String[] args) {
QueryCacheMain main1 = new QueryCacheMain();
main1.start();
try {
Thread.sleep( 2000 );
} catch (InterruptedException e) {
e.printStackTrace();
}
QueryCacheMain main2 = new QueryCacheMain();
main2.start();
}
}
主程序在實現的時候採用了多線程的方式來運行。首先將“from User”查詢結果進行緩存,然後再通過ID取得對象來檢查是否對對象進行了緩存。另外,多個線程的執行可以看出對於進行了緩存的查詢是不會執行第二次的。
運行測試主程序
接着就來運行測試主程序,其輸出結果應該如下所示:
Hibernate: select user0_.userId as userId0_, user0_.name as name0_, user0_.age as age0_ from USERINFO user0_
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
ID: 1
Namge: galaxy
Age: 32
通過上面的執行結果可以看到,在兩個線程執行中,只執行了一個SQL查詢語句。這是因爲根據ID所要獲取的對象在前面的查詢中已經得到了,並進行了緩存,所以沒有再次執行查詢語句。
14.2.4 Hibernate查詢方法與緩存的關係
在前面介紹了Hibernate的緩存技術以及基本的用法,在這裏就具體的Hibernate所提供的查詢方法與Hibernate緩存之間的關係做一個簡單的總結。
在開發中,通常是通過兩種方式來執行對數據庫的查詢操作的。一種方式是通過ID來獲得單獨的Java對象,另一種方式是通過HQL語句來執行對數據庫的查詢操作。下面就分別結合這兩種查詢方式來說明一下緩存的作用。
通過ID來獲得Java對象可以直接使用Session對象的load()或者get()方法,這兩種方式的區別就在於對緩存的使用上。
● load()方法
在使用了二級緩存的情況下,使用load()方法會在二級緩存中查找指定的對象是否存在。
在執行load()方法時,Hibernate首先從當前Session的一級緩存中獲取ID對應的值,在獲取不到的情況下,將根據該對象是否配置了二級緩存來做相應的處理。
如配置了二級緩存,則從二級緩存中獲取ID對應的值,如仍然獲取不到則還需要根據是否配置了延遲加載來決定如何執行,如未配置延遲加載則從數據庫中直接獲取。在從數據庫獲取到數據的情況下,Hibernate會相應地填充一級緩存和二級緩存,如配置了延遲加載則直接返回一個代理類,只有在觸發代理類的調用時才進行數據庫的查詢操作。
在Session一直打開的情況下,並在該對象具有單向關聯維護的時候,需要使用類似Session.clear(),Session.evict()的方法來強制刷新一級緩存。
● get()方法
get()方法與load()方法的區別就在於不會查找二級緩存。在當前Session的一級緩存中獲取不到指定的對象時,會直接執行查詢語句從數據庫中獲得所需要的數據。
在Hibernate中,可以通過HQL來執行對數據庫的查詢操作。具體的查詢是由Query對象的list()和iterator()方法來執行的。這兩個方法在執行查詢時的處理方法存在着一定的差別,在開發中應該依據具體的情況來選擇合適的方法。
● list()方法
在執行Query的list()方法時,Hibernate的做法是首先檢查是否配置了查詢緩存,如配置了則從查詢緩存中尋找是否已經對該查詢進行了緩存,如獲取不到則從數據庫中進行獲取。從數據庫中獲取到後,Hibernate將會相應地填充一級、二級和查詢緩存。如獲取到的爲直接的結果集,則直接返回,如獲取到的爲一些ID的值,則再根據ID獲取相應的值(Session.load()),最後形成結果集返回。可以看到,在這樣的情況下,list()方法也是有可能造成N次查詢的。
查詢緩存在數據發生任何變化的情況下都會被自動清空。
● iterator()方法
Query的iterator()方法處理查詢的方式與list()方法是不同的,它首先會使用查詢語句得到ID值的列表,然後再使用Session的load()方法得到所需要的對象的值。
在獲取數據的時候,應該依據這4種獲取數據方式的特點來選擇合適的方法。在開發中可以通過設置show_sql選項來輸出Hibernate所執行的SQL語句,以此來了解Hibernate是如何操作數據庫的
14.3 Hibernate的性能優化
Hibernate是對JDBC的輕量級封裝,因此在很多情況下Hibernate的性能比直接使用JDBC存取數據庫要低。然而,通過正確的方法和策略,在使用Hibernate的時候還是可以非常接近直接使用JDBC時的效率的,並且,在有些情況下還有可能高於使用JDBC時的執行效率。
在進行Hibernate性能優化時,需要從以下幾個方面進行考慮:
● 數據庫設計調整。
● HQL優化。
● API的正確使用(如根據不同的業務類型選用不同的集合及查詢API)。
● 主配置參數(日誌、查詢緩存、fetch_size、batch_size等)。
● 映射文件優化(ID生成策略、二級緩存、延遲加載、關聯優化)。
● 一級緩存的管理。
● 針對二級緩存,還有許多特有的策略。
● 事務控制策略。
數據的查詢性能往往是影響一個應用系統性能的主要因素。對查詢性能的影響會涉及到系統軟件開發的各個階段,例如,良好的設計、正確的查詢方法、適當的緩存都有利於系統性能的提升。
系統性能的提升設計到系統中的各個方面,是一個相互平衡的過程,需要在應用的各個階段都要考慮。並且在開發、運行的過程中要不斷地調整和優化才能逐步提升系統的性能。