兩個不同策略的緩存工具類
在我們系統的基建包裏,有一個基於redis的get/set等基礎api封裝的 CacheUtil。
CacheUtil 主要有下面2個靜態方法:
public class CacheUtil {
/**
* 獲取緩存。如果沒有,則設置
*/
public static <T> T getCache(String key, long seconds, Supplier<T> supplier) {
return getCache(key, seconds, false, supplier);
}
/**
* 獲取緩存。如果沒有,則設置
*/
public static <T> T getCache(String key, long seconds, boolean cacheNull, Supplier<T> supplier) {
Object obj = redisUtil.get(key); // 這裏的RedisUtil類封裝了 redis 的get/set等基礎操作
if (null == obj) {
T value = supplier.get();
......
redisUtil.set(key, value, seconds);
return value;
} else {
......
return (T) obj;
}
}
}
隨着後續系統迭代過程中,我增加了一個基於本地緩存框架 hutool-cache 的 LFUCache、TimedCache 來實現的 LocalCacheUtil。
與 CacheUtil 一樣的是,LocalCacheUtil 中也主要有下面2個靜態方法:
public class LocalCacheUtil {
/**
* 獲取緩存。如果沒有,則設置
*/
public static <T> T getCache(String key, long seconds, Supplier<T> supplier) {
return getCache(key, seconds, false, supplier);
}
/**
* 獲取緩存。如果沒有,則設置
*/
public static <T> T getCache(String key, long seconds, boolean allowCacheNullOrEmpty, Supplier<T> supplier) {
return getCache(timedCache, key, seconds, allowCacheNullOrEmpty, supplier);
}
private static <T> T getCache(Cache<String, Object> myCache, String key, Long seconds, boolean allowCacheNullOrEmpty, Supplier<T> supplier) {
Object cachedValue = myCache.get(key, false);
if (cachedValue != null) {
return (T) cachedValue;
}
// 允許緩存null值的情況下,如果存在緩存,則直接返回
if (allowCacheNullOrEmpty && myCache.containsKey(key)) {
return (T) myCache.get(key, false);
}
......
T result = supplier.get();
if (seconds == null) {
myCache.put(key, result);
} else {
myCache.put(key, result, TimeUnit.SECONDS.toMillis(seconds));
}
return result;
}
}
如何為兩個緩存工具類抽取公共能力?
翻閲代碼倉庫的commit記錄,我發現 CacheUtil 是2020-09 創建的,LocalCacheUtil 是 2022-12 創建的。
雖然兩年多過去了,但這其中有一個困擾着我的程序設計問題並沒有被遺忘。
這個程序設計問題是, CacheUtil 與 LocalCacheUtil 的職責是相同的,兩者都是用來緩存數據。那麼,如果能夠為兩者抽象出來一個緩存數據的 interface,該多香啊!
可是, getCache 方法是 static 靜態方法。我們知道,靜態方法是無法實現接口的。
我總不能把 getCache 方法改為非靜態方法吧?
我不能。倒不是因為需要改所有的調用代碼,而是在程序設計原則中,工具類的設計理念通常是為了提供一組相關的實用方法,這些方法不依賴於類的實例狀態,而是專注於執行特定的功能。代碼實現中,我們通常將工具類的方法定義為 static 或者通過其他方式(如私有化構造函數)防止類在外部被實例化。
那麼,我沒有辦法了!
我曾經在遙遠的2017年聽過一個架構師講過類似場景的解決方案,可惜的是,忘卻了,腦子裏只留下“講解過”這三個字了。
直到最近,我才想到方案。
知識就是力量,但更重要的是,運用知識的能力
設計模式裏的單例模式是良藥。是的,餓漢式單例模式(eager singleton pattern)。
以下是使用餓漢式單例模式為 CacheUtil 和 LocalCacheUtil 抽象出接口並進行代碼改造的實現。
首先,定義一個緩存接口 CacheService,包含兩個 getCache 方法,這兩個方法是緩存工具類的核心操作,不同的緩存實現類需要實現這些方法。
// 定義緩存服務接口
public interface CacheService {
/**
* 獲取緩存。如果沒有,則設置
*
* @param key
* @param seconds
* @param supplier 緩存數據提供者
* @param <T>
* @return
*/
<T> T getCache(String key, long seconds, Supplier<T> supplier);
/**
* 獲取緩存。如果沒有,則設置
*
* @param key
* @param seconds
* @param cacheNull 是否緩存null
* @param supplier 緩存數據提供者
* @param <T>
* @return
*/
<T> T getCache(String key, long seconds, boolean cacheNull, Supplier<T> supplier);
}
接着,讓原來的 CacheUtil 類改造為實現 CacheService 接口的單例類,提供基於 Redis 的緩存操作;讓 LocalCacheUtil 類改造為實現 CacheService 接口的單例類,提供基於本地緩存(hutool-cache)的緩存操作。
改造後的CacheUtil 類(為了易讀,我重命名成了RedisCacheUtil):
// Redis緩存實現類,使用餓漢式單例模式
public class RedisCacheUtil implements CacheService {
// 餓漢式單例,在類加載時就創建實例
public static final RedisCacheUtil INSTANCE = new RedisCacheUtil();
// 私有化構造函數,防止外部實例化
private RedisCacheUtil() {}
@Override
public <T> T getCache(String key, long seconds, Supplier<T> supplier) {
... ...
}
@Override
public <T> T getCache(String key, long seconds, boolean cacheNull, Supplier<T> supplier) {
... ...
}
}
改造後的LocalCacheUtil 類:
// 本地緩存實現類,使用餓漢式單例模式
public class LocalCacheUtil implements CacheService {
// 餓漢式單例,在類加載時就創建實例
public static final LocalCacheUtil INSTANCE = new LocalCacheUtil();
// 私有化構造函數,防止外部實例化
private LocalCacheUtil() {}
@Override
public <T> T getCache(String key, long seconds, Supplier<T> supplier) {
... ...
}
@Override
public <T> T getCache(String key, long seconds, boolean allowCacheNullOrEmpty, Supplier<T> supplier) {
... ...
}
}
通過這種引入餓漢式單例模式的方式,我們成功地為 CacheUtil 和 LocalCacheUtil 兩個工具類抽象出了接口。規範了緩存工具類的操作能力。(PS:比較熟悉java8的同學,一眼能看出來,CacheService裏,第一個 getCache 可以用 default 來修飾,同樣,兩個實現類不需要override這個 getCache。這會更香!————當然,這不在本文議題內)