跳至主要内容

快取模組

AppFuse Server 提供基於分層架構 + 適配器模式的快取工具集。

核心特色

1. 介面導向設計

底層可從 Ehcache 切換到 Caffeine/Redis,應用程式碼無需修改。

2. 管理功能

  • 停用/啟用: 系統維護時暫時停用快取
  • 層級清除: 精確控制清除範圍
  • 狀態查詢: 即時監控命中率、容量使用

3. 雙層快取架構 (DualLayerCache)

  • 快速層: 短期熱資料,可過期
  • 持久層: 長期全量資料,永不過期
  • 自動降級: 快速層 miss 時自動查詢持久層

標準快取

基本用法

import io.leandev.appfuse.cache.api.Cache;
import io.leandev.appfuse.cache.builder.CacheBuilder;

// 建立快取
Cache<String, User> userCache = CacheBuilder
.newCache(cacheManager, "users", String.class, User.class)
.heap(100) // 堆內 100 條
.offheap(20) // 堆外 20MB
.ttl(30) // 30 分鐘過期
.build();

// 使用快取
userCache.put("user1", user);
User user = userCache.get("user1");
userCache.remove("user1");

Cache-Aside Pattern

public Optional<User> findById(String id) {
// 1. 先查快取
User cached = userCache.get(id);
if (cached != null) {
return Optional.of(cached);
}

// 2. 快取未命中,查資料庫
return userRepository.findById(id)
.map(user -> {
// 3. 寫入快取
userCache.put(id, user);
return user;
});
}

雙層快取

適用於需要容錯的場景,如 Session 管理。

import io.leandev.appfuse.cache.builder.DualCacheBuilder;
import io.leandev.appfuse.cache.core.DualLayerCache;

// 建立雙層快取
DualLayerCache<String, Session> sessionCache = DualCacheBuilder
.newCache(cacheManager, "sessions", String.class, Session.class)
.fastHeap(100) // 快速層:堆內 100 條
.fastOffheap(10) // 快速層:堆外 10MB
.fastTtl(30) // 快速層:30 分鐘過期
.store(200) // 持久層:offheap 200MB
.build();

// 自動降級:快速層 miss 時自動查詢持久層
String userId = sessionCache.get(sessionId);

// 不降級模式
String userId = sessionCache.get(sessionId, false);

雙層快取架構圖

Hard-disable 與 soft bypass

兩種停用解決不同問題,不可混用:

機制何時決定是否建立 Ehcache/offheap/磁碟目錄可否執行期恢復用途
CacheManagerBuilder.disabled()啟動組裝時否,須重啟並改設定cache 資源造成鎖檔、記憶體或啟動問題時,讓應用仍可組裝
disableAll() / disableCache(name)執行期除錯、維運時暫時強制 miss

Hard-disabled manager 是 No-Op,但仍接受 createCache(),所以依賴 Cache<K,V> 的 bean 不需要額外條件分支;建立出的 cache 永遠 get → nullput → no-opisEnabled → false

CacheManager manager = CacheManagerBuilder.newCacheManager()
.disabled()
.build();

Soft bypass 的語意是邏輯旁路、不清空資料:停用期間 get 強制 miss、put 被忽略, 底層資料保留,重新啟用即可再服務。

管理器層總閘

disableAll() 一次停用此管理器下所有 managed 快取;enableAll() 翻回總閘。停用後新建立(含懶建)的 managed 快取也自動受總閘管制;enableAll() 僅翻回總閘,不會覆蓋個別快取以 disableCache() 設定的停用狀態。

cacheManager.disableAll(); // 全部停用:每次 get 都 miss、強制讀資料源
cacheManager.enableAll(); // 恢復服務
boolean on = cacheManager.isEnabled(); // 查詢總閘狀態

個別快取開關

disableCache(name) / enableCache(name) 停用/啟用單一快取,與總閘正交——即使總閘啟用,被指名的快取仍停用。可在快取建立之前先指名,之後懶建的同名快取會以停用狀態誕生。有效服務狀態為兩層 AND:總閘 isEnabled() AND 個別開關。

cacheManager.disableCache("users"); // 只停用 users 快取
boolean served = cacheManager.isCacheEnabled("users"); // 總閘 AND 個別開關

停用 ≠ 清空:停用是旁路、不動底層資料;如需新鮮資料另以 clear() 處理。

Property 接線由消費端承擔

框架提供 No-Op builder 與 soft toggle 原語,不出貨 @AutoConfiguration / @ConfigurationProperties。參考實作的 CacheConfig 採以下政策:

if (!properties.isEnabled()) {
// app.cache.enabled=false:在解析 path/budget 前 hard-disable
return CacheManagerBuilder.newCacheManager().disabled().build();
}

var builder = CacheManagerBuilder.newCacheManager().governed();
if (properties.getOffheapBudgetMb() != null) {
builder.offheapBudgetMB(properties.getOffheapBudgetMb());
}
CacheManager manager = builder.withPersistence(resolvePersistencePath(...)).build();
properties.getDisabledCaches().forEach(manager::disableCache); // soft bypass

app.cache.enabled=false 不只供除錯,也是 cache 造成系統無法正常運作時的安全出口。 參考實作同時提供 health contribution:hard-disable 或任何用途 cache soft-disabled 時回報 DEGRADED,並在啟動 log 明列安全降級。

參考實作的降級契約

No-Op cache 本身只提供一致的 miss/no-op 原語;每個用途必須明示 availability policy:

用途 cache停用時行為
一般 cache-asidemiss 後回資料源
loginAttemptsfail-open;不執行登入鎖定並警告
tokenBlacklistfail-open;不執行撤銷檢查並警告
clientCredentialsRateLimitfail-open;允許請求並警告
signedLinksfail-closed;拋 CacheUnavailableException,REST 映射 503

一次性 state store 不可把「cache 不可用」偽裝成「憑證無效」;否則呼叫端會收到錯誤的 401,也無法判斷是否可重試。詳見 ADR-007: 快取啟用開關

單機與多節點邊界

Ehcache reference implementation 是單 JVM/單節點 baseline

  • 不可讓多個節點共用同一個 persistence directory;Ehcache 目錄不是分散式協調機制。
  • cache key 必須自行帶入資料範圍。tenant 模式常用 tenantId + ":" + businessKey; tenantless 依實際 application/owner/resource scope 設計,不能假設全域 key。
  • token blacklist、登入鎖定與 M2M 限流若要求跨節點一致,替換各自 SPI 為 Redis、資料庫或 gateway 實作。
  • signed-link 的嚴格單次消費要替換 SignedLinkStore;本地 cache 只保證單 JVM best-effort。
  • OIDC exchange 不依賴本 cache Feature;reference implementation 的 OidcExchangeStore 使用 JPA row lock,亦可替換為具原子 consume 的 Redis adapter。

快取模式比較

模式適用場景範例
Cache-Aside使用者資訊、產品資料業務 service 直接組合 Cache<K,V> 與 repository
Write-Through需要強一致性的場景訂單狀態
Time-to-IdleAPI 限流、防爆破CacheClientCredentialsRateLimiter

詳細 API

請參閱 Cache API 參考Javadoc