一級緩存是Session級別、生命週期短、不可共享;二級緩存是SessionFactory級別、進程級全局共享,能跨事務、跨線程複用數據,是提升讀多寫少系統吞吐量的關鍵槓桿。但配置不當會導致髒讀、1+N查詢、內存洩漏等嚴重問題。本文從原理到實戰,系統梳理Hibernate二級緩存(以EhCache為例)的完整知識體系。
Hibernate二級緩存三大區域:Class緩存、Collection緩存、Query緩存
| 緩存類型 | Key結構 | Value內容 | 適用場景 |
|---|---|---|---|
| Class/實體緩存 | 實體ID (Serializable) | 實體屬性值數組/脫管對象 | 按ID查詢、關聯導航 |
| Collection/集合緩存 | 擁有者實體ID + 集合角色名 | 集合元素ID列表(不存完整對象) | 一對多/多對多關聯導航 |
| Query/查詢緩存 | SQL語句 + 參數值 + 分頁信息 | 實體ID列表(不存完整對象) | HQL/Criteria列表查詢 |
關鍵機制:Query緩存與Collection緩存只存ID列表,實際對象從Class緩存加載。因此開啟查詢緩存必須同步開啟相關實體的Class緩存,否則會退化為1+N查詢。
<class name="com.example.Foo" table="FOO">
<cache usage="read-write"/> <!-- 緩存策略 -->
<id name="id"><generator class="native"/></id>
<property name="name"/>
</class>
<cache name="com.example.Foo"
maxElementsInMemory="500"
eternal="false"
timeToLiveSeconds="7200"
timeToIdleSeconds="3600"
overflowToDisk="true" />
name:必須與實體全限定類名完全一致,否則Hibernate啟動時會警告並回退到defaultCache(多實體共享配置,難以精細調優)。eternal="false"配合timeToLiveSeconds:設置生存時間,避免長期佔用內存。overflowToDisk="true":內存不足時溢寫磁盤,防OOM。當通過Hibernate API(save/update/delete)修改實體時,框架自動調用cache.remove(id)失效對應條目,保證強一致性。
<property name="hibernate.cache.use_query_cache">true</property>
<property name="hibernate.cache.provider_class">org.hibernate.cache.EhCacheProvider</property>
<!-- 標準查詢緩存區域 -->
<cache name="org.hibernate.cache.StandardQueryCache"
maxElementsInMemory="50"
eternal="false"
timeToLiveSeconds="7200"
overflowToDisk="true" />
<!-- 時間戳緩存區域:必須永久存活 -->
<cache name="org.hibernate.cache.UpdateTimestampsCache"
maxElementsInMemory="5000"
eternal="true"
overflowToDisk="true" />
UpdateTimestampsCache必須設置eternal=true,它記錄每張表最後更新時間,用於判斷查詢緩存是否失效。若被淘汰會導致查詢緩存永不過期,讀取髒數據。
Query query = session.createQuery("from Cat c where c.name like ?");
query.setParameter(0, "tiger%");
query.setCacheable(true); // 關鍵開關
query.setCacheRegion("myCatRegion"); // 可選:自定義區域,需在ehcache.xml配置同名cache
List list = query.list();
Key ≈ SQL語句 + 參數值 + 分頁信息。同樣SQL、同參數、同分頁 → 命中緩存。
場景:Class緩存TTL=10分鐘,Query緩存TTL=60分鐘。第11分鐘查詢命中Query緩存,但Class緩存已失效 → 對每個ID發起load → 1+N查詢!
鐵律:Class緩存TTL ≥ Query緩存TTL。若配置timeToIdleSeconds,亦需Class ≥ Query。
UpdateTimestampsCache,記錄每張表最後更新時間戳。<set name="children" cascade="all">
<cache usage="read-write"/>
<key column="parent_id"/>
<one-to-many class="Child"/>
</set>
<cache name="com.example.Parent.children"
maxElementsInMemory="20"
eternal="false"
timeToLiveSeconds="7200"
overflowToDisk="true" />
名稱格式:擁有者實體全限定名.集合屬性名。
order-by,子實體排序字段更新導致順序變化時,集合緩存不會自動調整順序,可能導致展示順序錯誤。解決:避免緩存帶排序的集合,或手動evict。| 策略 | 適用場景 | 並發處理 | 事務支持 | 性能 |
|---|---|---|---|---|
| read-only | 只讀參考數據(字典、配置) | 無並發寫入 | 無 | 最高 |
| read-write | 常規讀寫業務數據 | 寫時加鎖(軟鎖),讀發現鎖直接查DB | 無(依賴DB事務) | 中等 |
| nonstrict-read-write | 極少並發衝突、容忍短暫髒讀 | 無鎖,異步失效 | 無 | 較好 |
| transactional | JTA環境、需緩存參與事務回滾 | XA事務協調 | 支持(JTA) | 較低 |
EhCache 2.x對read-write鎖超時默認60秒,異常導致鎖不釋放會阻塞後續讀取60秒。生產環境建議監控緩存命中率與鎖等待情況。
executeUpdate)均不會觸發緩存失效(Hibernate 3.1+部分改進,但不可靠)。sessionFactory.evict(Entity.class); // 清空實體緩存
sessionFactory.evict(Entity.class, id); // 清空單個實體
sessionFactory.evictCollection("Entity.children"); // 清空集合
sessionFactory.evictQueries(); // 清空默認查詢緩存
sessionFactory.evictQueries("myRegion"); // 清空指定查詢緩存區域
維護成本極高:每新增查詢緩存區域、修改表結構,都需同步更新手動清理代碼。僅作緊急補丁或遷移過渡期使用。