Spring Cache緩存技術的介紹
緩存用於提升系統的性能,特別適用於一些對資源需求比較高的操作。本文介紹如何基於spring boot cache技術,使用caffeine作為具體的緩存實現,對操作的結果進行緩存。
demo場景
本demo將創建一個web應用,提供兩個Rest接口。一個接口用於接受查詢請求,並有條件的緩存查詢結果。另一個接口用於獲取所有緩存的數據,用於監控緩存的內部狀態。
可以看到這次查詢耗時3秒左右。
可以看到我們的查詢結果已被緩存。這裏將一次查詢的結果緩存了兩份,具體技術細節後面介紹。
接下來介紹具體demo的實現過程。
demo實現
本demo已經上傳到github,讀者可以在github上獲取源碼。
本demo使用Maven作為項目構建工具。按照作者的日常編程習慣,首先創建了一個root module,用於統一管理依賴。具體的功能在子module caffeine-cache中。
本demo的代碼結構如下:
demo-spring-cache/ |- pom.xml L caffeine-cache/ |- pom.xml L src/ L main/ |- java/ | L heyikan | |- Application.yml | |- QueryController.java | L QueryService.java L resources/ L application.yml
創建root module
<?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>com.heyikan.demo</groupId> <artifactId>demo-spring-cache</artifactId> <version>1.0-SNAPSHOT</version> <packaging>pom</packaging> <modules> <module>caffeine-cache</module> </modules> <properties> <java.version>1.8</java.version> <maven.compiler.source>${java.version}</maven.compiler.source> <maven.compiler.target>${java.version}</maven.compiler.target> <spring-boot.version>2.1.3.RELEASE</spring-boot.version> </properties> <dependencyManagement> <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-dependencies</artifactId> <version>${spring-boot.version}</version> <type>pom</type> <scope>import</scope> </dependency> </dependencies> </dependencyManagement> </project>
root module的主要作用是統一管理依賴。當項目中有多個module的時候,作者一般會構建一個root module,然後其他的moudule都繼承自這個module,形成一個兩級module的繼承結構。
網上大部分的demo,一般是直接創建目標module,且繼承自
spring-boot-starter-parent
。spring-boot-starter-parent
管理了大部分常用的依賴,使用這些依賴我們不用再費心考慮版本的問題。但是maven是單繼承結構,繼承了
spring-boot-starter-parent
就無法繼承自己項目當中的parent module(root module)。在一個多module的項目當中,module之間的相互依賴就不是spring-boot-starter-parent
能預先管理的了。所以在實際項目當中,我們一般不會直接繼承
spring-boot-starter-parent
。而是通過在root module中import?spring-boot-dependencies
,來享受spring-boot為我們管理依賴的便利,同時在root module管理額外的依賴。具體的技術細節需要讀者參考Maven的知識。作者只是闡述下這麽做的原因,實際上跟demo本身的功能沒有多大關系。
創建目標module
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
<parent>
<artifactId>demo-spring-cache</artifactId>
<groupId>com.heyikan.demo</groupId>
<version>1.0-SNAPSHOT</version>
</parent>
<modelVersion>4.0.0</modelVersion>
<artifactId>caffeine-cache</artifactId>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
這個module主要引入了三個依賴:
- spring-boot-starter-web
打包了web項目的常規依賴 - spring-boot-starter-cache
打包了依賴功能的常規依賴 - caffeine
具體的依賴實現
spring cache提供了一層抽象和使用接口,底層可以切換不同的cache實現,caffeine就是其中之一,且性能表現較優。
spring cache還可以與redis集成,提供分布式緩存的能力。
創建Application
package heyikan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
熟悉spring-boot項目的讀者應該對此比較熟悉,spring-boot項目需要創建一個Application來啟動整個應用。
@EnableCaching
註解用於啟用緩存,沒有這個註解,我們後面的緩存功能將不會生效。
創建Controller
package heyikan;
import com.github.benmanes.caffeine.cache.Cache;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.CacheManager;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.Map;
import java.util.concurrent.ConcurrentMap;
import java.util.function.Function;
import java.util.stream.Collectors;
@RestController
public class QueryController {
@Autowired
private QueryService queryService;
@GetMapping("/query")
public ResponseEntity<?> query(String keyWord) {
String result = queryService.query(keyWord);
return ResponseEntity.ok(result);
}
@Autowired
@SuppressWarnings("all")
private CacheManager cacheManager;
@GetMapping("/caches")
public ResponseEntity<?> getCache() {
Map<String, ConcurrentMap> cacheMap = cacheManager.getCacheNames().stream()
.collect(Collectors.toMap(Function.identity(), name -> {
Cache cache = (Cache) cacheManager.getCache(name).getNativeCache();
return cache.asMap();
}));
return ResponseEntity.ok(cacheMap);
}
}
QueryController提供了兩個Rest接口,query用於模擬耗時的查詢請求,getCache用於獲取當前的緩存內容。
QueryController中引入了QueryService依賴,它是提供查詢和緩存功能的核心組件。
QueryController中引入了CacheManager依賴,它持有所有的緩存,並提供了遍歷的API。
創建緩存組件
package heyikan;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
@CacheConfig(cacheNames = {"query-result", "demo"})
public class QueryService {
private static Logger LOG = LoggerFactory.getLogger(QueryService.class);
@Cacheable(unless = "#result.length() > 20")
public String query(String keyWord) {
LOG.info("do query by keyWord: {}", keyWord);
String queryResult = doQuery(keyWord);
return queryResult;
}
private String doQuery(String keyWord) {
try {
Thread.sleep(3000L);
String result = "result of " + keyWord;
return result;
} catch (InterruptedException e) {
throw new IllegalStateException(e);
}
}
}
我們使用@CacheConfig
配置緩存,如代碼所示,數據將會同時緩存到"query-result"和"demo"中。
query
方法是查詢的入口,@Cacheable
註解用於表示query方法的返回結果將被放到緩存中,默認以方法的參數作為key。
@Cacheable
註解的unless屬性補充了緩存的條件,按照代碼所示,當query的返回結果其長度大於20的時候,就不會進行緩存。
doQuery方法代表實際的查詢操作,模擬耗時的查詢過程。
創建配置
application.yml文件內容如下:
spring:
cache:
caffeine:
spec: maximumSize=500, expireAfterAccess=30s
logging:
pattern:
console: "%-5level - %msg%n"
level:
- error
- heyikan=ALL
spring.cache.caffeine.spec
配置了兩個緩存指標:
- maximumSize
配置緩存的最大容量,當快要達到容量上限的時候,緩存管理器會根據一定的策略將部分緩存項移除。 - expireAfterAccess
配置緩存項的過期機制,如代碼所示當緩存項被訪問後30秒將會過期,從而被移除。
技術要點
緩存的結構
在上文獲取緩存的接口中,我們得到的結果是:
{
"query-result": {
"spring": "result of spring"
},
"demo": {
"spring": "result of spring"
}
}
緩存的結構大概像Map<cacheName, Map<key, value>>
,其中每一對key-value又稱為一個緩存項。
上文中,我們緩存組件的query方法的返回結果,就是以參數為key,以結果為value,構建緩存項進行緩存的。
另外,我們配置的超時時間,也是以緩存項為粒度進行控制的。
包含緩存項的Map我們稱為緩存實例,每一個實例有一個實例名(cacheName)。
cache結構相關的類圖如下:
上圖簡單繪制了Spring中定義的Cache接口和caffeine中定義的Cache接口。
Spring的Cache定義了極其通用的方法,包括獲取實例名、根據緩存項的key獲取、更新和移除緩存項。
Spring並沒有限定緩存所使用的具體存儲結構,不管使用哪一種存儲結構,在Spring的Cache中都以nativeCache進行表示,註意它是Object類型的。
caffeine的Cache接口,就是caffeine對nativeCache的又一層抽象,它提供了asMap方法可以對緩存項進行遍歷。
使用緩存
在上文中,我們已經簡單演示了如何使用緩存。除了獲取緩存之外,我們幾乎沒有任何額外的代碼,只是在合適的地方,添加了註解,就添加了緩存的功能。
所以在日常開發中,如果我們意識到某個操作可能會有很大開銷,不妨把它移到一個獨立的組件,實現之後根據具體情況考慮是否為它添加緩存。
註意:如果緩存的方法是組件內部調用的,可能沒有緩存的效果。
比如,上文中的QueryService的query方法,是由QueryController調用的,緩存生效了。如果該方法由QueryService自身的其他方法調用,緩存無效。
在上文的demo中,我們已經使用了一些基本的功能,還有一些常用的功能如下:
指定key構建規則
在上文中,我們使用默認的規則來構建緩存項的key,即以參數keyWord作為key。
在必要的情況下,我們可以指定key構建的規則,使用spring el表達式:
@Cacheable(cacheNames="books", key="#isbn")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="#isbn.rawNumber")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
@Cacheable(cacheNames="books", key="T(someType).hash(#isbn)")
public Book findBook(ISBN isbn, boolean checkWarehouse, boolean includeUsed)
第一個實例,我們使用三個參數中的其中一個來構建key。
第二個實例,我們使用參數內部的field來構建key。
第三個實例,我們使用靜態方法來生成key。
更多內容可以參考Custom Key Generation Declaration。
有選擇的cache
上文demo中我們使用unless屬性對方法返回的結果進行判斷,當返回結果滿足一定條件時才進行緩存。
另外,我們還可以使用condition屬性對方法的參數進行判斷:
@Cacheable(cacheNames="book", condition="#name.length() < 32")
public Book findBook(String name)
上述代碼表示,只有當參數的長度小於32時,我們才會緩存。
更多內容可以參考Conditional Caching。
擴展閱讀
- Spring官方demo
這裏提供了使用默認緩存的demo,內容更加簡單,適合對spring-boot不熟悉的讀者。 - Spring官方文檔
這裏有對如何使用cache的詳細介紹,比如如何主動更新緩存、移除緩存,都是本demo中沒有的內容。 - Spring Boot Caffeine Caching Example Configuration
這裏介紹了如何使用Caffeine緩存,本文的內容相當一部分參考了這篇文章。
Spring Cache緩存技術的介紹