Caching#
本章探討企業應用程式中的快取策略,涵蓋從資料庫層級到應用程式層級的各種快取機制,以及 Hibernate second-level cache 的深入分析。
Caching 的多層架構#
快取無所不在,從 CPU cache、OS page cache、database shared buffers 到 application-level cache,每一層都在效能與一致性之間取捨。
- 越靠近應用程式的快取效能越好,但維持與資料庫的一致性也越困難
- Database cache 保證 ACID 交易一致性,不會讀到過時資料,但無法減輕網路開銷
- Application-level cache 通常是 key-value store,可完全繞過資料庫,但需自行處理一致性
- Hibernate second-level cache 介於兩者之間,與 data access layer 緊密整合,可在不犧牲一致性的前提下優化 read-write transactions
Application-level cache 適合 read-heavy 場景,而 second-level cache 因為有一致性保證,更適合分擔 write traffic。

Figure 15.1: Enterprise caching layers
Cache 同步策略#
當資料被複製到快取層時,必須確保 database(system of record)與 cache 之間的同步。
Cache-aside#
- 應用程式同時管理 database 和 cache
- Cache miss 時從 database 讀取並放入 cache;Cache hit 時直接從 cache 回傳
- 更新時應用程式必須同時更新 database 和 cache
- 建議將快取邏輯移至 AOP interceptor,遵循 Single Responsibility Principle

Figure 15.2: Cache-aside synchronization strategy
Read-through#
- 應用程式只與 cache 互動,cache 負責在 miss 時從 database 讀取
- 簡化了 data access logic,因為只有一個資料來源需要溝通

Figure 15.3: Read-through synchronization strategy
Write-invalidate#
- 實體被修改時,cache 將變更寫入 database 並移除對應的 cache entry
- 下次請求時 cache 會從 database 載入最新版本

Figure 15.4: Write-invalidate synchronization strategy
Write-through#
- 實體被修改時,變更同時寫入 database 和 cache
- 可透過 JTA transactions 同步提交,或使用 soft lock 機制在 database transaction commit 後再更新 cache
- XA transactions 雖簡化開發,但 two-phase commit 帶來顯著效能開銷

Figure 15.5: Write-through synchronization strategy
Write-behind#
- 變更請求被排入佇列,批次寫入 database
- JPA Persistence Context 即採用此策略,entity state transitions 在 transaction 結束或 query 執行前才 flush

Figure 15.6: Write-behind synchronization strategy
Database Caching#
大多數資料庫引擎都有內建快取機制,且不會影響資料一致性。
- Oracle: Buffer pool、Shared pool、Large pool、Result cache;建議使用 automatic memory management
- SQL Server: 將系統記憶體作為 buffer pool,以 8KB page 為單位;SQL Server 2014 支援 SSD buffer pool extensions
- PostgreSQL: 以
shared_buffers設定 shared buffer 大小,使用 LFU(clock sweep)演算法;建議限制為 working set 大小 - MySQL:
innodb_buffer_pool_size控制 InnoDB buffer pool 大小;Linux 上建議設定innodb_flush_method為O_DIRECT避免 double buffering
Database caching 雖然重要,但僅適用於單一節點。當資料量超過單節點容量,或需要減輕網路開銷時,仍需搭配 application-layer caching。
Application-level Caching#
Entity Aggregates#
- 在關聯式資料庫中,資料正規化分散在多張表,但商業邏輯操作的是 entity graph
- 以論壇系統為例,一個
Postaggregate 包含Board、Tag、Comment、UserVote、PageViews、SocialMediaCounters等實體 - 快取整個 entity aggregate 可避免多次 join 或 secondary select

Figure 15.7: Entity aggregates
Distributed Key-value Stores#
- 可使用 Redis 或 Memcached 等分散式快取儲存 entity aggregate
- Key-value store 查詢複雜度為 O(1),即使在流量尖峰時也能維持低回應時間
- 關聯式資料庫仍是 system of record,key-value cache 作為替代資料提供者
Cache 同步模式#
- 不同業務場景有不同的一致性需求
PageViews和SocialMediaCounters可由 batch processor 定期更新(eventual consistency)Comment操作需要 read-your-writes consistency,否則使用者可能看不到自己的變更
同步更新(Synchronous Updates)#
- 使用 cache-aside 模式時,business logic 在同一 transaction 中更新 database 和所有相關 cache entries
- 因多數 key-value store 不支援 XA transactions,可選擇 invalidate(無過時資料風險)或在 database transaction commit 後更新(有短暫過時風險)
非同步更新(Asynchronous Updates)#
- 若容許 eventual consistency,可將快取邏輯與業務邏輯解耦
- 適用於需要將變更傳播到多個子系統(cache、in-memory framework、data warehouse)的場景
Change Data Capture (CDC)#
- 記錄資料庫變更的模式集合
- Timestamp 版本控制: 記錄每行的修改時間戳,適用於 soft delete 場景
- Database triggers: 在 insert/update/delete 時觸發事件,但會拖慢寫入操作
- Transaction log 解析: 非同步解析 database transaction log,不影響寫入效能,是最有效率的方式
- 各資料庫的 CDC 工具:Oracle GoldenGate / Databus、SQL Server Change Data Capture、PostgreSQL logical decoding、MySQL binary log (Databus)
Entity aggregate 的 denormalization 程度越高,資料變更的漣漪效應(ripple effect)越大。應避免在 cache entry 中儲存可能被多個 entity graph 共享的 association。

Figure 15.8: Application-level cache integration
Second-level Caching#
Persistence Context 常被稱為 first-level cache,但其主要目的是提供 application-level repeatable reads,而非降低 fetch 時間,且它不是 thread-safe 的。
Second-level cache 則綁定在 SessionFactory 層級,是 thread-safe 的,由第三方 caching provider(如 Infinispan、Ehcache、Hazelcast)實作。
- 不需要修改 data access layer 程式碼
- 採用 read-through 和 write-through 同步模式
- 不儲存 entity aggregates,而是以接近 database row 的格式(disassembled state)儲存
- Collection cache 只存 entity identifiers;query cache 也只存符合條件的 entity identifiers
Second-level cache 不是 application-level cache 的替代品。其最大價值在於 Master-Slave replication 架構中,可優化 read-write transactions 的回應時間,減少 Master node 的負擔。
啟用 Second-level Cache#
需設定 hibernate.cache.region.factory_class 指定 CacheRegionFactory 實作:
<property name="hibernate.cache.region.factory_class"
value="org.hibernate.cache.ehcache.EhCacheRegionFactory"/>並在 entity 上標註 @Cache 指定 CacheConcurrencyStrategy:
@Entity
@Cache(usage = CacheConcurrencyStrategy.READ_WRITE)
public class Post { ... }Entity Cache Loading Flow#
載入 entity 時,Hibernate 依序檢查:
- Persistence Context(first-level cache)
- Second-level cache
- Database

Figure 15.9: Entity loading control flow
Entity Cache Entry#
- Entity 以
CacheEntry形式儲存,包含 disassembled state(Object[]陣列) - Cache entry key 包含 entity type、identifier、identifier type,以及 multitenancy 時的 tenant identifier
@Version屬性和timestamp獨立於 disassembled state 儲存@OneToOne和@ManyToOne的 association 在 disassembled state 中以 null 或 foreign key 值表示
Entity Reference Cache Store: 對於不可變且無 association mapping 的 entity,可啟用 hibernate.cache.use_reference_entries 直接儲存 entity reference,避免從 disassembled state 重建物件的開銷。需搭配 @Immutable 和 READ_ONLY strategy 使用。

Figure 15.10: Disassembled state vs Entity references
Collection Cache Entry#
- 透過
hibernate.cache.use_second_level_cache啟用,需在 collection 上標註@Cache - 只儲存子 entity 的 identifiers,因此被包含的 entity 也必須被快取
- Cache entry key 以 collection attribute name 附加到 entity class name 組成 cache region name
Entity cache 搭配 collection cache 可在完全不存取 database 的情況下取得整個 entity aggregate。將 entity 和 collection 分開儲存的好處是 invalidation 或 update 只影響單一 cache entry。
Query Cache Entry#
- 預設關閉,需設定
hibernate.cache.use_query_cache為true - 查詢必須明確標記為 cacheable(Hibernate API 用
setCacheable(true),JPA 用org.hibernate.cacheablequery hint) - Cache key 包含 SQL query string、參數類型與值
- Cache value 包含查詢時的 timestamp 和符合條件的 entity identifiers
- 讀取時 Hibernate 會比對 query timestamp 與 tablespace update timestamp,確保快取結果不過時
因為 query cache 只儲存 entity identifiers,被查詢的 entity 也必須啟用 entity cache,否則每個 identifier 都會產生一次 database 查詢。
Cache Concurrency Strategies#
@Cache 的 usage 屬性指定 CacheConcurrencyStrategy,共四種策略:
READ_ONLY#
- 適用於不可變資料,無資料不一致風險
- Entity cache 採用 write-through 策略(insert 時即寫入 cache);但使用
GenerationType.IDENTITY時退化為 read-through - Collection cache 採用 read-through 策略(首次存取時才寫入 cache)
- 不允許更新,嘗試修改會拋出
UnsupportedOperationException - 允許刪除,會同時移除 database 和 cache 中的 entity
- 建議所有
READ_ONLYentity 和 collection 都標註@Immutable
NONSTRICT_READ_WRITE#
- 適用於不常更新且不需嚴格一致性的 entity
- 採用 read-through 策略(insert 時不寫入 cache,首次讀取才寫入)
- 更新和刪除時移除 cache entry(在 flush 和 transaction commit 時各移除一次)
- 不對 cache entry 加鎖,因此在 flush 到 commit 之間有極短暫的不一致視窗
NONSTRICT_READ_WRITE因為移除而非更新 cache entry,只適用於 entity 很少被修改的場景。否則過高的 cache miss rate 會使快取失效。

Figure 15.11: NONSTRICT_READ_WRITE update flow
READ_WRITE#
- 採用 write-through 策略,是 write-intensive 應用的較佳選擇
- 使用 soft locking 機制提供邏輯上的 transaction isolation
- 更新流程:
- Flush 時以
Lock物件取代 cache entry,阻止其他 transaction 讀取過時資料 - Transaction commit 後,以包含最新 disassembled state 的
Item取代Lock
- Flush 時以
- 當其他 transaction 讀到
Lock時,知道該 entry 正被修改,會改從 database 讀取 - 使用
IDENTITYgenerator 時不支援 write-through entity caching,建議使用 sequence generator

Figure 15.12: READ_WRITE update flow
READ_WRITE的運作方式類似 database transaction:直接套用變更,用鎖防止其他 concurrent transaction 讀到未提交的資料。

Figure 15.13: READ_WRITE delete flow
TRANSACTIONAL#
- 需要 JTA transaction manager 支援
- Cache 和 database 的操作在同一個 XA transaction 中同步提交
- 提供最強的一致性保證,但 two-phase commit protocol 帶來額外效能開銷
- 適用於對一致性要求極高的場景

Figure 15.14: TRANSACTIONAL XA_Strict flow

Figure 15.15: TRANSACTIONAL XA flow
Query Cache 策略#
Tablespace Invalidation#
- Query cache 使用 tablespace timestamp 機制追蹤 cache 有效性
- Hibernate 維護一個
UpdateTimestampsCache,記錄每個 table 最後被修改的時間 - 當任何 entity 變更影響到某個 table 時,該 table 的 timestamp 會被更新
- 讀取 query cache 時,若 query 的 timestamp 早於相關 table 的 update timestamp,cache entry 即被視為過時
- 這種粗粒度的 invalidation 策略意味著:即使修改的行不在 query 結果中,也會使整個 query cache 失效
Native SQL Invalidation#
- Native SQL query 繞過 Hibernate 的 entity model,因此 Hibernate 無法自動判斷哪些 table 受影響
- 需要手動指定 native query 影響的 table space,否則 Hibernate 可能無法正確 invalidate query cache
- 可透過
SQLQuery.addSynchronizedEntityClass()或addSynchronizedQuerySpace()指定相關 table
Query cache 最適合資料很少變更的場景。對於經常更新的 table,tablespace invalidation 會頻繁使 query cache 失效,反而增加開銷。