当前位置: 首页 > news >正文

SpringCache :让缓存开发更高效

一、SpringCache 是什么?

1.1 定义与核心思想

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

具体工作流程

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

典型应用场景

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

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/cacheNamesString[]必填,指定缓存名称(可以理解为缓存分区)@Cacheable("userCache")
keyString自定义缓存键,支持SpEL表达式key="#id"
conditionString缓存条件,结果为true才缓存condition="#id>10"
unlessString排除条件,结果为true则不缓存unless="#result==null"

典型应用场景

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

完整示例

@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. 任何需要保持缓存与数据库同步的操作

完整示例

@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:触发缓存删除

详细作用

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

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

属性详解

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

典型应用场景

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

完整示例

// 删除单个缓存
@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. 复杂业务逻辑需要同时操作多个缓存区域

完整示例

@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生成策略

完整示例

@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 的缓存自动配置机制。

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 特性与限制分析:

核心特性

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

生产环境限制

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

适用场景建议

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

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          # 开启缓存统计(监控用)

关键配置项说明

  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 示例

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();}
}

配置亮点说明

  1. 多缓存差异化配置

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

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

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

生产环境建议

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

    .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(带完整注释)
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 功能组织这些测试用例:

测试准备

  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)
  • 测试步骤

    • 发送删除请求:DELETE http://localhost:8080/user/1
    • 立即查询用户1
    • 观察缓存清除情况
  • 预期结果

    • 删除请求:
      • 控制台输出:执行数据库删除: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 脚本功能自动验证:

// 示例测试脚本
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 查看缓存数据:
    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 详细实现示例

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 注意事项

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

缓存监控指标

  1. 缓存命中率 = 缓存命中次数 / (缓存命中次数 + 缓存未命中次数)
  2. 平均响应时间
  3. 缓存大小和使用量
  4. 过期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();}
}

http://www.dtcms.com/a/568772.html

相关文章:

  • 电路分析 | Phasor Analysis(篇 1)
  • 网站备案取消长春网站建设模板样式
  • get_ccopt系列命令介绍(二)
  • 成都工业学院文献检索在哪个网站做破解wordpress密码
  • 做网站用什么系统好网站登录验证码是怎么做的
  • SQL语法基础教程
  • 算法25.0
  • 无穿戴动捕技术:解锁动作捕捉新维度,拓展多元应用边界
  • 高速PCB设计指南(5)
  • 栈与队列---算法题
  • 外包加工网站开发一个网页具体流程
  • 泰安肥城做网站的公司平台推广活动策划方案
  • 衡石科技跨平台数据网关技术解析:实现多源异构数据整合的新范式
  • 计算机网络实验04:IP与ICMP数据报分析实验
  • 基于python的天气预报系统设计和可视化数据分析源码+报告
  • lerobot so-arm101复现步骤
  • 司马阅与数之境科技达成生态战略合作,释放1+1>2的产业赋能价值
  • IE跳转Chrome浏览器及静默打包
  • Chrome恢复关闭网页的快捷键
  • python报修网站开发源码建设网站遇到的问题
  • 深入解析 Qt QListWidget 控件:功能、实现与最佳实践
  • Qt在线安装问题
  • Qt Quick ApplicationQt Quick Application (compat)
  • 快速搭建分布式链路追踪系统:SkyWalking全攻略
  • 45 C++智能指针的原理与模拟实现,内存泄漏与RAII
  • 时序数据库系列(二):InfluxDB安装配置从零搭建
  • Rust实战开发之图形界面开发入门(egui crate)
  • 如何在centos 中运行arm64程序
  • 工业时序数据库TDengine 架构、性能与实战全解析
  • 朗迪锋@2025人因工程与智能系统交互国际会议