### 1?第三方緩存插件
除了Ehcache這種輕量級的緩存方案外,幾乎所有IMDG產品都提供了對Hibernate二級緩存的直接支持,常用的有:
???Hazelcast
???GridGain
???JBoss Infinispan
???Terracotta(額外提供了直接替換Session對象的集成方式)
### 2?緩存工作過程
下面以JVM集群Terracotta為例,首先從最原始的JDBC到Hibernate到開啟Hibernate二級緩存,看一下應用對數據庫請求的情況。

### 2.1?自動提交模式下的JDBC
在自動提交模式下使用JDBC時,JDBC不會對請求有任何緩存,每次SQL操作都會直接發送到數據庫。因此下圖中三個用戶的訪問會導致9次數據庫訪問。

### 2.2 Hibernate一級緩存
Hibernate有兩級緩存(參考各種Hibernate進行復習),默認情況下第二級緩存是不開啟的,后面會看到導致的問題。所以默認情況下,Hibernate會開啟事務,并緩存更新操作,在最后事務提交時一起更新到數據庫。算上commit事務提交的話,一共就是6次數據庫訪問。一級緩存在Session關閉時會自動清除,或者應用通過evict()清除某個對象、clear()清除全部緩存內容、flush()同步緩存與數據庫。此外,與二級緩存相同的一點注意是:**使用HQL或SQL查詢屬性級別的數據時,是不會使用緩存的**。(第三部分緩存工作原理中會詳細解釋)

### 2.3?集成Hibernate二級緩存
Hibernate的二級緩存進一步將多個Session的數據加載請求緩存。然而風險也隨之而來,當開啟二級緩存后Hibernate不再每次都請求數據庫,于是緩存中的數據可能是過期的。有時我們可以使用TTL設置來強制Hibernate刷新,但有的場景下這種方案也是不可行的。此時,就可以使用Terracotta將這些二級緩存形成集群,Terracotta負責集群結點間數據的同步。這種方式只需改動Hibernate配置文件即可,但由于Hibernate對二級緩存的使用方式的限制,這種集成方式并不能最大限度地發揮Terracotta的威力。

### 2.4?集成Hibernate Session
Terracotta與Hibernate最快的集成方式就是直接與Hibernate的Session對象集成。因為POJO對象在Hibernate的二級緩存中實際是以字節數組的形式保存的,因此盡管已經在緩存中了,但也有序列化的開銷。而使用這種集成Hibernate Session的方式,應用代碼可以零延遲地直接訪問內存中的POJO對象,沒有任何多余開銷。同樣,我們也能夠零延遲地在內存中寫POJO對象。此時,數據庫成了系統的歷史記錄,只在需要時更新。

### 3?緩存內部工作原理
### 3.1?二級緩存
之所以叫二級緩存是因為當你打開session時,Hibernate會自動為你開啟一個一級緩存。官方文檔中對二級緩存是這樣描述的:
A Hibernate Session is a transaction-level cache of persistent data. It is possible to configure a cluster or JVM-level (SessionFactory-level) cache on a class-by-class and collection-by-collection basis. You may even plug in a clustered cache. Be careful. Caches are never aware of changes made to the persistent store by another application (though they may be configured to regularly expire cached data).
像上面所提到的,只要SessionFactory開啟著,二級緩存就存在。二級緩存持有每個標記為緩存的實體的所有**屬性**和**關聯**。下面以Person實體為例,說明一下二級緩存的內部工作原理:

我們先不開啟關聯的緩存,來看一下基本屬性的緩存。二級緩存中,Person對象的保存格式如下:key是id,value是firstName,middleInitial,lastName三個String,外加parent的id。所以很明顯,對象的實例沒有直接保存在緩存中,而是被拆分成一個個屬性保存,Hibernate將這個過程稱為對象**脫水(dehydrate)**,反之屬性組裝成對象的過程稱為補水(hydrate)。Hibernate為什么這樣做?
???用戶代碼對實體的實例上的修改不會破壞緩存,因為緩存里都是實例屬性的拷貝。
???對象間關聯不容易過期,即使過期了也只是更新一個id就行了,因為緩存中不是直接保存parent變量對應的父Person實例,而只是其id。

不開啟緩存和不開啟關聯緩存時,通過id加載操作對應的底層SQL執行情況如下。可以看出,如果不開啟關聯緩存的話,執行的SQL與不開啟緩存時幾乎一樣(只省掉了查詢父Person的第一條SQL):

現在開啟關聯緩存來看一下其效果。開啟關聯緩存后,Person的緩存格式如下。緩存內容多了子Person對象的id集合,與父Person緩存類似,也是只保存id而不是所有子Person實例。至此,**Person對象本身以及嵌套的parent(Person)和children(Set<Person>)都被完全脫水成屬性和關聯id**:

此時,**對Person對象的通過id加載操作完全不需要訪問數據庫了**,所以一定要開啟關聯緩存才能真正發揮出二級緩存的效果。但是,當執行HQL或SQL查詢時,依然不會使用二級緩存。例如,我們通過firstName進行HQL查詢時,Hibernate會先執行一條具有相同where條件的SQL獲取出Person的id,有了id之后才能開始使用二級緩存中的內容,也就是說:**二級緩存只能在通過id查詢時有用,對于其他復雜的條件查詢是失效的**。而這條**查詢出id的相同where條件的SQL就是查詢緩存能發揮用處的地方**,于是就引出了查詢緩存。

### 3.2?查詢緩存
首先要配置開啟查詢緩存
<property name="hibernate.cache.use_query_cache">true</property>
此外還要在使用Query對象前調用setCacheable(true)。
查詢緩存也是key-value的形式,其key是HQL或SQL以及參數,而value是查詢結果的id集合,所以其value與開啟關聯緩存后的二級緩存非常像。所以,**只有在HQL或SQL一致,并且查詢參數也完全一致的情況下,查詢緩存才會有用!**現在來看一下兩種緩存共存時的效果。當二級緩存的關聯緩存和查詢緩存都開啟時,**當我們執行一個特定參數的HQL/SQL,首先會去查詢緩存中找到對應的id,再去二級緩存中找到這些對象,以及這些對象關聯的對象集合**。

### 3.3?總結
不論是二級緩存還是查詢緩存,本質上都是key-value形式保存的,所以**只能對key(id或HQL/SQL+參數)進行匹配查詢。從這點上看,Hibernate緩存不夠靈活,不能像IMDG產品那樣,真正的將對象緩存在內存中,并提供面向對象的查詢**。理解了Hibernate緩存的底層工作原理,才能更好的管理我們的緩存數據,理解Hibernate緩存的行為。
### 參考資料
1?《The Definitive Guide to Terracotta》
2?[Hibernate: Truly Understanding the Second-Level and Query Caches](http://www.javalobby.org/java/forums/t48846.html)