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_methodO_DIRECT 避免 double buffering

Database caching 雖然重要,但僅適用於單一節點。當資料量超過單節點容量,或需要減輕網路開銷時,仍需搭配 application-layer caching。

Application-level Caching#

Entity Aggregates#

  • 在關聯式資料庫中,資料正規化分散在多張表,但商業邏輯操作的是 entity graph
  • 以論壇系統為例,一個 Post aggregate 包含 BoardTagCommentUserVotePageViewsSocialMediaCounters 等實體
  • 快取整個 entity aggregate 可避免多次 join 或 secondary select

Figure 15.7: Entity aggregates

Distributed Key-value Stores#

  • 可使用 RedisMemcached 等分散式快取儲存 entity aggregate
  • Key-value store 查詢複雜度為 O(1),即使在流量尖峰時也能維持低回應時間
  • 關聯式資料庫仍是 system of record,key-value cache 作為替代資料提供者

Cache 同步模式#

  • 不同業務場景有不同的一致性需求
  • PageViewsSocialMediaCounters 可由 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(如 InfinispanEhcacheHazelcast)實作。

  • 不需要修改 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 依序檢查:

  1. Persistence Context(first-level cache)
  2. Second-level cache
  3. Database

Figure 15.9: Entity loading control flow

Entity Cache Entry#

  • Entity 以 CacheEntry 形式儲存,包含 disassembled stateObject[] 陣列)
  • 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 重建物件的開銷。需搭配 @ImmutableREAD_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_cachetrue
  • 查詢必須明確標記為 cacheable(Hibernate API 用 setCacheable(true),JPA 用 org.hibernate.cacheable query 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#

@Cacheusage 屬性指定 CacheConcurrencyStrategy,共四種策略:

READ_ONLY#

  • 適用於不可變資料,無資料不一致風險
  • Entity cache 採用 write-through 策略(insert 時即寫入 cache);但使用 GenerationType.IDENTITY 時退化為 read-through
  • Collection cache 採用 read-through 策略(首次存取時才寫入 cache)
  • 不允許更新,嘗試修改會拋出 UnsupportedOperationException
  • 允許刪除,會同時移除 database 和 cache 中的 entity
  • 建議所有 READ_ONLY entity 和 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
  • 更新流程:
    1. Flush 時以 Lock 物件取代 cache entry,阻止其他 transaction 讀取過時資料
    2. Transaction commit 後,以包含最新 disassembled state 的 Item 取代 Lock
  • 當其他 transaction 讀到 Lock 時,知道該 entry 正被修改,會改從 database 讀取
  • 使用 IDENTITY generator 時不支援 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 失效,反而增加開銷。