Spring Cache缓存技术的介绍

Stella981
• 阅读 941

缓存用于提升系统的性能,特别适用于一些对资源需求比较高的操作。本文介绍如何基于spring boot cache技术,使用caffeine作为具体的缓存实现,对操作的结果进行缓存。

demo场景

本demo将创建一个web应用,提供两个Rest接口。一个接口用于接受查询请求,并有条件的缓存查询结果。另一个接口用于获取所有缓存的数据,用于监控缓存的内部状态。

Spring Cache缓存技术的介绍

可以看到这次查询耗时3秒左右。

Spring Cache缓存技术的介绍

可以看到我们的查询结果已被缓存。这里将一次查询的结果缓存了两份,具体技术细节后面介绍。

接下来介绍具体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-parentspring-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缓存技术的介绍

上图简单绘制了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缓存,本文的内容相当一部分参考了这篇文章。
点赞
收藏
评论区
推荐文章
blmius blmius
3年前
MySQL:[Err] 1292 - Incorrect datetime value: ‘0000-00-00 00:00:00‘ for column ‘CREATE_TIME‘ at row 1
文章目录问题用navicat导入数据时,报错:原因这是因为当前的MySQL不支持datetime为0的情况。解决修改sql\mode:sql\mode:SQLMode定义了MySQL应支持的SQL语法、数据校验等,这样可以更容易地在不同的环境中使用MySQL。全局s
皕杰报表之UUID
​在我们用皕杰报表工具设计填报报表时,如何在新增行里自动增加id呢?能新增整数排序id吗?目前可以在新增行里自动增加id,但只能用uuid函数增加UUID编码,不能新增整数排序id。uuid函数说明:获取一个UUID,可以在填报表中用来创建数据ID语法:uuid()或uuid(sep)参数说明:sep布尔值,生成的uuid中是否包含分隔符'',缺省为
待兔 待兔
5个月前
手写Java HashMap源码
HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程HashMap的使用教程22
kenx kenx
3年前
SpringBoot 整合缓存Cacheable实战详细使用
前言我知道在接口api项目中,频繁的调用接口获取数据,查询数据库是非常耗费资源的,于是就有了缓存技术,可以把一些不常更新,或者经常使用的数据,缓存起来,然后下次再请求时候,就直接从缓存中获取,不需要再去查询数据,这样可以提供程序性能,增加用户体验,也节省服务资源浪费开销,在springboot帮你我们做好了整合,有对应的场景启动器start,我们之间引入使用
Souleigh ✨ Souleigh ✨
3年前
前端性能优化 - 雅虎军规
无论是在工作中,还是在面试中,web前端性能的优化都是很重要的,那么我们进行优化需要从哪些方面入手呢?可以遵循雅虎的前端优化35条军规,这样对于优化有一个比较清晰的方向.35条军规1.尽量减少HTTP请求个数——须权衡2.使用CDN(内容分发网络)3.为文件头指定Expires或CacheControl,使内容具有缓存性。4.避免空的
Wesley13 Wesley13
3年前
35岁是技术人的天花板吗?
35岁是技术人的天花板吗?我非常不认同“35岁现象”,人类没有那么脆弱,人类的智力不会说是35岁之后就停止发展,更不是说35岁之后就没有机会了。马云35岁还在教书,任正非35岁还在工厂上班。为什么技术人员到35岁就应该退役了呢?所以35岁根本就不是一个问题,我今年已经37岁了,我发现我才刚刚找到自己的节奏,刚刚上路。
Stella981 Stella981
3年前
SpringBoot2.x版本整合Redis进行数据缓存
项目放在github:在缓存开发中,有两个重要的接口:在这里面:  @Cacheable:  如果用这个注解标注在方法上,那么方法的结果就会被缓存存起来,这个多用于在查询的时候进行使用    比如: publicusergetuser(Integerid) 这个方法用这个注解标注的话,通过id查到的内容就会杯存在缓存中进行保存
Easter79 Easter79
3年前
SpringBoot2.x版本整合Redis进行数据缓存
项目放在github:在缓存开发中,有两个重要的接口:在这里面:  @Cacheable:  如果用这个注解标注在方法上,那么方法的结果就会被缓存存起来,这个多用于在查询的时候进行使用    比如: publicusergetuser(Integerid) 这个方法用这个注解标注的话,通过id查到的内容就会杯存在缓存中进行保存
Wesley13 Wesley13
3年前
mysql基础之查询缓存、存储引擎
一、查询缓存    “查询缓存”,就是将查询的结果缓存下载,如果查询语句完全相同,则直接返回缓存中的结果。  如果应用程序在某个场景中,需要经常执行大量的相同的查询,而且查询出的数据不会经常被更新,那么,使用查询缓存会有一定的性能提升。查看当前服务是否开启了查询缓存功能:!复制代码(https://oscimg.oschina.n
京东云开发者 京东云开发者
7个月前
CaffeineCache Api介绍以及与Guava Cache性能对比| 京东物流技术团队
一、简单介绍:CaffeineCache和Guava的Cache是应用广泛的本地缓存。在开发中,为了达到降低依赖、提高访问速度的目的。会使用它存储一些维表接口的返回值和数据库查询结果,在有些场景下也会在分布式缓存上再加上一层本地缓存,用来减少对远程服务和数