Spring Cache
Spring Cache
一、Spring Cache 是什么?
简单来说,Spring Cache 是 Spring 框架提供的一套缓存抽象层(不是具体的缓存实现)。
你可以把它理解成一个“缓存操作的统一接口”。它本身并不提供缓存存储的功能,而是定义了一组标准的注解和API,允许你以声明式的方式轻松地将缓存功能添加到你的应用程序中。至于底层到底用哪种缓存技术(比如 Redis、Ehcache、Caffeine 等),你可以自由选择和切换,而无需修改你的核心业务代码。
核心思想: 通过注解,把方法调用的结果缓存起来。当下次用相同的参数调用该方法时,直接返回缓存中的结果,而不再真正执行方法体。
二、为什么要用 Spring Cache?它有什么用?
在业务开发中,我们经常会遇到一些“热点数据”:
- 不经常修改的配置信息。
- 首页的商品分类列表。
- 用户的基本信息。
如果每次有请求来,都去数据库查询这些数据,会给数据库造成巨大的压力,并且响应速度也会变慢。
Spring Cache 的作用就是解决这个问题,它的主要优点如下:
- 提升性能: 将频繁读取但很少变更的数据放入缓存,极大减少数据库访问,加快应用响应速度。
- 减少负载: 降低了数据库的并发访问压力。
- 声明式缓存: 通过简单的注解(如
@Cacheable)即可实现缓存逻辑,与业务代码解耦,代码侵入性低,非常优雅。 - 易于扩展: 可以轻松切换不同的缓存提供商,适应不同的业务规模(从单机应用到分布式集群)。
三、核心概念与使用方法
1. 核心注解
Spring Cache 主要提供了以下几个注解:
| 注解 | 作用 | 说明 |
|---|---|---|
@EnableCaching |
开启缓存支持 | 必须放在配置类(如 @SpringBootApplication 主类)上,表示启用缓存功能。 |
@Cacheable |
在方法执行前查询缓存 | 核心注解。如果缓存中存在,则直接返回缓存结果;否则执行方法,并将结果存入缓存。 |
@CacheEvict |
清除缓存 | 通常用在更新或删除方法上,当数据变更后,清除指定的缓存。 |
@CachePut |
更新缓存 | 无论缓存是否存在,都会执行方法,并将方法返回值更新/放入缓存。 |
@Caching |
组合多个缓存操作 | 用于在一个方法上同时使用多个 @Cacheable、@CacheEvict、@CachePut。 |
@CacheConfig |
类级别的共享缓存配置 | 在类级别统一配置缓存的名称等公共属性,避免在每个方法上重复写。 |
2. 缓存管理器 CacheManager
这是 Spring Cache 抽象的核心接口。它负责创建、管理和配置具体的 Cache 实例。你需要为项目配置一个 CacheManager 的实现。
- Spring Boot 会自动配置: 只要你引入了相关的缓存 Starter(如
spring-boot-starter-data-redis),Spring Boot 会自动为你配置好对应的CacheManager。 - 常用实现:
SimpleCacheManager: 使用简单的 Collection 来存储缓存,主要用于测试。ConcurrentMapCacheManager: 使用 JDK 的ConcurrentMap实现,默认的内存缓存。EhCacheCacheManager: 使用 EhCache 作为缓存实现。RedisCacheManager: 使用 Redis 作为缓存实现(分布式缓存)。
四、详细代码示例(基于 Spring Boot)
让我们通过一个完整的例子来学习。假设我们有一个用户服务 UserService。
步骤 1:添加依赖和启用缓存
首先,在 pom.xml 中,我们使用默认的内存缓存(ConcurrentMap):
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
然后,在主应用类上使用 @EnableCaching:
@SpringBootApplication
@EnableCaching // 关键!开启缓存功能
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
步骤 2:编写业务服务,使用缓存注解
@Service
@CacheConfig(cacheNames = "users") // 类级别配置,这个类里所有方法的缓存都属于 "users" 这个缓存区
public class UserService {
// 模拟数据库
private Map<Long, User> userDB = new HashMap<>();
{
// 初始化一些测试数据
userDB.put(1L, new User(1L, "Alice"));
userDB.put(2L, new User(2L, "Bob"));
}
/**
* 根据ID查询用户
* @Cacheable 表示会先检查缓存
* key: 缓存键,使用SpEL表达式。这里表示使用方法参数id作为键。
* unless: 条件,如果方法返回null,则不缓存结果。
*/
@Cacheable(key = "#id", unless = "#result == null")
public User getUserById(Long id) {
// 模拟数据库查询耗时
System.out.println("===> 查询数据库,id: " + id);
return userDB.get(id);
}
/**
* 更新用户信息
* @CacheEvict 表示方法执行成功后,清除指定的缓存。
* key: 指定要清除的缓存键,即要更新的用户ID对应的缓存。
* allEntries: 如果为true,会清除整个"users"缓存区的所有条目。这里我们选择精确清除。
* beforeInvocation: 如果为true,在方法执行前就清除缓存。适用于避免并发场景下的脏数据,但需谨慎。
*/
@CacheEvict(key = "#user.id")
public void updateUser(User user) {
System.out.println("===> 更新用户,id: " + user.getId());
userDB.put(user.getId(), user);
// 更新数据库后,缓存中对应id的数据已经过时,所以需要清除。
}
/**
* 新增用户
* 新增后,通常不需要立即缓存,因为还没有查询请求。
* 但可以考虑清除一些列表缓存(这里不演示),或者使用@CachePut立即缓存。
* 这里我们选择不进行缓存操作。
*/
public void addUser(User user) {
System.out.println("===> 新增用户,id: " + user.getId());
userDB.put(user.getId(), user);
}
/**
* 删除用户
* @CacheEvict 删除成功后,清除该用户的缓存。
*/
@CacheEvict(key = "#id")
public void deleteUserById(Long id) {
System.out.println("===> 删除用户,id: " + id);
userDB.remove(id);
}
}
步骤 3:编写测试类验证
@SpringBootTest
class UserServiceTest {
@Autowired
private UserService userService;
@Test
void testCache() {
System.out.println("=== 第一次查询id=1的用户 ===");
User user1 = userService.getUserById(1L); // 会打印 "查询数据库"
System.out.println("用户信息: " + user1);
System.out.println("\n=== 第二次查询id=1的用户 ===");
User user2 = userService.getUserById(1L); // 不会打印,直接从缓存返回
System.out.println("用户信息: " + user2);
System.out.println("\n=== 更新id=1的用户 ===");
user1.setName("Alice Updated");
userService.updateUser(user1); // 会清除缓存中key=1的数据
System.out.println("\n=== 第三次查询id=1的用户(缓存已被清除,会再次查询数据库) ===");
User user3 = userService.getUserById(1L); // 会再次打印 "查询数据库"
System.out.println("用户信息: " + user3);
System.out.println("\n=== 查询一个不存在的用户id=999 ===");
User user999 = userService.getUserById(999L); // 会查询数据库,但结果为null,因为unless条件,不会缓存null值
System.out.println("用户信息: " + user999);
}
}
预期输出:
=== 第一次查询id=1的用户 ===
===> 查询数据库,id: 1
用户信息: User(id=1, name=Alice)
=== 第二次查询id=1的用户 ===
用户信息: User(id=1, name=Alice) // 注意:没有打印"查询数据库"
=== 更新id=1的用户 ===
===> 更新用户,id: 1
=== 第三次查询id=1的用户(缓存已被清除,会再次查询数据库) ===
===> 查询数据库,id: 1
用户信息: User(id=1, name=Alice Updated)
=== 查询一个不存在的用户id=999 ===
===> 查询数据库,id: 999
用户信息: null
通过这个测试,你可以清晰地看到 @Cacheable 和 @CacheEvict 是如何工作的。
五、进阶用法与注意事项
-
条件缓存:
condition: 在方法执行前判断,满足条件才使用缓存。unless: 在方法执行后判断,满足条件则不缓存结果(如上面的unless = "#result == null")。
// 只缓存id大于2的用户 @Cacheable(key = "#id", condition = "#id > 2") public User getUserCondition(Long id) { ... } -
组合键(KeyGenerator):
默认的键是方法参数。如果参数复杂或多个参数共同决定唯一性,可以自定义键生成器。// 使用SpEL构造复杂key @Cacheable(key = "T(String).format('user:%s:%s', #id, #name)") public User getUserByIdAndName(Long id, String name) { ... } -
@CachePut的使用场景:
当你希望方法总是被执行,并且结果总是被更新到缓存时使用。例如,在查询并更新用户后,希望最新的数据立刻进入缓存。@CachePut(key = "#user.id") public User saveOrUpdateUser(User user) { // ... 保存或更新逻辑 return user; // 这个返回值会被放入缓存 } -
切换缓存实现(如集成 Redis):
非常简单!只需要修改pom.xml,Spring Boot 的自动配置会搞定一切。<dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency>然后在
application.yml中配置 Redis 连接信息:spring: redis: host: localhost port: 6379这样,你的缓存就会自动存到 Redis 里了,代码完全不用改!
总结
Spring Cache 是一个强大而优雅的缓存抽象工具,它通过声明式注解极大地简化了缓存功能的集成。你只需要关注 “什么时候缓存”(@Cacheable) 和 “什么时候清缓存”(@CacheEvict) 这两个核心问题,而底层的复杂实现都由 Spring 和对应的缓存提供商来处理。
希望这个详细的讲解对你有帮助!这是入门和日常使用最核心的知识点,多动手写写例子,就能很快掌握。

浙公网安备 33010602011771号