快取模組
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 → null、put → no-op、isEnabled → 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-aside | miss 後回資料源 |
loginAttempts | fail-open;不執行登入鎖定並警告 |
tokenBlacklist | fail-open;不執行撤銷檢查並警告 |
clientCredentialsRateLimit | fail-open;允許請求並警告 |
signedLinks | fail-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-Idle | API 限流、防爆破 | CacheClientCredentialsRateLimiter |
詳細 API
請參閱 Cache API 參考 或 Javadoc。