SpringCache :让缓存开发更高效

一、SpringCache 是什么?

1.1 定义与核心思想

SpringCache 是 Spring 框架提供的一套缓存抽象层,它本身不是具体的缓存实现,而是一个统一的缓存访问抽象框架。它基于 Spring 的 AOP(面向切面编程)机制实现,通过声明式注解的方式将缓存功能无缝集成到业务逻辑中。

具体工作流程

  1. 当标记了缓存注解的方法被调用时
  2. SpringCache 会先检查缓存中是否存在对应的结果
  3. 如果存在则直接返回缓存结果
  4. 如果不存在则执行方法体,并将结果存入缓存
  5. 后续相同参数的调用将直接从缓存获取结果

典型应用场景

  • 数据库查询结果缓存
  • 复杂计算结果缓存
  • 频繁访问的静态数据缓存
  • 高并发场景下的数据缓冲

1.2 SpringCache 的优势

1.2.1 低侵入性

通过 @Cacheable@CacheEvict 等注解实现缓存功能,无需修改业务方法的核心逻辑。例如:

java 复制代码
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {
    return userRepository.findById(id);
}

1.2.2 灵活性高

支持多种缓存实现:

  • 本地缓存:Caffeine、Ehcache
  • 分布式缓存:Redis、Memcached
  • 混合缓存:多级缓存架构

仅需修改配置即可切换缓存实现,无需修改业务代码:

yaml 复制代码
spring:
  cache:
    type: redis

1.2.3 简化开发

自动处理缓存操作:

  • 自动生成缓存key
  • 自动序列化/反序列化
  • 自动处理缓存一致性
  • 自动处理异常情况

1.2.4 丰富的缓存策略

支持多种缓存控制方式:

  • 条件缓存:@Cacheable(condition = "#id > 10")
  • 缓存排除:@Cacheable(unless = "#result == null")
  • 缓存过期:通过配置实现TTL
  • 自定义Key生成:SpEL表达式

1.2.5 与Spring生态完美集成

  • 与Spring Boot自动配置
  • 与Spring Security权限控制
  • 与Spring Data持久层整合
  • 与Spring Cloud微服务协同

典型配置示例

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("userCache", "productCache");
    }
}

二、SpringCache 核心注解详解

SpringCache 提供了一套强大的注解来实现缓存的各种操作,掌握这些注解是使用 SpringCache 的基础。以下是常用的核心注解及其详细用法说明:

2.1 @Cacheable:触发缓存写入

详细作用

@Cacheable 注解主要用于标记方法的返回值应该被缓存。在方法执行前,SpringCache 会先检查缓存中是否存在对应的 key:

  • 如果存在:直接返回缓存中的值,不执行方法体(缓存命中)
  • 如果不存在:执行方法体,并将方法的返回结果存入指定的缓存中(缓存未命中)

属性详解

属性 类型 说明 示例
value/cacheNames String[] 必填,指定缓存名称(可以理解为缓存分区) @Cacheable("userCache")
key String 自定义缓存键,支持SpEL表达式 key="#id"
condition String 缓存条件,结果为true才缓存 condition="#id>10"
unless String 排除条件,结果为true则不缓存 unless="#result==null"

典型应用场景

  1. 查询方法(如根据ID查询用户)
  2. 计算密集型方法(如复杂计算、报表生成)
  3. 频繁访问但数据变化不频繁的方法

完整示例

java 复制代码
@Cacheable(value = "userCache", 
           key = "#id",
           condition = "#id>0",
           unless = "#result==null")
public User getUserById(Long id) {
    // 模拟数据库查询
    System.out.println("执行数据库查询,ID:" + id);
    return userRepository.findById(id).orElse(null);
}

2.2 @CachePut:更新缓存

详细作用

@CachePut 强制方法执行并将结果更新到缓存中,无论缓存中是否已存在该key。常用于:

  • 数据更新后保持缓存一致性
  • 主动刷新缓存数据

注意事项

  1. 方法一定会被执行
  2. 应该只用于更新操作,不应用于查询方法
  3. 需要确保key与@Cacheable的key一致

典型应用场景

  1. 用户信息更新
  2. 订单状态变更
  3. 任何需要保持缓存与数据库同步的操作

完整示例

java 复制代码
@CachePut(value = "userCache", 
          key = "#user.id",
          condition = "#result!=null")
public User updateUser(User user) {
    // 先执行数据库更新
    User updatedUser = userRepository.save(user);
    System.out.println("更新用户信息:" + user.getId());
    return updatedUser;
}

2.3 @CacheEvict:触发缓存删除

详细作用

删除缓存中的指定数据,保证缓存一致性。主要用于:

  • 数据删除后清理相关缓存
  • 缓存数据过期时主动清理

属性详解

属性 类型 说明 默认值
allEntries boolean 是否清除缓存所有条目 false
beforeInvocation boolean 是否在方法执行前清除 false

典型应用场景

  1. 删除用户后清除用户缓存
  2. 批量操作后需要刷新缓存
  3. 数据变更时需要清除关联缓存

完整示例

java 复制代码
// 删除单个缓存
@CacheEvict(value = "userCache", 
            key = "#id",
            beforeInvocation = true)
public void deleteUser(Long id) {
    userRepository.deleteById(id);
    System.out.println("删除用户ID:" + id);
}

// 清除整个缓存区域
@CacheEvict(value = "userCache", 
            allEntries = true)
public void refreshAllUsers() {
    System.out.println("刷新所有用户缓存");
}

2.4 @Caching:组合多个缓存注解

详细作用

当需要在一个方法上组合多个缓存操作时使用,可以同时包含:

  • 多个@Cacheable
  • 多个@CachePut
  • 多个@CacheEvict

典型应用场景

  1. 更新主缓存同时清除列表缓存
  2. 多级缓存操作
  3. 复杂业务逻辑需要同时操作多个缓存区域

完整示例

java 复制代码
@Caching(
    put = {
        @CachePut(value = "userCache", key = "#user.id"),
        @CachePut(value = "userNameCache", key = "#user.name")
    },
    evict = {
        @CacheEvict(value = "userListCache", allEntries = true),
        @CacheEvict(value = "departmentCache", key = "#user.deptId")
    }
)
public User updateUser(User user) {
    // 更新操作
    User updatedUser = userRepository.save(user);
    System.out.println("更新用户信息:" + user.getId());
    return updatedUser;
}

2.5 @CacheConfig:统一配置缓存属性

详细作用

类级别的注解,用于统一配置该类的缓存相关属性,避免在每个方法上重复配置。

可配置属性

  1. cacheNames:统一缓存名称
  2. keyGenerator:统一key生成器
  3. cacheManager:统一缓存管理器
  4. cacheResolver:统一缓存解析器

典型应用场景

  1. 服务类中多个方法使用相同缓存配置
  2. 需要统一管理缓存命名空间
  3. 需要统一使用自定义key生成策略

完整示例

java 复制代码
@CacheConfig(cacheNames = "productCache",
             keyGenerator = "customKeyGenerator")
@Service
public class ProductService {
    
    @Cacheable(key = "#id")
    public Product getProduct(Long id) {
        return productRepository.findById(id);
    }
    
    @CachePut(key = "#product.id")
    public Product updateProduct(Product product) {
        return productRepository.save(product);
    }
    
    @CacheEvict(key = "#id")
    public void deleteProduct(Long id) {
        productRepository.deleteById(id);
    }
}

最佳实践建议

  1. key设计:使用业务主键而不是全参数组合
  2. 缓存粒度:建议按业务场景划分缓存区域
  3. null值处理:使用unless避免缓存null值
  4. 事务考虑:缓存操作与事务边界保持一致
  5. 监控告警:对重要缓存操作添加监控

三、SpringCache 配置方式(以 Spring Boot 为例)

Spring Boot 为 SpringCache 提供了开箱即用的自动配置支持,通过简单的配置即可集成多种缓存实现。不同的缓存产品(如 Redis、Caffeine、Ehcache 等)在 Spring Boot 中的配置方式略有不同。以下是两种最常用缓存产品的详细配置指南,包含从基础到生产环境的完整配置过程。

3.1 基础配置:使用默认缓存(ConcurrentMapCache)

Spring Boot 默认提供了基于内存的 ConcurrentMapCache 实现,这是最简单的缓存方案,特别适合开发环境快速测试缓存功能,无需任何额外依赖和复杂配置。

完整配置步骤说明:

1.启用缓存功能 : 在 Spring Boot 主启动类上添加 @EnableCaching 注解,该注解会触发 Spring 的缓存自动配置机制。

java 复制代码
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;

@EnableCaching  // 核心注解,开启Spring缓存功能
@SpringBootApplication
public class SpringCacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(SpringCacheDemoApplication.class, args);
    }
}

2.使用缓存注解 : 在业务方法上直接使用 @Cacheable@CachePut@CacheEvict 等标准注解即可,例如:

java 复制代码
@Service
public class ProductService {
    // 使用默认缓存"products"存储方法结果
    @Cacheable("products")
    public Product getProductById(Long id) {
        // 模拟耗时数据库查询
        simulateSlowService();
        return productRepository.findById(id).orElse(null);
    }
    
    private void simulateSlowService() {
        try {
            Thread.sleep(3000);
        } catch (InterruptedException e) {
            e.printStackTrace();
        }
    }
}

ConcurrentMapCache 特性与限制分析:

核心特性

  • 零配置开箱即用
  • 基于内存存储,访问速度极快
  • 线程安全的并发实现

生产环境限制

  1. 单节点局限:缓存数据仅存在于当前JVM内存中,无法在集群环境下共享
  2. 无持久化:应用重启后所有缓存数据立即丢失
  3. 无过期策略:无法设置自动失效时间,可能导致内存无限增长
  4. 无高级功能:不支持缓存统计、监听等管理功能

适用场景建议

  • 本地开发环境的功能验证
  • 单元测试中的缓存模拟
  • 小型非关键性应用的原型开发

3.2 生产配置:使用 Redis 作为缓存

Redis 是当前最流行的分布式内存数据库,作为缓存方案具有诸多优势。下面详细介绍 Spring Boot 中集成 Redis 缓存的完整流程。

3.2.1 依赖引入

Maven 配置

XML 复制代码
<!-- 核心Redis依赖(包含连接池和基本操作) -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!-- 可选:如果未引入web/starter依赖需要单独添加 -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 推荐:添加Jackson序列化支持 -->
<dependency>
    <groupId>com.fasterxml.jackson.core</groupId>
    <artifactId>jackson-databind</artifactId>
</dependency>

Gradle 配置

groovy 复制代码
implementation 'org.springframework.boot:spring-boot-starter-data-redis'
implementation 'org.springframework.boot:spring-boot-starter-cache'
implementation 'com.fasterxml.jackson.core:jackson-databind'

3.2.2 Redis 连接配置

YAML 完整配置示例

yaml 复制代码
spring:
  redis:
    host: redis-production.example.com  # 生产环境建议使用域名
    port: 6379
    password: securePassword123!       # 生产环境必须设置密码
    database: 0                        # 通常为0,可根据业务分库
    timeout: 3000ms                    # 连接超时控制
    lettuce:                           # 连接池配置(Lettuce是默认客户端)
      pool:
        max-active: 20                 # 最大连接数(根据QPS调整)
        max-idle: 10                   # 最大空闲连接
        min-idle: 5                    # 最小空闲连接
        max-wait: 1000ms               # 获取连接最大等待时间
  
  cache:
    type: redis                        # 显式指定缓存类型
    redis:
      time-to-live: 30m                # 默认全局过期时间(支持多种时间单位)
      cache-null-values: false         # 是否缓存null(防止缓存穿透)
      use-key-prefix: true             # 启用key前缀隔离
      key-prefix: "app_cache:"         # 自定义前缀(推荐加版本号如v1:)
      enable-statistics: true          # 开启缓存统计(监控用)

关键配置项说明

  1. time-to-live

    • 支持的时间单位:ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天)
    • 示例:1h30m 表示1小时30分钟
  2. 缓存穿透防护

    • cache-null-values: false 时,方法返回null不会被缓存
    • 对于高频访问但可能为null的数据,可设置为true并配合较短TTL
  3. Key命名策略

    • 默认格式:cacheName::key
    • 自定义前缀可避免多应用共用一个Redis时的key冲突

3.2.3 高级自定义配置

定制化 RedisCacheManager 示例

java 复制代码
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.*;

import java.time.Duration;
import java.util.HashMap;
import java.util.Map;

@Configuration
public class AdvancedRedisCacheConfig extends CachingConfigurerSupport {

    // 定义不同缓存名称的个性化配置
    private static final Map<String, RedisCacheConfiguration> CACHE_CONFIG_MAP = new HashMap<>();
    static {
        // 用户数据缓存1小时
        CACHE_CONFIG_MAP.put("userCache", 
            defaultConfig().entryTtl(Duration.ofHours(1)));
        
        // 商品数据缓存2小时且压缩值
        CACHE_CONFIG_MAP.put("productCache",
            defaultConfig()
                .entryTtl(Duration.ofHours(2))
                .serializeValuesWith(RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer())));
    }

    // 默认配置模板
    private static RedisCacheConfiguration defaultConfig() {
        return RedisCacheConfiguration.defaultCacheConfig()
            .computePrefixWith(cacheName -> "v2:" + cacheName + ":")  // 自定义前缀策略
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new JdkSerializationRedisSerializer()))
            .entryTtl(Duration.ofMinutes(30))
            .disableCachingNullValues();
    }

    @Bean
    @Override
    public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
        return RedisCacheManager.builder(connectionFactory)
            .withInitialCacheConfigurations(CACHE_CONFIG_MAP)  // 应用个性化配置
            .cacheDefaults(defaultConfig())                   // 设置默认配置
            .transactionAware()                               // 支持事务
            .enableStatistics()                               // 启用统计
            .build();
    }
}

配置亮点说明

  1. 多缓存差异化配置

    • 通过静态代码块预定义不同缓存名称的配置
    • 支持为每个缓存设置独立的TTL和序列化策略
  2. 序列化优化

    • Key使用String序列化保证可读性
    • Value可根据业务需求选择:
      • JdkSerializationRedisSerializer:通用但二进制不可读
      • GenericJackson2JsonRedisSerializer:JSON格式,跨语言友好
      • GenericFastJsonRedisSerializer:高性能JSON处理
  3. 运维友好设计

    • 添加版本前缀(v2:)便于缓存迁移
    • 启用统计功能方便监控缓存命中率
    • 事务支持确保缓存与数据库一致性

生产环境建议

  1. 对于大型值对象,建议配置值压缩:

    java 复制代码
    .serializeValuesWith(RedisSerializationContext.SerializationPair
        .fromSerializer(new SnappyRedisSerializer()))
  2. 考虑实现CacheErrorHandler处理Redis连接异常情况

  3. 对于关键业务数据,建议配置双写策略保证缓存可靠性

四、SpringCache 实战案例:用户管理系统

4.1 项目结构

复制代码
src/main/java/com/example/springcache/

├── SpringCacheDemoApplication.java // 启动类

├── config/

│ └── RedisCacheConfig.java // Redis 缓存配置类

├── controller/

│ └── UserController.java // 控制层(接收请求)

├── service/

│ ├── UserService.java // 服务层(业务逻辑 + 缓存)

│ └── impl/

│ └── UserServiceImpl.java // 服务层实现

├── entity/

│ └── User.java // 用户实体类

└── dao/

└── UserDao.java // 数据访问层(模拟数据库操作)

4.2 核心代码实现(详细版)

4.2.1 实体类 User.java(带完整注释)
java 复制代码
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;

/**
 * 用户实体类(实现Serializable接口确保可序列化) 
 * 使用Lombok简化代码:
 * @Data 自动生成getter/setter/toString等方法
 * @NoArgsConstructor 生成无参构造器
 * @AllArgsConstructor 生成全参数构造器
 */
@Data
@NoArgsConstructor
@AllArgsConstructor
public class User implements Serializable {
    // 序列化版本号(重要:修改类结构时需要保持一致)
    private static final long serialVersionUID = 1L;
    
    // 用户ID(主键)
    private Long id;
    
    // 用户名(真实场景可加@NotBlank等校验注解)
    private String name;
    
    // 用户年龄(真实场景可加@Min(1)等校验)
    private Integer age;
}
4.2.2 数据访问层 UserDao.java(带完整模拟数据库实现)
java 复制代码
import com.example.springcache.entity.User;
import org.springframework.stereotype.Repository;
import java.util.HashMap;
import java.util.Map;

/**
 * 用户数据访问层(模拟数据库操作)
 * @Repository 标注为Spring的数据访问组件
 */
@Repository
public class UserDao {
    // 使用线程安全的ConcurrentHashMap更佳(此处为示例简化)
    private static final Map<Long, User> USER_MAP = new HashMap<>();

    // 静态代码块初始化测试数据
    static {
        USER_MAP.put(1L, new User(1L, "张三", 25));
        USER_MAP.put(2L, new User(2L, "李四", 30));
        USER_MAP.put(3L, new User(3L, "王五", 28));
    }

    /**
     * 根据ID查询用户(模拟SELECT操作)
     * @param id 用户ID
     * @return 用户对象(未找到时返回null)
     */
    public User selectById(Long id) {
        System.out.println("[DAO] 查询数据库用户ID: " + id);
        return USER_MAP.get(id);
    }

    /**
     * 新增用户(模拟INSERT操作)
     * @param user 用户对象
     * @throws IllegalArgumentException 当用户ID已存在时抛出异常
     */
    public void insert(User user) {
        if (USER_MAP.containsKey(user.getId())) {
            throw new IllegalArgumentException("用户ID已存在");
        }
        System.out.println("[DAO] 新增用户: " + user);
        USER_MAP.put(user.getId(), user);
    }

    /**
     * 更新用户(模拟UPDATE操作)
     * @param user 用户对象
     * @return 影响行数(实际开发中可返回boolean)
     */
    public int update(User user) {
        if (!USER_MAP.containsKey(user.getId())) {
            return 0;
        }
        System.out.println("[DAO] 更新用户: " + user);
        USER_MAP.put(user.getId(), user);
        return 1;
    }

    /**
     * 删除用户(模拟DELETE操作)
     * @param id 用户ID
     * @return 被删除的用户对象
     */
    public User delete(Long id) {
        System.out.println("[DAO] 删除用户ID: " + id);
        return USER_MAP.remove(id);
    }
}
4.2.3 服务层接口 UserService.java(完整业务接口定义)
java 复制代码
import com.example.springcache.entity.User;

/**
 * 用户服务层接口(定义缓存业务契约)
 */
public interface UserService {
    /**
     * 根据ID查询用户(带缓存)
     * @param id 用户ID
     * @return 用户对象(可能为null)
     */
    User getUserById(Long id);

    /**
     * 新增用户(同步清除相关缓存)
     * @param user 用户对象
     */
    void addUser(User user);

    /**
     * 更新用户(同步更新缓存)
     * @param user 用户对象
     * @return 更新后的用户对象
     */
    User updateUser(User user);

    /**
     * 删除用户(同步清除缓存)
     * @param id 用户ID
     */
    void deleteUser(Long id);

    /**
     * 清除所有用户缓存(用于手动触发场景)
     */
    void clearUserCache();

    /**
     * 批量查询用户(示例方法,展示多参数缓存)
     * @param ids 用户ID集合
     * @return 用户列表
     */
    // List<User> batchGetUsers(List<Long> ids);
}
4.2.4 服务层实现 UserServiceImpl.java(带详细缓存策略说明)
java 复制代码
import com.example.springcache.dao.UserDao;
import com.example.springcache.entity.User;
import com.example.springcache.service.UserService;
import org.springframework.cache.annotation.*;
import org.springframework.stereotype.Service;
import javax.annotation.Resource;

/**
 * 用户服务实现类(核心缓存逻辑实现)
 * @CacheConfig 统一配置:
 *   - cacheNames: 指定缓存名称(对应缓存配置)
 *   - keyGenerator: 可指定自定义key生成器(示例使用默认)
 */
@CacheConfig(cacheNames = "userCache")
@Service
public class UserServiceImpl implements UserService {
    @Resource
    private UserDao userDao;

    /**
     * 查询方法缓存策略(首次查询后缓存结果)
     * @Cacheable 核心参数:
     *   - key: 使用SpEL表达式 "#id" 动态生成缓存key
     *   - condition: 仅当ID>0时才进行缓存
     *   - unless: 当返回null时不缓存(避免缓存空值)
     * 测试流程:
     *   1. 首次调用会打印数据库查询日志
     *   2. 后续相同ID调用直接从缓存返回
     */
    @Override
    @Cacheable(key = "#id", condition = "#id > 0", unless = "#result == null")
    public User getUserById(Long id) {
        System.out.println("[SERVICE] 执行真实数据库查询,ID: " + id);
        return userDao.selectById(id);
    }

    /**
     * 新增方法缓存策略(清除整个缓存区域)
     * @CacheEvict 核心参数:
     *   - allEntries: 清除userCache下的所有缓存(适合用户列表变更场景)
     * 注意:实际项目可能需要更细粒度的缓存清除策略
     */
    @Override
    @CacheEvict(allEntries = true)
    public void addUser(User user) {
        System.out.println("[SERVICE] 执行数据库新增: " + user);
        userDao.insert(user);
    }

    /**
     * 更新方法缓存策略(双写一致性保障)
     * @CachePut 核心机制:
     *   1. 无论缓存是否存在都会执行方法体
     *   2. 将返回结果写入缓存(key="#user.id")
     * 典型流程:
     *   1. 更新数据库记录
     *   2. 查询最新数据
     *   3. 更新缓存数据
     */
    @Override
    @CachePut(key = "#user.id", unless = "#result == null")
    public User updateUser(User user) {
        System.out.println("[SERVICE] 执行数据库更新: " + user);
        userDao.update(user);
        return userDao.selectById(user.getId()); // 确保缓存最新数据
    }

    /**
     * 删除方法缓存策略(精确清除指定key)
     * @CacheEvict 关键区别:
     *   - 不设置allEntries,仅删除key="#id"的缓存
     * 典型场景:
     *   - 删除用户后,避免下次查询仍返回缓存数据
     */
    @Override
    @CacheEvict(key = "#id")
    public void deleteUser(Long id) {
        System.out.println("[SERVICE] 执行数据库删除,ID: " + id);
        userDao.delete(id);
    }

    /**
     * 手动清除缓存(可用于定时任务或管理接口)
     */
    @Override
    @CacheEvict(allEntries = true)
    public void clearUserCache() {
        System.out.println("[SERVICE] 手动触发清除所有用户缓存");
        // 无数据库操作,仅通过注解触发缓存清除
    }
}
4.2.5 控制层 UserController.java(完整RESTful接口)
java 复制代码
import com.example.springcache.entity.User;
import com.example.springcache.service.UserService;
import org.springframework.web.bind.annotation.*;
import javax.annotation.Resource;

/**
 * 用户REST控制器(完整HTTP接口示例)
 * @RestController 自动包含@ResponseBody
 * @RequestMapping 基础路径"/user"
 */
@RestController
@RequestMapping("/user")
public class UserController {
    @Resource
    private UserService userService;

    /**
     * 用户查询接口(GET请求)
     * @param id 路径变量用户ID
     * @return 用户JSON(自动由Spring转换)
     * 示例请求:GET /user/1
     * 缓存效果:第二次相同请求不会触发服务层日志
     */
    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userService.getUserById(id);
    }

    /**
     * 用户新增接口(POST请求)
     * @param user 通过@RequestBody接收JSON
     * @return 操作结果
     * 示例请求:POST /user  
     * 请求体:{"id":4,"name":"赵六","age":35}
     * 缓存影响:会清除所有用户缓存
     */
    @PostMapping
    public String addUser(@RequestBody User user) {
        userService.addUser(user);
        return "操作成功:新增用户 " + user.getName();
    }

    /**
     * 用户更新接口(PUT请求)
     * @param user 更新后的用户数据
     * @return 更新后的用户对象(带最新缓存)
     * 示例请求:PUT /user
     * 请求体:{"id":1,"name":"张三-new","age":27}
     */
    @PutMapping
    public User updateUser(@RequestBody User user) {
        return userService.updateUser(user);
    }

    /**
     * 用户删除接口(DELETE请求)
     * @param id 要删除的用户ID
     * @return 操作结果
     * 示例请求:DELETE /user/2
     * 缓存影响:精确删除该ID的缓存
     */
    @DeleteMapping("/{id}")
    public String deleteUser(@PathVariable Long id) {
        userService.deleteUser(id);
        return "操作成功:删除用户ID " + id;
    }

    /**
     * 缓存管理接口(开发调试用)
     * @return 操作结果
     * 示例请求:GET /user/clearCache
     * 典型场景:数据批量导入后手动清除缓存
     */
    @GetMapping("/clearCache")
    public String clearUserCache() {
        userService.clearUserCache();
        return "操作成功:已清除所有用户缓存";
    }
}

4.3 测试验证

完成代码编写后,我们可以通过 Postman 或浏览器发送 HTTP 请求,验证 SpringCache 的缓存效果。以下是关键测试场景及预期结果,建议使用 Postman 的 Collection 功能组织这些测试用例:

测试准备

  1. 确保 Redis 服务已启动(如使用 Redis 作为缓存实现)
  2. 启动 SpringBoot 应用,默认端口 8080
  3. 准备测试数据:
    • 用户1: {"id":1,"name":"张三","age":25}
    • 用户2: {"id":2,"name":"李四","age":30}

详细测试场景

场景 1:查询用户(验证 @Cacheable)
  • 测试步骤

    1. 第一次发送请求:GET http://localhost:8080/user/1
    2. 观察控制台日志和响应
    3. 第二次发送相同请求
    4. 比较两次请求的差异
  • 预期结果

    • 第一次请求:
      • 控制台输出:执行数据库查询:getUserById(1)
      • 响应时间较长(约100-300ms)
      • 返回结果:{"id":1,"name":"张三","age":25}
    • 第二次请求:
      • 控制台无输出
      • 响应时间显著缩短(约10-50ms)
      • 返回相同结果
场景 2:更新用户(验证 @CachePut)
  • 测试步骤

    1. 发送更新请求:PUT http://localhost:8080/user
      • Headers: Content-Type=application/json
      • Body: {"id":1,"name":"张三-update","age":26}
    2. 立即查询用户1
    3. 观察缓存更新情况
  • 预期结果

    • 更新请求:
      • 控制台输出:执行数据库更新:updateUser(User(id=1, name=张三-update, age=26))
      • 返回状态码200
      • 返回结果:{"id":1,"name":"张三-update","age":26}
    • 查询请求:
      • 控制台无输出
      • 返回更新后的数据
场景 3:删除用户(验证 @CacheEvict)
  • 测试步骤

  • 预期结果

    • 删除请求:
      • 控制台输出:执行数据库删除:deleteUser(1)
      • 返回状态码200
      • 返回结果:删除用户成功
    • 查询请求:
      • 控制台输出:执行数据库查询:getUserById(1)
      • 返回状态码404
      • 返回结果:null
场景 4:新增用户(验证 @CacheEvict (allEntries = true))
  • 测试步骤

    1. 发送新增请求:POST http://localhost:8080/user
      • Headers: Content-Type=application/json
      • Body: {"id":3,"name":"王五","age":28}
    2. 查询之前已缓存的用户2
    3. 观察缓存清除情况
  • 预期结果

    • 新增请求:
      • 控制台输出:执行数据库新增:addUser(User(id=3, name=王五, age=28))
      • 返回状态码201
      • 返回结果:新增用户成功
    • 查询请求:
      • 控制台输出:执行数据库查询:getUserById(2)
      • 返回状态码200
      • 返回结果:{"id":2,"name":"李四","age":30}

测试结果分析

建议使用 Postman 的 Test 脚本功能自动验证:

javascript 复制代码
// 示例测试脚本
pm.test("Status code is 200", function() {
    pm.response.to.have.status(200);
});
pm.test("Response time is less than 200ms", function() {
    pm.expect(pm.response.responseTime).to.be.below(200);
});

其他验证方式

  1. 使用 Redis CLI 查看缓存数据:

    bash 复制代码
    redis-cli
    keys *
    get user::1
  2. 在应用日志中搜索缓存相关日志:

    复制代码
    DEBUG org.springframework.cache - Cache hit for key 'user::1'
    DEBUG org.springframework.cache - Cache miss for key 'user::1'

五、SpringCache 高级特性与注意事项

5.1 自定义 KeyGenerator(缓存 key 生成器)

默认情况下,SpringCache 使用 SimpleKeyGenerator 生成缓存 key(根据方法参数组合生成)。当需要实现更复杂的缓存 key 生成策略时,如以下几种场景:

  1. 需要为所有缓存 key 添加统一前缀(如业务标识)
  2. 需要忽略方法中的某些参数(如分页参数)
  3. 需要基于对象特定属性生成 key(而非整个对象)
  4. 需要实现跨方法的统一 key 生成规则

在这些情况下,可以实现 KeyGenerator 接口来自定义 key 生成逻辑。

5.1.1 详细实现示例

java 复制代码
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Collectors;

@Configuration
public class CustomKeyGeneratorConfig {
    
    /**
     * 自定义key生成器bean
     * 命名"myKeyGenerator"以便在其他地方引用
     */
    @Bean("myKeyGenerator")
    public KeyGenerator keyGenerator() {
        return new KeyGenerator() {
            @Override
            public Object generate(Object target, Method method, Object... params) {
                // 生成格式:类名-方法名-[参数1,参数2,...]
                String className = target.getClass().getSimpleName();
                String methodName = method.getName();
                
                // 处理参数:过滤null值并转换为字符串
                String paramStr = Arrays.stream(params)
                    .map(p -> p != null ? p.toString() : "null")
                    .collect(Collectors.joining(","));
                
                String key = String.format("%s-%s-[%s]", 
                    className, methodName, paramStr);
                
                // 日志输出便于调试
                System.out.println("生成缓存key:" + key);
                return key;
            }
        };
    }
}

5.1.2 应用示例

java 复制代码
@Service
public class OrderService {
    
    /**
     * 使用自定义key生成器的缓存示例
     * @param orderId 订单ID
     * @return 订单对象
     */
    @Cacheable(value = "orderCache", keyGenerator = "myKeyGenerator")
    public Order getOrderById(Long orderId) {
        System.out.println("查询数据库获取订单:" + orderId);
        // 模拟数据库查询
        return new Order(orderId, "订单" + orderId, 100.0 * orderId);
    }
    
    /**
     * 另一个使用相同key生成器的方法
     */
    @Cacheable(value = "userCache", keyGenerator = "myKeyGenerator")
    public User getUserById(Long userId, Boolean loadDetail) {
        System.out.println("查询用户:" + userId);
        // 模拟数据库查询
        return new User(userId, "用户" + userId);
    }
}

5.1.3 注意事项

  1. 确保生成的key具有唯一性,避免不同方法产生相同key导致缓存冲突
  2. 考虑key的可读性,便于后期排查问题
  3. 注意key的长度,避免生成过长的key影响Redis性能
  4. 对于复杂对象作为参数的情况,建议重写toString()方法

5.2 缓存穿透、缓存击穿、缓存雪崩解决方案

5.2.1 缓存穿透(Cache Penetration)

典型场景

  • 恶意攻击者不断查询不存在的数据ID
  • 业务代码bug导致大量无效查询
  • 新业务上线初期数据不完整

详细解决方案

1.缓存空对象

yaml 复制代码
# application.yml配置
spring:
  cache:
    redis:
      cache-null-values: true  # 允许缓存null值
      time-to-live: 300s      # 设置较短的过期时间(5分钟)

2.布隆过滤器实现

java 复制代码
// 初始化布隆过滤器
@Bean
public BloomFilter<String> orderBloomFilter() {
    return BloomFilter.create(
        Funnels.stringFunnel(Charset.defaultCharset()),
        1000000,  // 预期元素数量
        0.01      // 误判率
    );
}

// 在Service中使用
@Service
public class OrderService {
    @Autowired
    private BloomFilter<String> orderBloomFilter;
    
    @PostConstruct
    public void init() {
        // 初始化阶段加载所有有效订单ID到布隆过滤器
        List<Long> allOrderIds = orderRepository.findAllIds();
        allOrderIds.forEach(id -> orderBloomFilter.put("order:" + id));
    }
    
    @Cacheable(value = "orderCache")
    public Order getOrderById(Long orderId) {
        // 先检查布隆过滤器
        if (!orderBloomFilter.mightContain("order:" + orderId)) {
            return null;  // 肯定不存在
        }
        // 查询数据库...
    }
}

5.2.2 缓存击穿(Cache Breakdown)

典型场景

  • 热点商品信息缓存过期
  • 秒杀活动开始时的库存查询
  • 新闻热点事件详情

详细解决方案

1.互斥锁实现

java 复制代码
@Cacheable(value = "hotProduct", key = "#productId")
public Product getHotProduct(String productId) {
    // 尝试获取分布式锁
    String lockKey = "lock:product:" + productId;
    try {
        // 使用Redis的SETNX实现分布式锁
        Boolean locked = redisTemplate.opsForValue().setIfAbsent(
            lockKey, "1", Duration.ofSeconds(10));
        
        if (Boolean.TRUE.equals(locked)) {
            // 获取锁成功,查询数据库
            Product product = productDao.getById(productId);
            // 模拟数据库查询耗时
            Thread.sleep(100);
            return product;
        } else {
            // 获取锁失败,等待并重试
            Thread.sleep(50);
            return getHotProduct(productId);
        }
    } catch (InterruptedException e) {
        Thread.currentThread().interrupt();
        throw new RuntimeException("获取产品信息失败", e);
    } finally {
        // 释放锁
        redisTemplate.delete(lockKey);
    }
}

2.逻辑过期方案

java 复制代码
@Data
public class RedisData<T> {
    private T data;          // 实际数据
    private LocalDateTime expireTime;  // 逻辑过期时间
}

// 在Service中
public Product getProductWithLogicalExpire(String productId) {
    // 1. 从缓存查询数据
    RedisData<Product> redisData = redisTemplate.opsForValue()
        .get("product:" + productId);
    
    // 2. 判断是否过期
    if (redisData == null || 
        redisData.getExpireTime().isAfter(LocalDateTime.now())) {
        // 3. 未过期,直接返回
        return redisData.getData();
    }
    
    // 4. 已过期,获取互斥锁重建缓存
    String lockKey = "lock:product:" + productId;
    if (redisTemplate.opsForValue().setIfAbsent(lockKey, "1", Duration.ofSeconds(10))) {
        try {
            // 5. 获取锁成功,开启独立线程重建缓存
            CompletableFuture.runAsync(() -> {
                Product product = productDao.getById(productId);
                RedisData<Product> newData = new RedisData<>();
                newData.setData(product);
                newData.setExpireTime(LocalDateTime.now().plusHours(1));
                redisTemplate.opsForValue().set(
                    "product:" + productId, 
                    newData
                );
            });
        } finally {
            redisTemplate.delete(lockKey);
        }
    }
    
    // 6. 返回旧数据
    return redisData.getData();
}

5.2.3 缓存雪崩(Cache Avalanche)

典型场景

  • 缓存服务器重启
  • 大量缓存同时到期
  • 缓存集群故障

详细解决方案

1.随机过期时间实现

java 复制代码
@Configuration
public class RedisCacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
        // 创建随机数生成器
        Random random = new Random();
        
        // 基础配置
        RedisCacheConfiguration defaultConfig = RedisCacheConfiguration
            .defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30))  // 基础过期时间30分钟
            .computePrefixWith(cacheName -> "cache:" + cacheName + ":")
            .serializeKeysWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(RedisSerializationContext.SerializationPair
                .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        // 为每个缓存创建独立配置
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("productCache", 
            defaultConfig.entryTtl(Duration.ofMinutes(30 + random.nextInt(10))));
        cacheConfigurations.put("userCache", 
            defaultConfig.entryTtl(Duration.ofMinutes(30 + random.nextInt(10))));
        
        return RedisCacheManager.builder(redisConnectionFactory)
            .cacheDefaults(defaultConfig)
            .withInitialCacheConfigurations(cacheConfigurations)
            .build();
    }
}

2.多级缓存架构

java 复制代码
@Service
public class ProductService {
    // 本地缓存(使用Caffeine)
    private final Cache<String, Product> localCache = Caffeine.newBuilder()
        .maximumSize(1000)
        .expireAfterWrite(5, TimeUnit.MINUTES)
        .build();
    
    @Autowired
    private RedisTemplate<String, Product> redisTemplate;
    
    public Product getProduct(String productId) {
        // 1. 查询本地缓存
        Product product = localCache.getIfPresent(productId);
        if (product != null) {
            return product;
        }
        
        // 2. 查询Redis缓存
        product = redisTemplate.opsForValue().get("product:" + productId);
        if (product != null) {
            // 回填本地缓存
            localCache.put(productId, product);
            return product;
        }
        
        // 3. 查询数据库
        product = productDao.getById(productId);
        if (product != null) {
            // 写入Redis和本地缓存
            redisTemplate.opsForValue().set(
                "product:" + productId, 
                product, 
                Duration.ofMinutes(30 + new Random().nextInt(10))
            );
            localCache.put(productId, product);
        }
        
        return product;
    }
}

5.3 注意事项与最佳实践

5.3.1 缓存一致性

双写问题解决方案

java 复制代码
@Service
public class OrderService {
    
    @CacheEvict(value = "orderCache", key = "#order.id")
    @Transactional
    public Order updateOrder(Order order) {
        // 先更新数据库
        Order updated = orderRepository.save(order);
        // 手动清除缓存确保事务提交后执行
        return updated;
    }
    
    // 或者使用@CachePut
    @CachePut(value = "orderCache", key = "#result.id")
    @Transactional
    public Order updateOrderWithCache(Order order) {
        return orderRepository.save(order);
    }
}

5.3.2 事务与缓存顺序

调整AOP执行顺序

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig implements Ordered {
    
    // 设置缓存AOP的顺序(值越小优先级越高)
    // 事务AOP的默认order是Ordered.LOWEST_PRECEDENCE - 1 (即Integer.MAX_VALUE - 1)
    private static final int CACHE_ORDER = Ordered.LOWEST_PRECEDENCE;
    
    @Bean
    public CacheInterceptor cacheInterceptor() {
        CacheInterceptor interceptor = new CacheInterceptor();
        interceptor.setOrder(CACHE_ORDER);
        return interceptor;
    }
    
    @Override
    public int getOrder() {
        return CACHE_ORDER;
    }
}

5.3.3 监控与运维

缓存监控指标

  1. 缓存命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)
  2. 平均响应时间
  3. 缓存大小和使用量
  4. 过期key数量

监控实现示例

java 复制代码
@Service
public class CacheMonitorService {
    
    @Autowired
    private RedisTemplate<String, Object> redisTemplate;
    
    /**
     * 获取缓存统计信息
     */
    public CacheStats getCacheStats(String cacheName) {
        Set<String> keys = redisTemplate.keys(cacheName + ":*");
        long totalSize = keys.size();
        long expireCount = keys.stream()
            .filter(key -> redisTemplate.getExpire(key) != null)
            .count();
        
        return new CacheStats(cacheName, totalSize, expireCount);
    }
    
    /**
     * 定期清理过期缓存
     */
    @Scheduled(fixedRate = 3600000)  // 每小时执行一次
    public void cleanExpiredCaches() {
        Set<String> allKeys = redisTemplate.keys("*");
        allKeys.forEach(key -> {
            Long ttl = redisTemplate.getExpire(key);
            if (ttl != null && ttl < 60) {  // 即将过期的key
                redisTemplate.delete(key);
            }
        });
    }
}

5.3.4 性能优化建议

1.批量操作

java 复制代码
@Cacheable(value = "userCache")
public Map<Long, User> batchGetUsers(List<Long> userIds) {
    // 使用multiGet批量查询
    List<String> cacheKeys = userIds.stream()
        .map(id -> "user:" + id)
        .collect(Collectors.toList());
    
    List<User> cachedUsers = redisTemplate.opsForValue().multiGet(cacheKeys);
    // 处理缓存命中与未命中的逻辑...
}

2.缓存预热

java 复制代码
@Component
public class CacheWarmUp implements ApplicationRunner {
    
    @Autowired
    private ProductService productService;
    
    @Override
    public void run(ApplicationArguments args) {
        // 应用启动时预热热门商品
        List<Long> hotProductIds = productService.getHotProductIds();
        hotProductIds.forEach(productService::getProductById);
    }
}

3.缓存分区

java 复制代码
@Configuration
public class PartitionedCacheConfig {
    
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory factory) {
        // 创建不同分区的缓存配置
        Map<String, RedisCacheConfiguration> cacheConfigs = new HashMap<>();
        
        // 高频访问数据(短时间缓存)
        cacheConfigs.put("hotData", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(5))
            .serializeValuesWith(...));
            
        // 低频访问数据(长时间缓存)
        cacheConfigs.put("coldData", RedisCacheConfiguration.defaultCacheConfig()
            .entryTtl(Duration.ofHours(24))
            .serializeValuesWith(...));
            
        return RedisCacheManager.builder(factory)
            .withInitialCacheConfigurations(cacheConfigs)
            .build();
    }
}
复制代码
相关推荐
Tonyzz2 小时前
开发编程进化论:openspec的魔力
前端·ai编程·vibecoding
undefined在掘金390412 小时前
Flutter应用图标生成插件flutter_launcher_icons的使用
前端
快手技术2 小时前
从“拦路虎”到“修路工”:基于AhaEdit的广告素材修复
前端·算法·架构
程序员小赵同学2 小时前
Spring AI Alibaba语音合成实战:从零开始实现文本转语音功能
人工智能·spring·语音识别
weixin_438694392 小时前
pnpm 安装依赖后 仍然启动报的问题
开发语言·前端·javascript·经验分享
学IT的周星星2 小时前
Spring 框架整合 JUnit 单元测试
java·spring·junit·单元测试
烟袅3 小时前
深入 V8 引擎:JavaScript 执行机制全解析(从编译到调用栈)
前端·javascript
金梦人生3 小时前
UniApp + Vue3 + TS 工程化实战笔记
前端·微信小程序
海云前端13 小时前
移动端 CSS 十大避坑指南 熬夜总结的实战解决方案
前端