SpringCache

日常开发中,对数据进行缓存是优化性能极为重要的一环。Java可以使用的缓存组件有很多,比如JDK自带并发工具包中的ConcurrentMap,或是基于JVM内存的EhCache,以及后来流行起来的缓存中间件MemcachedRedis等。我们直接使用这些组件实现数据缓存,要不胜其烦的编写很多代码,比如“判断缓存中是否存在数据,存在则取缓存,不存在则查数据库并写入缓存”这样的逻辑。SpringCache则对缓存做了一个封装,并支持注解配置,使用非常方便,一些最简单的缓存逻辑我们可以使用该功能来极大的简化代码。

这篇笔记我们以SpringBoot3.x工程为例,介绍SpringCache的用法。

缓存相关注解

SpringCache提供了几个注解封装了不同的缓存组件,使用非常方便。

@Cacheable:先判断缓存是否存在,如果缓存存在直接返回缓存,如果缓存不存在则将方法执行后的结果添加到缓存。

@CachePut:执行方法后,将方法执行后的结果更新到缓存。

@CacheEvict:执行方法后移除指定key的缓存。

这些注解可以标注在方法上,它们可以单独使用也可以同时使用。以上方法常用的参数有valuekey

  • value:缓存命名空间,一个命名空间对应一组缓存,该参数是必传的。
  • key:缓存键,可以使用固定字符串,也可以使用SpEL表达式指定,可以指定为操作的数据的主键。如果不指定,SpringCache会根据方法参数值生成缓存键。
  • condition:缓存逻辑执行的条件,使用SpEL进行条件判断。

@Caching:用于组合以上操作。

引入依赖和框架配置

SpringCache提供了SpringBoot的起步依赖,我们将其引入。

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

此外,还需要在application.properties配置文件中配置缓存类型。

spring.cache.type=jcache

这里我们需要根据实际使用的缓存底层实现选择,后文会介绍集成EhCache(通过JCache规范)和Redis的方法,但要注意不同的缓存底层实现可能在配置方法上有区别。

最后,我们需要在工程的启动类上添加@EnableCaching注解配置开启缓存。

package com.gacfox.demo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@SpringBootApplication
@EnableCaching
public class DemocacheApplication {
    public static void main(String[] args) {
        SpringApplication.run(DemocacheApplication.class, args);
    }
}

不过此时还SpringCache还不能使用,我们还得集成缓存组件,下面我们介绍集成EhCache和Redis两种组件。

集成EhCache

对于单机应用,使用内存作为缓存性能相比Redis更好,EhCache是一个比较常用的内存缓存框架。如果使用EhCache缓存,我们需要引入JSR-107(JCache)和EhCache依赖。在SpringBoot工程中,这两个依赖的版本都由SpringBoot基础工程管理,我们可以不指定版本号。

<dependency>
    <groupId>org.ehcache</groupId>
    <artifactId>ehcache</artifactId>
    <classifier>jakarta</classifier>
</dependency>
<dependency>
    <groupId>javax.cache</groupId>
    <artifactId>cache-api</artifactId>
</dependency>

注意:SpringBoot2.x和3.x这里发生了破坏性变更。对于旧版本SpringBoot2.x,需要引入的是net.sf.ehcache下的ehcache包,而升级到Spring3.x后,首先是EhCache的groupIdversion都发生了变化,其次是集成缓存的API从直接调用EhCache变更为了JSR-107(JCache)。

application.properties配置中,我们需要配置缓存类型为jcache,此外还需要指定ehcache.xml配置文件的路径。

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

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"
        xmlns:jsr107="http://www.ehcache.org/v3/jsr107"
        xsi:schemaLocation="http://www.ehcache.org/v3
                            http://www.ehcache.org/schema/ehcache-core-3.10.xsd
                            http://www.ehcache.org/v3/jsr107
                            http://www.ehcache.org/schema/ehcache-107-ext-3.10.xsd">
    <!-- 磁盘持久化目录 -->
    <persistence directory="java.io.tmpdir"/>
    <cache alias="users">
        <!-- 键类型 -->
        <key-type>java.lang.String</key-type>
        <!-- 值类型 -->
        <value-type>java.util.List</value-type>
        <!-- 过期策略 -->
        <expiry>
            <ttl unit="seconds">120</ttl>
        </expiry>
        <!-- 存储层配置 -->
        <resources>
            <!-- 堆内最多1024条 -->
            <heap unit="entries">1024</heap>
            <!-- 开启磁盘溢出,重启后不持久化 -->
            <disk unit="MB" persistent="false">100</disk>
        </resources>
    </cache>
</config>

ehcache.xml配置文件中,我们需要根据实际需要配置缓存的类型、过期策略、持久化策略等参数。此时SpringCache缓存就可以正常工作了。

集成Redis

SpringCache集成Redis非常简单,我们需要引入Spring Data Redis依赖。

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

引入依赖后,首先配置Spring Data Redis的连接信息,然后配置SpringCache缓存类型为redis即可。此外,这里也可以对缓存的键前缀、超时时间等进行配置。

spring.data.redis.host=127.0.0.1
spring.data.redis.port=6379

spring.cache.type=redis
spring.cache.redis.time-to-live=120s
spring.cache.redis.use-key-prefix=true
spring.cache.redis.key-prefix=CACHE_

缓存方法返回值

下面例子中,我们使用@Cacheable缓存了queryUserList()方法的查询结果。该方法第一次调用时会真实执行内部的代码,但随后的调用中,在缓存过期之前,都会返回缓存的数据。

package com.gacfox.demo.service;

import com.gacfox.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

import java.util.List;

@Slf4j
@Service("demoService")
public class DemoService {
    @Cacheable(cacheNames = "users", key = "'ALL'")
    public List<User> queryUserList() {
        // 模拟查询用户列表
        log.info("查询用户列表");
        User u1 = User.builder().name("Tom").build();
        User u2 = User.builder().name("Jerry").build();
        return List.of(u1, u2);
    }
}

代码中,cacheNames是缓存名,如果你使用的是EhCache,这个名字需要和配置文件中的配置项对应;如果你用的是Redis,它会作为Redis Key的一部分自动创建。key属性是缓存的键,这个属性非常重要,我们可以看到它是一个SpEL表达式,通常是和方法的参数完全对应的,我们这里被缓存的方法没有参数,因此使用了固定的键名ALL。此外由于key属性的值是SpEL表达式,因此字符串ALL外我们需要增加单引号。

如果被缓存的方法有参数,我们可以取参数作为缓存键。

@Cacheable(cacheNames = "users", key = "#id")

实际上,SpEL是一种表达式,其中可以包含字符串拼接、计算等逻辑,因此如果有多个逻辑也问题不大,我们用SpEL拼接即可。

@Cacheable(cacheNames = "district", key = "#cityCode + '_' + #districtName")
public District getAreaByName(String cityCode, String districtName) { }

手动清除缓存

默认情况下,缓存会在超时后删除,但有时我们也需要手动让缓存失效,此时可以使用@CacheEvict。下面例子代码中,我们对queryUserList()方法进行了缓存,它使用ALL作为键。而执行updateUsers()方法会导致ALL键缓存的清除。

@Slf4j
@Service("demoService")
public class DemoService {
    @Cacheable(cacheNames = "users", key = "'ALL'")
    public List<User> queryUserList() {
        // ... 具体的业务逻辑
        return users;
    }

    @CacheEvict(cacheNames = "users", key = "'ALL'")
    public void updateUsers() {
        // ... 具体的业务逻辑
    }
}

默认情况下,@CacheEvict在方法执行后移除缓存,我们也可以指定参数beforeInvocation = true指定在方法执行前移除缓存。

编程式操作缓存

除了使用SpringCache提供的注解,我们也可以通过代码的方式读写缓存,这需要使用SpringCache的CacheManager接口。项目中配置SpringCache的起步依赖后,我们可以通过依赖注入的方式获取配置好的缓存实现,下面例子中,我们注入CacheManager实现并读取缓存。

package com.gacfox.demo.service;

import com.gacfox.demo.model.User;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.stereotype.Service;
import org.springframework.util.StringUtils;

@Slf4j
@Service("demoService")
public class DemoService {
    private final CacheManager cacheManager;

    @Autowired
    public DemoService(CacheManager cacheManager) {
        this.cacheManager = cacheManager;
    }

    public User queryUserById(String id) {
        if (!StringUtils.hasText(id)) {
            return null;
        }
        Cache cache = cacheManager.getCache("users");
        if (cache == null) {
            throw new RuntimeException("找不到缓存配置");
        }
        User user = cache.get(id, User.class);
        if (user != null) {
            return user;
        }

        // 模拟从数据库查询
        log.info("查询用户列表");
        User dbUser = User.builder().name("Tom").build();
        if (dbUser != null) {
            cache.put(id, dbUser);
        }
        return dbUser;
    }
}

代码中,我们先通过CacheManager实现获取了Cache实例,它需要一个缓存名,这和@CacheablecacheNames作用是相同的。获取Cache实例后,我们就可以通过get()put()evict()clear()等方法操作缓存了。

作者:Gacfox
版权声明:本网站为非盈利性质,文章如非特殊说明均为原创,版权遵循知识共享协议CC BY-NC-ND 4.0进行授权,转载必须署名,禁止用于商业目的或演绎修改后转载。