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 → 数据库                       |
相关推荐
夜瞬2 小时前
【Flask 框架学习】01:编写第一个 Flask 应用
后端·python·学习·flask
JavaLearnerZGQ2 小时前
Spring Boot 流式响应接口核心组件解析
java·spring boot·后端
春和景明3602 小时前
springboot
spring boot·spring
Loo国昌2 小时前
【AI应用开发实战】07_文档解析路由与质量评估:从传统PDF解析到Docling现代化方案
人工智能·后端·python·自然语言处理·pdf
Pr Young2 小时前
有限状态机
服务器·后端
凌云拓界2 小时前
TypeWell全攻略:AI健康教练+实时热力图开发实战 引言
前端·人工智能·后端·python·交互·pyqt·数据可视化
indexsunny2 小时前
互联网大厂Java面试实战:Spring Boot与微服务在电商场景的应用
java·spring boot·微服务·面试·kafka·prometheus·电商
倚肆2 小时前
Spring WebSocket 核心注解详解
java·websocket·spring
百锦再2 小时前
Spring Boot Web 后端开发注解核心
开发语言·spring boot·python·struts·spring cloud·kafka·maven