SpringCache :让缓存开发更高效
一、SpringCache 是什么?
1.1 定义与核心思想
SpringCache 是 Spring 框架提供的一套缓存抽象层,它本身不是具体的缓存实现,而是一个统一的缓存访问抽象框架。它基于 Spring 的 AOP(面向切面编程)机制实现,通过声明式注解的方式将缓存功能无缝集成到业务逻辑中。
具体工作流程:
- 当标记了缓存注解的方法被调用时
- SpringCache 会先检查缓存中是否存在对应的结果
- 如果存在则直接返回缓存结果
- 如果不存在则执行方法体,并将结果存入缓存
- 后续相同参数的调用将直接从缓存获取结果
典型应用场景:
- 数据库查询结果缓存
- 复杂计算结果缓存
- 频繁访问的静态数据缓存
- 高并发场景下的数据缓冲
1.2 SpringCache 的优势
1.2.1 低侵入性
通过 @Cacheable、@CacheEvict 等注解实现缓存功能,无需修改业务方法的核心逻辑。例如:
@Cacheable(value = "userCache", key = "#id")
public User getUserById(Long id) {return userRepository.findById(id);
}
1.2.2 灵活性高
支持多种缓存实现:
- 本地缓存:Caffeine、Ehcache
- 分布式缓存:Redis、Memcached
- 混合缓存:多级缓存架构
仅需修改配置即可切换缓存实现,无需修改业务代码:
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微服务协同
典型配置示例:
@Configuration
@EnableCaching
public class CacheConfig {@Beanpublic 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" |
典型应用场景
- 查询方法(如根据ID查询用户)
- 计算密集型方法(如复杂计算、报表生成)
- 频繁访问但数据变化不频繁的方法
完整示例
@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。常用于:
- 数据更新后保持缓存一致性
- 主动刷新缓存数据
注意事项
- 方法一定会被执行
- 应该只用于更新操作,不应用于查询方法
- 需要确保key与@Cacheable的key一致
典型应用场景
- 用户信息更新
- 订单状态变更
- 任何需要保持缓存与数据库同步的操作
完整示例
@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 |
典型应用场景
- 删除用户后清除用户缓存
- 批量操作后需要刷新缓存
- 数据变更时需要清除关联缓存
完整示例
// 删除单个缓存
@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
典型应用场景
- 更新主缓存同时清除列表缓存
- 多级缓存操作
- 复杂业务逻辑需要同时操作多个缓存区域
完整示例
@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:统一配置缓存属性
详细作用
类级别的注解,用于统一配置该类的缓存相关属性,避免在每个方法上重复配置。
可配置属性
- cacheNames:统一缓存名称
- keyGenerator:统一key生成器
- cacheManager:统一缓存管理器
- cacheResolver:统一缓存解析器
典型应用场景
- 服务类中多个方法使用相同缓存配置
- 需要统一管理缓存命名空间
- 需要统一使用自定义key生成策略
完整示例
@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);}
}
最佳实践建议
- key设计:使用业务主键而不是全参数组合
- 缓存粒度:建议按业务场景划分缓存区域
- null值处理:使用unless避免缓存null值
- 事务考虑:缓存操作与事务边界保持一致
- 监控告警:对重要缓存操作添加监控
三、SpringCache 配置方式(以 Spring Boot 为例)
Spring Boot 为 SpringCache 提供了开箱即用的自动配置支持,通过简单的配置即可集成多种缓存实现。不同的缓存产品(如 Redis、Caffeine、Ehcache 等)在 Spring Boot 中的配置方式略有不同。以下是两种最常用缓存产品的详细配置指南,包含从基础到生产环境的完整配置过程。
3.1 基础配置:使用默认缓存(ConcurrentMapCache)
Spring Boot 默认提供了基于内存的 ConcurrentMapCache 实现,这是最简单的缓存方案,特别适合开发环境快速测试缓存功能,无需任何额外依赖和复杂配置。
完整配置步骤说明:
1.启用缓存功能: 在 Spring Boot 主启动类上添加 @EnableCaching 注解,该注解会触发 Spring 的缓存自动配置机制。
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 等标准注解即可,例如:
@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 特性与限制分析:
核心特性:
- 零配置开箱即用
- 基于内存存储,访问速度极快
- 线程安全的并发实现
生产环境限制:
- 单节点局限:缓存数据仅存在于当前JVM内存中,无法在集群环境下共享
- 无持久化:应用重启后所有缓存数据立即丢失
- 无过期策略:无法设置自动失效时间,可能导致内存无限增长
- 无高级功能:不支持缓存统计、监听等管理功能
适用场景建议:
- 本地开发环境的功能验证
- 单元测试中的缓存模拟
- 小型非关键性应用的原型开发
3.2 生产配置:使用 Redis 作为缓存
Redis 是当前最流行的分布式内存数据库,作为缓存方案具有诸多优势。下面详细介绍 Spring Boot 中集成 Redis 缓存的完整流程。
3.2.1 依赖引入
Maven 配置:
<!-- 核心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 配置:
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 完整配置示例:
spring:redis:host: redis-production.example.com # 生产环境建议使用域名port: 6379password: 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 # 开启缓存统计(监控用)
关键配置项说明:
time-to-live:
- 支持的时间单位:
ms(毫秒)、s(秒)、m(分钟)、h(小时)、d(天) - 示例:
1h30m表示1小时30分钟
- 支持的时间单位:
缓存穿透防护:
cache-null-values: false时,方法返回null不会被缓存- 对于高频访问但可能为null的数据,可设置为true并配合较短TTL
Key命名策略:
- 默认格式:
cacheName::key - 自定义前缀可避免多应用共用一个Redis时的key冲突
- 默认格式:
3.2.3 高级自定义配置
定制化 RedisCacheManager 示例:
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@Overridepublic CacheManager cacheManager(RedisConnectionFactory connectionFactory) {return RedisCacheManager.builder(connectionFactory).withInitialCacheConfigurations(CACHE_CONFIG_MAP) // 应用个性化配置.cacheDefaults(defaultConfig()) // 设置默认配置.transactionAware() // 支持事务.enableStatistics() // 启用统计.build();}
}
配置亮点说明:
多缓存差异化配置:
- 通过静态代码块预定义不同缓存名称的配置
- 支持为每个缓存设置独立的TTL和序列化策略
序列化优化:
- Key使用String序列化保证可读性
- Value可根据业务需求选择:
JdkSerializationRedisSerializer:通用但二进制不可读GenericJackson2JsonRedisSerializer:JSON格式,跨语言友好GenericFastJsonRedisSerializer:高性能JSON处理
运维友好设计:
- 添加版本前缀(v2:)便于缓存迁移
- 启用统计功能方便监控缓存命中率
- 事务支持确保缓存与数据库一致性
生产环境建议:
对于大型值对象,建议配置值压缩:
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new SnappyRedisSerializer()))考虑实现
CacheErrorHandler处理Redis连接异常情况对于关键业务数据,建议配置双写策略保证缓存可靠性
四、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(带完整注释)
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(带完整模拟数据库实现)
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(完整业务接口定义)
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(带详细缓存策略说明)
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 {@Resourceprivate 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接口)
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 {@Resourceprivate 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}* 缓存影响:会清除所有用户缓存*/@PostMappingpublic String addUser(@RequestBody User user) {userService.addUser(user);return "操作成功:新增用户 " + user.getName();}/*** 用户更新接口(PUT请求)* @param user 更新后的用户数据* @return 更新后的用户对象(带最新缓存)* 示例请求:PUT /user* 请求体:{"id":1,"name":"张三-new","age":27}*/@PutMappingpublic 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 功能组织这些测试用例:
测试准备
- 确保 Redis 服务已启动(如使用 Redis 作为缓存实现)
- 启动 SpringBoot 应用,默认端口 8080
- 准备测试数据:
- 用户1: {"id":1,"name":"张三","age":25}
- 用户2: {"id":2,"name":"李四","age":30}
详细测试场景
场景 1:查询用户(验证 @Cacheable)
测试步骤:
- 第一次发送请求:GET http://localhost:8080/user/1
- 观察控制台日志和响应
- 第二次发送相同请求
- 比较两次请求的差异
预期结果:
- 第一次请求:
- 控制台输出:
执行数据库查询:getUserById(1) - 响应时间较长(约100-300ms)
- 返回结果:
{"id":1,"name":"张三","age":25}
- 控制台输出:
- 第二次请求:
- 控制台无输出
- 响应时间显著缩短(约10-50ms)
- 返回相同结果
- 第一次请求:
场景 2:更新用户(验证 @CachePut)
测试步骤:
- 发送更新请求:PUT http://localhost:8080/user
- Headers: Content-Type=application/json
- Body:
{"id":1,"name":"张三-update","age":26}
- 立即查询用户1
- 观察缓存更新情况
- 发送更新请求:PUT http://localhost:8080/user
预期结果:
- 更新请求:
- 控制台输出:
执行数据库更新:updateUser(User(id=1, name=张三-update, age=26)) - 返回状态码200
- 返回结果:
{"id":1,"name":"张三-update","age":26}
- 控制台输出:
- 查询请求:
- 控制台无输出
- 返回更新后的数据
- 更新请求:
场景 3:删除用户(验证 @CacheEvict)
测试步骤:
- 发送删除请求:DELETE http://localhost:8080/user/1
- 立即查询用户1
- 观察缓存清除情况
预期结果:
- 删除请求:
- 控制台输出:
执行数据库删除:deleteUser(1) - 返回状态码200
- 返回结果:
删除用户成功
- 控制台输出:
- 查询请求:
- 控制台输出:
执行数据库查询:getUserById(1) - 返回状态码404
- 返回结果:
null
- 控制台输出:
- 删除请求:
场景 4:新增用户(验证 @CacheEvict (allEntries = true))
测试步骤:
- 发送新增请求:POST http://localhost:8080/user
- Headers: Content-Type=application/json
- Body:
{"id":3,"name":"王五","age":28}
- 查询之前已缓存的用户2
- 观察缓存清除情况
- 发送新增请求:POST http://localhost:8080/user
预期结果:
- 新增请求:
- 控制台输出:
执行数据库新增:addUser(User(id=3, name=王五, age=28)) - 返回状态码201
- 返回结果:
新增用户成功
- 控制台输出:
- 查询请求:
- 控制台输出:
执行数据库查询:getUserById(2) - 返回状态码200
- 返回结果:
{"id":2,"name":"李四","age":30}
- 控制台输出:
- 新增请求:
测试结果分析
建议使用 Postman 的 Test 脚本功能自动验证:
// 示例测试脚本
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);
});
其他验证方式
- 使用 Redis CLI 查看缓存数据:
redis-cli keys * get user::1 - 在应用日志中搜索缓存相关日志:
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 生成策略时,如以下几种场景:
- 需要为所有缓存 key 添加统一前缀(如业务标识)
- 需要忽略方法中的某些参数(如分页参数)
- 需要基于对象特定属性生成 key(而非整个对象)
- 需要实现跨方法的统一 key 生成规则
在这些情况下,可以实现 KeyGenerator 接口来自定义 key 生成逻辑。
5.1.1 详细实现示例
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() {@Overridepublic 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 应用示例
@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 注意事项
- 确保生成的key具有唯一性,避免不同方法产生相同key导致缓存冲突
- 考虑key的可读性,便于后期排查问题
- 注意key的长度,避免生成过长的key影响Redis性能
- 对于复杂对象作为参数的情况,建议重写toString()方法
5.2 缓存穿透、缓存击穿、缓存雪崩解决方案
5.2.1 缓存穿透(Cache Penetration)
典型场景:
- 恶意攻击者不断查询不存在的数据ID
- 业务代码bug导致大量无效查询
- 新业务上线初期数据不完整
详细解决方案:
1.缓存空对象:
# application.yml配置
spring:cache:redis:cache-null-values: true # 允许缓存null值time-to-live: 300s # 设置较短的过期时间(5分钟)
2.布隆过滤器实现:
// 初始化布隆过滤器
@Bean
public BloomFilter<String> orderBloomFilter() {return BloomFilter.create(Funnels.stringFunnel(Charset.defaultCharset()),1000000, // 预期元素数量0.01 // 误判率);
}// 在Service中使用
@Service
public class OrderService {@Autowiredprivate BloomFilter<String> orderBloomFilter;@PostConstructpublic 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.互斥锁实现:
@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.逻辑过期方案:
@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.随机过期时间实现:
@Configuration
public class RedisCacheConfig {@Beanpublic 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.多级缓存架构:
@Service
public class ProductService {// 本地缓存(使用Caffeine)private final Cache<String, Product> localCache = Caffeine.newBuilder().maximumSize(1000).expireAfterWrite(5, TimeUnit.MINUTES).build();@Autowiredprivate 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 缓存一致性
双写问题解决方案:
@Service
public class OrderService {@CacheEvict(value = "orderCache", key = "#order.id")@Transactionalpublic Order updateOrder(Order order) {// 先更新数据库Order updated = orderRepository.save(order);// 手动清除缓存确保事务提交后执行return updated;}// 或者使用@CachePut@CachePut(value = "orderCache", key = "#result.id")@Transactionalpublic Order updateOrderWithCache(Order order) {return orderRepository.save(order);}
}
5.3.2 事务与缓存顺序
调整AOP执行顺序:
@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;@Beanpublic CacheInterceptor cacheInterceptor() {CacheInterceptor interceptor = new CacheInterceptor();interceptor.setOrder(CACHE_ORDER);return interceptor;}@Overridepublic int getOrder() {return CACHE_ORDER;}
}
5.3.3 监控与运维
缓存监控指标:
- 缓存命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)
- 平均响应时间
- 缓存大小和使用量
- 过期key数量
监控实现示例:
@Service
public class CacheMonitorService {@Autowiredprivate 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) { // 即将过期的keyredisTemplate.delete(key);}});}
}
5.3.4 性能优化建议
1.批量操作:
@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.缓存预热:
@Component
public class CacheWarmUp implements ApplicationRunner {@Autowiredprivate ProductService productService;@Overridepublic void run(ApplicationArguments args) {// 应用启动时预热热门商品List<Long> hotProductIds = productService.getHotProductIds();hotProductIds.forEach(productService::getProductById);}
}
3.缓存分区:
@Configuration
public class PartitionedCacheConfig {@Beanpublic 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();}
}
