一 基於SpringCache的快取方案
1.1 SpringCache 簡介
springcache 是spring3.1版本發布的,它對快取的使用進行封裝和抽象化,透過在方法上使用annotation註解就能取得快取結果。正是因為用了annotation,所以它解決了業務程式碼和快取程式碼的耦合問題,也就是在不侵入業務程式碼的基礎上讓現有程式碼即刻支援快取,它讓開發人員在無感的情況下使用了快取。
1.2 SpringCache概述
快取框架有很多,各有各的優勢,比如Redis、Memcached、Guava、Caffeine等等。
如果我們的程式想要使用快取,就要與這些框架耦合。聰明的架構師已經在利用介面來降低耦合了,利用物件導向的抽象和多型的特性,做到業務程式碼與具體的框架分離。
但我們仍然需要明確地在程式碼中呼叫與快取有關的介面和方法,在合適的時候將資料插入快取裡,在合適的時候從快取中讀取資料。想一想「AOP」的適用情境,這不就是天生就應該由AOP來做的嗎?
自Spring
3.1起,提供了類似於 [@Transactional](/Transactional ) 註解式交易的註解式Cache支援,且提供了Cache抽象,在此之前一般透過AOP實作。
使用Spring Cache的好處:
-
提供基本的Cache抽象,方便切換各種底層Cache;
-
透過Cache註解可以實現類似於交易的效果,將快取邏輯透明地套用到我們的業務程式碼上,且只需要更少的程式碼就可以完成;
-
提供交易回滾時也自動回滾快取;
-
支援比較複雜的快取邏輯;

1.3 Spring Cache核心設計
Spring 3.1開始,引入了Spring Cache,即Spring 快取抽象。透過定義 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 介面來統一不同的快取技術,並支援使用JCache註解簡化開發過程。
Cache介面為快取的元件規範定義,包含快取的各種操作集合。Spring中為Cache介面提供了各種 xxxCache 的實作: RedisCache , EhCacheCache , ConcurrentMapCache 等。
透過部分原始碼了解一下Cache介面和 CacheManager 介面。
Cache介面
public interface Cache { //Cache名称 String getName(); //Cache负责缓存的对象 Object getNativeCache(); /** * 获取key对应的ValueWrapper * 没有对应的key,则返回null * key对应的value是null,则返回null对应的ValueWrapper */ @Nullable Cache.ValueWrapper get(Object key); //返回key对应type类型的value @Nullable <T> T get(Object key, @Nullable Class<T> type); //返回key对应的value,没有则缓存Callable::call,并返回 @Nullable <T> T get(Object key, Callable<T> valueLoader); //缓存目标key-value(替换旧值),不保证实时性 void put(Object key, @Nullable Object value); //插入缓存,并返回该key对应的value;先调用get,不存在则用put实现 @Nullable default Cache.ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Cache.ValueWrapper existingValue = this.get(key); if (existingValue == null) { this.put(key, value); } return existingValue; } //删除缓存,不保证实时性 void evict(Object key); //立即删除缓存:返回false表示剔除前不存在制定key活不确定是否存在;返回true,表示该key之前存在 default boolean evictIfPresent(Object key) { this.evict(key); return false; } //清除所有缓存,不保证实时性 void clear(); //立即清楚所有缓存,返回false表示清除前没有缓存或不能确定是否有;返回true表示清除前有缓存 default boolean invalidate() { this.clear(); return false; } public static class ValueRetrievalException extends RuntimeException { @Nullable private final Object key; public ValueRetrievalException(@Nullable Object key, Callable<?> loader, Throwable ex) { super(String.format("Value for key '%s' could not be loaded using '%s'", key, loader), ex); this.key = key; } @Nullable public Object getKey() { return this.key; } } //缓存值的一个包装器接口,实现类为SimpleValueWrapper @FunctionalInterface public interface ValueWrapper { @Nullable Object get(); } }
可以看出,Cache介面抽象化了快取的 get put evict 等相關操作。
AbstractValueAdaptingCache 抽象類別
public abstract class AbstractValueAdaptingCache implements Cache { //是否允许Null值 private final boolean allowNullValues; protected AbstractValueAdaptingCache(boolean allowNullValues) { this.allowNullValues = allowNullValues; } public final boolean isAllowNullValues() { return this.allowNullValues; } @Nullable public ValueWrapper get(Object key) { return this.toValueWrapper(this.lookup(key)); } @Nullable public <T> T get(Object key, @Nullable Class<T> type) { //查询到的缓存值做fromStoreValue转换 Object value = this.fromStoreValue(this.lookup(key)); //转换后非null值且无法转换为type类型则抛出异常 if (value != null && type != null && !type.isInstance(value)) { throw new IllegalStateException("Cached value is not of required type [" + type.getName() + "]: " + value); } else { return value; } } //从缓存中获取key对应的value,子类实现 @Nullable protected abstract Object lookup(Object key); //对于从缓存中获取的值,允许为空且值为NullValue时,处理为null @Nullable protected Object fromStoreValue(@Nullable Object storeValue) { return this.allowNullValues && storeValue == NullValue.INSTANCE ? null : storeValue; } //对于要插入缓存的null值,在允许null值的情况下处理为NullValue;否则抛出异常IllegalArgumentException protected Object toStoreValue(@Nullable Object userValue) { if (userValue == null) { if (this.allowNullValues) { return NullValue.INSTANCE; } else { throw new IllegalArgumentException("Cache '" + this.getName() + "' is configured to not allow null values but null was provided"); } } else { return userValue; } } //get操作依据,查询到缓存值非null,则fromStoreValue转换后包装成SimpleValueWrapper返回 @Nullable protected ValueWrapper toValueWrapper(@Nullable Object storeValue) { return storeValue != null ? new SimpleValueWrapper(this.fromStoreValue(storeValue)) : null; } }
抽象類別 AbstractValueAdaptingCache 實作了Cache介面,主要抽象了對NULL值的處理邏輯。
-
allowNullValues屬性表示是否允許處理NULL值的快取 -
fromStoreValue方法處理NULL值的get操作,在屬性allowNullValues為true的情況下,將NullValue處理為NULL -
toStoreValue方法處理NULL值的put操作,在屬性allowNullValues為 true 的情況下,將 NULL 處理為 NullValue,否則拋出例外 -
toValueWrapper方法提供Cache介面中get方法的預設實作,從快取中讀取值,再透過fromStoreValue轉換,最後包裝為SimpleValueWrapper返回 -
ValueWrapper get(Object key)和T get(Object key, @Nullable Classtype)方法基於上述方法實作 -
ValueWrapper get(Object key)和@Nullable Classtype)方法基於上述方法實作 -
lookup抽象方法用於讓子類別取得真正的快取值
ConcurrentMapCache
public class ConcurrentMapCache extends AbstractValueAdaptingCache { private final String name; //定义ConcurrentMap缓存 private final ConcurrentMap<Object, Object> store; //如果要缓存的是值对象的copy,则由此序列化代理类处理 @Nullable private final SerializationDelegate serialization; public ConcurrentMapCache(String name) { this(name, new ConcurrentHashMap(256), true); } //默认允许处理null public ConcurrentMapCache(String name, boolean allowNullValues) { this(name, new ConcurrentHashMap(256), allowNullValues); } //默认serialization = null public ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues) { this(name, store, allowNullValues, (SerializationDelegate)null); } protected ConcurrentMapCache(String name, ConcurrentMap<Object, Object> store, boolean allowNullValues, @Nullable SerializationDelegate serialization) { super(allowNullValues); Assert.notNull(name, "Name must not be null"); Assert.notNull(store, "Store must not be null"); this.name = name; this.store = store; this.serialization = serialization; } //serialization不为空,缓存值对象的copy public final boolean isStoreByValue() { return this.serialization != null; } public final String getName() { return this.name; } public final ConcurrentMap<Object, Object> getNativeCache() { return this.store; } //实现lookup:store#get @Nullable protected Object lookup(Object key) { return this.store.get(key); } //基于ConcurrentMap::computeIfAbsent方法实现;get和put的值由fromStoreValue和toStoreValue处理Null @Nullable public <T> T get(Object key, Callable<T> valueLoader) { return this.fromStoreValue(this.store.computeIfAbsent(key, (k) -> { try { return this.toStoreValue(valueLoader.call()); } catch (Throwable var5) { throw new ValueRetrievalException(key, valueLoader, var5); } })); } public void put(Object key, @Nullable Object value) { this.store.put(key, this.toStoreValue(value)); } @Nullable public ValueWrapper putIfAbsent(Object key, @Nullable Object value) { Object existing = this.store.putIfAbsent(key, this.toStoreValue(value)); return this.toValueWrapper(existing); } public void evict(Object key) { this.store.remove(key); } public boolean evictIfPresent(Object key) { return this.store.remove(key) != null; } public void clear() { this.store.clear(); } public boolean invalidate() { boolean notEmpty = !this.store.isEmpty(); this.store.clear(); return notEmpty; } protected Object toStoreValue(@Nullable Object userValue) { Object storeValue = super.toStoreValue(userValue); if (this.serialization != null) { try { return this.serialization.serializeToByteArray(storeValue); } catch (Throwable var4) { throw new IllegalArgumentException("Failed to serialize cache value '" + userValue + "'. Does it implement Serializable?", var4); } } else { return storeValue; } } protected Object fromStoreValue(@Nullable Object storeValue) { if (storeValue != null && this.serialization != null) { try { return super.fromStoreValue(this.serialization.deserializeFromByteArray((byte[])((byte[])storeValue))); } catch (Throwable var3) { throw new IllegalArgumentException("Failed to deserialize cache value '" + storeValue + "'", var3); } } else { return super.fromStoreValue(storeValue); } } }
ConcurrentMapCache 繼承了抽象類別 AbstractValueAdaptingCache ,是Spring的預設快取實作。它支援以快取物件副本進行快取,由SerializationDelegate
serialization 處理序列化,預設為 null 即基於參照的快取。快取相關操作基於基底類別 AbstractValueAdaptingCache 的
null 值處理,預設允許為 null。
CacheManager
public interface CacheManager { @Nullable //获取指定name的Cache,可能延迟创建 Cache getCache(String name); //获取当前CacheManager下的Cache name集合 Collection<String> getCacheNames(); }
CacheManager 基於
name 管理一組 Cache。當然, CacheManager 也有很多實作類別,如 ConcurrentMapCacheManager 、 AbstractCacheManager 及 SimpleCacheManager ,這些 xxxCacheManager 類別都是為了制定Cache的管理規則,這裡就不再深入探討了。
二 使用SpringCache
2.1 如何使用
Spring Cache 就是這樣一個框架。它利用了 AOP,實作了基於註解的快取功能,並且進行了合理的抽象,業務程式碼不用關心底層使用了什麼快取框架,只需要簡單地加上一個註解,就能實現快取功能了。
新增相依套件
<!--springboot的cache支持--> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-cache</artifactId> </dependency> <!--配置拓展还需添加的依赖--> <dependency> <groupId>com.github.ben-manes.caffeine</groupId> <artifactId>caffeine</artifactId> <version>2.9.2</version> </dependency>
設定Cache
在Application啟動類別加入@EnableCaching註解
@SpringBootApplication @MapperScan("com.xxxx.dao") @EnableCaching public class AxxxxApplication { public static void main(String[] args) { SpringApplication.run(AxxxxApplication.class, args); } }
設定cache的快取類型,這裡主要介紹的是本機快取
//方式一:ConcurrentMapCache (不好设置过期时间) @Configuration public class SpringCachingConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); Set<ConcurrentMapCache> mapCaches=new HashSet<>(); //配置cache类型与目录 mapCaches.add(new ConcurrentMapCache("cacheNames1")); mapCaches.add(new ConcurrentMapCache("cacheNames2")); mapCaches.add(new ConcurrentMapCache("cacheNames3")); mapCaches.add(new ConcurrentMapCache("cacheNames4")); cacheManager.setCaches(mapCaches); cacheManager.afterPropertiesSet(); return cacheManager; } } //方式二:CaffeineCache (可设置过期时间) @Configuration public class SpringCachingConfig { @Bean public CacheManager cacheManager() { SimpleCacheManager cacheManager = new SimpleCacheManager(); List<CaffeineCache> caffeineCaches = new ArrayList<>(); //SysConstants.CACHE_NAMES=new ArrayList( //Arrays.asList("cacheNames1","cacheNames2","cacheNames3","cacheNames4") //); for (String cacheName: SysConstants.CACHE_NAMES) { caffeineCaches.add(new CaffeineCache(cacheName, Caffeine.newBuilder() .initialCapacity(50) .maximumSize(30000)//最大存储数量 .expireAfterWrite(2, TimeUnit.HOURS)//过期时间2 小时 .build())); } cacheManager.setCaches(caffeineCaches); return cacheManager; } } //方式三。。。。还可以配置redis以及其他的方式,这里主要介绍以上两种方式
實作介面快取
//控制器1 @RestController @RequestMapping("/path1") @Cacheable(cacheNames = "cacheNames1" ,key="#root.methodName+#a0" ) public class Controller1 { } //控制器2 @RestController @RequestMapping("/path2") @Cacheable(cacheNames = "cacheNames2" ,key="#root.methodName+#a0" ) public class Controller2 { } //控制器3 @RestController @RequestMapping("/path3") @Cacheable(cacheNames = "cacheNames3" ,key="#root.methodName+#a0" ) public class Controller3 { } //控制器4 @RestController @RequestMapping("/path4") @Cacheable(cacheNames = "cacheNames4" ,key="#root.methodName+#a0" ) public class Controller4 { }
2.2 SpringCache註解詳解
@Cacheable註解
如果快取中沒有:查詢資料庫,儲存快取,回傳結果,
如果快取中有:直接回傳結果
| 參數 | 解釋 | example |
| value | 快取的名稱,在 spring 設定檔中定義,必須指定至少一個 |
例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
| key | 快取的 key,可以為空,如果指定,要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 | @Cacheable(value=”testcache”,key=”#userName”) |
| condition | 快取的條件,可以為空,使用 SpEL 編寫,回傳 true 或者 false,只有為 true 才進行快取 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
作用:可以用來進行快取的寫入,將結果儲存在快取中,以便於在後續呼叫的時候可以直接回傳快取中的值,而不必再執行實際的方法。 最簡單的使用方式,註解名稱=快取名稱,使用範例如下:
@Cacheable(value="accountCache")// 使用了一个缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 System.out.println("real query account."+userName); return getFromDB(userName); }
@CacheEvict註解
@CacheEvict:刪除快取的註解,這對刪除舊的資料和無用的資料是非常有用的。這裡還多了一個參數(allEntries),設定allEntries=true時,可以對整個項目進行批次刪除
| 參數 | 解釋 | example |
| value | 快取的名稱,在 spring 設定檔中定義,必須指定至少一個 | @CacheEvict(value=”my cache”) |
| key | 快取的 key,可以為空,如果指定,要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 | @CacheEvict(value=”testcache”,key=”#userName”) |
| condition | 快取的條件,可以為空,使用 SpEL 編寫,回傳 true 或者 false,只有為 true 才進行快取 | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
| allEntries | 是否清空所有快取內容,預設為 false,如果指定為 true,則方法呼叫後將立即清空所有快取 | @CachEvict(value=”testcache”,allEntries=true) |
| beforeInvocation | 是否在方法執行前就清空,預設為 false,如果指定為 true,則在方法還沒有執行的時候就清空快取,預設情況下,如果方法執行拋出例外,則不會清空快取 | @CachEvict(value=”testcache”,beforeInvocation=true) |
@CacheEvict(value="accountCache",key="#account.getName()")// 清空accountCache 缓存 public void updateAccount(Account account) { updateDB(account); } @CacheEvict(value="accountCache",allEntries=true)// 清空accountCache 缓存 public void reload() { reloadAll() } @Cacheable(value="accountCache",condition="#userName.length() <=4")// 缓存名叫 accountCache public Account getAccountByName(String userName) { // 方法内部实现不考虑缓存逻辑,直接实现业务 return getFromDB(userName); }
@CachePut註解
@CachePut:當需要更新快取而不干擾方法的執行時 ,可以使用該註解。也就是說,始終執行該方法,並將結果放入快取;
| 參數 | 解釋 | example |
| value | 快取的名稱,在 spring 設定檔中定義,必須指定至少一個 | @CachePut(value=”my cache”) |
| key | 快取的 key,可以為空,如果指定,要按照 SpEL 運算式編寫,如果不指定,則預設按照方法的所有參數進行組合 | @CachePut(value=”testcache”,key=”#userName”) |
| condition | 快取的條件,可以為空,使用 SpEL 編寫,回傳 true 或者 false,只有為 true 才進行快取 | @CachePut(value=”testcache”,condition=”#userName.length()>2”) |
@CachePut 註解,這個註解可以確保方法被執行,同時方法的回傳值也被記錄到快取中,實現快取與資料庫的同步更新。
@CachePut(value="accountCache",key="#account.getName()")// 更新accountCache 缓存 public Account updateAccount(Account account) { return updateDB(account); }
@Caching 註解
在使用快取的時候,有可能會同時進行更新和刪除,會出現同時使用多個註解的情況.而 @Caching 可以實現
//添加user缓存的同时,移除userPage的缓存 @Caching( put =@CachePut(value = "user",key ="#userVo.id"), evict = @CacheEvict(value = "userPage",allEntries = true) )
@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) public User save(User user) {
@CacheConfig
所有的@Cacheable()裡面都有一個value=「xxx」的屬性,這顯然如果方法多了,寫起來也是挺累的,如果可以一次性宣告完 那就省事了, 所以,有了@CacheConfig這個設定,@CacheConfig 是一個 類別層級的註解,允許共用快取名稱,如果你在你的方法寫別的名稱,那麼依然以方法的名稱為準。
@CacheConfig("books") public class BookRepositoryImpl implements BookRepository { @Cacheable public Book findBook(ISBN isbn) {...} }
條件式快取
下面提供一些常用的條件快取
//@Cacheable将在执行方法之前( #result还拿不到返回值)判断condition,如果返回true,则查缓存; @Cacheable(value = "user", key = "#id", condition = "#id lt 10") public User conditionFindById(final Long id) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断condition,如果返回true,则放入缓存; @CachePut(value = "user", key = "#id", condition = "#result.username ne 'zhang'") public User conditionSave(final User user) //@CachePut将在执行完方法后(#result就能拿到返回值了)判断unless,如果返回false,则放入缓存;(即跟condition相反) @CachePut(value = "user", key = "#user.id", unless = "#result.username eq 'zhang'") public User conditionSave2(final User user) //@CacheEvict, beforeInvocation=false表示在方法执行之后调用(#result能拿到返回值了);且判断condition,如果返回true,则移除缓存; @CacheEvict(value = "user", key = "#user.id", beforeInvocation = false, condition = "#result.username ne 'zhang'") public User conditionDelete(final User user)
自訂快取註解
比如之前的那個@Caching組合,會讓方法上的註解顯得整段程式碼比較亂,此時可以使用自訂註解把這些註解組合到一個註解中,如:
@Caching(put = { @CachePut(value = "user", key = "#user.id"), @CachePut(value = "user", key = "#user.username"), @CachePut(value = "user", key = "#user.email") }) @Target({ElementType.METHOD, ElementType.TYPE}) @Retention(RetentionPolicy.RUNTIME) @Inherited public @interface UserSaveCache { }
這樣我們在方法上使用如下程式碼即可,整個程式碼顯得比較乾淨。
@UserSaveCache public User save(User user)
擴充
比如findByUsername時,不應該只放username–>user,應該連同id—>user和email—>user一起放入;這樣下次如果按照id查找,就能直接命中快取
@Caching( cacheable = { @Cacheable(value = "user", key = "#username") }, put = { @CachePut(value = "user", key = "#result.id", condition = "#result != null"), @CachePut(value = "user", key = "#result.email", condition = "#result != null") } ) public User findByUsername(final String username) { System.out.println("cache miss, invoke find by username, username:" + username); for (User user : users) { if (user.getUsername().equals(username)) { return user; } } return null; }
其實對於:id—>user;username—->user;email—>user;更好的方式可能是:id—>user;username—>id;email—>id;確保user只存一份;如:
@CachePut(value="cacheName", key="#user.username", cacheValue="#user.username") public void save(User user) @Cacheable(value="cacheName", key="#user.username", cacheValue="#caches[0].get(#caches[0].get(#username).get())") public User findByUsername(String username)
SpEL 上下文資料
Spring Cache 提供了一些供我們使用的 SpEL 上下文資料,下表直接摘自 Spring 官方文件:
| 名稱 | 位置 | 描述 | 範例 |
| methodName | root物件 | 目前被呼叫的方法名稱 | root.methodName |
| method | root物件 | 目前被呼叫的方法 | root.method.name |
| target | root物件 | 目前被呼叫的目標物件 | root.target |
| targetClass | root物件 | 目前被呼叫的目標物件類別 | root.targetClass |
| args | root物件 | 目前被呼叫的方法的參數列表 | root.args[0] |
| caches | root物件 | 目前方法呼叫使用的快取清單(如@Cacheable(value={“cache1”, “cache2”})),則有兩個cache | root.caches[0].name |
| 引數名稱 | 執行上下文 | 目前被呼叫的方法的參數,如 findById(Long id),我們可以透過 #id 取得參數 | user.id |
| 結果 | 執行上下文 | 方法執行後的回傳值(僅當方法執行之後的判斷有效,如‘unless','cache evict'的beforeInvocation=false) | 結果 |
@CacheEvict(value = "user", key = "#user.id", condition = "#root.target.canCache() and #root.caches[0].get(#user.id).get().username ne #user.username", beforeInvocation = true) public void conditionUpdate(User user)
小結
對於快取宣告,spring的快取提供了一組java註解:
· @Cacheable
o
功能:觸發快取寫入,如果快取中沒有,查詢資料庫,儲存快取,傳回結果,如果快取中有,直接傳回結果
o 應用:查詢資料庫方法
· @CacheEvict
o
功能:觸發快取清除
o 應用:刪除或修改資料庫方法
· @CachePut
o
功能:快取寫入(不會影響到方法的執行)。
o 應用:新增至資料庫方法
· @快取
o
功能:重新組合要套用於方法的多個快取操作
o 應用:上面的註解的組合使用