Spring Cache 使用详解

引言

缓存是高并发架构中提升系统响应速度、降低数据库压力的常规手段,Spring Framework 提供了一套优雅、简洁且高度抽象的缓存机制——Spring Cache,让我们无需编写繁琐的缓存逻辑,仅通过几个注解即可实现方法级别的缓存控制。

本文将从核心原理、常用注解、配置方式以及在实际项目中的最佳实践等多维度对 Spring Cache详解。


一、Spring Cache 是什么?

Spring Cache 并不是一个具体的缓存实现,而是一套缓存抽象层。它定义了一组标准接口和注解,允许开发者以统一的方式接入各种底层缓存框架,如:

  • 本地缓存:Caffeine、EhCache、ConcurrentMap
  • 分布式缓存:Redis、Hazelcast、Memcached(需适配)
  • JSR-107 兼容实现:如 Infinispan

其核心思想是:“缓存方法的返回结果”。当一个被 @Cacheable 注解的方法被调用时,Spring 会根据方法参数生成一个 key,检查缓存中是否存在该 key 对应的值:

  • 若存在 → 直接返回缓存值,跳过方法执行
  • 若不存在 → 执行方法,将结果存入缓存后返回

这种“透明化”的缓存机制,让业务代码几乎无需关心缓存细节。


二、快速上手:启用 Spring Cache

1. 添加依赖(以 Spring Boot 为例)

<!-- Spring Boot Starter Cache -->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

<!-- 可选:具体缓存实现,例如 Caffeine -->
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

如果不指定具体实现,Spring Boot 会自动配置一个基于 ConcurrentHashMap 的简单缓存(适合开发测试)。

2. 启用缓存支持

在主配置类或启动类上添加 @EnableCaching:

@SpringBootApplication
@EnableCaching
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

至此,Spring Cache 已启用,可以开始使用注解了!


三、核心注解详解

Spring Cache 提供了 6 个核心注解,覆盖缓存的“读、写、删”全生命周期。

1. @Cacheable—— 缓存读取(最常用)

作用:将方法结果缓存,后续一样参数调用直接返回缓存值。

@Cacheable("users")
public User findUserById(Long id) {
    return userRepository.findById(id).orElse(null);
}

关键属性:

属性

说明

value/ cacheNames

缓存名称(必须)

key

SpEL 表达式,自定义缓存 key(默认使用所有参数)

condition

SpEL 表达式,满足条件才尝试缓存(方法执行前判断)

unless

SpEL 表达式,满足条件则不缓存结果(方法执行后判断)

sync

是否同步加载(防止缓存击穿,默认 false)

示例场景:

// 仅缓存 VIP 用户
@Cacheable(
    value = "users",
    key = "#id",
    condition = "#id > 1000",
    unless = "#result == null || !#result.vip"
)
public User findUserById(Long id) { ... }

注意:#result 只能在 unless 中使用,由于此时方法已执行完毕。


2. @CachePut—— 缓存更新

作用:总是执行方法,并将结果写入缓存(用于更新操作)。

@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
    return userRepository.save(user); // 方法必定会执行
}

与 @Cacheable 不同,@CachePut不会跳过方法体,适用于“先更新 DB,再刷新缓存”的场景。


3. @CacheEvict—— 缓存清除

作用:删除缓存条目。

@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
    userRepository.deleteById(id);
}

常用属性:

  • allEntries = true:清空整个缓存(慎用!)
  • beforeInvocation = true:在方法执行清除缓存(默认为 false,即执行后清除)
// 清空所有用户缓存
@CacheEvict(value = "users", allEntries = true)
public void clearAllUsers() { ... }

4. @Caching—— 组合操作

当一个方法需要同时进行多个缓存操作时使用:

@Caching(
    evict = {
        @CacheEvict("users"),
        @CacheEvict(value = "userProfiles", key = "#user.id")
    },
    put = @CachePut(value = "recentUsers", key = "#user.id")
)
public User processUser(User user) {
    // 处理逻辑
    return user;
}

5. @CacheConfig—— 类级别配置

避免在每个方法上重复写 cacheNames:

@Service
@CacheConfig(cacheNames = "books")
public class BookService {

    @Cacheable(key = "#isbn")
    public Book getBook(String isbn) { ... }

    @CachePut(key = "#book.isbn")
    public Book saveBook(Book book) { ... }
}

四、缓存 Key 的生成策略

默认策略

Spring 使用 SimpleKeyGenerator:

  • 无参 → SimpleKey.EMPTY
  • 单参 → 直接使用该参数
  • 多参 → 封装为 SimpleKey 对象

但这种方式在分布式环境下可能不够灵活或可读性差。

自定义 KeyGenerator

@Bean
public KeyGenerator customKeyGenerator() {
    return (target, method, params) -> {
        StringBuilder sb = new StringBuilder();
        sb.append(method.getDeclaringClass().getSimpleName())
          .append(".")
          .append(method.getName());
        for (Object param : params) {
            sb.append(":").append(param);
        }
        return sb.toString(); // 如:BookService.getBook:978-3-16-148410-0
    };
}

使用:

@Cacheable(value = "books", keyGenerator = "customKeyGenerator")
public Book getBook(String isbn) { ... }

五、集成主流缓存实现

1. 本地缓存:Caffeine(推荐)

高性能、内存友善的 Java 本地缓存库。

@Configuration
@EnableCaching
public class CaffeineCacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("users", "books");
        cacheManager.setCaffeine(Caffeine.newBuilder()
            .maximumSize(1000)               // 最大条目数
            .expireAfterWrite(Duration.ofMinutes(10)) // 写入后10分钟过期
            .recordStats()                   // 开启统计(用于监控)
        );
        return cacheManager;
    }
}

2. 分布式缓存:Redis

适用于集群环境,支持高可用和持久化。

@Bean
public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
    RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
        .entryTtl(Duration.ofHours(1)) // TTL 1小时
        .disableCachingNullValues()    // 不缓存 null(可选)
        .serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));

    return RedisCacheManager.builder(factory)
        .cacheDefaults(config)
        .build();
}

注意:序列化方式影响兼容性和安全性,提议使用 JSON 而非 JDK 原生序列化。


六、高级特性与实战技巧

1. 防缓存穿透:缓存 null 值

当查询一个不存在的 ID 时,若不缓存 null,每次都会穿透到 DB。

@Cacheable(value = "users", key = "#id", unless = "#result == null")
public User findById(Long id) {
    return userRepository.findById(id).orElse(null);
}

但需注意:null 值也应设置较短 TTL,避免长期占用缓存空间。

2. 防缓存击穿:sync = true

热点 key 过期瞬间,大量请求涌入 DB。

@Cacheable(value = "hotData", key = "#id", sync = true)
public Data loadHotData(String id) {
    // 耗时操作
}

sync = true 保证同一 key 的并发请求中,只有一个线程执行方法,其余等待结果。

3. 防缓存雪崩:随机 TTL

大量 key 同时过期导致 DB 压力骤增。

// 在 Caffeine 或 Redis 配置中加入随机过期时间
.expireAfterWrite(Duration.ofMinutes(10 + new Random().nextInt(5)))

七、事务与缓存的一致性问题

重大警告:Spring Cache 不是事务感知的

思考以下场景:

@Transactional
@Cacheable("users")
public User createUser(User user) {
    return userRepository.save(user); // 事务中
}

如果事务回滚,但缓存已写入,就会导致缓存与数据库不一致

推荐做法:Cache-Aside 模式

  • 读操作:先查缓存 → 未命中查 DB → 回填缓存
  • 写操作:先更新 DB → 再删除缓存(而非更新)
public User updateUser(User user) {
    // 1. 更新数据库
    User saved = userRepository.save(user);
    // 2. 删除缓存(下次读时自动重建)
    // 注意:这里不能用 @CachePut,由于事务可能未提交
    redisTemplate.delete("users::" + user.getId());
    return saved;
}

更严谨的做法是使用“延迟双删”或消息队列保证最终一致性。


八、监控与调试

开启缓存日志

logging.level.org.springframework.cache=DEBUG

输出示例:

DEBUG o.s.c.i.CacheInterceptor - Computed cache key "123" for operation ...
DEBUG o.s.c.i.CacheInterceptor - No cache entry for key "123" in cache(s) [users]

Caffeine 统计信息

启用 .recordStats() 后,可通过 JMX 或 Actuator 查看命中率、加载次数等。


九、生产环境最佳实践总结

问题

解决方案

缓存穿透

缓存 null 值 + 短 TTL + 布隆过滤器(可选)

缓存击穿

@Cacheable(sync = true)

或互斥锁

缓存雪崩

设置随机 TTL,避免聚焦过期

数据一致性

采用 Cache-Aside 模式,写 DB 后删缓存

序列化安全

Redis 中避免使用 JDK 序列化,改用 JSON

缓存命名

使用清晰的命名空间,如 user:profile:{id}

容量规划

本地缓存设最大 size,Redis 设内存上限和淘汰策略


十、结语

Spring Cache 通过注解极大简化了缓存集成,但需注意:

  • 选择合适的底层缓存(本地 or 分布式)
  • 合理设计 key 和过期策略
  • 处理缓存穿透/击穿/雪崩
  • 保证缓存与数据库的一致性
  • 在事务环境中谨慎使用

掌握这些,你才能高效、安全地使用 Spring Cache 提升系统性能。

© 版权声明

相关文章

暂无评论

您必须登录才能参与评论!
立即登录
none
暂无评论...