SpringCache 缓存使用总结

近期在基于 SpringBoot 3.x 集成最新版 JetCache 时,发现该组件对 SpringBoot 3.x 的支持并不完善。最突出的问题是:使用 @Cached 注解配置本地缓存时,以变量作为缓存 key 的方式完全不生效,经过多轮排查和资料查阅后仍未找到有效解决方案。因此,我最终决定采用 SpringCache 作为替代方案,该方案能够与 SpringBoot 3.x 完美兼容。

一、纯本地缓存(Caffeine)完整版

1)依赖(正确版)

xml 复制代码
<dependencies>
    <!-- Spring Cache 核心 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <!-- Caffeine 本地缓存 -->
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
</dependencies>

2)配置类(直接用)

java 复制代码
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching // 开启缓存
public class LocalCacheConfig {

    @Bean
    public CaffeineCacheManager cacheManager() {
        CaffeineCacheManager manager = new CaffeineCacheManager();
        manager.setCaffeine(Caffeine.newBuilder()
                .expireAfterWrite(60, TimeUnit.SECONDS) // 缓存60秒过期
                .maximumSize(10000)                   // 最多存1万条
        );
        return manager;
    }
}

3)本地缓存 · 增删改查(带参数解释)

java 复制代码
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;

@Service
public class SysUserService {

    // ==================== 查询 ====================
    /**
     * @Cacheable 查询缓存:先查缓存,没有才执行方法
     * value      = 缓存名称(必须)
     * key        = 缓存key,#p0 = 第一个参数
     * unless     = 结果为null时不缓存(防穿透)
     * sync       = true 并发只放一个请求查库(防击穿)
     */
    @Cacheable(value = "sysUser", key = "#p0", unless = "#result == null", sync = true)
    public SysUserDetailResponse getUserById(Long userId) {
        System.out.println("查询数据库");
        return new SysUserDetailResponse();
    }

    // ==================== 新增 ====================
    /**
     * @CachePut 新增/更新缓存:方法一定执行,结果写入缓存
     * key = #result.id 用返回对象的id做缓存key
     */
    @CachePut(value = "sysUser", key = "#result.id", unless = "#result == null")
    public SysUserDetailResponse createUser(SysUserAddRequest request) {
        System.out.println("新增数据库");
        SysUserDetailResponse user = new SysUserDetailResponse();
        user.setId(100L);
        return user;
    }

    // ==================== 修改 ====================
    @CachePut(value = "sysUser", key = "#p0.id", unless = "#result == null")
    public SysUserDetailResponse updateUser(SysUserUpdateRequest request) {
        System.out.println("修改数据库");
        return new SysUserDetailResponse();
    }

    // ==================== 删除 ====================
    /**
     * @CacheEvict 删除缓存
     * allEntries = false(默认)删单个key
     */
    @CacheEvict(value = "sysUser", key = "#p0")
    public void deleteUser(Long userId) {
        System.out.println("删除数据库");
    }

    // 清空整个缓存
    @CacheEvict(value = "sysUser", allEntries = true)
    public void clearAllUserCache() {
    }
}

二、本地 + 远程(Redis)混合缓存完整版

1)依赖

xml 复制代码
<dependencies>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
    </dependency>
    <dependency>
        <groupId>com.github.ben-manes.caffeine</groupId>
        <artifactId>caffeine</artifactId>
    </dependency>
</dependencies>

2)application.yml

yaml 复制代码
spring:
  redis:
    host: 127.0.0.1
    port: 6379
    database: 0

3)混合缓存配置(本地优先 → Redis)

java 复制代码
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.caffeine.CaffeineCache;
import org.springframework.cache.support.CompositeCacheManager;
import org.springframework.cache.support.SimpleCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.util.List;
import java.util.concurrent.TimeUnit;

@Configuration
@EnableCaching
public class HybridCacheConfig {

    // 本地缓存
    @Bean
    public SimpleCacheManager localCacheManager() {
        SimpleCacheManager manager = new SimpleCacheManager();
        manager.setCaches(List.of(new CaffeineCache("sysUser",
            Caffeine.newBuilder()
                .expireAfterWrite(60, TimeUnit.SECONDS)
                .maximumSize(10000)
                .build()
        )));
        return manager;
    }

    // Redis缓存
    @Bean
    public RedisCacheManager redisCacheManager(RedisConnectionFactory factory) {
        var serializer = new GenericJackson2JsonRedisSerializer();
        var config = org.springframework.data.redis.cache.RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(java.time.Duration.ofSeconds(300))
            .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer));

        return RedisCacheManager.builder(factory).cacheDefaults(config).build();
    }

    // 组合:先本地 → 再Redis → 最后数据库
    @Bean
    public CacheManager cacheManager(SimpleCacheManager localCacheManager, RedisCacheManager redisCacheManager) {
        CompositeCacheManager composite = new CompositeCacheManager();
        composite.setCacheManagers(List.of(localCacheManager, redisCacheManager));
        return composite;
    }
}

三、注解所有参数 · 极简解释(一眼看懂)

注解 参数 核心含义 常用值示例
@Cacheable value 缓存名称(必须,用于隔离不同业务缓存) "sysUser"、"sysRole"
@Cacheable key 缓存唯一标识(SpEL表达式) #p0、#userId、#result.id
@Cacheable unless 满足条件时不缓存(防穿透) #result == null
@Cacheable sync 并发仅放行一个请求查库(防击穿) true
@CachePut value 缓存名称 "sysUser"
@CachePut key 要更新的缓存key #p0.id、#result.id
@CachePut unless 结果为null时不更新缓存 #result == null
@CacheEvict key 要删除的缓存key #p0
@CacheEvict allEntries 是否清空该缓存下所有key(默认false) true、false

四、最主流、最标准、最推荐的写法(速记版)

java 复制代码
// 查询(防穿透+防击穿)
@Cacheable(value = "sysUser", key = "#p0", unless = "#result == null", sync = true)

// 新增(用返回值ID做key)
@CachePut(value = "sysUser", key = "#result.id", unless = "#result == null")

// 修改(用入参ID做key)
@CachePut(value = "sysUser", key = "#p0.id", unless = "#result == null")

// 删除单个key
@CacheEvict(value = "sysUser", key = "#p0")

// 清空整个缓存
@CacheEvict(value = "sysUser", allEntries = true)

五、一页纸速记图(Markdown版)

markdown 复制代码
# Spring Cache 速记表(Spring Boot 3.x)
## 核心依赖
| 缓存类型       | 依赖坐标                                                                 |
|----------------|--------------------------------------------------------------------------|
| 纯本地         | spring-boot-starter-cache + com.github.ben-manes:caffeine                |
| 本地+Redis     | 纯本地依赖 + spring-boot-starter-data-redis                             |

## 核心注解
| 操作   | 注解       | 标准写法                                 | 核心作用                     |
|--------|------------|------------------------------------------|------------------------------|
| 查询   | @Cacheable | @Cacheable(value="sysUser",key="#p0",unless="#result==null",sync=true) | 先查缓存,未命中执行方法     |
| 新增   | @CachePut  | @CachePut(value="sysUser",key="#result.id",unless="#result==null")     | 执行方法后写入缓存           |
| 修改   | @CachePut  | @CachePut(value="sysUser",key="#p0.id",unless="#result==null")        | 执行方法后更新缓存           |
| 删除   | @CacheEvict| @CacheEvict(value="sysUser",key="#p0")                                | 删除指定key缓存              |
| 清空   | @CacheEvict| @CacheEvict(value="sysUser",allEntries=true)                          | 清空该缓存下所有key          |

## 关键参数
| 参数     | 含义                                  | 避坑点                          |
|----------|---------------------------------------|---------------------------------|
| value    | 缓存名称(必须)                      | 不同业务用不同名称,避免冲突    |
| key      | 缓存唯一标识                          | 优先用#p0(参数索引),不报错   |
| unless   | 结果过滤条件                          | 必加#result==null,防缓存穿透   |
| sync     | 并发控制                              | 查询必加true,防缓存击穿        |
| allEntries| 清空所有key                          | 仅批量删除时用,避免误删        |

## 配置要点
| 缓存类型       | 配置核心                                                                 |
|----------------|--------------------------------------------------------------------------|
| 纯本地(Caffeine) | expireAfterWrite(60s) + maximumSize(10000)                              |
| Redis          | JSON序列化 + entryTtl(300s) + key前缀隔离                               |
| 混合缓存       | CompositeCacheManager,顺序:本地 → Redis → 数据库                       |
相关推荐
qwert10373 分钟前
跨域问题解释及前后端解决方案(SpringBoot)
spring boot·后端·okhttp
yuweiade7 分钟前
【Spring】Spring MVC案例
java·spring·mvc
90后的晨仔31 分钟前
OpenClaw Windows 完整安装指南
后端
码喽7号32 分钟前
springboot学习四:RESTful风格+swagger
spring boot·学习·restful
IT_陈寒1 小时前
Vue组件复用率提升300%?这5个高阶技巧让你的代码焕然一新!
前端·人工智能·后端
y = xⁿ2 小时前
【从零开始学习Redis|第七篇】Redis 进阶原理篇:消息队列、分布式锁、缓存击穿与事务实现
java·redis·学习·缓存
beata2 小时前
Spring Boot基础-2:Spring Boot 3.x 起步依赖(Starter)深度拆解:为什么引入一个依赖就够了?
spring boot·后端
享棣2 小时前
Win11 安装 Nacos 2.0.4 完整版文档 文档说明
后端
90后的晨仔2 小时前
windows安装 openclaw 报错
后端
繁华如雪亦如歌2 小时前
Linux:Cache
缓存