Spring Cache 使用详解:从入门到实战

1. 引言

在 Web 应用开发中,缓存是提升系统性能最直接、最有效的手段之一。Spring 框架从 3.1 版本开始提供了基于注解的缓存抽象(Spring Cache Abstraction),它不依赖任何具体的缓存实现,而是通过统一的注解和 API,让开发者能够以极低的侵入成本为方法添加缓存能力。

本文将带你系统掌握 Spring Cache 的核心概念、常用注解、配置方式以及实战中的最佳实践,帮助你快速在项目中落地缓存方案。

2. Spring Cache 核心概念

Spring Cache 的核心思想是对方法的返回值进行缓存。当调用一个被缓存注解标记的方法时,Spring 会先检查缓存中是否存在对应的数据,如果存在则直接返回缓存结果,不再执行方法体;如果不存在则执行方法,并将返回值存入缓存。

理解下面三个核心接口,就掌握了 Spring Cache 的骨架:

  • Cache :缓存接口,定义了缓存的读写、删除等基本操作,如 getputevict
  • CacheManager :缓存管理器,负责管理一组 Cache 实例,根据缓存名称创建和管理具体的缓存区域。
  • KeyGenerator :键生成器,用于根据方法参数生成缓存 key。默认使用 SimpleKeyGenerator,基于方法参数生成 key。

3. 快速开始:环境准备

3.1 引入依赖

以 Maven 为例,在 pom.xml 中添加 Spring Cache 依赖。如果使用 Spring Boot,只需引入 spring-boot-starter-cache

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

如果使用 Spring 传统项目,则需要引入 spring-contextspring-context-support

3.2 开启缓存支持

在配置类上添加 @EnableCaching 注解,即可开启 Spring Cache 的注解驱动支持:

java 复制代码
@Configuration
@EnableCaching
public class CacheConfig {
}

3.3 配置缓存管理器

Spring Boot 默认使用 ConcurrentMapCacheManager,无需额外配置即可运行。生产环境通常使用 Redis 或 Caffeine,后面章节会详细介绍。

4. 核心注解详解

4.1 @Cacheable

@Cacheable 是最常用的注解,用于标记一个方法的返回值需要被缓存。当方法被调用时,Spring 会先根据 key 查找缓存,命中则直接返回,未命中则执行方法并缓存结果。

java 复制代码
@Service
public class UserService {

    @Cacheable(value = "user", key = "#id")
    public User getUserById(Long id) {
        // 模拟耗时查询
        return userMapper.selectById(id);
    }
}
  • value / cacheNames:指定缓存名称,可以是一个数组,表示多个缓存区域。
  • key:指定缓存的 key,支持 SpEL 表达式。默认 key 由方法参数决定。
  • condition:满足条件才缓存,如 condition = "#id > 10"
  • unless:满足条件则不缓存,如 unless = "#result == null"

4.2 @CachePut

@CachePut 用于更新缓存,它会始终执行方法,并将返回值写入缓存。适合在数据更新后同步刷新缓存的场景。

java 复制代码
@CachePut(value = "user", key = "#user.id")
public User updateUser(User user) {
    userMapper.updateById(user);
    return user;
}

4.3 @CacheEvict

@CacheEvict 用于删除缓存,通常在删除数据或数据失效时使用。

java 复制代码
@CacheEvict(value = "user", key = "#id")
public void deleteUser(Long id) {
    userMapper.deleteById(id);
}
  • allEntries = true:清空该缓存区域下的所有数据。
  • beforeInvocation = true:在方法执行前删除缓存,默认在方法执行后删除。

4.4 @Caching

当需要在一个方法上同时应用多个缓存操作时,可以使用 @Caching 组合注解:

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

4.5 @CacheConfig

@CacheConfig 是类级别注解,用于统一指定该类中缓存注解的公共属性,如缓存名称:

java 复制代码
@Service
@CacheConfig(cacheNames = "user")
public class UserService {
    // 类中的 @Cacheable 无需再写 value
    @Cacheable(key = "#id")
    public User getUserById(Long id) { ... }
}

5. 缓存 Key 的生成策略

默认情况下,Spring 使用 SimpleKeyGenerator 生成 key:无参方法使用 SimpleKey.EMPTY,单参数方法直接使用该参数,多参数方法使用 SimpleKey 包装所有参数。

实际开发中,推荐使用 SpEL 表达式自定义 key,以更精确地控制缓存粒度:

java 复制代码
// 使用对象属性作为 key
@Cacheable(value = "user", key = "#user.id")

// 组合多个参数
@Cacheable(value = "order", key = "#userId + ':' + #orderNo")

// 使用方法返回值属性(仅 @CachePut 可用)
@CachePut(value = "user", key = "#result.id")

当默认策略无法满足需求时,可以实现 KeyGenerator 接口自定义 key 生成器:

java 复制代码
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
    @Override
    public Object generate(Object target, Method method, Object... params) {
        return target.getClass().getSimpleName() + "_" + method.getName() + "_" + Arrays.toString(params);
    }
}

使用时通过 keyGenerator 属性指定:

java 复制代码
@Cacheable(value = "user", keyGenerator = "customKeyGenerator")
public User getUserById(Long id) { ... }

6. 集成 Redis 缓存

生产环境中,Redis 是最常用的分布式缓存方案。在 Spring Boot 中集成 Redis 缓存非常简单。

6.1 引入依赖

xml 复制代码
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

6.2 配置 Redis 连接

application.yml 中配置 Redis 连接信息:

yaml 复制代码
spring:
  redis:
    host: localhost
    port: 6379
    password: 
    database: 0

6.3 自定义 RedisCacheManager

为了让缓存值以 JSON 格式存储,并设置合理的过期时间,需要自定义 RedisCacheManager

java 复制代码
@Configuration
public class RedisCacheConfig {

    @Bean
    public RedisCacheManager cacheManager(RedisConnectionFactory factory) {
        // 配置序列化方式
        GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
                .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(serializer))
                .entryTtl(Duration.ofMinutes(30))  // 默认过期时间 30 分钟
                .disableCachingNullValues();

        // 为不同缓存名称设置不同的过期时间
        Map<String, RedisCacheConfiguration> cacheConfigurations = new HashMap<>();
        cacheConfigurations.put("user", config.entryTtl(Duration.ofHours(1)));
        cacheConfigurations.put("order", config.entryTtl(Duration.ofMinutes(10)));

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

7. 集成 Caffeine 本地缓存

Caffeine 是高性能的本地缓存库,适合单机场景或作为多级缓存的一级缓存。

7.1 引入依赖

xml 复制代码
<dependency>
    <groupId>com.github.ben-manes.caffeine</groupId>
    <artifactId>caffeine</artifactId>
</dependency>

7.2 配置 CaffeineCacheManager

java 复制代码
@Configuration
public class CaffeineCacheConfig {

    @Bean
    public CacheManager cacheManager() {
        CaffeineCacheManager cacheManager = new CaffeineCacheManager("user", "order");
        cacheManager.setCaffeine(Caffeine.newBuilder()
                .maximumSize(500)          // 最大缓存条目数
                .expireAfterWrite(Duration.ofMinutes(10))  // 写入后 10 分钟过期
                .recordStats());           // 开启统计
        return cacheManager;
    }
}

8. 多级缓存实战

在大型系统中,常采用「本地缓存 + 分布式缓存」的多级缓存架构,兼顾性能与一致性。下面是一个简单的二级缓存示例:

java 复制代码
@Service
public class ProductService {

    @Autowired
    private CacheManager cacheManager;

    public Product getProduct(Long id) {
        // 1. 先查本地缓存(Caffeine)
        Cache localCache = cacheManager.getCache("localProduct");
        Product product = localCache.get(id, Product.class);
        if (product != null) {
            return product;
        }

        // 2. 再查 Redis 缓存
        Cache redisCache = cacheManager.getCache("redisProduct");
        product = redisCache.get(id, Product.class);
        if (product != null) {
            localCache.put(id, product);
            return product;
        }

        // 3. 最后查数据库并回填缓存
        product = productMapper.selectById(id);
        if (product != null) {
            redisCache.put(id, product);
            localCache.put(id, product);
        }
        return product;
    }
}

9. 常见问题与最佳实践

9.1 缓存穿透

缓存穿透指查询一个不存在的数据,导致请求直接打到数据库。解决方案:

  • 缓存空值:在 @Cacheable 中设置 unless = "#result == null" 会跳过缓存,应改为缓存空值并设置较短过期时间。
  • 使用布隆过滤器:在缓存前先判断 key 是否存在。

9.2 缓存雪崩

缓存雪崩指大量缓存同时过期,导致请求全部打到数据库。解决方案:

  • 设置随机过期时间,避免同时失效。
  • 使用多级缓存,本地缓存兜底。
  • 限流降级,保护数据库。

9.3 缓存击穿

缓存击穿指某个热点 key 过期瞬间,大量请求同时打到数据库。解决方案:

  • 使用互斥锁(如 synchronized、Redisson 分布式锁)保证只有一个线程去查询数据库。
  • 热点数据设置永不过期,后台异步更新。

9.4 缓存一致性

当数据更新时,需要保证缓存与数据库的一致性。推荐策略:

  • 先更新数据库,再删除缓存(Cache Aside 模式)。
  • 对于强一致场景,可使用分布式事务或消息队列异步同步。

9.5 注意事项

  • @Cacheable 基于 Spring AOP 实现,同类内部方法调用不会触发缓存,因为代理不生效。
  • 缓存注解作用于 public 方法,private 方法无法被代理。
  • 序列化对象必须实现 Serializable 接口(使用 JDK 序列化时)。
  • 合理设置缓存过期时间,避免数据长期不一致。

10. 总结

Spring Cache 通过简洁的注解抽象,大幅降低了缓存接入的成本。本文从核心概念出发,依次介绍了常用注解、Key 生成策略、Redis 与 Caffeine 集成、多级缓存实战以及常见问题的最佳实践。

在实际项目中,建议根据业务场景选择合适的缓存方案:单机小规模应用可使用 Caffeine,分布式系统优先选择 Redis,追求极致性能可组合使用多级缓存。同时务必关注缓存穿透、雪崩、击穿等经典问题,做好兜底设计,才能真正发挥缓存的威力。

相关推荐
ZGG0031 小时前
MySQL 索引详解:B+ 树、聚簇索引与最左前缀
java·数据库·mysql
萧瑟余晖1 小时前
Java深入解析篇五十四之对象模型详解
java·开发语言
张文是假的啊1 小时前
Java | record | Controller逻辑
java·开发语言
勿忘,瞬间2 小时前
Mybatis高阶
java·数据库·mybatis
嘻哈baby3 小时前
Go 函数中的参数为什么不支持默认值?
java·开发语言·jvm
慧都小项3 小时前
MyEclipse 2026:当 Agent、MCP 与 Java 26 进入 Eclipse 工作流
java·eclipse·springboot·copilot·myeclipse
CoderYanger4 小时前
前端基础——JavaScript(基础语法)(下篇)
java·开发语言·前端·javascript·程序人生·面试·职场和发展
京师20万禁军教头4 小时前
39面向对象(高级)-设计模式
java·开发语言·设计模式
白山编程大哥4 小时前
Java 迭代器接口详解:从 Iterator 到 ListIterator 的完整指南
java·开发语言·windows