如何理解SpringBoot的緩存

寫在前面

本文參考自 Spring Boot 官方文檔


簡介

對於緩存,我們常說的應該是 redis來做緩存了,但是 Spring 難道僅僅只支持特定的 redis 來做緩存嘛?Spring 經常強調的思想是:”俺們不重複造輪子,俺們只是輪子的適配者>“。

Spring 框架支持嚮應用程序透明地添加緩存。你可以自由地選擇緩存的具體實現。抽象的核心是將緩存應用於方法,從而減少了基於緩存中可用信息的執行次數。緩存邏輯對於應用程序來說是透明的,對調用程序沒有干擾。只要通過 @EnableCaching 註釋啓用了緩存支持,Spring Boot 就會自動配置緩存的基礎設施。

緩存的抽象並沒有提供實際的存儲,抽象的好處就在於我們不用依賴某種具體實現,我們可以在生產期間更換不同的實現,對代碼卻不會產生什麼影響(除非你覺得更換是有必要的,否則仍然不建議這麼做)。緩存的抽象主要依賴於 org.springframework.cache.Cacheorg.springframework.cache.CacheManager 接口。


如果沒有添加任何特定的緩存庫,Spring Boot 將自動配置一個簡單的 ConcurrentMap 實現的緩存。


支持的緩存實現庫

如果沒有定義 CacheManager 類型的 bean 或 名爲 cachedResolverCacheResolver , Spring Boot 將按以下的順序檢測提供程序:

  1. Generic
  2. JCache (JSR-107) (EhCache 3, Hazelcast, Infinispan, and others)
  3. EhCache 2.x
  4. Hazelcast
  5. Infinispan
  6. Couchbase
  7. Redis
  8. Caffeine
  9. Simple

我們仍然可以選擇通過 spring.cache.type 來強制使用特定的緩存提供程序。或者 spring.cache.type = none 來禁用緩存。有關以上特定的緩存庫介紹,可以查看文檔(指導了你需要確保怎樣的jar包需要在你的類路徑下)。


簡單使用

抽象的好處開始體現,想要我們針對不同的緩存庫寫代碼??

instrest
所以,我們只需要 “簡單使用”。


這裏只介紹基於註解的聲明式緩存使用,它還支持基於xml的,並且從 4.1 開始,它還支持 JCache 標準註解。

  • @Cacheable:標誌着方法調用的結果可以被緩存,如果註解在類上,則代表該類的所有方法的執行結果都可以被緩存。
  • @CacheEvict:標誌着方法將會觸發 org.springframework.cache.Cacheevict 方法(任然可以註解在類上),通俗地來說,就是移除緩存。
  • @CachePut:標誌着方法將會觸發 cacheput 方法。與 @Cacheable 註解所不同的是,該註解標識的方法並不會跳過執行,它總是保存方法執行的結果到關聯的緩存中。
  • @Caching:緩存註解的組註解。
  • @CacheConfig:提供了一種在類級別上共用的緩存配置。它將給被註解類的任何緩存操作提供默認設置。

用例:操作 ”student“ 實體,以下部分代碼,均建立在啓動類上已經添加 @EnableCaching 註解,並且類路徑下不存在其它特定的緩存庫實現。

@Cacheable

	@Override
    @Cacheable("students")
    public Student getStudentById(String id){
        for (Student student : container) {
            if(student.getId().equals(id)){
                return student;
            }
        }
        return null;
    }

在可調試模式下,爲上面的代碼打上斷點,基於相同的訪問時,只會執行一次。

如果你想了解的更多,你可以通過注入 ApplicationContext bean 來查看 cacheManager 這個 bean,看一下緩存的具體實現到底是怎樣的。下面計算來自於第一次執行 getStudentById 方法體的時候:
CacheManager

如果不會該註解添加 value 值或者 cacheNames 值,你將得到一個錯誤;從上面的圖片也能發現在第一次進入方法體執行之前,需要爲抽象的緩存構建具體的存儲,需要 “students” 這個 key。

以下是在依次請求了 id 爲 1,2以後,此次請求id爲 3時,在方法體內斷點時,重新計算的 cacheManager
CacheManager

我們的暫時拋棄以上 key 這個概念,它所代表的是一個 Cache 的具體實現。我們依賴它在衆多 @Cacheable 註解標記的方法中,找到具體的 Cache 實現。那麼如何在單個方法的多次調用定位到緩存的值呢?這纔是 Spring 所說的 “key”。

  • 如果方法沒有任何參數,將使用 SimpleKey.EMPTY
  • 如果僅僅只有一個參數,將使用它的實例
  • 如果有多個參數,將使用SimpleKey 包含所有的參數。

可以參考 SimpleKey 這個類,理解它如何處理參數,來生成 key 的。以上的方法適用於大多數用例,只要參數具有自然鍵並實現有效的hashCode()和equals()方法。如果不是這樣,你需要改變策略,自己實現 org.springframework.cache.interceptor.KeyGenerator 接口。

如何實現自定義key生成

拓展以上方法

	@Override
    @Cacheable(cacheNames = "studentsOfTeacher", keyGenerator = "keyGeneratorForTeacher")
    public List<Student> getStudentByTeacher(Teacher teacher){
        List<Student> findStudents = new ArrayList<>();
        for (Student student : container) {
            if(student.getName().equals(teacher.getName())){
                findStudents.add(student);
            }
        }
        return findStudents;
    }

在使用自定義 key 生成之前,你需要確保通過 “key” 屬性,無法完成你的需求,否則,沒有必要做負責的工作。如果使用 key 屬性(支持 SpEL), 我們僅僅需要修改 @Cacheable :

@Cacheable(cacheNames = "studentsOfTeacher", key="#teacher.name")

KeyGenerator 接口的實現如下:

@Component("keyGeneratorForTeacher")
public class KeyGeneratorForTeacher implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        if(params.length != 1){
            throw new IllegalArgumentException("i just need one param!");
        }
        if(params[0] instanceof Teacher){
            Teacher teacher = (Teacher) params[0];
            return teacher.getName();
        }
        return method.getName() + Arrays.stream(params)
                .map(Object::toString).collect(Collectors.joining(","));
    }
}

需要注意的是在第一次請求該方法時,以上 key 的生成策略會被調用兩次;第二次請求該方法(使用相同的參數),生成策略僅僅只會被調用一次。

該註解還支持使用其它 CacheManager 的實現(cacheManager 屬性),甚至支持自定義如何從上下文中解析出 CachecacheResolver屬性)。

如何應對多線程環境

在一個多線程環境中,某些操作可能併發調用相同的點(通常在啓動時)。默認情況下,緩存抽象不鎖,相同的值可能會計算幾次。

對於這些特定的情況下,可以使用 sync 屬性指示底層的緩存提供者鎖定緩存條目。結果只會有一個線程正在忙着計算值, 而其他人則將被阻塞,直到該條目更新緩存。

這是一個可選的特性,某些緩存庫可能不支持它。

如何實現只緩存某些特定的調用(基於參數或者調用結果)

conditionunless 可以很好的幫助你完成這項需求。unless參數否決向緩存添加值的操作。與 condition 不同,unless 表達式是在方法被調用後求值的。


@CachePut

當需要在不影響方法執行的情況下更新緩存時,可以使用 @CachePut 註釋。也就是說,方法總是被執行,它的結果被放到緩存中(根據 @CachePut 選項)。它支持與 @Cacheable 相同的選項,應該用於緩存填充而不是方法流優化。

並不應該講 @CachePut@Cacheable 聲明在同一個方法上,它們具有不同的行爲。


@CacheEvict

此註解用於從緩存中刪除過時或未使用的數據。由於方法僅僅充當觸發器,返回值將被忽略;但如果方法中拋出了異常,此時,你並仍然想觸發緩存清除操作怎麼辦?我們可以通過 beforeInvocation 這個屬性來解決。某種時刻,你想清楚緩存的所有條目,可以使用 allEntries 屬性來解決。


@Caching

有時,需要指定同一類型的多個註釋(如 @CacheEvict@CachePut )—例如,因爲條件或鍵表達式在不同的緩存之間是不同的。@Caching 允許在同一方法上使用多個嵌套的 @Cacheable@CachePut@CacheEvict 註釋。


@CachConfig

存操作提供了許多定製選項,您可以爲每個操作設置這些選項。但是,如果某些自定義選項應用於類的所有操作,那麼配置它們可能會很繁瑣。例如,爲類的每個緩存操作指定要使用的緩存名稱可以由單個類級別定義替換。這就是 @CacheConfig 發揮作用的地方。

操作級定製總是覆蓋 @CacheConfig 上的定製集。因此,這爲每個緩存操作提供了三層定製:

  • 全局配置,可用的 CacheManager , KeyGenerator

  • 在類級別,使用 @CacheConfig

  • 在操作層面。

總結

緩存的使用是非常簡單的,多個註解擁有大多重複的屬性,並且由於它的 透明性(應用程序),我們可以不依賴某種特定的緩存庫來寫代碼。對於需要應用緩存的方法,應保證相同的參數調用,結果是一樣的,否則,使用緩存就沒有什麼意義了。在多進程環境下,緩存的支持就和特定的緩存庫實現有關,這個需要注意。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章