接著上一篇講了 Spring Cache 如何被 Spring Aop 代理載入對應的程式碼,以及何如注入相關介面邏輯。
本篇我們圍繞兩個要點展開:
-
一個資料是如何被Spring Cache 放入快取的。
-
Spring Cache 如何擴充套件儲存源,即支援不同的快取技術。
Spring Cache 的資料儲存之路
Spring Cache 相關的註解有 5 個:
- @Cacheable 在呼叫方法的同時能夠根據方法的請求引數對結果進行快取。
- @CachePut 呼叫發放的同時進行 Cache 儲存,作用於方法上。
- @CacheEvict 刪除,作用於方法上。
- @Caching 用於處理複雜的快取情況,一次性設定多個快取,作用於方法上。
- @CacheConfig 可以在類級別上標註一些公用的快取屬性,所有方法共享。
@Cacheable
@Cacheable 是我們最常使用的註解:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String condition() default "";
String unless() default "";
boolean sync() default false;
}
cacheNames 和 value 這兩個屬性任意使用一個都可以,它們的作用可以理解為 key 的字首。
@Cacheable(value = "user:cache")
public User findById(String id) {
User user = this.getById(id);
if (user != null){
System.out.println("user.name = " + user.getName());
}
return user;
}
key 和 keyGenerator 是互斥的一對。當指定了 key 的時候就會使用你指定的 key + 引數 作為快取 key。否則則使用預設 keyGenerator(SimpleKeyGenerator)或者你自定義的 Generator 來生成 key。
預設的 SimpleKeyGenerator 通過原始碼我們能看到它的生成規則:
public static Object generateKey(Object... params) {
if (params.length == 0) {
return SimpleKey.EMPTY;
}
if (params.length == 1) {
Object param = params[0];
if (param != null && !param.getClass().isArray()) {
return param;
}
}
return new SimpleKey(params);
}
- 如果方法沒有入參則拋異常,即必須要有入參才能構建 key;
- 如果只有一個入參,則使用該入參作為 key=入參值。
- 如果有多個入參則返回包含所有入參的建構函式
new SimpleKey(params)
。
Spring 官方推薦使用顯式指定 key 的方式來生成 key。當然你也可以通過自定義 KeyGenerator 來實現自己制定規則的 key 生成方式,只需要實現 KeyGenerator 介面即可。
注意 key 屬性為 spEL 表示式,如果要寫字串需要將該字串用單引號括起來。比如我們有如下配置:
@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name")
public String getName(String name) {
return "hello:" + name;
}
假設 name = xiaoming
,那麼快取的 key = userInfo::p_xiaoming
。
condition 引數的作用是限定儲存條件:
@Cacheable(cacheNames = "userInfo", key = "'p_'+ #name",condition = "#sex == 1")
public String getName(String name, int sex) {
return "hello:" + name;
}
上例限制條件為 sex == 1
的時候才寫入快取,否則不走快取。
unless 引數跟 condition 引數相反,作用是當不滿足某個條件的時候才寫入快取。
sync 欄位上一篇說過,多執行緒情況下併發更新的情況是否只需要一個執行緒更新即可。
還有個屬性 cacheManager 比較大頭放在後面單獨說,從命名上能看出它是 cache 的管理者,即指定當前 Cache 使用何種 Cache 配置,比如是 Redis 還是 local Cache 等等。這也是我們這一篇要討論的重點。
@CacheConfig
CacheConfig 註解包含以下配置:
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface CacheConfig {
String[] cacheNames() default {};
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
}
如果你在一個類中使用多個 Cache 註解,並且這些 Cache 註解有公共的基礎操作,比如:使用相同的 Cache key 生成規則,使用相同的 Cache Name 字首等等,那麼你就可以定義一個 CacheConfig 來統一單獨管理這些 Cache 操作。
@CacheConfig(cacheNames = "user")
public class UserService {
@Cacheable(key = "#userInfoDTO.uid")
public GirgirUser.UserInfo getUser(UserInfoDTO userInfoDTO) {
return xxx;
}
@Cacheable(key = "'base_' + #userInfoDTO.uid")
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
}
上面示例中的 兩個 Cache Key 都會有一個公共字首 ”user“。需要注意的是:CacheConfig 註解的優先順序高於同類當中別的註解,如果你在 CacheConfig 中配置了 cacheNames,方法中也配置了,那麼 CacheConfig 中的 cacheNames 會覆蓋掉方法上的配置。
@Caching
@Caching 註解適用於複雜快取操作的場景,當你有多個快取操作的需求,比如下例:你需要先刪除就快取,再插入新資料到快取:
@Caching(evict = @CacheEvict(key = "'base' + #userInfoDTO.uid"),
put = @CachePut(key = "'base' + #userInfoDTO.uid"))
public GirgirUser.UserInfo getBaseUser(UserInfoDTO userInfoDTO) {
return xxx;
}
那麼你可以使用 @Caching 註解來操作多個快取。
註解的使用就說到這裡,其餘幾個註解的配置基本同 @Cacheable 差不多,剩下的大家可以自己學習。接下來我們要說的重點來了:待快取的資料到底是如何被儲存起來的。Spring Cache 如何知道當前要使用的資料來源。
Spring EL 對 Cache 的支援
Name | Location | Description | Example |
---|---|---|---|
methodName | Root object | 被呼叫的方法的名稱 | #root.methodName |
method | Root object | 被呼叫的方法 | #root.method.name |
target | Root object | 當前呼叫方法的物件 | #root.target |
targetClass | Root object | 當前呼叫方法的類 | #root.targetClass |
args | Root object | 當前方法的引數 | #root.args[0] |
caches | Root object | 當前方法的快取集合 | #root.caches[0].name |
Argument name | Evaluation context | 當前方法的引數名稱 | #iban or #a0 (you can also use #p0 or #p<#arg> notation as an alias). |
result | Evaluation context | 方法返回的結果(要快取的值)。只有在 unless 、@CachePut(用於計算鍵)或@CacheEvict(beforeInvocation=false) 中才可用.對於支援的包裝器(例如Optional),#result 引用的是實際物件,而不是包裝器 |
#result |
Spring Cache 資料來源配置
Spring 在 application.yml 中提供配置檔案支援,通過配置 spring.cache.type
標籤來指定當前要使用的儲存方案,目前支援的有:
public enum CacheType {
GENERIC,
JCACHE,
EHCACHE,
HAZELCAST,
INFINISPAN,
COUCHBASE,
REDIS,
CAFFEINE,
SIMPLE,
NONE;
private CacheType() {
}
}
使用的時候需要引入相關儲存對應的 jar 包以及相關的配置。
Java Caching 定義了 5 個核心介面,分別是 CachingProvider, CacheManager, Cache, Entry和 Expiry
- CachingProvider 用於配置和管理 CacheManager,目前它只有一個唯一的實現類 EhcacheCachingProvider,ehcache 也是 Spring 預設提供的實現之一。其餘的第三方快取元件都沒有用到。
- CacheManager 定義了建立、配置、獲取、管理和控制多個唯一命名的 Cache,這些 Cache 存在於 CacheManager 的上下文中。一個 CacheManager 僅被一個 CachingProvider 所擁有。
- Cache 是一個類似 Map 的資料結構並臨時儲存以 Key 為索引的值。一個 Cache 僅被一個 CacheManager 所擁有。
- Entry是一個儲存在 Cache 中的 key-value 對。
- Expiry 每一個儲存在 Cache 中的條目有一個定義的有效期。一旦超過這個時間,條目為過期的狀態。一旦過期,條目將不可訪問、更新和刪除。快取有效期可以通過 ExpiryPolicy 設定。
Spring 定義了org.springframework.cache.CacheManager
和org.springframework.cache.Cache
介面來統一不同的快取技術。其中,CacheManager 是 Spring 提供的各種快取技術抽象介面,Cache 介面包含了快取的各種操作。
針對不同的快取方案需要提供不同的 CacheManager,Spring提供的實現類包括:
- SimpleCacheManager:使用檢點的 Collection 來儲存快取,主要用來測試
- ConcurrentMapCacheManager:使用 ConcurrentMap 來儲存快取
- NoOpCacheManager:僅測試用途,不會實際儲存快取
- EhCacheManager:使用 EhCache 作為快取技術
- GuavaCacheManager:使用 Google Guava 的 GuavaCache 作為快取技術
- HazelcastCacheManager:使用 Hazelcast 作為快取技術
- JCacheManager:支援 JCache(JSR—107)標準的實現作為快取技術
- RedisCacheManager:使用 Redis 作為快取技術
CacheManager 的載入來自於 spring.factories
檔案中的配置:org.springframework.boot.autoconfigure.cache.CacheAutoConfiguration
,即在 Spring啟動的時候載入:
@Configuration
@ConditionalOnClass(CacheManager.class)
@ConditionalOnBean(CacheAspectSupport.class)
@ConditionalOnMissingBean(value = CacheManager.class, name = "cacheResolver")
@EnableConfigurationProperties(CacheProperties.class)
@AutoConfigureBefore(HibernateJpaAutoConfiguration.class)
@AutoConfigureAfter({ CouchbaseAutoConfiguration.class, HazelcastAutoConfiguration.class,
RedisAutoConfiguration.class })
@Import(CacheConfigurationImportSelector.class)
public class CacheAutoConfiguration {
......
}
那不同的儲存實現是如何載入各自的 CacheManger 的呢?我們就拿 Redis 來說,在配置類:
@Configuration
@AutoConfigureAfter({RedisAutoConfiguration.class})
@ConditionalOnBean({RedisConnectionFactory.class})
@ConditionalOnMissingBean({CacheManager.class})
@Conditional({CacheCondition.class})
class RedisCacheConfiguration {
......
@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(this.determineConfiguration(resourceLoader.getClassLoader()));
List<String> cacheNames = this.cacheProperties.getCacheNames();
if (!cacheNames.isEmpty()) {
builder.initialCacheNames(new LinkedHashSet(cacheNames));
}
return (RedisCacheManager)this.customizerInvoker.customize(builder.build());
}
......
}
Redis 的配置類啟動的時候先檢查 CacheManager 是否有載入成功,有的話則去執行各種配置相關操作。上面程式碼截出來了初始化 RedisCacheManager 的步驟。RedisCacheManager 實現了 CacheManager 介面。
當使用 RedisCacheManager 進行儲存的時候,通過被包裝的 Cache 物件來使用相關的儲存操作,我們看一下 RedisCache 對應的操作:
public class RedisCache extends AbstractValueAdaptingCache {
......
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
ValueWrapper result = this.get(key);
if (result != null) {
return result.get();
} else {
T value = valueFromLoader(key, valueLoader);
this.put(key, value);
return value;
}
}
public void put(Object key, @Nullable Object value) {
Object cacheValue = this.preProcessCacheValue(value);
if (!this.isAllowNullValues() && cacheValue == null) {
throw new IllegalArgumentException(String.format("Cache '%s' does not allow 'null' values. Avoid storing null via '@Cacheable(unless=\"#result == null\")' or configure RedisCache to allow 'null' via RedisCacheConfiguration.", this.name));
} else {
this.cacheWriter.put(this.name, this.createAndConvertCacheKey(key), this.serializeCacheValue(cacheValue), this.cacheConfig.getTtl());
}
}
......
}
可以看到 Redis 的儲存使用的是普通的 KV 結構,value 的序列化方式是 yml 檔案中的配置。另外很重要的一點是 ttl 的配置,這裡能看到也是獲取配置檔案的屬性。所以當你想給每個 key 單獨設定過期時間的話就不能使用預設的 Redis 配置。而是需要自己實現 CacheManager。