一个系统的瓶颈通常在与数据库交互的过程中。当我们重复地获取相同的数据的时候,一次又一次的请求数据库或者远程服务,这无疑是性能上的浪费——会导致大量的时间耗费在数据库查询或者远程方法调用上,导致程序性能恶化。缓存(Cache)作为数据交换的缓冲区,是应用系统性能提升的大杀器。内存的读写速度通常是硬盘的几十倍到上百倍,缓存实际上就是把热点数据保存在内存中,利用内存的高速读写特性提高热点数据的操作速度。

Spring Boot 中使用缓存非常简单,并且支持多种实现缓存的方式。本章讨论比较常用的几种缓存实现方式,及其对应的场景。

15.1 Spring Boot 默认缓存

Spring Boot 默认缓存是基于 ConcurrenMapCacheManager 缓存管理器实现的,从这个类名就能发现它本质上应该是一个 Map 集合容器。它的结构比较简单,一般用于比较轻量级的缓存使用场景。也就是缓存的数据量比较小,缓存操作不是特别频繁的场景。本节简单介绍默认缓存的使用方法。

15.1.1 引入项目依赖

我们引入如下的缓存依赖:

  1. <dependency>
  2. <groupId>org.springframework.boot</groupId>
  3. <artifactId>spring-boot-starter-cache</artifactId>
  4. </dependency>

15.1.3 缓存的注解说明

@CacheConfig

当我们需要缓存的地方越来越多,你可以使用@CacheConfig(cacheNames = {“cacheName”})注解在 class 之上来统一指定value的值,这时可省略value,如果你在你的方法依旧写上了value,那么依然以方法的value值为准。

@Cacheable

根据方法对其返回结果进行缓存,下次请求时,如果缓存存在,则直接读取缓存数据返回;如果缓存不存在,则执行方法,并把返回的结果存入缓存中。一般用在查询方法上。 查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
keyGenerator key的生成器。key/keyGenerator二选一使用
cacheManager 指定缓存管理器
cacheResolver 指定获取解析器
condition 条件符合则缓存
unless 条件符合则不缓存
sync 是否使用异步模式,默认为false

@CachePut

使用该注解标志的方法,每次都会执行,并将结果存入指定的缓存中。其他方法可以直接从响应的缓存中读取缓存数据,而不需要再去查询数据库。一般用在新增方法上。 查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
keyGenerator key的生成器。key/keyGenerator二选一使用
cacheManager 指定缓存管理器
cacheResolver 指定获取解析器
condition 条件符合则缓存
unless 条件符合则不缓存

@CacheEvict

使用该注解标志的方法,会清空指定的缓存。一般用在更新或者删除方法上 查看源码,属性值如下:

属性/方法名 解释
value 缓存名,必填,它指定了你的缓存存放在哪块命名空间
cacheNames 与 value 差不多,二选一即可
key 可选属性,可以使用 SpEL 标签自定义缓存的key
keyGenerator key的生成器。key/keyGenerator二选一使用
cacheManager 指定缓存管理器
cacheResolver 指定获取解析器
condition 条件符合则缓存
allEntries 是否清空所有缓存,默认为 false。如果true,则方法调用后将立即清空所有的缓存
beforeInvocation 是否在方法执行前就清空,默认为 false。如果 true,则在方法执行前就会清空缓存

@Caching

该注解可以实现同一个方法上同时使用多种注解。

15.2 设计示例代码

15.2.1 开启缓存

在启动类上添加注解 @EnableCaching 开启缓存功能。

@EnableCaching
@SpringBootApplication
public class CloudApplication {

15.2.2 示例数据对象

package com.longser.union.cloud.data.model;

import lombok.Data;

import java.io.Serializable;

@Data
public class Goods implements Serializable {
    private static final long serialVersionUID = 1L;

    long id;
    String name;
}

这里一定要实现 Serializable 接口

15.2.3 数据访问的模拟代码

正常服务层方法会调用数据访问层方法访问数据库,此处我们只需要演示缓存的作用,所以打印日志代替数据库访问方法。

package com.longser.union.cloud.service;

import com.longser.union.cloud.data.model.Goods;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.cache.annotation.CacheConfig;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

/**
 * 商品服务类
 * @author David
 *
 * 注解 @CacheConfig 注解用于指定本类中方法使用的缓存名称,该类使用的缓存名称为 GoodsCache ,
 *  与其他缓存区域是隔离的。
 */
@Service
@CacheConfig(cacheNames = "GoodsCache")
public class GoodsService {

    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    /**
     * 按id获取商品信息
     *
     * 注解 @Cacheable 用于开启方法缓存,缓存的键是方法的参数,缓存的值是方法的返回值。
     * 如果多次调用该方法时参数 id 值相同,则第一次会执行方法体,并将返回值放入缓存;
     * 后续方法不会再执行方法体,直接将缓存的值返回。
     */
    @Cacheable
    public Goods getById(Long id) {
        logger.info("getById({})", id);
        Goods goods = new Goods();
        goods.setId(id);
        goods.setName("goods-" + id);
        return goods;
    }

    /**
     * 删除商品
     *
     * 注解 @CachePut 可以更新缓存,key = "#id" 表示采用参数中的 id 属性作为键。
     * 当缓存中该键的值不存在时,则将返回值放入缓存;当缓存中该键的值已存在时,
     * 会更新缓存的内容。
     */
    @CacheEvict(key = "#id")
    public void remove(Long id) {
        logger.info("remove({})", id);
    }

    /**
     * 编辑商品信息
     *
     * 注解 @CacheEvict 可以移除缓存,当调用该方法时,会移除 goods 中 id 属性对应的缓存内容。
     */
    @CachePut(key = "#goods.id")
    public Goods edit(Goods goods) {
        logger.info("edit id:{}", goods.getId());
        return goods;
    }
}

15.2.4 测试默认缓存

为了充分理解缓存的含义,我们通过测试类发起测试。

package com.longser.union.cloud.cache;

import com.longser.union.cloud.data.model.Goods;
import com.longser.union.cloud.service.GoodsService;
import org.junit.jupiter.api.Test;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.cache.CacheManager;

@SpringBootTest
public class CacheTest {
    private final Logger logger = LoggerFactory.getLogger(this.getClass());

    @Autowired
    private CacheManager cacheManager;

    @Autowired
    private GoodsService goodsService;

    // 显示当前使用的缓存管理器类型
    @Test
    public void showCacheManager() {
        // 输出当前的缓存管理器名称
        // 默认Cache值为 org.springframework.cache.concurrent.ConcurrentMapCacheManager
        // JCache 值为 org.springframework.cache.jcache.JCacheCacheManager
       System.out.println("Cache Manager: " + cacheManager.getClass().toString());
    }

    // 缓存测试
    @Test
    public void cacheTest() {
        // 第一次执行,没有缓存,执行方法体
        goodsService.getById(1L);
        // 再次执行,直接取出缓存,不执行方法体
        goodsService.getById(1L);
        // 第一次执行,没有缓存,执行方法体
        goodsService.getById(8L);
        // 移除缓存
        goodsService.remove(1L);
        // 再次执行,已经没有对应缓存,所以执行方法体
        Goods oldGoods = goodsService.getById(1L);
        // 打印缓存内容
        logger.info("old goods id:{} name:{}", oldGoods.getId(), oldGoods.getName());
        // 更新缓存
        Goods temp = new Goods();
        temp.setId(1L);
        temp.setName("新的商品");
        goodsService.edit(temp);
        // 查询并打印已更新的缓存内容
        Goods newGoods = goodsService.getById(1L);
        logger.info("new goods id:{} name:{}", newGoods.getId(), newGoods.getName());
    }
}

showCacheManager 的输出是

Cache Manager: class org.springframework.cache.concurrent.ConcurrentMapCacheManager

cacheTest 的输出如下,验证了设计的缓存机制。
image.png

15.3 使用 Ehcache 缓存

Spring Boot 默认的缓存实现比较简单,功能也十分有限。如果是企业级的中大型应用,需要寻求更加稳定、可靠的缓存框架。

Ehcache 是 Java 编程领域非常著名的缓存框架,具备两级缓存数据——内存和磁盘,因此不必担心内存容量问题。另外 Ehcache 缓存的数据会在 JVM 重启时自动加载,不必担心断电丢失缓存的问题。

Ehcache 的功能完整性和运行稳定性远远强于 Spring Boot 默认的缓存实现方式,而且 Spring Boot 使用 Ehcache 非常便捷,它可以直接使用或者做为 JCache 的 Provider。接下来我们就后者的方式来实现缓存,它的好处是只需要完成简单的配置,代码中已经有的那些调用 JSR 107 标准 JCache API 的代码完全不需要修改。

15.3.1 添加 Ehcache 依赖

在 spring-boot-cache 项目的基础上添加 Ehcache 依赖。

        <dependency>
            <groupId>org.ehcache</groupId>
            <artifactId>ehcache</artifactId>
            <version>3.9.7</version>
        </dependency>
        <dependency>
            <groupId>javax.cache</groupId>
            <artifactId>cache-api</artifactId>
            <version>1.1.1</version>
        </dependency>
        <!-- API, java.xml.bind module -->
        <dependency>
            <groupId>jakarta.xml.bind</groupId>
            <artifactId>jakarta.xml.bind-api</artifactId>
            <version>2.3.2</version>
        </dependency>
        <!-- Runtime, com.sun.xml.bind module -->
        <dependency>
            <groupId>org.glassfish.jaxb</groupId>
            <artifactId>jaxb-runtime</artifactId>
            <version>2.3.2</version>
        </dependency>

java.xml.bind 和 com.sun.xml.bind 本来是包含在 JDK 8中的,但 JDK11 开始不再包括他们,因此这里需要专门设置依赖。

15.3.2 添加 Ehcache 配置文件

首先在 application.yaml 中指定配置文件的位置。

spring:
  cache:
    type: jcache
    jcache:
      config: classpath:config/ehcache.xml

然后在 resource/ 文件夹中添加 ehcache.xml 配置文件,内容如下:

<?xml version="1.0" encoding="UTF-8"?>
<config xmlns:xsi='http://www.w3.org/2001/XMLSchema-instance'
        xmlns='http://www.ehcache.org/v3'
        xsi:schemaLocation="http://www.ehcache.org/v3 http://www.ehcache.org/schema/ehcache-core-3.0.xsd">
    <!-- 持久化路径 -->
    <persistence directory="~/" />
    <!--缓存模板 -->
    <cache-template name="CacheTemplate">
        <expiry>
            <!--存活时间 -->
            <tti>60</tti>
        </expiry>
        <resources>
            <!--堆空间 -->
            <heap unit="entries">2000</heap>
            <!-- 堆外空间 -->
            <offheap unit="MB">500</offheap>
        </resources>
    </cache-template>

    <!--缓存对象 -->
    <cache alias="GoodsCache" uses-template="CacheTemplate">
    </cache>
</config>

Ehcache 的配置比较复杂,此处只是给出简单的示例,感兴趣的可以查阅更多资料。

15.3.3 测试Ehcache缓存

无须额外配置,再次运行测试代码。cacheTest() 的运行结果和之前的一致,而showCacheManager()输出的缓存管理器名称应该为 **org.springframework.cache.jcache.JCacheCacheManager**

15.4 小结

Spring Boot 支持多种缓存实现方式,可以根据项目需求灵活选择。

  • 缓存数据量较小的项目,可以使用 Spring Boot 默认缓存。
  • 缓存数据量较大的项目,可以考虑使用 Ehcache 缓存框架。
  • 如果是大型系统,对缓存的依赖性比较高,应该采用下一章讨论的方式,使用独立的缓存组件 Redis ,通过主备、集群等形式提高缓存服务的性能和稳定性。

版权说明:本文由北京朗思云网科技股份有限公司原创,向互联网开放全部内容但保留所有权力。