解決快取雪崩
這組 Java 程式碼提供了一個基於 Spring 和 Redis 的通用快取刷新框架。它允許應用程式註冊需要定期刷新的快取資訊,並通過排程任務和分佈式鎖機制,確保快取數據在過期前被預先載入或刷新,從而有效緩解快取雪崩問題。該實現著重於快取數據的預熱 (Warm-up) 和異步刷新 (Asynchronous Refresh),以避免大量快取同時失效導致資料庫壓力驟增。
程式碼說明文件:基於 Redis 的定時快取刷新機制
核心目的
解決快取雪崩: 通過定時檢查快取狀態並在快取過期前進行刷新,避免大量請求同時穿透到資料庫。
數據預熱: 應用程式啟動時,預先載入必要的快取數據。
分佈式環境下的安全刷新: 利用 Redis 分佈式鎖,確保在多個應用實例部署時,同一個快取鍵只會由一個實例刷新,避免重複操作和資源浪費。
未涵蓋範圍
未解決快取穿透: 此實現沒有針對查詢一個「不存在」的數據時,資料庫仍然會被查詢的問題(即快取穿透)提供解決方案,例如未實作布隆篩檢器或快取空值的功能。
public interface RefreshCacheProcess<S extends RefreshCacheInfo<Object, Object>> {
void addRefreshCache(S info);
}
1. RefreshCacheProcess.java
(介面)
RefreshCacheProcess.java
(介面)這個介面定義了快取刷新服務應具備的基本功能。
套件:
com.caster.normal.refresh
用途: 規範任何快取刷新處理器的行為,使其能夠註冊要刷新的快取資訊。
方法:
void addRefreshCache(S info)
: 用於向快取刷新服務中添加一個RefreshCacheInfo
物件,表明這個快取需要被管理和刷新。其中S
是RefreshCacheInfo
的泛型子類。
2. RefreshCacheInfo.java
(數據模型)
RefreshCacheInfo.java
(數據模型)這個類別是快取刷新任務的數據載體,定義了每個快取需要刷新的詳細資訊和行為。
套件:
com.caster.normal.refresh
Lombok註解:
@Data
(自動生成 Getter/Setter/equals/hashCode/toString)、@Accessors(chain = true)
(使 Setter 方法可鏈式呼叫)。核心欄位:
cacheName
(String): 快取的唯一名稱,用於在 Redis 中作為 Key 的一部分。queryData
(Q): 用於獲取數據的查詢參數,泛型 Q。resultData
(T): 獲取數據的結果,泛型 T (此欄位在刷新邏輯中可能不直接使用,但作為Function
的回傳型別)。cacheExpireTime
(Long): 快取的過期時間(秒)。這是一個關鍵參數,在註釋中特別強調:這個時間必須比排程的刷新間隔長一點,以避免在排程觸發刷新時,快取已經過期,造成短暫的數據空窗期。例如,如果排程每小時刷新一次,則快取過期時間應至少為(60 * 60) + 60
秒(1 小時 1 分鐘)。getDataFunction
(Function<Q, T>): 一個java.util.function.Function
介面,定義了如何從原始數據源(例如資料庫或外部 API)獲取最新數據的邏輯。它接收queryData
作為輸入,返回類型為T
。isCacheOutdatedFunction
(Function<T, Boolean>): 另一個Function
介面,用於定義判斷快取數據是否「邏輯上」過期或需要刷新的自定義邏輯。它接收當前快取中的數據T
作為輸入,返回一個Boolean
值(true
表示過期/需要刷新,false
表示最新)。
equals
和hashCode
方法: 覆寫了這兩個方法,使得RefreshCacheInfo
物件能基於cacheName
進行相等判斷和 Hash 計算,這對於在HashSet
中管理這些資訊至關重要。
3. DefaultRefreshCacheProcessImpl.java
(核心實現)
DefaultRefreshCacheProcessImpl.java
(核心實現)這是 RefreshCacheProcess
介面的主要實作類別,負責快取刷新任務的具體執行。
套件:
com.caster.normal.refresh.impl
Spring註解:
@Service
(標記為 Spring 服務組件)、@RequiredArgsConstructor
(自動生成帶有final
欄位的構造函數)、@Slf4j
(Lombok,用於日誌)。關鍵組件:
RedisTemplate<String, Object> redisTemplate
: 用於與 Redis 互動的 Spring Data Redis 模板,負責快取數據的存取和鎖的實現。ExecutorService executorService
: 一個固定大小的執行緒池 (10 個執行緒),用於異步執行快取刷新任務,避免阻塞主排程執行緒。Set<RefreshCacheInfo> refreshCacheInfoSet
: 儲存所有需要管理的RefreshCacheInfo
物件的集合。CACHE_LOCK_PREFIX
,CACHE_KEY_PREFIX
,LAST_UPDATE_TIME_KEY_PREFIX
: 定義了 Redis 中不同類型鍵的前綴,用於區分快取數據、鎖和上次更新時間。
核心方法及邏輯:
@PostConstruct init()
:在 Spring 容器初始化
DefaultRefreshCacheProcessImpl
後執行。用於「初始化已完成的快取結構」,並執行一次快取預熱 (Warm-up),即對
refreshCacheInfoSet
中所有已註冊的快取立即執行一次refresh()
操作,確保應用啟動時快取中已有數據。
addRefreshCache(RefreshCacheInfo info)
:實現自
RefreshCacheProcess
介面。允許外部程式碼向這個刷新管理器註冊新的快取資訊,這些快取將會被排程器管理和刷新。
@Scheduled(fixedRate = 3000) public void refreshCacheMain()
:這是觸發快取刷新邏輯的排程任務。
fixedRate = 3000
表示每 3 秒執行一次此方法。異步執行: 整個刷新邏輯被提交到
executorService
中異步執行,防止排程任務本身因刷新時間過長而阻塞。刷新判斷邏輯: 遍歷
refreshCacheInfoSet
中的每個快取:if (!existed(info) || isCacheOutdated(info))
: 判斷快取是否「不存在」或「已過期」。!existed(info)
: 快取在 Redis 中完全不存在。isCacheOutdated(info)
: 根據cacheExpireTime
和isCacheOutdatedFunction
判斷快取是否需要刷新。
分佈式鎖:
if (acquireLock(info.getCacheName()))
:核心雪崩解決方案之一。 在多個應用實例部署時,確保只有一個實例能獲取到針對該快取鍵的鎖,從而防止多個實例同時去資料庫刷新同一個快取,避免對資料庫造成重複壓力。
如果成功獲取鎖,則執行
refresh(info)
。刷新完成後,
releaseLock(info.getCacheName())
釋放鎖。如果未能獲取鎖,則跳過本次刷新,避免競爭。
getCacheData(String cacheName)
:從 Redis 中獲取指定快取的數據。
existed(RefreshCacheInfo<Q, T> info)
:判斷指定快取是否在 Redis 中存在數據。
private void refresh(RefreshCacheInfo<Q, T> info)
:執行實際的快取數據獲取和寫入 Redis 的操作。
info.getGetDataFunction().apply(info.getQueryData())
: 呼叫RefreshCacheInfo
中定義的數據獲取函數,從數據源獲取最新數據。redisTemplate.opsForValue().set(...)
: 將獲取的數據存入 Redis,並設定cacheExpireTime
。redisTemplate.opsForValue().set(LAST_UPDATE_TIME_KEY_PREFIX + ..., System.currentTimeMillis(), ...)
: 同步儲存快取的最後更新時間戳,用於isCacheOutdated
方法判斷。
private long getLastUpdateTime(String className)
:從 Redis 獲取指定快取的上次更新時間。如果不存在,則返回 0。
private boolean isCacheOutdated(RefreshCacheInfo<Q, T> info)
:快取過期判斷邏輯。
基於時間戳判斷:
currentTime - cacheLastUpdateTime > cacheAgeThreshold
:判斷距離上次更新的時間是否超過了cacheExpireTime
。這是在快取過期前進行預刷新的核心邏輯。自定義邏輯判斷:
info.getIsCacheOutdatedFunction().apply(getCacheData(info.getCacheName()))
: 呼叫RefreshCacheInfo
中定義的自定義判斷函數。這允許使用者根據業務需求(例如,數據版本號、數據內容變化)來判斷快取是否需要刷新。
private boolean acquireLock(String className)
:分佈式鎖的獲取。
使用
redisTemplate.opsForValue().setIfAbsent(key, value)
方法,這是一個原子操作,只有當 Key 不存在時才能設置成功。成功設置表示獲取到鎖。此處為簡化版,真實生產環境的分佈式鎖通常還需要設置過期時間 (TTL) 和防死鎖機制 (例如看門狗)。
private void releaseLock(String lockName)
:分佈式鎖的釋放。
直接刪除鎖的 Key。同樣,真實環境中釋放鎖還需考慮鎖的持有者是否為當前線程,防止誤刪。
解決快取雪崩的機制
此實現透過以下機制來解決快取雪崩:
排程預刷新:
refreshCacheMain
方法會定期檢查快取,如果快取數據即將達到其cacheExpireTime
,或者根據isCacheOutdatedFunction
判斷需要更新,就會觸發刷新。這使得快取在過期前就能被更新,避免了大量快取同時失效導致的「雪崩」。分佈式鎖:
acquireLock
和releaseLock
確保了在多個服務實例部署時,針對同一個快取鍵,只有一個實例能夠執行刷新操作。這避免了所有實例同時衝擊資料庫的狀況,即使快取真的失效,也只有一個請求會到達資料庫,其他請求會等待。異步執行:
ExecutorService
的使用,將實際的刷新邏輯從排程主線程中分離出來,即使單次刷新耗時較長,也不會阻塞排程器本身,保證了排程任務的穩定性。初始化預熱:
init()
方法確保應用程式啟動時,部分或全部快取數據能夠被載入,提高初期的快取命中率,減少啟動時的資料庫壓力。
Last updated