一、SpringCache 是什么?
1.1 定义与核心思想
SpringCache 是 Spring 框架提供的一套缓存抽象层,它本身不是具体的缓存实现,而是一个统一的缓存访问抽象框架。它基于 Spring 的 AOP(面向切面编程)机制实现,通过声明式注解的方式将缓存功能无缝集成到业务逻辑中。
具体工作流程:
- 当标记了缓存注解的方法被调用时
- SpringCache 会先检查缓存中是否存在对应的结果
- 如果存在则直接返回缓存结果
- 如果不存在则执行方法体,并将结果存入缓存
- 后续相同参数的调用将直接从缓存获取结果
典型应用场景:
- 数据库查询结果缓存
- 复杂计算结果缓存
- 频繁访问的静态数据缓存
- 高并发场景下的数据缓冲
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" |
典型应用场景
- 查询方法(如根据ID查询用户)
- 计算密集型方法(如复杂计算、报表生成)
- 频繁访问但数据变化不频繁的方法
完整示例
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。常用于:
- 数据更新后保持缓存一致性
- 主动刷新缓存数据
注意事项
- 方法一定会被执行
- 应该只用于更新操作,不应用于查询方法
- 需要确保key与@Cacheable的key一致
典型应用场景
- 用户信息更新
- 订单状态变更
- 任何需要保持缓存与数据库同步的操作
完整示例
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 |
典型应用场景
- 删除用户后清除用户缓存
- 批量操作后需要刷新缓存
- 数据变更时需要清除关联缓存
完整示例
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
典型应用场景
- 更新主缓存同时清除列表缓存
- 多级缓存操作
- 复杂业务逻辑需要同时操作多个缓存区域
完整示例
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:统一配置缓存属性
详细作用
类级别的注解,用于统一配置该类的缓存相关属性,避免在每个方法上重复配置。
可配置属性
- cacheNames:统一缓存名称
- keyGenerator:统一key生成器
- cacheManager:统一缓存管理器
- cacheResolver:统一缓存解析器
典型应用场景
- 服务类中多个方法使用相同缓存配置
- 需要统一管理缓存命名空间
- 需要统一使用自定义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);
}
}
最佳实践建议
- 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 的缓存自动配置机制。
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 特性与限制分析:
核心特性:
- 零配置开箱即用
- 基于内存存储,访问速度极快
- 线程安全的并发实现
生产环境限制:
- 单节点局限:缓存数据仅存在于当前JVM内存中,无法在集群环境下共享
- 无持久化:应用重启后所有缓存数据立即丢失
- 无过期策略:无法设置自动失效时间,可能导致内存无限增长
- 无高级功能:不支持缓存统计、监听等管理功能
适用场景建议:
- 本地开发环境的功能验证
- 单元测试中的缓存模拟
- 小型非关键性应用的原型开发
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 # 开启缓存统计(监控用)
关键配置项说明:
-
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 示例:
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();
}
}
配置亮点说明:
-
多缓存差异化配置:
- 通过静态代码块预定义不同缓存名称的配置
- 支持为每个缓存设置独立的TTL和序列化策略
-
序列化优化:
- Key使用String序列化保证可读性
- Value可根据业务需求选择:
JdkSerializationRedisSerializer:通用但二进制不可读GenericJackson2JsonRedisSerializer:JSON格式,跨语言友好GenericFastJsonRedisSerializer:高性能JSON处理
-
运维友好设计:
- 添加版本前缀(v2:)便于缓存迁移
- 启用统计功能方便监控缓存命中率
- 事务支持确保缓存与数据库一致性
生产环境建议:
-
对于大型值对象,建议配置值压缩:
java.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(带完整注释)
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 功能组织这些测试用例:
测试准备
- 确保 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 脚本功能自动验证:
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);
});
其他验证方式
-
使用 Redis CLI 查看缓存数据:
bashredis-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 详细实现示例
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 注意事项
- 确保生成的key具有唯一性,避免不同方法产生相同key导致缓存冲突
- 考虑key的可读性,便于后期排查问题
- 注意key的长度,避免生成过长的key影响Redis性能
- 对于复杂对象作为参数的情况,建议重写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 监控与运维
缓存监控指标:
- 缓存命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)
- 平均响应时间
- 缓存大小和使用量
- 过期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();
}
}