什么是 Spring Cache?

Spring Cache是Spring框架提供的一层缓存抽象,旨在简化应用程序中的缓存管理。通过使用Spring Cache,开发者能够在方法级别方便地定义缓存策略,提高应用性能、响应速度,并减轻底层数据源的负载。该框架提供一系列注解,如@Cacheable@CacheEvict@CachePut,以及对多种底层缓存实现的支持,如EhCache、Redis等。它为应用程序提供了一种统一、方便、灵活的缓存管理方式,允许开发者通过简单的配置实现复杂的缓存逻辑,同时与Spring框架紧密集成。这种抽象层的存在使得在更改底层缓存框架时更为轻松,同时提供了一致的配置接口和更强大的高级特性。

Spring Cache 常用 API

@Cacheable

@Cacheable 用于标记一个方法的结果应该被缓存。当在该方法被调用时,Spring首先检查缓存中是否已经有了预期的结果,如果有,直接返回缓存中的结果而不执行实际的方法体。

javaCopy code@Cacheable(cacheNames = "exampleCache", key = "'exampleKey'")
public String getCachedData() {
    // 执行实际的方法体,结果会被缓存
}

@CacheEvict

@CacheEvict 用于标记一个方法在执行后清空缓存。可以指定清空的缓存名称和清空的条件。

javaCopy code@CacheEvict(cacheNames = "exampleCache", key = "'exampleKey'")
public void clearCache() {
    // 执行实际的方法体,之后清空缓存
}

@CachePut

@CachePut 用于标记一个方法的结果应该被放入缓存,常用于在方法执行后更新缓存。

javaCopy code@CachePut(cacheNames = "exampleCache", key = "'exampleKey'")
public String updateCache() {
    // 执行实际的方法体,结果会被放入缓存
}

@Caching

@Caching 允许同时应用多个缓存操作注解。

javaCopy code@Caching(
    evict = {
        @CacheEvict(cacheNames = "cache1", key = "'key1'"),
        @CacheEvict(cacheNames = "cache2", key = "'key2'")
    }
)
public void clearMultipleCaches() {
    // 执行实际的方法体,清空多个缓存
}

Spring Boot 整合 Spring Cache (Redis 缓存)

完整项目源码:youlai-boot

项目依赖 pom.xml

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

项目配置 application.yml

spring:
  data:
    redis:
      database: 6
      host: localhost
      port: 6379
      password: 123456
      timeout: 10s
      lettuce:
        pool:
          # 连接池最大连接数 默认8 ,负数表示没有限制
          max-active: 8
          # 连接池最大阻塞等待时间(使用负值表示没有限制) 默认-1
          max-wait: -1
          # 连接池中的最大空闲连接 默认8
          max-idle: 8
          # 连接池中的最小空闲连接 默认0
          min-idle: 0
  cache:
    # 缓存类型 redis、none(不使用缓存)
    type: redis
    # 缓存时间(单位:ms)
    redis:
      time-to-live: 3600000
      # 缓存null值,防止缓存穿透
      cache-null-values: true

自动装配配置类 RedisCacheConfig

package com.youlai.system.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.RedisSerializer;

/**
 * Redis 缓存配置
 *
 * @author haoxr
 * @since 2023/12/4
 */
@EnableCaching
@EnableConfigurationProperties(CacheProperties.class)
@Configuration
public class RedisCacheConfig {

    /**
     * 自定义 RedisCacheManager
     * <p>
     * 修改 Redis 序列化方式,默认 JdkSerializationRedisSerializer
     *
     * @param redisConnectionFactory {@link RedisConnectionFactory}
     * @param cacheProperties        {@link CacheProperties}
     * @return {@link RedisCacheManager}
     */
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory redisConnectionFactory, CacheProperties cacheProperties){
        return RedisCacheManager.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                .cacheDefaults(redisCacheConfiguration(cacheProperties))
                .build();
    }

    /**
     * 自定义 RedisCacheConfiguration
     *
     * @param cacheProperties {@link CacheProperties}
     * @return {@link RedisCacheConfiguration}
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.string()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(RedisSerializer.json()));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();

        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        config = config.computePrefixWith(name -> name + ":");// 覆盖默认key双冒号  CacheKeyPrefix#prefixed
        return config;
    }

}

Spring Boot 路由缓存实战

完整项目源码:youlai-boot

获取路由数据缓存

获取路由列表,通过 @Cacheable 注解,将方法返回的路由列表数据缓存至 Redis。当再次请求时,不再进入方法体,而是直接从 Redis 中读取缓存数据并返回,以提高性能。

    /**
     * 获取路由列表
     */
    @Override
    @Cacheable(cacheNames = "menu", key = "'routes'") // cacheNames 为必填项,key 需要使用引号,否则会被识别为变量。
    public List<RouteVO> listRoutes() {
        List<RouteBO> menuList = this.baseMapper.listRoutes();
        return buildRoutes(SystemConstants.ROOT_NODE_ID, menuList);
    }

Spring Boot 3 整合 Spring Cache 与 Redis 缓存实战-LMLPHP

更新路由缓存失效

若需要在路由信息更新时使缓存失效,可以使用 @CacheEvict 注解,它用于在方法执行之后(默认)从缓存中移除条目。

    /**
     * 新增/修改菜单
     */
    @Override
    @CacheEvict(cacheNames = "menu", key = "'routes'",beforeInvocation = true)
    public boolean saveMenu(MenuForm menuForm) {
        String path = menuForm.getPath();
        MenuTypeEnum menuType = menuForm.getType();

        // 如果是目录
        if (menuType == MenuTypeEnum.CATALOG) {
            if (menuForm.getParentId() == 0 && !path.startsWith("/")) {
                menuForm.setPath("/" + path); // 一级目录需以 / 开头
            }
            menuForm.setComponent("Layout");
        }
        // 如果是外链
        else if (menuType == MenuTypeEnum.EXTLINK) {
            menuForm.setComponent(null);
        }

        SysMenu entity = menuConverter.form2Entity(menuForm);
        String treePath = generateMenuTreePath(menuForm.getParentId());
        entity.setTreePath(treePath);

        return this.saveOrUpdate(entity);
    }

更新菜单后,Redis 中缓存的路由数据将被清除。再次获取路由时,才会将新的路由数据缓存到 Redis 中。

Spring Cache 问题整理

@Cacheable 缓存的 key 双冒号

默认情况下 @Cacheable 使用双冒号 :: 拼接 cacheNameskey

@Cacheable(cacheNames = "menu", key = "'routes'") 

Spring Boot 3 整合 Spring Cache 与 Redis 缓存实战-LMLPHP

参考源码 RedisCacheConfiguration#prefixCacheNameWith

Spring Boot 3 整合 Spring Cache 与 Redis 缓存实战-LMLPHP

Spring Boot 3 整合 Spring Cache 与 Redis 缓存实战-LMLPHP

如果需要将双冒号改成单个冒号,需要重写 RedisCacheConfiguration#computePrefixWith 方法

config = config.computePrefixWith(name -> name + ":");//覆盖默认key双冒号  CacheKeyPrefix#prefixed

Spring Boot 3 整合 Spring Cache 与 Redis 缓存实战-LMLPHP

结语

Spring Cache 为 Spring 应用程序提供了简便的缓存管理抽象,通过注解如 @Cacheable@CacheEvict@CachePut,开发者能方便定义缓存策略。整合Spring Boot 与 Redis 缓存实例展示了如何配置、使用Spring Cache,提升应用性能。通过实战演示菜单路由缓存,突显 @Cacheable@CacheEvict 的实际应用,以及解决 @Cacheable 默认key双冒号的问题。

开源项目

  • SpringCloud + Vue3 微服务商城
  • SpringBoot 3+ Vue3 单体权限管理系统
12-05 09:54