hibernate緩存詳解

轉載整理自:

https://blog.csdn.net/woshichenxu/article/details/586361

https://blog.csdn.net/xtayhicbladwin/article/details/4739852

http://www.cnblogs.com/xiaoming0601/p/5882980.html

https://www.jianshu.com/p/78267e834091

1.     關於hibernate緩存的問題:

1.1.1.         基本的緩存原理

Hibernate緩存分爲二級,第一級存放於session中稱爲一級緩存,默認帶有且不能卸載。

 

第二級是由sessionFactory控制的進程級緩存。是全局共享的緩存,凡是會調用二級緩存的查詢方法 都會從中受益。只有經正確的配置後二級緩存纔會發揮作用。同時在進行條件查詢時必須使用相應的方法才能從緩存中獲取數據。比如Query.iterate()方法、loadget方法等。必須注意的是session.find方法永遠是從數據庫中獲取數據,不會從二級緩存中獲取數據,即便其中有其所需要的數據也是如此。

 

查詢時使用緩存的實現過程爲:首先查詢一級緩存中是否具有需要的數據,如果沒有,查詢二級緩存,如果二級緩存中也沒有,此時再執行查詢數據庫的工作。要注意的是:此3種方式的查詢速度是依次降低的。

1.2.   存在的問題

1.2.1.      一級緩存的問題以及使用二級緩存的原因

     因爲Session的生命期往往很短,存在於Session內部的第一級最快緩存的生命期當然也很短,所以第一級緩存的命中率是很低的。其對系統性能的改善也是很有限的。當然,這個Session內部緩存的主要作用是保持Session內部數據狀態同步。並非是hibernate爲了大幅提高系統性能所提供的。

爲了提高使用hibernate的性能,除了常規的一些需要注意的方法比如:

使用延遲加載、迫切外連接、查詢過濾等以外,還需要配置hibernate的二級緩存。其對系統整體性能的改善往往具有立竿見影的效果!

(經過自己以前作項目的經驗,一般會有3~4倍的性能提高)

 

1.2.2.      N+1次查詢的問題

執行條件查詢時,iterate()方法具有著名的 n+1”次查詢的問題,也就是說在第一次查詢時iterate方法會執行滿足條件的查詢結果數再加一次(n+1)的查詢。但是此問題只存在於第一次查詢時,在後面執行相同查詢時性能會得到極大的改善。此方法適合於查詢數據量較大的業務數據。

但是注意:當數據量特別大時(比如流水線數據等)需要針對此持久化對象配置其具體的緩存策略,比如設置其存在於緩存中的最大記錄數、緩存存在的時間等參數,以避免系統將大量的數據同時裝載入內存中引起內存資源的迅速耗盡,反而降低系統的性能!!!

這裏介紹一下n+1次查詢的問題:

  • 舉個例子,我們數據庫中有兩張表,一個是Users,一個是BlogsBlogs中含有一個外鍵user_id,指向了Users的主鍵id
    想要得到所有User以及其分別對應的Blog,一種寫法是
SELECT * FROM Users; 
SELECT * FROM Users WHERE Blogs.user_id = #{user.id}

這樣我們實際對數據庫做了N+1次查詢:選擇所有User一次得到N個User,對於N個User分別選擇其對應的Blog一共N次。所以,一共執行了N+1次查詢,這就是N+1問題。

再舉個例子:

在Session的緩存中存放的是相互關聯的對象圖。默認情況下,當Hibernate從數據庫中加載Customer對象時,會同時加載所有關聯的Order對象。以Customer和Order類爲例,假定ORDERS表的CUSTOMER_ID外鍵允許爲null,圖1列出了CUSTOMERS表和ORDERS表中的記錄。

 

n+1查詢

以下Session的find()方法用於到數據庫中檢索所有的Customer對象:

List customerLists=session.find("from Customer as c");

運行以上find()方法時,Hibernate將先查詢CUSTOMERS表中所有的記錄,然後根據每條記錄的ID,到ORDERS表中查詢有參照關係的記錄,Hibernate將依次執行以下select語句:

select * from CUSTOMERS; 
select * from ORDERS where CUSTOMER_ID=1;
select * from ORDERS where CUSTOMER_ID=2;
select * from ORDERS where CUSTOMER_ID=3;
select * from ORDERS where CUSTOMER_ID=4;

通過以上5條select語句,Hibernate最後加載了4個Customer對象和5個Order對象,在內存中形成了一幅關聯的對象圖,參見圖2。

n+1查詢

Hibernate在檢索與Customer關聯的Order對象時,使用了默認的立即檢索策略。這種檢索策略存在兩大不足:

(1) select語句的數目太多,需要頻繁的訪問數據庫,會影響檢索性能。如果需要查詢n個Customer對象,那麼必須執行n+1次select查詢語句。這就是經典的n+1次select查詢問題。 這種檢索策略沒有利用SQL的連接查詢功能,例如以上5條select語句完全可以通過以下1條select語句來完成:

select * from CUSTOMERS left outer join ORDERS 
on CUSTOMERS.ID=ORDERS.CUSTOMER_ID 

以上select語句使用了SQL的左外連接查詢功能,能夠在一條select語句中查詢出CUSTOMERS表的所有記錄,以及匹配的ORDERS表的記錄。

(2)在應用邏輯只需要訪問Customer對象,而不需要訪問Order對象的場合,加載Order對象完全是多餘的操作,這些多餘的Order對象白白浪費了許多內存空間。
爲了解決以上問題,Hibernate提供了其他兩種檢索策略:延遲檢索策略和迫切左外連接檢索策略。延遲檢索策略能避免多餘加載應用程序不需要訪問的關聯對象,迫切左外連接檢索策略則充分利用了SQL的外連接查詢功能,能夠減少select語句的數目。

 

 

hibernate解決n+1的方法:

 

    Hibernate的檢索策略包括類級別檢索策略和關聯級別檢索策略。 

    類級別檢索策略有立即檢索和延遲檢索,默認的檢索策略是立即檢索。在Hibernate映射文件中,通過在<class>上配置 lazy屬性來確定檢索策略。對於Session的檢索方式,類級別檢索策略僅適用於load方法;也就說,對於get、qurey檢索,持久化對象都會被立即加載而不管lazy是false還是true.一般來說,我們檢索對象就是要訪問它,因此立即檢索是通常的選擇。由於load方法在檢索不到對象時會拋出異常(立即檢索的情況下),因此我個人並不建議使用load檢索;而由於<class>中的lazy屬性還影響到多對一及一對一的檢索策略,因此使用load方法就更沒必要了。

   關聯級別檢索策略有立即檢索、延遲檢索和迫切左外連接檢索。對於關聯級別檢索,又可分爲一對多和多對多、多對一和一對一兩種情況討論。

   1)立即檢索:這是一對多默認的檢索策略,此時lazy=false,outer-join=false.儘管這是默認的檢索策略,但如果關聯的集合是無用的,那麼就不要使用這種檢索方式。

  2)延遲檢索:此時lazy=true,outer-join=false(outer-join=true是無意義的),這是優先考慮的檢索方式。

  3)迫切左外連接檢索:此時 lazy=false,outer-join=true,這種檢索策略只適用於依靠id檢索方式(load、get),而不適用於query的集合檢索(它會採用立即檢索策略)。相比於立即檢索,這種檢索策略減少了一條sql語句,但在Hibernate中,只能有一個<set>配置成 outer-join=true.

 

多對一和一對一檢索策略一般使用<many-to-one>、<one-to-one>配置。<many- to-one>中需要配置的屬性是 outer-join,同時還需要配置one端關聯的<class>的lazy屬性(配置的可不是<many-to-one>中的lazy哦),它們的組合後的檢索策略如下:

 

1) outer-join=auto:這是默認值,如果lazy=true爲延遲檢索,如果lazy=false爲迫切左外連接檢索

 

2) outer-join=true,無關於lazy,都爲迫切左外連接檢索。

 

3) outer-join=false,如果lazy=true爲延遲檢索,否則爲立即檢索。

 

   可以看到,在默認的情況下(outer-join=auto,lazy=false),對關聯的one端對象Hibernate採用的迫切左外連接檢索。依我看,很多情況下,我們並不需要加載one端關聯的對象(很可能我們需要的僅僅是關聯對象的id);另外,如果關聯對象也採用了迫切左外連接檢索,就會出現select語句中有多個外連接表,如果個數多的話會影響檢索性能,這也是爲什麼Hibernate通過 hibernate.max_fetch_depth屬性來控制外連接的深度。對於迫切左外連接檢索,query的集合檢索並不適用,它會採用立即檢索策略。

 

對於檢索策略,需要根據實際情況進行選擇。對於立即檢索和延遲檢索,它們的優點在於select語句簡單(每張表一條語句)、查詢速度快,缺點在於關聯表時需要多條select語句,增加了訪問數據庫的頻率。因此在選擇即檢索和延遲檢索時,可以考慮使用批量檢索策略來減少select語句的數量(配置batch-size屬性)。對於切左外連接檢索,優點在於select較少,但缺點是select語句的複雜度提高,多表之間的關聯會是很耗時的操作。另外,配置文件是死的,但程序是活的,可以根據需要在程序裏顯示的指定檢索策略(可能經常需要在程序中顯示指定迫切左外連接檢索)。爲了清楚檢索策略的配置效果如何,可以配置show_sql屬性查看程序運行時Hibernate執行的sql語句。


 

1.3.   使用hibernate二級緩存的其他注意事項:

1.3.1.      關於數據的有效性

另外,hibernate會自行維護二級緩存中的數據,以保證緩存中的數據和數據庫中的真實數據的一致性!無論何時,當你調用save()update() saveOrUpdate()方法傳遞一個對象時,或使用load() get()list()iterate() scroll()方法獲得一個對象時該對象都將被加入到Session的內部緩存中。 當隨後flush()方法被調用時,對象的狀態會和數據庫取得同步。

 

也就是說刪除、更新、增加數據的時候,同時更新緩存。當然這也包括二級緩存!

 

只要是調用hibernate API執行數據庫相關的工作。hibernate都會爲你自動保證 緩存數據的有效性!!

 

但是,如果你使用了JDBC繞過hibernate直接執行對數據庫的操作。此時,Hibernate不會/也不可能自行感知到數據庫被進行的變化改動,也就不能再保證緩存中數據的有效性!!

 

這也是所有的ORM產品共同具有的問題。幸運的是,Hibernate爲我們暴露了Cache的清除方法,這給我們提供了一個手動保證數據有效性的機會!!

一級緩存,二級緩存都有相應的清除方法。

 

其中二級緩存提供的清除方法爲:

按對象class清空緩存

                按對象class和對象的主鍵id清空緩存

                清空對象的集合中的緩存數據等。

   

1.3.2.      適合使用的情況

並非所有的情況都適合於使用二級緩存,需要根據具體情況來決定。同時可以針對某一個持久化對象配置其具體的緩存策略。

 

適合於使用二級緩存的情況:

1、數據不會被第三方修改;

 

一般情況下,會被hibernate以外修改的數據最好不要配置二級緩存,以免引起不一致的數據。但是如果此數據因爲性能的原因需要被緩存,同時又有可能被第3方比如SQL修改,也可以爲其配置二級緩存。只是此時需要在sql執行修改後手動調用cache的清除方法。以保證數據的一致性

 

  2、數據大小在可接收範圍之內;

 

     如果數據表數據量特別巨大,此時不適合於二級緩存。原因是緩存的數據量過大可能會引起內存資源緊張,反而降低性能。

 

如果數據表數據量特別巨大,但是經常使用的往往只是較新的那部分數據。此時,也可爲其配置二級緩存。但是必須單獨配置其持久化類的緩存策略,比如最大緩存數、緩存過期時間等,將這些參數降低至一個合理的範圍(太高會引起內存資源緊張,太低了緩存的意義不大)。

 

  3、數據更新頻率低;

 

     對於數據更新頻率過高的數據,頻繁同步緩存中數據的代價可能和 查詢緩存中的數據從中獲得的好處相當,壞處益處相抵消。此時緩存的意義也不大。

 

 

  4、非關鍵數據(不是財務數據等)

 

  財務數據等是非常重要的數據,絕對不允許出現或使用無效的數據,所以此時爲了安全起見最好不要使用二級緩存。

  因爲此時 “正確性”的重要性遠遠大於 “高性能”的重要性。

 

2.     目前系統中使用hibernate緩存的建議

1.4.   目前情況

 一般系統中有三種情況會繞開hibernate執行數據庫操作:

1、多個應用系統同時訪問一個數據庫

   此種情況使用hibernate二級緩存會不可避免的造成數據不一致的問題,

   此時要進行

詳細的設計。比如在設計上避免對同一數據表的同時的寫入操作,

   使用數據庫各種級別的鎖定機制等。

 

2、動態表相關

   所謂“動態表”是指在系統運行時根據用戶的操作系統自動建立的數據表。

   比如“自定義表單”等屬於用戶自定義擴展開發性質的功能模塊,因爲此時數據表是運行時建立的,所以不能進行hibernate的映射。因此對它的操作只能是繞開hibernate的直接數據庫JDBC操作。

      如果此時動態表中的數據沒有設計緩存,就不存在數據不一致的問題。

   如果此時自行設計了緩存機制,則調用自己的緩存同步方法即可。

3、使用sql對hibernate持久化對象表進行批量刪除時

     此時執行批量刪除後,緩存中會存在已被刪除的數據。

分析: 

   當執行了第3條(sql批量刪除)後,後續的查詢只可能是以下三種方式:

a. session.find()方法:

根據前面的總結,find方法不會查詢二級緩存的數據,而是直接查詢數據庫。

所以不存在數據有效性的問題。

b. 調用iterate方法執行條件查詢時:

根據iterate查詢方法的執行方式,其每次都會到數據庫中查詢滿足條件的id值,然後再根據此id 到緩存中獲取數據,當緩存中沒有此id的數據纔會執行數據庫查詢;

如果此記錄已被sql直接刪除,則iterate在執行id查詢時不會將此id查詢出來。所以,即便緩存中有此條記錄也不會被客戶獲得,也就不存在不一致的情況。(此情況經過測試驗證)

 

c. getload方法按id執行查詢:

 

客觀上此時會查詢得到已過期的數據。但是又因爲系統中執行sql批量刪除一般是

針對中間關聯數據表,對於

中間關聯表的查詢一般都是採用條件查詢 ,id來查詢某一條關聯關係的機率很低,所以此問題也不存在!

 

   如果某個值對象確實需要按id查詢一條關聯關係,同時又因爲數據量大使用 sql執行批量刪除。當滿足此兩個條件時,爲了保證按id 的查詢得到正確的結果,可以使用手動清楚二級緩存中此對象的數據的方法!!

(此種情況出現的可能性較小)

 

1.5.   建議

1、建議不要使用sql直接執行數據持久化對象的數據的更新,但是可以執行 批量刪除。(系統中需要批量更新的地方也較少)

 

2、如果必須使用sql執行數據的更新,必須清空此對象的緩存數據。調用

SessionFactory.evict(class)

SessionFactory.evict(class,id)

等方法。

 

3、在批量刪除數據量不大的時候可以直接採用hibernate的批量刪除,這樣就不存在繞開hibernate執行sql產生的緩存數據一致性的問題。

 

4、不推薦採用hibernate的批量刪除方法來刪除大批量的記錄數據。

原因是hibernate的批量刪除會執行1條查詢語句外加 滿足條件的n條刪除語句。而不是一次執行一條條件刪除語句!!

當待刪除的數據很多時會有很大的性能瓶頸!!!如果批量刪除數據量較大,比如超過50,可以採用JDBC直接刪除。這樣作的好處是隻執行一條sql刪除語句,性能會有很大的改善。同時,緩存數據同步的問題,可以採用 hibernate清除二級緩存中的相關數據的方法。

調用 SessionFactory.evict(class) SessionFactory.evict(class,id)等方法。

 

所以說,對於一般的應用系統開發而言(不涉及到集羣,分佈式數據同步問題等),因爲只在中間關聯表執行批量刪除時調用了sql執行,同時中間關聯表一般是執行條件查詢不太可能執行按id查詢。所以,此時可以直接執行sql刪除,甚至不需要調用緩存的清除方法。這樣做不會導致以後配置了二級緩存引起數據有效性的問題。

 

退一步說,即使以後真的調用了按id查詢中間表對象的方法,也可以通過調用清除緩存的方法來解決。

 

 

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章