1. 程式人生 > >hibernate一級快取和二級快取的區別與聯絡

hibernate一級快取和二級快取的區別與聯絡

快取是介於應用程式和物理資料來源之間,其作用是為了降低應用程式對物理資料來源訪問的頻次,從而提高了應用的執行效能。快取內的資料是對物理資料來源中的資料的複製,應用程式在執行時從快取讀寫資料,在特定的時刻或事件會同步快取和物理資料來源的資料。

  快取的介質一般是記憶體,所以讀寫速度很快。但如果快取中存放的資料量非常大時,也會用硬碟作為快取介質。快取的實現不僅僅要考慮儲存的介質,還要考慮到管理快取的併發訪問和快取資料的生命週期。

  Hibernate的快取包括Session的快取和SessionFactory的快取,其中SessionFactory的快取又可以分為兩類:內建快取和外接快取。Session的快取是內建的,不能被解除安裝,也被稱為Hibernate的第一級快取。SessionFactory的內建快取和Session的快取在實現方式上比較相似,前者是SessionFactory物件的一些集合屬性包含的資料,後者是指Session的一些集合屬性包含的資料。SessionFactory的內建快取中存放了對映元資料和預定義SQL語句,對映元資料是對映檔案中資料的拷貝,而預定義SQL語句是在Hibernate初始化階段根據對映元資料推匯出來,SessionFactory的內建快取是隻讀的,應用程式不能修改快取中的對映元資料和預定義SQL語句,因此SessionFactory不需要進行內建快取與對映檔案的同步。SessionFactory的外接快取是一個可配置的外掛。在預設情況下,SessionFactory不會啟用這個外掛。外接快取的資料是資料庫資料的拷貝,外接快取的介質可以是記憶體或者硬碟。SessionFactory的外接快取也被稱為Hibernate的第二級快取。

  Hibernate的這兩級快取都位於持久化層,存放的都是資料庫資料的拷貝,那麼它們之間的區別是什麼呢?為了理解二者的區別,需要深入理解持久化層的快取的兩個特性:快取的範圍和快取的併發訪問策略。

  持久化層的快取的範圍

  快取的範圍決定了快取的生命週期以及可以被誰訪問。快取的範圍分為三類。

  1 事務範圍:快取只能被當前事務訪問。快取的生命週期依賴於事務的生命週期,當事務結束時,快取也就結束生命週期。在此範圍下,快取的介質是記憶體。事務可以是資料庫事務或者應用事務,每個事務都有獨自的快取,快取內的資料通常採用相互關聯的的物件形式。

  2 程序範圍:快取被程序內的所有事務共享。這些事務有可能是併發訪問快取,因此必須對快取採取必要的事務隔離機制。快取的生命週期依賴於程序的生命週期,程序結束時,快取也就結束了生命週期。程序範圍的快取可能會存放大量的資料,所以存放的介質可以是記憶體或硬碟。快取內的資料既可以是相互關聯的物件形式也可以是物件的鬆散資料形式。鬆散的物件資料形式有點類似於物件的序列化資料,但是物件分解為鬆散的演算法比物件序列化的演算法要求更快。

  3 叢集範圍:在叢集環境中,快取被一個機器或者多個機器的程序共享。快取中的資料被複制到叢集環境中的每個程序節點,程序間通過遠端通訊來保證快取中的資料的一致性,快取中的資料通常採用物件的鬆散資料形式。

  對大多數應用來說,應該慎重地考慮是否需要使用叢集範圍的快取,因為訪問的速度不一定會比直接訪問資料庫資料的速度快多少。

  持久化層可以提供多種範圍的快取。如果在事務範圍的快取中沒有查到相應的資料,還可以到程序範圍或叢集範圍的快取內查詢,如果還是沒有查到,那麼只有到資料庫中查詢。事務範圍的快取是持久化層的第一級快取,通常它是必需的;程序範圍或叢集範圍的快取是持久化層的第二級快取,通常是可選的。

  持久化層的快取的併發訪問策略

  當多個併發的事務同時訪問持久化層的快取的相同資料時,會引起併發問題,必須採用必要的事務隔離措施。

  在程序範圍或叢集範圍的快取,即第二級快取,會出現併發問題。因此可以設定以下四種類型的併發訪問策略,每一種策略對應一種事務隔離級別。

  事務型:僅僅在受管理環境中適用。它提供了Repeatable Read事務隔離級別。對於經常被讀但很少修改的資料,可以採用這種隔離型別,因為它可以防止髒讀和不可重複讀這類的併發問題。

  讀寫型:提供了Read Committed事務隔離級別。僅僅在非叢集的環境中適用。對於經常被讀但很少修改的資料,可以採用這種隔離型別,因為它可以防止髒讀這類的併發問題。

  非嚴格讀寫型:不保證快取與資料庫中資料的一致性。如果存在兩個事務同時訪問快取中相同資料的可能,必須為該資料配置一個很短的資料過期時間,從而儘量避免髒讀。對於極少被修改,並且允許偶爾髒讀的資料,可以採用這種併發訪問策略。   只讀型:對於從來不會修改的資料,如參考資料,可以使用這種併發訪問策略。

  事務型併發訪問策略是事務隔離級別最高,只讀型的隔離級別最低。事務隔離級別越高,併發效能就越低。

  什麼樣的資料適合存放到第二級快取中?

  1、很少被修改的資料

  2、不是很重要的資料,允許出現偶爾併發的資料

  3、不會被併發訪問的資料

  4、參考資料

  不適合存放到第二級快取的資料?

  1、經常被修改的資料

  2、財務資料,絕對不允許出現併發

  3、與其他應用共享的資料。

  Hibernate的二級快取

  如前所述,Hibernate提供了兩級快取,第一級是Session的快取。由於Session物件的生命週期通常對應一個數據庫事務或者一個應用事務,因此它的快取是事務範圍的快取。第一級快取是必需的,不允許而且事實上也無法比卸除。在第一級快取中,持久化類的每個例項都具有唯一的OID。

  第二級快取是一個可插拔的的快取外掛,它是由SessionFactory負責管理。由於SessionFactory物件的生命週期和應用程式的整個過程對應,因此第二級快取是程序範圍或者叢集範圍的快取。這個快取中存放的物件的鬆散資料。第二級物件有可能出現併發問題,因此需要採用適當的併發訪問策略,該策略為被快取的資料提供了事務隔離級別。快取介面卡用於把具體的快取實現軟體與Hibernate整合。第二級快取是可選的,可以在每個類或每個集合的粒度上配置第二級快取。

  Hibernate的二級快取策略的一般過程如下:

  1) 條件查詢的時候,總是發出一條select * from table_name where …. (選擇所有欄位)這樣的SQL語句查詢資料庫,一次獲得所有的資料物件。

  2) 把獲得的所有資料物件根據ID放入到第二級快取中。

  3) 當Hibernate根據ID訪問資料物件的時候,首先從Session一級快取中查;查不到,如果配置了二級快取,那麼從二級快取中查;查不到,再查詢資料庫,把結果按照ID放入到快取。

  4) 刪除、更新、增加資料的時候,同時更新快取。

  Hibernate的二級快取策略,是針對於ID查詢的快取策略,對於條件查詢則毫無作用。為此,Hibernate提供了針對條件查詢的Query快取。

  Hibernate的Query快取策略的過程如下:

  1) Hibernate首先根據這些資訊組成一個Query Key,Query Key包括條件查詢的請求一般資訊:SQL, SQL需要的引數,記錄範圍(起始位置rowStart,最大記錄個數maxRows),等。

  2) Hibernate根據這個Query Key到Query快取中查詢對應的結果列表。如果存在,那麼返回這個結果列表;如果不存在,查詢資料庫,獲取結果列表,把整個結果列表根據Query Key放入到Query快取中。

  3) Query Key中的SQL涉及到一些表名,如果這些表的任何資料發生修改、刪除、增加等操作,這些相關的Query Key都要從快取中清空。


很多人對二級快取都不太瞭解,或者是有錯誤的認識,我一直想寫一篇文章介紹一下hibernate的二級快取的,今天終於忍不住了。 
我的經驗主要來自hibernate2.1版本,基本原理和3.0、3.1是一樣的,請原諒我的頑固不化。 

hibernate的session提供了一級快取,每個session,對同一個id進行兩次load,不會發送兩條sql給資料庫,但是session關閉的時候,一級快取就失效了。 

二級快取是SessionFactory級別的全域性快取,它底下可以使用不同的快取類庫,比如ehcache、oscache等,需要設定hibernate.cache.provider_class,我們這裡用ehcache,在2.1中就是 
hibernate.cache.provider_class=net.sf.hibernate.cache.EhCacheProvider 
如果使用查詢快取,加上 
hibernate.cache.use_query_cache=true 


快取可以簡單的看成一個Map,通過key在快取裡面找value。 

Class的快取 
對於一條記錄,也就是一個PO來說,是根據ID來找的,快取的key就是ID,value是POJO。無論list,load還是iterate,只要讀出一個物件,都會填充快取。但是list不會使用快取,而iterate會先取資料庫select id出來,然後一個id一個id的load,如果在快取裡面有,就從快取取,沒有的話就去資料庫load。假設是讀寫快取,需要設定: 
<cache usage="read-write"/> 
如果你使用的二級快取實現是ehcache的話,需要配置ehcache.xml 
<cache name="com.xxx.pojo.Foo" maxElementsInMemory="500" eternal="false" timeToLiveSeconds="7200" timeToIdleSeconds="3600" overflowToDisk="true" /> 
其中eternal表示快取是不是永遠不超時,timeToLiveSeconds是快取中每個元素(這裡也就是一個POJO)的超時時間,如果eternal="false",超過指定的時間,這個元素就被移走了。timeToIdleSeconds是發呆時間,是可選的。當往快取裡面put的元素超過500個時,如果overflowToDisk="true",就會把快取中的部分資料儲存在硬碟上的臨時檔案裡面。 
每個需要快取的class都要這樣配置。如果你沒有配置,hibernate會在啟動的時候警告你,然後使用defaultCache的配置,這樣多個class會共享一個配置。 
當某個ID通過hibernate修改時,hibernate會知道,於是移除快取。 
這樣大家可能會想,同樣的查詢條件,第一次先list,第二次再iterate,就可以使用到快取了。實際上這是很難的,因為你無法判斷什麼時候是第一次,而且每次查詢的條件通常是不一樣的,假如資料庫裡面有100條記錄,id從1到100,第一次list的時候出了前50個id,第二次iterate的時候卻查詢到30至70號id,那麼30-50是從快取裡面取的,51到70是從資料庫取的,共傳送1+20條sql。所以我一直認為iterate沒有什麼用,總是會有1+N的問題。 
(題外話:有說法說大型查詢用list會把整個結果集裝入記憶體,很慢,而iterate只select id比較好,但是大型查詢總是要分頁查的,誰也不會真的把整個結果集裝進來,假如一頁20條的話,iterate共需要執行21條語句,list雖然選擇若干欄位,比iterate第一條select id語句慢一些,但只有一條語句,不裝入整個結果集hibernate還會根據資料庫方言做優化,比如使用mysql的limit,整體看來應該還是list快。) 
如果想要對list或者iterate查詢的結果快取,就要用到查詢快取了 

查詢快取 
首先需要配置hibernate.cache.use_query_cache=true 
如果用ehcache,配置ehcache.xml,注意hibernate3.0以後不是net.sf的包名了 
<cache name="net.sf.hibernate.cache.StandardQueryCache" 
maxElementsInMemory="50" eternal="false" timeToIdleSeconds="3600" 
timeToLiveSeconds="7200" overflowToDisk="true"/> 
<cache name="net.sf.hibernate.cache.UpdateTimestampsCache" 
maxElementsInMemory="5000" eternal="true" overflowToDisk="true"/> 
然後 
query.setCacheable(true);//啟用查詢快取 
query.setCacheRegion("myCacheRegion");//指定要使用的cacheRegion,可選 
第二行指定要使用的cacheRegion是myCacheRegion,即你可以給每個查詢快取做一個單獨的配置,使用setCacheRegion來做這個指定,需要在ehcache.xml裡面配置它: 
<cache name="myCacheRegion" maxElementsInMemory="10" eternal="false" timeToIdleSeconds="3600" timeToLiveSeconds="7200" overflowToDisk="true" /> 
如果省略第二行,不設定cacheRegion的話,那麼會使用上面提到的標準查詢快取的配置,也就是net.sf.hibernate.cache.StandardQueryCache 

對於查詢快取來說,快取的key是根據hql生成的sql,再加上引數,分頁等資訊(可以通過日誌輸出看到,不過它的輸出不是很可讀,最好改一下它的程式碼)。 
比如hql: 
from Cat c where c.name like ? 
生成大致如下的sql: 
select * from cat c where c.name like ? 
引數是"tiger%",那麼查詢快取的key*大約*是這樣的字串(我是憑記憶寫的,並不精確,不過看了也該明白了): 
select * from cat c where c.name like ? , parameter:tiger% 
這樣,保證了同樣的查詢、同樣的引數等條件下具有一樣的key。 
現在說說快取的value,如果是list方式的話,value在這裡並不是整個結果集,而是查詢出來的這一串ID。也就是說,不管是list方法還是iterate方法,第一次查詢的時候,它們的查詢方式很它們平時的方式是一樣的,list執行一條sql,iterate執行1+N條,多出來的行為是它們填充了快取。但是到同樣條件第二次查詢的時候,就都和iterate的行為一樣了,根據快取的key去快取裡面查到了value,value是一串id,然後在到class的快取裡面去一個一個的load出來。這樣做是為了節約記憶體。 
可以看出來,查詢快取需要開啟相關類的class快取。list和iterate方法第一次執行的時候,都是既填充查詢快取又填充class快取的。 
這裡還有一個很容易被忽視的重要問題,即開啟查詢快取以後,即使是list方法也可能遇到1+N的問題!相同條件第一次list的時候,因為查詢快取中找不到,不管class快取是否存在資料,總是傳送一條sql語句到資料庫獲取全部資料,然後填充查詢快取和class快取。但是第二次執行的時候,問題就來了,如果你的class快取的超時時間比較短,現在class快取都超時了,但是查詢快取還在,那麼list方法在獲取id串以後,將會一個一個去資料庫load!因此,class快取的超時時間一定不能短於查詢快取設定的超時時間!如果還設定了發呆時間的話,保證class快取的發呆時間也大於查詢的快取的生存時間。這裡還有其他情況,比如class快取被程式強制evict了,這種情況就請自己注意了。 

另外,如果hql查詢包含select字句,那麼查詢快取裡面的value就是整個結果集了。 

當hibernate更新資料庫的時候,它怎麼知道更新哪些查詢快取呢?