Spring Cache 全景指南

一、 核心定义与作用

Spring Cache 不是一个具体的缓存实现(它不是 Redis,也不是 EhCache),而是一套 缓存抽象层(Cache Abstraction)

1. 核心定位

它类似于 JDBC 之于数据库。

  • JDBC 定义了标准接口,你可以底层换 MySQL 或 Oracle。
  • Spring Cache 定义了标准注解 (@Cacheable),你可以底层换 Redis、Caffeine 或 ConcurrentMap,而业务代码不需要修改一行

2. 核心价值

  1. 代码解耦:将业务逻辑(Service)与缓存技术细节分离。
  2. 消除样板代码 :通过 AOP(面向切面编程)管理缓存,不再需要手动编写 redisTemplate.get/set
  3. 统一标准:无论底层用什么,上层注解用法一致。

二、 核心注解体系

Spring Cache 主要通过以下 5 个注解控制缓存行为:

注解 核心作用 执行逻辑 (通俗版) 适用场景
@EnableCaching 总开关 开启 Spring 的缓存代理功能。 启动类 / 配置类
@Cacheable 查/存 1. 先查缓存,有则直接返回。2. 无则执行方法,并将结果存入缓存。 查询 (Get)
@CachePut 改/存 始终执行方法,并将返回值强制更新到缓存中。 新增/修改 (Save/Update)
@CacheEvict 执行方法(前或后),从缓存中删除指定数据。 删除 (Delete)
@Caching 组合 在一个方法上叠加多个操作(如删 A 缓存同时删 B 缓存)。 复杂联动场景

通用关键参数

所有注解(除开关外)都支持以下参数:

  • value / cacheNames:缓存名称(对应 Redis 的 Key 前缀)。
  • key :缓存 Key,支持 SpEL 表达式 (如 #id, #user.name)。
  • condition事前判断 。满足条件才处理缓存(例如:#id > 10)。
  • unless事后判断 。满足条件 缓存(例如:#result == null)。

三、 实践案例详解

以下模拟一个用户系统,展示如何结合 Redis 使用。

1. 查询缓存(@Cacheable)

需求 :查询用户,缓存有直接返回,无则查库并回填。同时防止缓存 null 值。

java 复制代码
@Service
public class UserService {

    @Autowired
    private UserMapper userMapper;

    /**
     * value = "users": 对应 Redis 前缀 "users::" (或自定义的 "users:")
     * key = "#id": 取参数 id 作为后缀
     * unless = "#result == null": 如果查不到数据,不要把 null 存进 Redis
     */
    @Cacheable(value = "users", key = "#id", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("--- 走数据库查询 ---");
        return userMapper.selectById(id);
    }
}

2. 保持一致性(@CacheEvict 推荐)

需求:修改用户信息后,清理缓存,保证下次查询获取最新数据。

  • 最佳实践 :优先使用 删除模式 (Evict) 而非更新模式 (Put),避免并发写导致脏数据。
java 复制代码
    /**
     * 更新完成后,删除对应的缓存 Key
     * key 必须与查询时的 key 生成规则一致
     */
    @CacheEvict(value = "users", key = "#user.id")
    public void updateUser(User user) {
        userMapper.updateById(user);
    }

    /**
     * 场景:删除用户,或者清空整个缓存桶
     * allEntries = true: 删除 users 下的所有 key
     */
    @CacheEvict(value = "users", allEntries = true)
    public void clearAllCache() {
        System.out.println("清空用户缓存");
    }

3. 复杂 Key 生成 (SpEL)

需求:根据多个参数组合生成 Key。

java 复制代码
    /**
     * 假设 type=VIP, page=1
     * Redis Key: user_list:VIP_1
     */
    @Cacheable(value = "user_list", key = "#type + '_' + #page")
    public List<User> getUsersByType(String type, int page) {
        return userMapper.selectByType(type, page);
    }

四、 避坑指南:内部调用失效 (Self-Invocation)

这是 Spring Cache 最经典的"大坑"。

1. 现象描述

在同一个 Service 类内部,方法 A 调用带 @Cacheable 的方法 B,方法 B 的缓存注解失效,每次都会执行 SQL。

2. 原理图解

Spring Cache 基于 代理模式 (Proxy Pattern)

  • 外部调用:Controller -> Proxy对象 -> 拦截处理缓存 -> 目标对象。
  • 内部调用this.method() 是目标对象自己调用自己,绕过了 Proxy 对象,所以 AOP 切面无法执行。

3. 代码示例与修复

❌ 错误写法 (失效)
java 复制代码
@Service
public class UserService {

    // 方法 A:批量查询
    public List<User> getUsersByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        for (Long id : ids) {
            // 【失效原因】:这里等同于 this.getUserById(id)
            // 直接调用了类内部的方法,没有经过 Spring 的代理类
            users.add(getUserById(id)); 
        }
        return users;
    }

    // 方法 B:单查(注解在此)
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        System.out.println("查询数据库...");
        return new User(id, "name");
    }
}
✅ 修复方案 1:自我注入 (简单有效)

通过注入自身(代理对象)来调用,强行经过代理层。

java 复制代码
@Service
public class UserService {

    // 1. 注入自己 (加上 @Lazy 防止循环依赖报错)
    @Autowired
    @Lazy
    private UserService self;

    public List<User> getUsersByIds(List<Long> ids) {
        List<User> users = new ArrayList<>();
        for (Long id : ids) {
            // 2. 【修复】:使用代理对象 self 调用,而不是 this
            // 流程:self -> Proxy -> 检查缓存 -> Target
            users.add(self.getUserById(id)); 
        }
        return users;
    }

    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) {
        // ...
    }
}
✅ 修复方案 2:服务拆分 (架构更优)

将缓存方法抽离到另一个 Service 中,符合单一职责原则。

java 复制代码
// 新服务:专门负责缓存操作
@Service
public class UserCacheService {
    @Cacheable(value = "users", key = "#id")
    public User getUserById(Long id) { ... }
}

// 原服务:注入上面的服务
@Service
public class UserService {
    @Autowired
    private UserCacheService userCacheService;

    public List<User> getUsersByIds(List<Long> ids) {
        // 外部调用,缓存必然生效
        return ids.stream().map(userCacheService::getUserById).collect(Collectors.toList());
    }
}
相关推荐
Master_Azur4 分钟前
java内部类与匿名内部类
后端
开心就好202510 分钟前
不依赖 Mac 也能做 iOS 开发?跨设备开发流程
后端·ios
一直都在57211 分钟前
线程间的通信
java·jvm
一只叫煤球的猫12 分钟前
RAG 如何落地?从原理解释到工程实现
人工智能·后端·ai编程
卷心菜投手ovo22 分钟前
一个页面支持自定义字段,后端该怎么设计数据库?
后端
隔壁家滴怪蜀黍27 分钟前
AgentScope MsgHub 多智能体通信机制详解
后端
孟陬27 分钟前
国外技术周刊 #3:“最差程序员”带动高效团队、不写代码的创业导师如何毁掉创新…
前端·后端·设计模式
GIOTTO情30 分钟前
Infoseek危机公关全链路技术解析:基于近期热点舆情的落地实践
java
Cosolar32 分钟前
Transformer训练与生成背后的数学基础
人工智能·后端·开源
我是人✓1 小时前
从零入门 Servlet:JavaWeb 核心组件的实操与理解
java·servlet