前言
緩存是幾乎所有應用程序性能的關鍵。很多時候需要分布式緩存(比如常用的 Redis、Codis),但在許多情況下,本地緩存也可以很好地工作,並且不需要分布式緩存的開銷和復雜性。
對於 DotNet 開發來說,本地 cache 很方便使用(比如 RuntimeCache 等); 對於 Java 說,也有很多優秀的本地 cache 庫(比如 Ehcache、GuavaCache 等),而 Java 這個帝國中,spring 是一個偉大且壟斷的存在。針對不同的緩存技術,Spring 定義了如下的 cacheManger 實現。
| CacheManger | 描述 |
|---|---|
SimpleCacheManager |
使用簡單的 Collection 來存儲緩存,主要用於測試 |
ConcurrentMapCacheManager |
使用 ConcurrentMap 作為緩存技術(默認),需要顯式的刪除緩存,無過期機制 |
NoOpCacheManager |
僅測試用途,不會實際存儲緩存 |
EhCacheCacheManager |
使用 EhCache 作為緩存技術,以前在 hibernate 的時候經常用 |
GuavaCacheManager |
使用 google guava 的 GuavaCache 作為緩存技術(1.5 版本已不建議使用) |
CaffeineCacheManager |
是使用 Java8 對 Guava 緩存的重寫,spring5(springboot2)開始用 Caffeine 取代 guava |
HazelcastCacheManager |
使用 Hazelcast 作為緩存技術 |
JCacheCacheManager |
使用 JCache 標准的實現作為緩存技術,如 Apache Commons JCS |
RedisCacheManager |
使用 Redis 作為緩存技術 |
因此,在許多應用程序中(包括普通的 Spring 和 Spring Boot),你都可以引入相應的依賴包后,簡單的把 @Cacheable 打在任何方法上使用它,以使其結果被緩存,以便下次調用該方法時,將返回緩存的結果。
Tips:spring cache 使用基於動態生成子類的代理機制來對方法的調用進行切面,如果緩存的方法是內部調用而不是外部引用,會導致代理失敗,切面失效。
雖然 Spring 有一些默認的緩存管理器實現,有時候,一些外部庫總是比簡單的實現更好、更靈活。例如,其中一個高性能的 Java 緩存庫 Caffeine 。
今天,我們主要目標就是:認識 Caffeine 以及如何實現多緩存靈活配置。
那么,簡單認識一下 Caffeine
Caffeine 是使用 Java8 對 Guava 緩存的重寫版本,緩存類似於 ConcurrentMap,但並不完全相同,可提供接近最佳的命中率。它基於 LRU 算法實現,支持多種緩存過期策略。在 Spring Boot 2.0 中將取代 GuavaCache。 特性如下:
- 自動將實體加載到緩存中,可以選擇異步加載;
- 基於頻率和新近度超過最大值時基於大小的淘汰;
- 自上次訪問或上次寫入以來的基於時間的過期;
- 基於第一個請求舊數據時的異步刷新(只放行一個請求去刷新數據);
- 其他
Caffeine 的一些參數,我們后續也會用到
| 參數 | 描述 |
|---|---|
initialCapacity=[integer] |
初始的緩存空間大小(比較常用) |
maximumSize=[long] |
緩存的最大條數 (比較常用) |
maximumWeight=[long] |
緩存的最大權重 |
expireAfterAccess=[duration] |
最后一次寫入或訪問后經過固定時間過期 (比較常用) |
expireAfterWrite=[duration] |
最后一次寫入后經過固定時間過期(比較常用) |
refreshAfterWrite=[duration] |
創建緩存或者最近一次更新緩存后經過固定的時間間隔,刷新緩存 refreshAfterWrite requires a LoadingCache |
weakKeys |
打開 key 的弱引用 |
weakValues |
打開 value 的弱引用 |
softValues |
打開 value 的軟引用 |
recordStats |
開發統計功能 |
注意:
refreshAfterWrite必須實現 LoadingCache,跟 expire 的區別是,指定時間過后 expire 是 remove 該 key,下次訪問是同步去獲取返回新值,而 refresh 則是指定時間后,不會 remove 該 key,下次訪問會觸發刷新,新值沒有回來時返回舊值。expireAfterWrite和expireAfterAccess同時存在時,以expireAfterWrite為准。maximumSize和maximumWeight不可以同時使用。weakValues和softValues不可以同時使用。
簡單了解了 caffeine 是什么,有哪些屬性可用,那么我們回過頭來,你會發現,其實 SpringBoot 內部已經提供了一個默認實現 CaffeineCacheManager(具體可以參見源碼 org.springframework.cache.caffeine.CaffeineCacheManager )。這里不再過多的展開,可以自行閱讀一下源碼了解下~
因此,理想情況下,這就是你所需要的一切了:只需簡單的創建一個 CacheManager 的 bean,就可以為帶 @Cacheable 注釋的方法進行緩存。
到此,我們大概了解了 caffeine 是個什么,以及應該如何用,那么接下來,我們就用示例說話。看不到代碼瞎 BB 也是很讓人討厭的,不是么。
實戰
階段一目標:定義兩個 manager,實現不同的緩存配置。
舉例如下:
@Configuration
public class CaffeineConfig extends CachingConfigurerSupport {
@Override
@Bean(name = "cacheManager")
public CacheManager cacheManager() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 方案一(常用):定制化緩存Cache
cacheManager.setCaffeine(Caffeine.newBuilder()
.expireAfterWrite(10, TimeUnit.MINUTES)
.initialCapacity(100)
.maximumSize(10_000));
return cacheManager;
}
/**
* 在@cacheable使用時,指定cacheManager=specCacheManager
*
* @return CacheManager
*/
@Bean(name = "specCacheManager")
public CacheManager cacheManagerWithSpec() {
CaffeineCacheManager cacheManager = new CaffeineCacheManager();
// 不允許空值
cacheManager.setAllowNullValues(false);
// 傳入一個CaffeineSpec定制緩存,它的好處是可以把配置方便寫在配置文件里
cacheManager.setCaffeineSpec(CaffeineSpec.parse("initialCapacity=20,maximumSize=100,expireAfterWrite=10m"));
// 指定使用該策略的CacheNames
cacheManager.setCacheNames(new ArrayList<String>(Arrays.asList("fetchById", "fetchByName")));
return cacheManager;
}
}
使用起來也很簡單,畢竟 springBoot 這么牛 x 的框架提供了很好的集成和靈活性。
@Slf4j
@Service
public class UserServiceImpl implements UserService {
@Override
@Cacheable(cacheNames = "userSelectOrDefault", cacheManager = "cacheManager", key = "#userId")
public MyUser selectOrDefault(Integer userId) {
System.out.println("我要執行【selectOrDefault】方法的查詢邏輯啦~ userId=" + userId);
System.out.println("當前時間:" + LocalDateTime.now().toString());
return new MyUser()
.setUserId(userId)
.setGender(userId % 2)
.setUserName("userName_" + userId);
}
@Override
@Cacheable(cacheNames = "fetchByName", cacheManager = "specCacheManager", key = "#userName")
public MyUser fetchByName(String userName) {
System.out.println("我要執行【fetchByName】方法的查詢邏輯啦~userName=" + userName);
System.out.println("當前時間:" + LocalDateTime.now().toString());
int hashCode = userName.hashCode();
return new MyUser()
.setUserId(hashCode)
.setGender(hashCode % 2)
.setUserName(userName);
}
}
通過指定 @Cacheable 的 cacheNames、 cacheManager 就可以“靈活”的使用不同的緩存策略了。可能你覺得已經有點小滿足了,畢竟能靈活配置了嘛~~
然鵝~~
冷靜下,真的“靈活”么?
假如一個項目中有很多要緩存(而且也肯定很常見),並且緩存的策略規則也不盡相同時(比如重要的到期時間、初始容量、最大大小等),你是不是就覺得寫很多類似的 cacheManger 有點不爽?
筆者也翻閱了網上一些文章,但大多是告訴你如何使用自定義規范定義自定義緩存。但是,沒有一個實現了我希望的理想狀態。
我期望的是:既可以使用默認的一些策略規范自動創建緩存,又可以靈活的自定義設置你想要的緩存策略。
是不是聽起來有點貪心?其實,我個人覺得這是追求完美的人很正常的一個想法。
畢竟方法總比困那多~
那么,接下來,我們就要把這個想法落地。
階段二:目標:實現一個既可以手動配置,又可以默認的 CacheManger
更多實現細節,請通過移步公眾號~

