二級緩存是 SessionFactory 級別的進程/叢集範圍緩存,能大幅減少資料庫查詢。本文以 Ehcache 為例,系統講解整合步驟、四種並發策略、類緩存/集合緩存/查詢緩存三大維度配置、時間戳緩存區域原理、批量處理建議與完整驗證代碼。
| 策略 | 對應隔離級別 | 適用場景 | Ehcache 支援 |
|---|---|---|---|
| Read-Only (只讀) | Serializable | 參考數據、從不修改 | ✅ 最快、無鎖 |
| Nonstrict-Read-Write (非嚴格讀寫) | Read Uncommitted | 極少修改、允許髒讀 | ✅ |
| Read-Write (讀寫) | Read Committed | 常規業務、需一致性 | ✅ 推薦預設 |
| Transactional (事務型) | Repeatable Read | 高一致性、JTA 環境 | ❌ Ehcache 不支援 (需 JBoss Cache/Infinispan) |
<dependency>
<groupId>org.hibernate</groupId>
<artifactId>hibernate-ehcache</artifactId>
<version>5.6.15.Final</version> <!-- 對應 Hibernate 版本 -->
</dependency>
<!-- net.sf.ehcache:ehcache 會傳遞引入 -->
<property name="cache.use_second_level_cache">true</property> <property name="hibernate.cache.region.factory_class">org.hibernate.cache.ehcache.EhCacheRegionFactory</property> <!-- 可選:查詢緩存 --> <property name="hibernate.cache.use_query_cache">true</property>
方式 A:hibernate.cfg.xml 內 <class-cache>/<collection-cache>
<class-cache class="com.example.Department" usage="read-write"/> <class-cache class="com.example.Employee" usage="read-write"/> <collection-cache collection="com.example.Department.emps" usage="read-write"/>
方式 B:映射文件 .hbm.xml 內 <cache>
<class name="Department" table="department">
<cache usage="read-write"/>
<set name="emps" inverse="true">
<cache usage="read-write"/> <!-- 集合緩存 -->
<key column="dept_id"/>
<one-to-many class="Employee"/>
</set>
</class>
<ehcache xmlns:xsi="https://www.w3.org/2001/XMLSchema-instance"
xsi:noNamespaceSchemaLocation="ehcache.xsd">
<diskStore path="java.io.tmpdir"/>
<defaultCache
maxElementsInMemory="10000"
eternal="false"
timeToIdleSeconds="120"
timeToLiveSeconds="120"
overflowToDisk="true"
memoryStoreEvictionPolicy="LRU"/>
<!-- 針對特定區域 (實體全限定名) 精細控制 -->
<cache name="com.example.Employee"
maxElementsInMemory="5000"
eternal="false"
timeToIdleSeconds="300"
timeToLiveSeconds="600"
overflowToDisk="true"/>
<cache name="com.example.Department.emps"
maxElementsInMemory="2000"
eternal="false"
timeToIdleSeconds="180"
timeToLiveSeconds="300"
overflowToDisk="true"/>
</ehcache>
| 屬性 | 含義 |
|---|---|
| maxElementsInMemory | 記憶體中最多緩存對象數,超過按策略淘汰或溢寫磁碟 |
| eternal | true=永不過期 (忽略 TTL/TTI),false=依 timeToLive/Idle 過期 |
| timeToIdleSeconds (TTI) | 對象閒置最大秒數,超過則過期淘汰 |
| timeToLiveSeconds (TTL) | 對象生存最大秒數 (從建立起算),必須 ≥ TTI |
| overflowToDisk | 記憶體滿時是否溢寫磁碟 (path 由 diskStore 指定) |
| memoryStoreEvictionPolicy | LRU (最近最少使用) / LFU (最不常用) / FIFO |
@Test
public void testEntityCache() {
// 第一次查詢
Employee e1 = session.get(Employee.class, 1L); // 發 SQL: select ...
tx.commit(); session.close();
// 新 Session (模擬二級緩存命中)
session = sf.openSession(); tx = session.beginTransaction();
Employee e2 = session.get(Employee.class, 1L); // **無 SQL**,直接從二級緩存取
assertSame(e1, e2); // 同一物件實例 (Ehcache 返回實際物件)
}
@Test
public void testCollectionCache() {
// 僅開啟 Department 類緩存
Department d1 = session.get(Department.class, 1L); // SQL: select dept
d1.getEmps().size(); // SQL: select emps (集合未緩存)
// 同時開啟 Department 類緩存 + Department.emps 集合緩存 + Employee 實體緩存
Department d2 = session.get(Department.class, 1L); // 無 SQL (類命中)
d2.getEmps().size(); // 無 SQL (集合命中,返回 Employee ID 列表)
// 但遍歷 Employee 仍會逐個查詢 → **必須同時開啟 Employee 實體緩存**
for (Employee e : d2.getEmps()) {
System.out.println(e.getName()); // 若 Employee 有二級緩存 → 無 SQL
}
}
@Test
public void testQueryCache() {
// 必須啟用 hibernate.cache.use_query_cache=true
Query q = session.createQuery("from Employee");
q.setCacheable(true); // 關鍵:標記此查詢可緩存
List<Employee> list1 = q.list(); // 發 SQL,結果存入查詢緩存 (key: 查詢語句+參數, value: ID 列表)
List<Employee> list2 = q.list(); // **無 SQL**,從查詢緩存取 ID 列表 → 再從實體二級緩存取物件
// 修改數據後,時間戳區域更新,查詢緩存自動失效
Employee e = session.get(Employee.class, 1L);
e.setName("Modified");
// 提交事務 → 更新時間戳區域
List<Employee> list3 = q.list(); // 發新 SQL (查詢緩存已失效)
}
CurrentSessionContext + hibernate.current_session_context_class=thread,sessionFactory.getCurrentSession() 自動參與事務、隨事務關閉。jta 模式,應用伺服器管理事務邊界。openSession()、beginTransaction()、commit()、close() 完全手控。@Test
public void testBatchInsert() {
// 方案 1:JDBC 批次 (推薦大數據量,繞過 Hibernate 機制)
session.doWork(connection -> {
try (PreparedStatement ps = connection.prepareStatement("INSERT INTO employee (name, salary) VALUES (?, ?)")) {
for (int i = 0; i < 10000; i++) {
ps.setString(1, "Emp" + i);
ps.setDouble(2, 5000 + i);
ps.addBatch();
if (i % 500 == 0) ps.executeBatch(); // 分批提交
}
ps.executeBatch();
}
});
// 方案 2:Hibernate 批次 (較小量、需實體邏輯)
// hibernate.jdbc.batch_size=50 (配置檔)
// session.setJdbcBatchSize(50);
// for (int i=0; i<1000; i++) { session.save(new Employee(...)); if (i%50==0) { session.flush(); session.clear(); } }
}
hibernate-ehcache 依賴版本與 Hibernate 核心一致。cache.use_second_level_cache=true + EhCacheRegionFactory。<class-cache usage="read-write"/> 或 <cache usage="read-write"/>。<collection-cache collection="Owner.field" usage="read-write"/> + **元素實體同樣開啟類緩存**。use_query_cache=true + query.setCacheable(true)。ehcache.xml 針對熱點區域調整 TTL/TTI/淘汰策略/磁碟溢寫。doWork + PreparedStatement.addBatch(),避免一級緩存 OOM。