第 4-4 課:Spring Boot 中使用 Cache 緩存的使用

我們知道絕大多數的網站/系統,最先遇到的一個性能瓶頸就是數據庫,使用緩存做數據庫的前置緩存,可以非常有效地降低數據庫的壓力,從而提升整個系統的響應效率和併發量。

以往使用緩存時,通常創建好緩存工具類,使用時將對應的工具類注入,操作工具類在前端處理緩存的邏輯。其實這種方式是低效的,大部分使用緩存的場景是基於數據庫的緩存,這類緩存場景的邏輯往往是:如果緩存中存在數據,就從緩存中讀取,如果緩存中不存在數據或者數據失效,就再從數據庫中讀取。

爲了實現這樣的邏輯,往往需要在業務代碼中寫很多的邏輯判斷,那麼有沒有通用的代碼來實現這樣的邏輯呢?其實有,按照這個邏輯我們可以寫一個工具類來實現,每次需要這樣判斷邏輯時調用工具類中的方法即可,還有沒有更優雅的使用方式呢?答案是肯定的,如果我們把這種固定的邏輯使用 Java 註解來實現,每次需要使用時只需要在對應的方法或者類上寫上註解即可。

Spring 也看到了這樣的使用場景,於是有了註釋驅動的 Spring Cache。它的原理是 Spring Cache 利用了 Spring AOP 的動態代理技術,在項目啓動的時候動態生成它的代理類,在代理類中實現了對應的邏輯。

Spring Cache 是在 Spring 3.1 中引入的基於註釋(Annotation)的緩存(Cache)技術,它本質上不是一個具體的緩存實現方案,而是一個對緩存使用的抽象,通過在既有代碼中添加少量它定義的各種 Annotation,即能夠達到緩存方法的返回對象的效果。

Spring 的緩存技術還具備相當的靈活性,不僅能夠使用 SpEL(Spring Expression Language)來定義緩存的 key 和各種 condition,還提供了開箱即用的緩存臨時存儲方案,也支持和主流的專業緩存如 EHCache 集成。

SpEL(Spring Expression Language)是一個支持運行時查詢和操作對象圖的強大的表達式語言,其語法類似於統一 EL,但提供了額外特性,顯式方法調用和基本字符串模板函數

其特點總結如下:

  • 通過少量的配置 Annotation 註釋即可使得既有代碼支持緩存;
  • 支持開箱即用 Out-Of-The-Box,即不用安裝和部署額外第三方組件即可使用緩存;
  • 支持 Spring Express Language,能使用對象的任何屬性或者方法來定義緩存的 key 和 condition;
  • 支持 AspectJ,並通過其實現任何方法的緩存支持;
  • 支持自定義 key 和自定義緩存管理者,具有相當的靈活性和擴展性。

Spring Boot 中 Cache 的使用

Spring Boot 提供了非常簡單的解決方案,這裏給大家演示最核心的三個註解:@Cacheable、@CacheEvict、@CachePut。spring-boot-starter-cache 是 Spring Boot 體系內提供使用 Spring Cache 的 Starter 包。

在開始使用這三個註解之前,來介紹一個新的組件 spring-boot-starter-cache。

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

spring-boot-starter-cache 是 Spring Boot 提供緩存支持的 starter 包,其會進行緩存的自動化配置和識別,Spring Boot 爲 Redis 自動配置了 RedisCacheConfiguration 等信息,spring-boot-starter-cache 中的註解也主要是使用了 Spring Cache 提供的支持。

@Cacheable

@Cacheable 用來聲明方法是可緩存的,將結果存儲到緩存中以便後續使用相同參數調用時不需執行實際的方法,直接從緩存中取值。@Cacheable 可以標記在一個方法上,也可以標記在一個類上。當標記在一個方法上時表示該方法是支持緩存的,當標記在一個類上時則表示該類所有的方法都是支持緩存的。

我們先來一個最簡單的例子體驗一下:

@RequestMapping("/hello")
@Cacheable(value="helloCache")
public String hello(String name) {
    System.out.println("沒有走緩存!");
    return "hello "+name;
}

來測試一下,啓動項目後訪問網址 http://localhost:8080/hello?name=neo,輸出:沒有走緩存!,再次訪問網址 http://localhost:8080/hello?name=neo,輸出欄沒有變化,說明這次沒有走 hello() 這個方法,內容直接由緩存返回。

@Cacheable(value="helloCache") 這個註釋的意思是,當調用這個方法時,會從一個名叫 helloCache 的緩存中查詢,如果沒有,則執行實際的方法(也可是查詢數據庫),並將執行的結果存入緩存中,否則返回緩存中的對象。這裏的緩存中的 key 就是參數 name,value 就是返回的 String 值。

@Cacheable 支持如下幾個參數。

  • value:緩存的名稱。
  • key:緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫;如果不指定,則缺省按照方法的所有參數進行組合。
  • condition:觸發條件,只有滿足條件的情況纔會加入緩存,默認爲空,既表示全部都加入緩存,支持 SpEL。

我們把上面的方法稍微改成這樣:

@RequestMapping("/condition")
@Cacheable(value="condition",condition="#name.length() <= 4")
public String condition(String name) {
    System.out.println("沒有走緩存!");
    return "hello "+name;
}

啓動後在瀏覽器中輸入網址 http://localhost:8080/condition?name=neo,第一次輸出欄輸出:沒有走緩存!再次執行無輸出,表明已經走緩存。在瀏覽器中輸入網址 http://localhost:8080/condition?name=ityouknow,瀏覽器執行多次仍然一直輸出:沒有走緩存!說明條件 condition 生效。

結合數據庫的使用來做測試:

@RequestMapping("/getUsers")
@Cacheable(value="usersCache",key="#nickname",condition="#nickname.length() >= 6")
public List<User> getUsers(String nickname) {
    List<User> users=userRepository.findByNickname(nickname);
    System.out.println("執行了數據庫操作");
    return users;
}

啓動後在瀏覽器中輸入網址 http://localhost:8080/getUsers?nickname=neo

輸出欄輸出:

Hibernate: select user0_.id as id1_0_, user0_.email as email2_0_, user0_.nickname as nickname3_0_, user0_.pass_word as pass_wor4_0_, user0_.reg_time as reg_time5_0_, user0_.user_name as user_nam6_0_ from user user0_ where user0_.nickname=?
執行了數據庫操作

多次執行,仍然輸出上面的結果,說明每次請求都執行了數據庫操作,再輸入 http://localhost:8080/getUsers?nickname=ityoukonw 進行測試。只有第一次返回了上面的內容,再次執行輸出欄沒有變化,說明後面的請求都已經從緩存中拿取了數據。

最後總結一下:當執行到一個被 @Cacheable 註解的方法時,Spring 首先檢查 condition 條件是否滿足,如果不滿足,執行方法,返回;如果滿足,在緩存空間中查找使用 key 存儲的對象,如果找到,將找到的結果返回,如果沒有找到執行方法,將方法的返回值以 key-value 對象的方式存入緩存中,然後方法返回。

需要注意的是當一個支持緩存的方法在對象內部被調用時是不會觸發緩存功能的。

@CachePut

項目運行中會對數據庫的信息進行更新,如果仍然使用 @Cacheable 就會導致數據庫的信息和緩存的信息不一致。在以往的項目中,我們一般更新完數據庫後,再手動刪除掉 Redis 中對應的緩存,以保證數據的一致性。Spring 提供了另外的一種解決方案,可以讓我們以優雅的方式去更新緩存。

與 @Cacheable 不同的是使用 @CachePut 標註的方法在執行前,不會去檢查緩存中是否存在之前執行過的結果,而是每次都會執行該方法,並將執行結果以鍵值對的形式存入指定的緩存中。

以上面的方法爲例,我們再來做一個測試:

@RequestMapping("/getPutUsers")
@CachePut(value="usersCache",key="#nickname")
public List<User> getPutUsers(String nickname) {
    List<User> users=userRepository.findByNickname(nickname);
    System.out.println("執行了數據庫操作");
    return users;
}

我們新增一個 getPutUsers 方法,value、key 設置和 getUsers 方法保持一致,使用 @CachePut。同時手動在數據庫插入一條 nikename 爲 ityouknow 的用戶數據。

INSERT INTO `user` VALUES ('1', '[email protected]', 'ityouknow', '123456', '2018', 'keepSmile');

在瀏覽器中輸入網址 http://localhost:8080/getUsers?nickname=ityouknow,並沒有返回用戶暱稱爲 ityouknow 的用戶信息,再次輸入網址 http://localhost:8080/getPutUsers?nickname=ityouknow 可以查看到此用戶的信息,再次輸入網址 http://localhost:8080/getUsers?nickname=ityouknow 就可以看到用戶暱稱爲 ityouknow 的信息了。

說明執行在方法上聲明 @CachePut 會自動執行方法,並將結果存入緩存。

@CachePut 配置方法

  • value 緩存的名稱。
  • key 緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合。
  • condition 緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才進行緩存。

可以看出 @CachePut 的參數和使用方法基本和 @Cacheable 一致。

@CachePut 也可以標註在類上和方法上。

@CacheEvict

@CacheEvict 是用來標註在需要清除緩存元素的方法或類上的,當標記在一個類上時表示其中所有的方法的執行都會觸發緩存的清除操作。@CacheEvict 可以指定的屬性有 value、key、condition、allEntries 和 beforeInvocation,其中 value、key 和 condition 的語義與 @Cacheable 對應的屬性類似。

即 value 表示清除操作是發生在哪些 Cache 上的(對應 Cache 的名稱);key 表示需要清除的是哪個 key,如未指定則會使用默認策略生成的 key;condition 表示清除操作發生的條件。下面來介紹一下新出現的兩個屬性 allEntries 和 beforeInvocation。

allEntries 屬性

allEntries 是 boolean 類型,表示是否需要清除緩存中的所有元素,默認爲 false,表示不需要。當指定了 allEntries 爲 true 時,Spring Cache 將忽略指定的 key,有的時候我們需要 Cache 一下清除所有的元素,這比一個一個清除元素更有效率。

在上一個方法中我們使用註解:@CachePut(value="usersCache",key="#nickname") 來更新緩存,但如果不寫key="#nickname",Spring Boot 會以默認的 key 值去更新緩存,導致最上面的 getUsers() 方法並沒有獲取最新的數據。但是現在我們使用 @CacheEvict 就可以解決這個問題了,它會將所有以 usersCache 爲名的緩存全部清除。我們來看個例子:

@RequestMapping("/allEntries")
@CacheEvict(value="usersCache", allEntries=true)
public List<User> allEntries(String nickname) {
    List<User> users=userRepository.findByNickname(nickname);
    System.out.println("執行了數據庫操作");
    return users;
}

手動修改用戶表的相關信息,比如註冊時間。在瀏覽器中輸入網址 http://localhost:8080/getUsers?nickname=ityouknow 發現緩存中的數據並沒有更新,再次訪問地址 http://localhost:8080/getUsers?nickname=ityouknow 會發現數據已經更新,並且輸出欄輸出“執行了數據庫操作”,這表明已經將名爲 usersCache 的緩存記錄清空了。

beforeInvocation 屬性

清除操作默認是在對應方法成功執行之後觸發的,即方法如果因爲拋出異常而未能成功返回時也不會觸發清除操作。使用 beforeInvocation 可以改變觸發清除操作的時間,當我們指定該屬性值爲 true 時,Spring 會在調用該方法之前清除緩存中的指定元素。

@RequestMapping("/beforeInvocation")
@CacheEvict(value="usersCache", allEntries=true, beforeInvocation=true)
public void beforeInvocation() {
    throw new RuntimeException("test beforeInvocation");
}

我們來做一個測試,在方法中添加一個異常,訪問網址 http://localhost:8080/beforeInvocation 查看 usersCache 的緩存是否被更新。

按照上面的實驗步驟,手動修改用戶表的相關信息,訪問網址 http://localhost:8080/getUsers?nickname=ityouknow 發現緩存中的數據並沒有更新;再訪問網址 http://localhost:8080/beforeInvocation 會報錯誤,先不用管這裏,再次訪問地址 http://localhost:8080/getUsers?nickname=ityouknow 會發現數據已經更新,並且輸出欄輸出“執行了數據庫操作”。這表明雖然在測試的過程中方法拋出了異常,但緩存中名爲 usersCache 的記錄都已被清空。

總結一下其作用和配置方法

@Cacheable 作用和配置方法

主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存:

主要參數 解釋 舉例
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")

@CachePut 作用和配置方法

@CachePut 的作用是主要針對方法配置,能夠根據方法的請求參數對其結果進行緩存,和 @Cacheable 不同的是,它每次都會觸發真實方法的調用。

主要參數 解釋 舉例
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")

@CacheEvict 作用和配置方法

主要針對方法配置,能夠根據一定的條件對緩存進行清空。

主要參數 解釋 舉例
value 緩存的名稱,在 spring 配置文件中定義,必須指定至少一個 如 @CachEvict(value="mycache") 或者 @CachEvict(value={"cache1","cache2"}
key 緩存的 key,可以爲空,如果指定要按照 SpEL 表達式編寫,如果不指定,則缺省按照方法的所有參數進行組合 如 @CachEvict(value="testcache",key="#userName")
condition 緩存的條件,可以爲空,使用 SpEL 編寫,返回 true 或者 false,只有爲 true 才清空緩存 如 @CachEvict(value="testcache",condition="#userName.length()>2")
allEntries 是否清空所有緩存內容,缺省爲 false,如果指定爲 true,則方法調用後將立即清空所有緩存 如 @CachEvict(value="testcache",allEntries=true)
beforeInvocation 是否在方法執行前就清空,缺省爲 false,如果指定爲 true,則在方法還沒有執行的時候就清空緩存,缺省情況下,如果方法執行拋出異常,則不會清空緩存 如 @CachEvict(value="testcache",beforeInvocation=true)

@Cacheable、@CacheEvict、@CachePut 三個註解非常靈活,滿足了我們對數據緩存的絕大多數使用場景,並且使用起來非常的簡單而又強大,在實際工作中我們可以靈活搭配使用。

總結

Spring 提供了基於註釋驅動的 Spring Cache,它是一個對緩存使用的抽象,將我們常用的緩存策略都進行了高度抽象,讓我們在項目中使用時只需要添加幾個註解,即可完成大多數緩存策略的實現。Spring Boot Starter Cache 是 Spring Boot 提供給我們在 Spring Boot 中使用 Spring Cache 的 Starter 包,集成後方便在 Spring Boot 體系中使用緩存。

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