跳到主要內容

搜尋酷無極

选择搜索范围,输入关键词后按回车。

語言 / Language

Spring Cache使用介紹

一 基於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註解可以實現類似於交易的效果,將快取邏輯透明地套用到我們的業務程式碼上,且只需要更少的程式碼就可以完成;

  • 提供交易回滾時也自動回滾快取;

  • 支援比較複雜的快取邏輯;

Spring 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 應用:上面的註解的組合使用


評論區

傳送中

選擇喜歡的樣式

所選樣式會套用至全站。