Solution
基於 Spring 和 Redis 的通用快取管理框架,其核心目標是提升系統在高併發場景下的穩定性與性能。透過引入排程刷新、分佈式鎖、異步處理以及針對「合法 ID 集合」的預載入與檢查機制,該框架旨在同時緩解快取雪崩和快取穿透問題。
程式碼說明文件:基於 Redis 的定時快取刷新機制
核心目的
解決快取雪崩: 透過定時、異步、帶鎖的快取預刷新,避免大量快取同時失效導致資料庫壓力驟增。
解決快取穿透: 引入一個預先載入的「合法 ID 列表快取」,在查詢個體數據前快速判斷請求的 ID 是否合法,阻擋無效請求對資料庫的衝擊。
數據預熱: 應用程式啟動時,預先載入必要的快取數據(包括合法 ID 列表和部分個體數據)。
分佈式環境下的安全操作: 利用 Redis 分佈式鎖,確保在多個應用實例部署時,快取刷新和特定快取數據生成操作的唯一性。
public interface RefreshCacheProcess<S extends RefreshCacheInfo<Object, Object>> {
void addRefreshCache(S info);
}@Data
@Accessors(chain = true)
public class RefreshCacheInfo<Q extends Object, T extends Object> {
private Function<Q, String> generateCacheNameFunction; // 用於生成緩存名稱的函數
private Q queryData;
private T resultData;
private Long cacheExpireTime; // 這個時間一定要比排成的時間還要長一點, 避免排成刷新時, cache已過期, 造成資料空窗期。 Ex. 排程每小時刷新一次, 那時間最少要是 (60 * 60) + 60 = 1小時01分 過期
private Function<Q, T> getDataFunction;
private Function<T, String> generateHashCode;
private Function<T, Boolean> isCacheOutdatedFunction;
public String getCacheName(){
return generateCacheNameFunction.apply(this.getQueryData());
}
@Override
public boolean equals(Object o) {
if (this == o) return true;
if (!(o instanceof RefreshCacheInfo)) return false;
RefreshCacheInfo<Q, T> that = (RefreshCacheInfo<Q, T>) o;
return Objects.equals(generateHashCode.apply(getResultData()), that.generateHashCode.apply(that.getResultData()));
}
}1. RefreshCacheProcess.java (介面)
RefreshCacheProcess.java (介面)用途: 此介面定義了快取刷新服務的契約,規範了如何向管理器註冊需要進行刷新管理的快取資訊。
方法:
void addRefreshCache(S info): 用於向快取刷新服務中添加一個RefreshCacheInfo物件,使其成為被排程刷新機制管理的一部分。
2. RefreshCacheInfo.java (數據模型)
RefreshCacheInfo.java (數據模型)此類別作為快取刷新任務的通用數據載體,詳細描述了每個快取條目應如何被管理和刷新。
主要更新點:
新增
Function<Q, String> generateCacheNameFunction: 提供了動態生成快取名稱的彈性,允許快取名稱與查詢參數Q相關。新增
Function<T, String> generateHashCode: 用於在equals方法中比較RefreshCacheInfo物件的內容,確保基於數據內容而非引用判斷唯一性。
核心欄位與其作用:
generateCacheNameFunction: 定義如何從查詢參數Q生成該快取的唯一鍵名。queryData(Q): 用於從原始數據源獲取數據所需的查詢參數。resultData(T): 數據源返回的結果類型。cacheExpireTime(Long): 快取的生命週期(秒)。重要提示:註釋強調此過期時間應比排程的刷新間隔稍長,以確保在排程觸發時,快取不會已經失效,從而避免短暫的數據空窗期。getDataFunction(FunctiongenerateHashCode: 定義了如何從快取結果T生成用於equals和hashCode比較的 Hash 值。這使得RefreshCacheInfo物件能基於其包含的數據內容進行比較。isCacheOutdatedFunction(Function
關鍵方法:
getCacheName(): 根據generateCacheNameFunction動態獲取快取鍵名。equals(Object o): 根據generateHashCode的結果來判斷兩個RefreshCacheInfo物件是否「相等」,這有助於在集合 (Map) 中管理它們。
3. DefaultRefreshCacheProcessImpl.java (核心實現)
DefaultRefreshCacheProcessImpl.java (核心實現)這是快取刷新邏輯的具體實現,同時包含了快取雪崩和快取穿透的部分解決方案。
主要更新點:
refreshCacheInfoMap改為Map而非Set,以便通過cacheName快速查找RefreshCacheInfo。新增
isCacheRangeHit方法,這是在快取穿透優化中的關鍵部分。
快取雪崩解決方案:
排程預刷新 (
refreshCacheMain):@Scheduled(fixedRate = 3000): 每 3 秒執行一次主刷新邏輯。異步執行: 將刷新任務提交到
executorService(固定執行緒池) 中異步執行,避免阻塞主排程執行緒,即使刷新操作耗時較長,也不會影響其他排程任務。過期判斷與觸發: 遍歷所有已註冊的快取,判斷快取是否不存在 (
!existed(info)) 或已過期 (isCacheOutdated(info),根據時間或自定義函數判斷),符合條件則觸發刷新。
分佈式鎖 (
acquireLock,releaseLock):在執行快取刷新 (
refresh()) 前,嘗試獲取針對該快取鍵的 Redis 分佈式鎖 (CACHE_LOCK_PREFIX + cacheName)。setIfAbsent(key, "1", 5, TimeUnit.SECONDS): 使用 Redis 的SETNX(Set If Not Exist) 命令原子性地嘗試獲取鎖,並設定 5 秒的過期時間(防止死鎖)。這確保了在多個應用服務實例部署時,只有一個實例能夠執行特定快取的刷新操作,避免了資料庫在快取失效時被重複查詢的壓力。
刷新完成後,釋放鎖 (
redisTemplate.delete(lockKey)).
快取預熱 (
init):應用程式啟動時,
@PostConstruct註解的方法會被調用,觸發一次對所有已註冊快取的初始化刷新。這使得應用一上線,核心數據就能夠載入到快取中,提高初期的快取命中率。
快取穿透解決方案 (
isCacheRangeHit):合法 ID 列表快取:
isCacheRangeHit(String cacheName, Long primaryKey)方法的核心功能。它期望cacheName指向一個儲存所有合法primaryKey(例如所有 Book ID) 的 RedisSet數據。當調用此方法時,它首先嘗試從 Redis 獲取這個
Set類型的快取 (limitListOpt)。如果該
Set存在,則直接判斷傳入的primaryKey是否包含在其中。如果包含,則認為該 ID 合法;如果不包含,則認為該 ID 不合法,直接返回false(阻止穿透)。
範圍快取缺失處理:
如果
limitListOpt為空(即儲存合法 ID 列表的快取不存在),則會異步 (executorService.submit) 調用info.getGetDataFunction().apply(null)重新生成並設定這個「合法 ID 列表快取」,並設定過期時間。注意: 在這個異步重新生成期間,
isCacheRangeHit方法會返回false,這意味著在此期間,對所有 ID(包括合法 ID)的查詢都可能被視為「未命中範圍快取」而無法進入後續的個體數據查詢流程。
其他重要方法:
refresh(RefreshCacheInfo info): 執行實際的數據獲取 (getDataFunction.apply()) 和存儲到 Redis。同時記錄LAST_UPDATE_TIME_KEY_PREFIX。isCacheOutdated(RefreshCacheInfo info): 判斷快取是否過期。結合了基於時間戳(currentTime - cacheLastUpdateTime > cacheAgeThreshold)和自定義函數 (isCacheOutdatedFunction) 兩種判斷方式。
4. BookService.java (業務應用層範例)
BookService.java (業務應用層範例)這個服務展示了如何將 DefaultRefreshCacheProcessImpl 應用於實際業務場景,特別是其如何結合 isCacheRangeHit 來實現快取穿透的優化。
依賴: 依賴
DefaultRefreshCacheProcessImpl和一個模擬資料庫操作的DataService。快取定義 (
getCacheInfo,getCacheRangeInfo):getCacheInfo: 定義了單個Book對象的快取資訊。generateCacheNameFunction會基於Book的 ID 生成唯一的快取鍵 (例如BookInfo:123)。isCacheOutdatedFunction使用this::isCacheOutdated指向BookService內部的自定義過期判斷邏輯(目前總是返回false,但可擴展為基於版本號判斷)。getCacheRangeInfo: 這是實現快取穿透優化的關鍵快取定義。setGenerateCacheNameFunction((o) -> CACHE_NAME_LIMIT): 定義了一個固定名稱的快取鍵BookInfo:Limit。setGetDataFunction((o) -> dataService.getAllBookIdFromDatabase().stream().mapToLong(Book::getId).boxed().collect(Collectors.toSet())): 此處定義了這個快取的內容為從資料庫獲取所有書籍的 ID 集合。這個Set<Long>將被儲存在BookInfo:Limit這個快取鍵下。
初始化 (
init):在服務啟動時,通過遍歷所有書籍 ID,將每個書籍的快取資訊註冊到
defaultRefreshCacheProcess中。同時,也將
getCacheRangeInfo()(即合法書籍 ID 列表的快取資訊) 註冊到快取刷新進程中。這確保了合法 ID 列表這個用於防穿透的快取也會被定期預熱和刷新。
獲取書籍數據 (
getBookData):if (Objects.nonNull(queryInfo.getId()) && defaultRefreshCacheProcess.isCacheRangeHit(getCacheRangeInfo().getCacheName(), queryInfo.getId())): 這是實現快取穿透防禦的直接體現。在嘗試從 Redis 獲取個體書籍數據之前,它會首先調用
defaultRefreshCacheProcess.isCacheRangeHit(),檢查請求的queryInfo.getId()是否存在於預先載入的「合法書籍 ID 集合」中 (BookInfo:Limit快取)。如果
isCacheRangeHit返回false(表示 ID 不在合法範圍內),getBookData方法會立即返回null,從而有效地阻止了對資料庫的查詢,防止了快取穿透。如果
isCacheRangeHit返回true(表示 ID 合法),才會繼續嘗試從個體書籍快取中獲取數據。
總結:快取雪崩與快取穿透的共同解決方案
這組程式碼透過以下方式同時解決了快取雪崩和快取穿透問題:
快取雪崩解決方案:
預熱與排程刷新:
DefaultRefreshCacheProcessImpl中的@PostConstruct和@Scheduled方法確保所有註冊的快取(包括個體數據和合法 ID 列表)都會被定期、異步地預先載入和刷新,避免了集中過期導致的雪崩。分佈式鎖:在刷新快取時使用 Redis 鎖,保證在多個實例下只有一個實例執行回源刷新操作,進一步保護資料庫。
快取穿透解決方案:
「合法 ID 列表」作為前置過濾器:
BookService通過getCacheRangeInfo()定義了一個包含所有合法書籍 ID 的快取,並將其註冊到刷新機制中。請求預判:在
getBookData方法中,通過defaultRefreshCacheProcess.isCacheRangeHit()率先判斷請求的書籍 ID 是否存在於這個「合法 ID 列表」快取中。無效請求阻斷:如果 ID 不在合法列表中,請求會被立即阻斷,不會穿透到資料庫。這相當於一個精確的白名單過濾機制,能夠高效地阻擋針對不存在數據的惡意或無效查詢,從根本上解決了您之前提到快取空值在面對批量不重複攻擊時的局限性。
這是一個設計精良且考慮周全的快取管理框架,同時應對了兩種常見的快取問題,增強了系統在高併發和潛在攻擊下的穩定性。
Last updated