【Spring】Spring Cache 深度解析

Spring Cache 深度解析

一、什么是 Spring Cache

Spring Cache 是 Spring 框架提供的声明式缓存抽象层 ,它通过少量注解即可为应用添加缓存能力,无需侵入业务代码 。核心设计目标是解耦缓存实现与业务逻辑,支持在运行时灵活切换 EhCache、Redis、Caffeine、Hazelcast 等缓存实现。

关键特性

  • 声明式编程 :仅通过 @Cacheable@CacheEvict 等注解实现缓存
  • 实现无关性:接口抽象,支持多种缓存提供者
  • 与 Spring 生态无缝集成:事务、AOP、SpEL 表达式

二、核心注解与生命周期

1. @Cacheable:查询缓存

作用:方法执行前检查缓存,存在则直接返回,不存在则执行方法并将结果存入缓存。

java 复制代码
@Service
public class UserService {
    // 缓存key为用户ID,缓存不存在时查询数据库
    @Cacheable(value = "users", key = "#id")
    public User getUser(Long id) {
        return userMapper.selectById(id); // 仅缓存未命中时执行
    }
    
    // 支持条件缓存:仅当id > 100时缓存
    @Cacheable(value = "users", key = "#id", condition = "#id > 100")
    public User getVipUser(Long id) {
        return userMapper.selectById(id);
    }
    
    // 使用SpEL生成key:如 "user:100:admin"
    @Cacheable(value = "users", key = "'user:' + #id + ':' + #type")
    public User getUserByType(Long id, String type) {
        return userMapper.selectByIdAndType(id, type);
    }
}

执行流程

  1. 解析 SpEL 表达式生成缓存 Key
  2. 根据 value/cacheNames 定位缓存对象
  3. 查询缓存:
    • 命中:直接返回缓存值(方法体不执行)
    • 未命中:执行方法 → 将返回值存入缓存 → 返回结果

2. @CachePut:更新缓存

作用无论缓存是否存在,都执行方法 ,并将结果更新到缓存。适用于创建或更新操作

java 复制代码
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    userMapper.updateById(user);
    return user; // 返回值会更新到缓存
}

⚠️ 注意@CachePut@Cacheable 不能用在同一方法(逻辑冲突)。


3. @CacheEvict:清除缓存

作用 :删除缓存项,通常用于删除或修改操作

java 复制代码
// 删除指定key的缓存
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
    userMapper.deleteById(id);
}

// 删除整个缓存区的所有条目
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() {
    // 批量删除后的清理操作
}

// 在方法执行前清除缓存(默认是执行后)
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void deleteUserBefore(Long id) {
    userMapper.deleteById(id);
}

4. @Caching:组合操作

作用:单个方法上组合多个缓存操作。

java 复制代码
@Caching(
    put = {
        @CachePut(value = "users", key = "#user.id"),
        @CachePut(value = "users", key = "#user.email")
    },
    evict = {
        @CacheEvict(value = "userList", allEntries = true)
    }
)
public User createUser(User user) {
    userMapper.insert(user);
    return user;
}

5. @CacheConfig:类级别统一配置

作用:在类上统一指定缓存名称等公共配置,避免重复书写。

java 复制代码
@Service
@CacheConfig(cacheNames = "users")
public class UserService {
    @Cacheable(key = "#id") // 无需重复指定value
    public User getUser(Long id) { /*...*/ }
    
    @CacheEvict(key = "#id")
    public void deleteUser(Long id) { /*...*/ }
}

三、缓存管理器配置

1. 启用缓存

java 复制代码
@SpringBootApplication
@EnableCaching // 启用缓存功能
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

2. 配置 Caffeine 本地缓存(推荐)

java 复制代码
@Configuration
public class CacheConfig {
    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager();
        
        // 全局默认配置
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .initialCapacity(100)
            .maximumSize(500)
            .expireAfterWrite(10, TimeUnit.MINUTES)
            .recordStats()); // 开启统计
        
        // 为特定缓存区定制配置
        cacheManager.registerCustomCache("users", 
            Caffeine.newBuilder()
                .maximumSize(1000)
                .expireAfterAccess(30, TimeUnit.MINUTES)
                .build());
        
        return cacheManager;
    }
}

3. 配置 Redis 分布式缓存

java 复制代码
@Configuration
public class RedisCacheConfig {
    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        RedisCacheConfiguration config = RedisCacheConfiguration
            .defaultCacheConfig()
            .entryTtl(Duration.ofMinutes(30)) // 默认30分钟过期
            .serializeKeysWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new StringRedisSerializer()))
            .serializeValuesWith(
                RedisSerializationContext.SerializationPair
                    .fromSerializer(new GenericJackson2JsonRedisSerializer()));
        
        return RedisCacheManager.builder(factory)
            .cacheDefaults(config)
            .withCacheConfiguration("users", 
                RedisCacheConfiguration
                    .defaultCacheConfig()
                    .entryTtl(Duration.ofHours(2))) // users缓存2小时过期
            .build();
    }
}

Spring Boot 自动配置

  • 引入 spring-boot-starter-data-redis 自动配置 RedisCacheManager
  • 引入 spring-boot-starter-cache + caffeine 自动配置 CaffeineCacheManager

四、缓存 Key 生成策略

默认策略

  • 无参数:SimpleKey.EMPTY
  • 单个参数:参数值作为 Key(如 "100"
  • 多个参数:SimpleKey [arg1, arg2, ...]

自定义 KeyGenerator

java 复制代码
@Configuration
public class CacheConfig implements CachingConfigurer {
    @Bean
    @Override
    public KeyGenerator keyGenerator() {
        return (target, method, params) -> {
            StringBuilder sb = new StringBuilder();
            sb.append(target.getClass().getSimpleName())
              .append(":")
              .append(method.getName())
              .append(":");
            // 自定义参数序列化逻辑
            return sb.toString();
        };
    }
}

五、应用场景与解决方案

1. 缓存穿透(Cache Penetration)

场景 :查询不存在的数据,大量请求直达数据库。

解决方案

java 复制代码
@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findUser(Long id) {
    return userMapper.selectById(id);
}

// 或使用空值缓存
@Cacheable(value = "users", key = "#id")
public User findUser(Long id) {
    User user = userMapper.selectById(id);
    return user == null ? User.NULL : user; // 缓存空对象
}

2. 缓存击穿(Cache Breakdown)

场景:热点数据过期瞬间,大量请求涌入数据库。

解决方案

java 复制代码
// 设置热点数据永不过期 + 后台异步更新
@Cacheable(value = "hotData", key = "#key")
public HotData getHotData(String key) {
    return loadFromDatabase(key);
}

// 或使用分布式锁(Redisson)
@Cacheable(value = "users", key = "#id")
public User getUserWithLock(Long id) {
    RLock lock = redissonClient.getLock("user:lock:" + id);
    try {
        if (lock.tryLock(100, 10, TimeUnit.SECONDS)) {
            return userMapper.selectById(id);
        }
    } finally {
        lock.unlock();
    }
    return null;
}

3. 缓存雪崩(Cache Avalanche)

场景:大量缓存同时过期,数据库压力骤增。

解决方案

yaml 复制代码
# application.yml
spring:
  cache:
    caffeine:
      spec: maximumSize=1000,expireAfterWrite=30m,expireAfterAccess=25m
      # 设置随机过期时间,避免集中失效
      # expireAfterWrite=30m,randomTime=5m

4. 缓存与事务一致性

问题:事务未提交,缓存已更新,导致脏读。

解决方案

java 复制代码
@Transactional
@CacheEvict(value = "users", key = "#id", beforeInvocation = true)
public void updateUser(Long id, User user) {
    // beforeInvocation=true 确保事务回滚时缓存也被清除
    userMapper.updateById(user);
}

六、缓存监控与统计

1. 开启 Caffeine 统计

java 复制代码
Caffeine.newBuilder()
    .recordStats()
    .build();

2. 暴露监控端点

java 复制代码
@Component
public class CacheMetrics {
    @Autowired
    private CacheManager cacheManager;
    
    @Scheduled(fixedRate = 60000)
    public void printStats() {
        Cache usersCache = cacheManager.getCache("users");
        if (usersCache.getNativeCache() instanceof com.github.benmanes.caffeine.cache.Cache) {
            com.github.benmanes.caffeine.cache.Cache nativeCache = 
                (com.github.benmanes.caffeine.cache.Cache) usersCache.getNativeCache();
            System.out.println("缓存命中率: " + nativeCache.stats().hitRate());
        }
    }
}

Spring Boot Actuator 自动暴露 /actuator/caches 端点,可查看和清除缓存。


七、最佳实践总结

推荐使用

  1. 优先本地缓存:Caffeine(性能最优)或 EhCache
  2. 分布式场景:Redis(集群高可用)
  3. Key 设计类名:方法名:参数 格式,如 user:100
  4. TTL 策略:热点数据短过期,冷数据长过期
  5. 异常处理:缓存操作不应影响主流程,捕获异常并降级
java 复制代码
@Cacheable(value = "users", key = "#id")
public User getUser(Long id) {
    try {
        return userMapper.selectById(id);
    } catch (Exception e) {
        log.error("查询用户失败", e);
        return User.EMPTY; // 返回空对象避免缓存穿透
    }
}

⚠️ 注意事项

  1. 缓存 Key 必须唯一:避免不同方法数据覆盖
  2. 慎用 @CachePut:确保返回值为完整数据,避免缓存不完整对象
  3. 序列化成本:Redis 缓存注意对象序列化开销
  4. 缓存一致性:更新操作必须清除相关缓存
  5. 大对象缓存:评估内存占用,避免 OOM

📊 性能对比

缓存类型 读取性能 写入性能 适用场景
Caffeine 极高(本地) 极高 高频读、数据量适中
Redis 高(网络) 分布式、数据共享
EhCache 传统项目、功能丰富

Spring Cache 通过统一的抽象层,让开发者以极简的注解实现强大的缓存能力,是提升应用性能的必备利器。

相关推荐
计算机毕设VX:Fegn08952 小时前
计算机毕业设计|基于springboot + vue个人博客系统(源码+数据库+文档)
java·数据库·vue.js·spring boot·后端·课程设计
2501_946675642 小时前
Flutter与OpenHarmony打卡轮播图组件
java·javascript·flutter
独自破碎E2 小时前
Spring Boot 3.x和2.x版本相比有哪些区别与改进?
java·spring boot·后端
坚持学习前端日记3 小时前
个人运营小网站的最佳策略
java·学习·程序人生·职场和发展·创业创新
幽络源小助理3 小时前
SpringBoot+Vue美食网站系统源码 | Java餐饮项目免费下载 – 幽络源
java·vue.js·spring boot
k***92163 小时前
C++:继承
java·数据库·c++
Cache技术分享3 小时前
280. Java Stream API - Debugging Streams:如何调试 Java 流处理过程?
前端·后端
Charlie_Byte3 小时前
在 Kratos 中设置自定义 HTTP 响应格式
后端·go
Coder_Boy_3 小时前
基于SpringAI企业级智能教学考试平台考试模块全业务闭环方案
java·人工智能·spring boot·aiops