缓存用于提高系统的性能,特别适用于一些对资源需求比较高的操做。本文介绍如何基于spring boot cache技术,使用caffeine做为具体的缓存实现,对操做的结果进行缓存。html
本demo将建立一个web应用,提供两个Rest接口。一个接口用于接受查询请求,并有条件的缓存查询结果。另外一个接口用于获取全部缓存的数据,用于监控缓存的内部状态。java
能够看到此次查询耗时3秒左右。git
能够看到咱们的查询结果已被缓存。这里将一次查询的结果缓存了两份,具体技术细节后面介绍。github
接下来介绍具体demo的实现过程。web
本demo已经上传到github,读者能够在github上获取源码。redis
本demo使用Maven做为项目构建工具。按照做者的平常编程习惯,首先建立了一个root module,用于统一管理依赖。具体的功能在子module caffeine-cache中。spring
本demo的代码结构以下:apache
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
<?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
管理了大部分经常使用的依赖,使用这些依赖咱们不用再费心考虑版本的问题。json可是maven是单继承结构,继承了
spring-boot-starter-parent
就没法继承本身项目当中的parent module(root module)。在一个多module的项目当中,module之间的相互依赖就不是spring-boot-starter-parent
能预先管理的了。因此在实际项目当中,咱们通常不会直接继承
spring-boot-starter-parent
。而是经过在root module中importspring-boot-dependencies
,来享受spring-boot为咱们管理依赖的便利,同时在root module管理额外的依赖。具体的技术细节须要读者参考Maven的知识。做者只是阐述下这么作的缘由,实际上跟demo自己的功能没有多大关系。
<?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 cache提供了一层抽象和使用接口,底层能够切换不一样的cache实现,caffeine就是其中之一,且性能表现较优。
spring cache还能够与redis集成,提供分布式缓存的能力。
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
注解用于启用缓存,没有这个注解,咱们后面的缓存功能将不会生效。
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
配置了两个缓存指标:
在上文获取缓存的接口中,咱们获得的结果是:
{ "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,即以参数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。
上文demo中咱们使用unless属性对方法返回的结果进行判断,当返回结果知足必定条件时才进行缓存。
另外,咱们还可使用condition属性对方法的参数进行判断:
@Cacheable(cacheNames="book", condition="#name.length() < 32") public Book findBook(String name)
上述代码表示,只有当参数的长度小于32时,咱们才会缓存。
更多内容能够参考Conditional Caching。