Hibernate獲取數據的方式有不同的幾種,其與緩存結合使用的效果也不盡相同,而Hibernate中具體怎么使用緩存其實是我們很關心的一個問題,直接涉及到性能方面。
緩存在Hibernate中主要有三個方面:一級緩存、二級緩存和查詢緩存
①一級緩存在Hibernate中對應的為session范圍的緩存,也就是當session關閉時緩存即被清除,一級緩存在Hibernate中是不可配置的
②二級緩存在Hibernate中對應的為SessionFactory范圍的緩存,通常來講SessionFactory的生命周期和應用的生命周期相同,所以可以看成是進程緩存或集群緩存,二級緩存在Hibernate中是可以配置的,可以通過class-cache配置類粒度級別的緩存(class-cache在class中數據發生任何變化的情況下自動更新),同時也可通過collection-cache配置集合粒度級別的緩存(collection-cache僅在collection中增加了元素或者刪除了元素的情況下才自動更新,也就是當collection中元素發生值的變化的情況下它是不會自動更新的),跨session的緩存自然會帶來并發的訪問題,這個時候相應的就要根據應用來設置緩存所采用的事務隔離級別,和數據庫的事務隔離級別概念基本一樣,沒什么多介紹的
③查詢緩存在Hibernate同樣是可配置的,默認是關閉的,可以通過設置cache.use_ query_cache為true來打開查詢緩存。
根據緩存的通常實現策略,我們可以來理解Hibernate的這三種緩存,緩存的實現通過是通過key/value的Map方式來實現,在Hibernate的一級、二級和查詢緩存也同樣如此。一級、二級緩存使用的key均為po的主鍵ID,value即為po實例對象,查詢緩存使用的則為查詢的條件(hql轉化而成的sql語句)、查詢的參數、查詢的頁數,value有兩種情況,如果采用的是select po.property這樣的方式那么value為整個結果集,如采用的是from這樣的方式那么value為獲取的結果集中各po對象的主鍵ID,這樣的作用很明顯,節省內存。
簡單介紹完Hibernate的緩存后,再結合Hibernate的獲取數據方式來說明緩存的具體使用方式,在Hibernate中獲取數據常用的方式主要有四種:Session.load、Session.get、Query.list、Query.iterator。
1、Session.load
在執行session.load時,Hibernate首先從當前session的一級緩存中獲取id對應的值,在獲取不到的情況下,將根據該對象是否配置了二級緩存來做相應的處理,如配置了二級緩存,則從二級緩存中獲取id對應的值,如仍然獲取不到則還需要根據是否配置了延遲加載來決定如何執行,如未配置延遲加載則從數據庫中直接獲取,在從數據庫獲取到數據的情況下,Hibernate會相應的填充一級緩存和二級緩存,如配置了延遲加載則直接返回一個代理類,只有在觸發代理類的調用時才進行數據庫查詢的操作。
備注 load方法過程:一級緩存 ---> 二級緩存 ----> DB訪問 ---> 填充一級緩存[、二級緩存] / 返回一個代理類
在這樣的情況下我們就可以看到,在session一直打開的情況下,要注意在適當的時候對一級緩存進行刷新操作,通常是在該對象具有單向關聯維護的時候,在Hibernate中可以使用象session.clear、session.evict的方式來強制刷新一級緩存。 二級緩存則在數據發生任何變化(新增、更新、刪除)的情況下都會自動的被更新。
備注:由于一級緩存是位于物理內存空間的,畢竟大小有限。過多的對象緩存在這里會造成較大的壓力。適當的時候刷新緩存除了可以保證數據庫和內存的狀態同步外,還可以移除不再用的對象,騰出空間來給后面的對象緩存使用。
2、Session.get
在執行Session.get時,和Session.load不同的就是在當從緩存中獲取不到時,直接從數據庫中獲取id對應的值。
備注:load和get方法的另外一個區別就是:當對象在DB中找不到時,load會拋出異常,而get僅僅返回null
3、Query.list
在執行Query.list時,Hibernate的做法是首先檢查是否配置了查詢緩存,如配置了則從查詢緩存中查找key為查詢語句+查詢參數+分頁條件的值,如獲取不到則從數據庫中進行獲取,從數據庫獲取到后Hibernate將會相應的填充一級、二級和查詢緩存,如獲取到的為直接的結果集,則直接返回,如獲取到的為一堆id的值,則再根據id獲取相應的值(Session.load),最后形成結果集返回,可以看到,在這樣的情況下,list也是有可能造成N次的查詢的。
查詢緩存在數據發生任何變化的情況下都會被自動的清空。
備注:list()方法的SQL成本
①沒有配置查詢緩存或者緩存匹配不到時,發出一次SQL查詢
②緩存的值為一組對象ID,則每個ID執行一次Session.load操作。視乎緩存對象是否還在會再發出0~N次SQL查詢
③緩存的值為一組集合對象,則直接返回。此時不需要發出SQL查詢
由此可見list()方法的SQL成本從0~N次不等,具體的次數和一、二級緩存的對象生命周期時間設置、SQL語句寫法有關。如果采用select po.value寫法,那么SQL成本最低(0),如果采用from po寫法,那么SQL成本從0~N不等。
雖然表明看采用select po.value的寫法好像會更加高效,但是考慮到對象的大小,但一次性讀入時會造成大量的內存空間浪費。但是緩存ID又可能帶來N次額外的查詢消耗,應該怎么辦呢?最好的方法就是設置一、二級緩存的對象生命周期時間比查詢緩存的生命周期長,這樣不至于在Hibernate對每個ID執行Session.load方法時實體緩存都過期而被清空了。
另外一個要注意的是:如果查詢緩存引用的表在查詢后被修改了,那么不管緩存的數據是否有變,該查詢都會被清空而重新獲取。因為Hibernate是靠比較每個表的修改時間和查詢緩存的填入時間來判斷表是否被修改了,它不會去判斷具體的內容
4、Query.iterator
在執行Query.iterator時,和Query.list的不同的在于從數據庫獲取的處理上,Query.iterator向數據庫發起的是select id from這樣的語句,也就是它是先獲取符合查詢條件的id,之后在進行iterator.next調用時才再次發起session.load的調用獲取實際的數據。
可見,在擁有二級緩存并且查詢參數多變的情況下,Query.iterator會比Query.list更為高效。
備注:iterator的SQL成本
①最佳情況下,只有一次SQL查詢(ID查詢),其它的通過一、二級緩存可以得到
②最壞情況下,變成1+N次查詢(ID查詢 + 實體查詢)
這四種獲取數據的方式都各有適用的場合,要根據實際情況做相應的決定,最好的方式無疑就是打開show_sql選項看看執行的情況來做分析,系統結構上只用保證這種調整是容易實現的就好了,在cache這個方面的調整自然是非常的容易,只需要調整配置文件里的設置,而查詢的方式則可對外部進行屏蔽,這樣要根據實際情況調整也非常容易。
備注:關于list()和iterator()的查詢成本比較
※沒有配置查詢緩存,或者緩存中沒有對應的key :
list():執行一次SQL查詢全部取出
iterator():執行一次SQL語句先取出符合條件的所有對象ID,在迭代期間通過Session.load方法查詢(額外的0~N次查詢)
※緩存值是一組對象ID:
list():執行Session.load方法查詢(額外的0~N次查詢)
iterator():執行Session.load方法查詢(額外的0~N次查詢)
※緩存值是一組集合:
list():直接返回,沒有SQL查詢
iterator():直接返回,沒有SQL查詢
可見最大的區別在于從數據庫讀取數據的地方,list雖然只有1次SQL查詢,但它是直接從物理數據文件中讀取。而iterator雖然是1+N次查詢,但后面的N次可能是從緩存中拿數據,所以SQL語句次數相同,兼之iterator只取id,在緩存配置得當的情況下,iterator確實高效。但是如果緩存配置不當,那么iterator的后續next操作將不得不發出一次又一次的SQL查詢,反而大大比不上list。
從分頁查詢的情況來看,用iterator()的效率反而不及list():因為每頁的對象的id都是不同的。iterator()使用Session.load時總是讀取不到實體緩存(一級、二級緩存)的值,只能再發出一次SQL查詢。
-------------------------------------------------------------
生活就像打牌,不是要抓一手好牌,而是要盡力打好一手爛牌。