1.情景展示
從3.1開始,Spring引入了對Cache的支持。其使用方法和原理都類似於Spring對事務管理的支持。Spring Cache是作用在方法上的,其核心思想是這樣的:
當我們在調用一個緩存方法時會把該方法參數和返回結果作為一個鍵值對存放在緩存中,等到下次利用同樣的參數來調用該方法時將不再執行該方法,而是直接從緩存中獲取結果進行返回。
spring中內部集成了緩存,我們可以拿來直接使用,如何實現對緩存的增、刪、改、查操作?
2.@Cacheable、@CachePut、@CacheEvict
通過這三個注解,完全可以實現對緩存數據庫的增刪改查。
@Cacheable
使用范圍:
@Cacheable可以標記在一個方法上,也可以標記在一個類上。當標記在一個方法上時表示該方法是支持緩存的,當標記在一個類上時則表示該類所有的方法都是支持緩存的。
特性:初次將數據存入緩存,以后從緩存讀取,可以當作讀取操作
對於使用@Cacheable標注的方法,Spring在每次執行前都會檢查Cache中是否存在相同key的緩存元素,如果存在就不再執行該方法,而是直接從緩存中獲取結果進行返回(不需要再次執行該方法);
否則才會執行並將返回結果存入指定的緩存中。
Spring在緩存方法的返回值時是以鍵值對進行緩存的;
值:就是方法的返回結果;
鍵:支持兩種策略,默認策略和自定義策略。
自定義策略是指我們可以通過Spring的EL表達式來指定我們的key。這里的EL表達式可以使用方法參數及它們對應的屬性。使用方法參數時我們可以直接使用“#參數名”或者“#p參數index”。
注意:當一個支持緩存的方法在對象內部被調用時是不會觸發緩存功能的。
可用屬性:
- value/cacheNames:兩個等同的參數(cacheNames為Spring 4新增,作為value的別名),用於指定緩存存儲的集合名。由於Spring 4中新增了@CacheConfig,因此在Spring 3中原本必須有的value屬性,也成為非必需項了
- key:緩存對象存儲在Map集合中的key值,非必需,缺省按照函數的所有參數組合作為key值,若自己配置需使用SpEL表達式,比如:@Cacheable(key = "#p0"):使用函數第一個參數作為緩存的key值,更多關於SpEL表達式的詳細內容可參考官方文檔
- condition:緩存對象的條件,非必需,也需使用SpEL表達式,只有滿足表達式條件的內容才會被緩存,比如:@Cacheable(key = "#p0", condition = "#p0.length() < 3"),表示只有當第一個參數的長度小於3的時候才會被緩存。
- unless:另外一個緩存條件參數,非必需,需使用SpEL表達式。它不同於condition參數的地方在於它的判斷時機,該條件是在函數被調用之后才做判斷的,所以它可以通過對result進行判斷。
- keyGenerator:用於指定key生成器,非必需。若需要指定一個自定義的key生成器,我們需要去實現org.springframework.cache.interceptor.KeyGenerator接口,並使用該參數來指定。需要注意的是:該參數與key是互斥的
- cacheManager:用於指定使用哪個緩存管理器,非必需。只有當有多個時才需要使用
- cacheResolver:用於指定使用那個緩存解析器,非必需。需通過org.springframework.cache.interceptor.CacheResolver接口來實現自己的緩存解析器,並用該參數指定。
@Cacheable("userCache") User selectUserById(final Integer id);
@Cacheable(value={"users1", "user2"}, key="caches[1].name") public User find(User user);
@Cacheable(value={"userCache"}, key="#user.id", condition="#user.id%2==0") User getSomeUsers(User user);
@CachePut
使用范圍:@CachePut也可以標注在類上和方法上;
特性:一直往緩存中存,可以當作新增或者更新操作
@CachePut也可以聲明一個方法支持緩存功能;
與@Cacheable不同的是使用@CachePut標注的方法在執行前不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。
可用屬性:(同上)
@CachePut(value = "userCache") User updateUserById(final Integer id);
@CacheEvict
使用范圍:(同上)
當標記在一個類上時表示其中所有的方法的執行都會觸發緩存的清除操作。
特性:一直從緩存中刪除,可以當作刪除操作
清除緩存
可用屬性:(同上)
@CacheEvict還有下面兩個參數:
- allEntries:非必需,默認為false。當為true時,清除緩存中的所有元素
- beforeInvocation:非必需,默認為false,會在調用方法之后移除數據。當為true時,會在調用方法之前移除數據。
@CacheEvict(cacheNames = {"userCache"}, allEntries = true) public Integer delete(Integer id);
@CacheEvict(cacheNames = {"userCache"}, beforeInvocation = true) public Integer delete(Integer id);
@CacheEvict(value = "userCache") User deleteUserById(final Integer id);
@Caching
使用范圍:(同上)
特性:可同時指定多個Spring Cache相關的注解
可用屬性:
擁有三個屬性:cacheable、put和evict,分別用於指定@Cacheable、@CachePut和@CacheEvict。
@Caching( put = { @CachePut(cacheNames = "userCache", key = "#user.id"), @CachePut(cacheNames = "userCache", key = "#user.username"), @CachePut(cacheNames = "userCache", key = "#user.age") } }
自定義注解
前提:要想使用spring緩存注解,需要開啟spring緩存,否則,一切都是枉然。
開啟方法:@EnableCache
在springboot的項目啟動類上加上此注解即可。
3.解決方案
上面簡單的使用方式,這里不在贅述,下面我們來看高級一點的用法。
方式一:提供接口
接口:
/** * 單位信息緩存 * @description: * @author: Marydon * @date: 2020-12-12 9:42 * @version: 1.0 * @email: marydon20170307@163.com */ public interface IUnitInfoCache { UNITINFO insertOrUpdateCache(String unitKey,UNITINFO unitInfo); UNITINFO getCache(String unitKey); void deleteCache(String unitKey); }
實現類:
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; import org.springframework.stereotype.Component; /** * 單位信息表緩存實現類 * @description: * @author: Marydon * @date: 2020-12-12 10:05 * @version: 1.0 * @email: marydon20170307@163.com */ @Component @CacheConfig(cacheNames = {"unitInfo2"}) public class UnitInfoCacheImpl implements IUnitInfoCache{ /* * 緩存單位信息 * @description: 存入緩存 * @attention: 在執行前不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。 * 指定key后,就不再自動將所有入參當成key的組成部分啦 * @date: 2020年12月12日 0012 9:53 * @param: unitKey 作為緩存的主鍵 * @param: unitInfo 作為緩存的值 * @return: UNITINFO * 在這里添加返回值的目的不是為了調用返回值,因為我們把它作為入參傳進來了,所以,看似是畫蛇添足; * 實際上是因為@CachePut注解會將返回值作為value存入緩存中,所以返回值不能改成void。 */ @CachePut(key = "#unitKey", unless = "#result == null") @Override public UNITINFO insertOrUpdateCache(String unitKey, UNITINFO unitInfo) { return unitInfo; } /* * 獲取單位信息 * @description: 從緩存中取 * @attention: 對於使用@Cacheable標注的方法,Spring在每次執行前都會檢查Cache中是否存在相同key的緩存元素,如果存在就不再執行該方法,而是直接從緩存中獲取結果進行返回,否則才會執行並將返回結果存入指定的緩存中。 * @date: 2020年12月12日 0012 9:55 * @param: unitKey * @return: UNITINFO * 緩存中有就取,沒有就執行方法並返回null。 */ @Cacheable(key = "#unitKey", unless = "#result == null") @Override public UNITINFO getCache(String unitKey) { // 緩存中沒有對應的可以時,會進來 return null; } /* * 刪除單位信息 * @description: 從緩存中刪除 * @attention: * @date: 2020年12月12日 0012 9:56 * @param: unitKey * @return: void */ @CacheEvict(key = "#unitKey") @Override public void deleteCache(String unitKey) { // 不需要內部實現,@CacheEvict會自動將入參作為key進行移除 } }
方式二:提供父類
父類:
import org.springframework.cache.annotation.CacheConfig; import org.springframework.cache.annotation.CacheEvict; import org.springframework.cache.annotation.CachePut; import org.springframework.cache.annotation.Cacheable; /** * 緩存操作父類 * @description: 使用這種方式雖然可以實現多個子類共享方法, * 但是,這些子類需要共用一個cacheName,如果不想共用緩存名稱,那就只能重寫這些方法 * @author: Marydon * @date: 2020-12-12 11:04 * @version: 1.0 * @email: marydon20170307@163.com */ @CacheConfig(cacheNames = {"myCache"}) public abstract class CacheAbstractParent<T> { /* * 存入或更新指定緩存 * @description: * @attention: 在執行前不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。 * 指定key后,就不再自動將所有入參當成key的組成部分啦 * @date: 2020年12月12日 0012 9:53 * @param: key 作為緩存的主鍵 * @param: javaBean 作為緩存的值 * @return: java對象 * 在這里添加返回值的目的不是為了調用返回值,因為我們把它作為入參傳進來了,所以,看似是畫蛇添足; * 實際上是因為@CachePut注解會將返回值作為value存入緩存中,所以返回值不能改成void。 */ @CachePut(key = "#key", unless = "#result == null") public T insertOrUpdateCache(String key, T value) { return value; } /* * 獲取單位信息 * @description: 從緩存中取 * @attention: 對於使用@Cacheable標注的方法,Spring在每次執行前都會檢查Cache中是否存在相同key的緩存元素,如果存在就不再執行該方法,而是直接從緩存中獲取結果進行返回,否則才會執行並將返回結果存入指定的緩存中。 * @date: 2020年12月12日 0012 9:55 * @param: key * @return: java對象或null * 緩存中有就取,沒有;就執行方法並返回null。 */ @Cacheable(key = "#key", unless = "#result == null") public T getCache(String key) { // 緩存中沒有對應的可以時,會進來 return null; } /* * 刪除單位信息 * @description: 從緩存中刪除 * @attention: * @date: 2020年12月12日 0012 9:56 * @param: key * @return: 無返回值 */ @CacheEvict(key = "#key") public void deleteCache(String key) { // 不需要內部實現,@CacheEvict會自動將入參作為key進行移除 } }
子類:
import org.springframework.cache.annotation.CacheConfig; import org.springframework.stereotype.Component; /** * 單位信息表緩存 * @description: 在這里我們只需要定義好緩存的名稱,以及要被緩存的對象就可以啦 * 緩存的操作:增、刪、改、查,父類已經實現過了。 * 這樣,我們就可以無限擴展N個緩存實體類 * @author: Marydon * @date: 2020-12-12 11:14 * @version: 1.0 * @email: marydon20170307@163.com */ @Component // 這里配置緩存名稱無效 // @CacheConfig(cacheNames = {"unitCache"}) public class UnitInfoCache extends CacheAbstractParent<UNITINFO>{ // 繼承了父類所有操作緩存的方法 }
4.調用及測試
在需要緩存的地方,注入緩存對象即可。比如:
調用
如果你開啟了redis緩存,那我們可以看到具體的存儲數據:
結果會形如這個樣子。
如何才能從緩存中取出來呢?
第一步:在使用緩存的地方注入緩存管理對象
第二步:取值
一開始,我以為通過這種方式能取到值,事實證明我錯了,無法直接通過key拿到value。
然后,按照redis的存儲方式取值也拿不到
要想獲得value,在調用get方法時,需要指定value的返回值類型,或者說用什么樣的數據類型來接收返回值。
控制台輸出結果:
這次,我們就可以拿到value啦。
5.緩存策略
如果緩存滿了,從緩存中移除數據的策略,常見的有FIFO, LRU 、LFU。
- FIFO (First in First Out) 先進先出策略,即先放入緩存的數據先被移除
- LRU (Least Recently Used) 最久未使用策略, 即使用時間距離現在最久的那個數據被移除
- LFU (Least Frequently Used) 最少使用策略,即一定時間內使用次數(頻率)最少的那個數據被移除
- TTL(Time To Live)存活期,即從緩存中創建時間點開始至到期的一個時間段(不管在這個時間段內有沒被訪問過都將過期)
- TTI (Time To Idle)空閑期,即一個數據多久沒有被訪問就從緩存中移除的時間。
6.緩存管理器
通過@EnableCaching注解自動化配置合適的緩存管理器(CacheManager),Spring Boot根據下面的順序去偵測緩存提供者:
- Generic
- JCache (JSR-107)
- EhCache 2.x
- Hazelcast
- Infinispan
- Redis
- Guava
- Simple
可以通過配置屬性spring.cache.type來強制指定,即:
spring.cache.type=Generic
2021-04-14
關於緩存條件參數的補充
unless = "#result == null and #result != ''",表示的含義是:當滿足該條件時,不進行緩存(當方法返回值為空時,不將key添加到緩存當中);
condition = "#myKey != null and #myKey != ''",表示的含義是:當滿足該條件時,才會進行緩存(當作為緩存主鍵的參數不為空時,才將該key-value存入緩存)