Spring Cache 详解:构建高性能应用的利器
在现代企业级应用开发中,性能优化始终是一个核心关注点。面对高并发访问和海量数据处理,如何有效降低数据库压力、缩短响应时间成为了开发者必须解决的难题。缓存(Caching)作为一种经典的优化手段,通过将热点数据存储在内存或其他快速存储介质中,显著提升了数据读取速度,从而极大地改善了应用的整体性能。
Spring Framework 提供了一个强大的缓存抽象层(Spring Cache Abstraction),它为开发者提供了一套统一的 API 和注解,使得在 Spring 应用中集成各种缓存解决方案变得异常简单,而无需关心底层具体的缓存实现细节(如 EhCache、Caffeine、Redis、JCache/JSR-107等)。本文将深入探讨 Spring Cache 的方方面面,帮助读者全面掌握这一提升应用性能的利器。
一、为什么选择 Spring Cache?
直接使用底层的缓存 API(例如 EhCache 的 CacheManager
或 Redis 的 Jedis
)来管理缓存数据是可行的,但这样做会带来一些问题:
- 代码侵入性强: 业务逻辑代码中会充斥着缓存操作的代码(如
cache.get()
、cache.put()
、cache.evict()
),使得业务逻辑与缓存逻辑高度耦合,难以维护和测试。 - 切换缓存实现困难: 如果需要更换缓存提供者,需要修改大量业务代码,成本高昂。
- 缺乏统一的标准: 不同的缓存提供者 API 各异,增加了学习成本。
Spring Cache 抽象层正是为了解决这些问题而诞生的。它提供了一个基于注解或 AOP 的、与具体缓存实现无关的编程模型。开发者只需要在业务方法上标注相应的注解,Spring 就会在运行时自动管理缓存的存取,将开发者从繁琐的缓存操作细节中解放出来。
Spring Cache 的优势总结:
- 降低耦合: 将缓存逻辑从业务代码中分离,提高了代码的可读性和可维护性。
- 灵活性高: 可以轻松切换不同的缓存实现,只需修改配置,无需改动业务代码。
- 开发效率: 基于注解的声明式缓存极大地简化了缓存开发工作。
- 功能强大: 支持丰富的配置选项,如自定义缓存 key、缓存条件判断、缓存结果过滤等。
- 与 Spring 生态集成: 无缝集成到 Spring IoC 容器和 AOP 体系中。
二、Spring Cache 的核心概念与工作原理
Spring Cache 抽象层的核心在于其基于 AOP(面向切面编程)的实现机制。当带有 Spring Cache 注解的方法被调用时,Spring AOP 会拦截这个方法调用,并在方法执行之前或之后执行缓存逻辑。
核心组件包括:
- Cache Abstraction (缓存抽象): Spring Cache 提供了一套通用的接口,如
CacheManager
和Cache
。CacheManager
: 缓存管理器,是 Spring Cache 的核心接口,负责管理和获取Cache
实例。一个应用可以配置一个或多个CacheManager
Bean。Cache
: 缓存实例,代表一个具体的缓存区域或命名空间,例如一个名为 “users” 的缓存。它提供了用于存取数据的基本方法,如get(key)
、put(key, value)
、evict(key)
、clear()
等。
- Cache Annotations (缓存注解): Spring 提供了多个核心注解,用于在方法上声明缓存行为。
@Cacheable
: 用于标注在方法上,表示方法的返回值可以被缓存。在方法执行前,先根据缓存 key 查找缓存。如果找到,则直接返回缓存的值,不再执行方法体;如果没有找到,则执行方法体,并将方法的返回值存入缓存。@CachePut
: 用于标注在方法上,表示方法的返回值需要更新到缓存中。与@Cacheable
不同,它 总会 执行方法体,然后将方法的返回值按指定的 key 放入缓存。常用于更新操作。@CacheEvict
: 用于标注在方法上,表示需要从缓存中移除数据。在方法执行之前或之后,根据指定的 key 或条件从缓存中移除一个或多个条目。常用于删除或更新操作导致缓存失效的场景。@Caching
: 用于分组多个 Spring Cache 注解。当一个方法需要同时应用多个不同类型的缓存操作时(例如,更新一个对象并同时清除一个列表缓存),可以使用@Caching
将它们组合起来。
- AOP Interceptor (AOP 拦截器): Spring Cache 的核心实现是基于 AOP 拦截器。当 Spring 容器检测到
@EnableCaching
注解并扫描到带有缓存注解的方法时,它会自动创建一个 AOP 代理。当调用这些被代理的方法时,拦截器会介入,执行缓存的存取、更新或淘汰逻辑。 - KeyGenerator (Key 生成器): 用于根据方法的参数、目标对象等信息生成缓存的 key。Spring 提供了默认的 KeyGenerator,也可以自定义。
- CacheErrorHandler (缓存错误处理器): 用于处理缓存操作过程中发生的异常(如网络问题、序列化失败等)。默认情况下,异常会被简单地记录日志,并继续执行方法体(对于
@Cacheable
),或者忽略缓存操作。可以自定义错误处理器来改变这种行为。
工作流程简述 (以 @Cacheable
为例):
- 方法被调用。
- AOP 拦截器捕获方法调用。
- 拦截器根据方法签名、参数、
@Cacheable
注解的配置(如key
表达式、keyGenerator
)生成缓存 key。 - 拦截器根据
@Cacheable
注解指定的缓存名称 (value
属性) 获取对应的Cache
实例(通过CacheManager
)。 - 拦截器使用生成的 key 从
Cache
实例中查找缓存数据。 - 如果找到缓存数据:
- 如果缓存数据有效,直接将缓存的值作为方法的返回值返回。方法体 不会 执行。
- 如果未找到缓存数据 或 缓存数据已失效:
- 执行原始方法体。
- 获取方法的返回值。
- 根据
@Cacheable
注解的配置(如unless
表达式)判断是否需要缓存。 - 如果需要缓存,拦截器使用生成的 key 和方法的返回值,通过
Cache
实例将数据存入缓存。 - 将方法的返回值返回。
三、启用 Spring Cache 与基本配置
在 Spring Boot 应用中启用 Spring Cache 非常简单,只需在主应用类或配置类上添加 @EnableCaching
注解即可。
“`java
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching // 启用 Spring Cache
public class YourApplication {
public static void main(String[] args) {
SpringApplication.run(YourApplication.class, args);
}
}
“`
在添加 @EnableCaching
后,Spring 会自动寻找一个 CacheManager
Bean。如果找不到,并且没有配置特定的缓存提供者,Spring Boot 会自动配置一个简单的基于内存的 ConcurrentMapCacheManager
。
配置 CacheManager:
为了使用更专业的缓存解决方案,你需要配置一个对应的 CacheManager
Bean。以下是几种常见 CacheManager
的配置示例:
-
SimpleCacheManager (简单内存缓存): 主要用于开发和测试。
“`java
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;@Configuration
@EnableCaching
public class CacheConfig {@Bean public CacheManager cacheManager() { // 创建一个简单的ConcurrentMapCacheManager // 可以指定默认的缓存名称,或在这里手动添加多个ConcurrentMapCache return new ConcurrentMapCacheManager("users", "products"); }
}
``
ConcurrentMapCacheManager默认会根据
Cache名称按需创建
ConcurrentMapCache` 实例。 -
RedisCacheManager (分布式缓存): 常用于生产环境,需要引入 Spring Data Redis 依赖。
首先添加依赖(Maven):
xml
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId> <!-- 包含了 Spring Cache 抽象 -->
</dependency>
然后配置RedisCacheManager
Bean。Spring Boot 提供了自动配置,但在某些情况下你可能需要自定义配置,例如序列化方式或缓存过期时间。
“`java
import org.springframework.cache.CacheManager;
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.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext.SerializationPair;
import org.springframework.data.redis.serializer.StringRedisSerializer;import java.time.Duration;
@Configuration
@EnableCaching
public class CacheConfig {@Bean public CacheManager cacheManager(RedisConnectionFactory connectionFactory) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig() // 配置key序列化器 .serializeKeysWith(SerializationPair.fromSerializer(new StringRedisSerializer())) // 配置value序列化器 (推荐使用JSON) .serializeValuesWith(SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())) // 设置默认缓存过期时间 (例如:30分钟) .entryTtl(Duration.ofMinutes(30)) // 禁用空值缓存 (如果不需要缓存null) .disableCachingNullValues(); return RedisCacheManager.builder(connectionFactory) .cacheDefaults(config) // 可以为特定的缓存名称配置不同的过期时间等 // .withCacheConfiguration("users", config.entryTtl(Duration.ofHours(1))) .transactionAware() // 可选,使缓存操作感知Spring事务 .build(); }
}
“`
注意:使用 Redis 等分布式缓存时,序列化 是一个重要的考虑因素。需要确保存入缓存的对象能够被正确地序列化和反序列化。 -
CaffeineCacheManager: 高性能的本地内存缓存,是 Guava Cache 的继任者。需要引入 Caffeine 依赖。
xml
<dependency>
<groupId>com.github.ben-manes.caffeine</groupId>
<artifactId>caffeine</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
配置:
“`java
import com.github.benmanes.caffeine.cache.Caffeine;
import org.springframework.cache.CacheManager;
import org.springframework.cache.caffeine.CaffeineCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;import java.util.concurrent.TimeUnit;
@Configuration
@EnableCaching
public class CacheConfig {@Bean public CacheManager cacheManager() { CaffeineCacheManager cacheManager = new CaffeineCacheManager(); // 配置 Caffeine 缓存策略,例如设置最大容量、过期时间 cacheManager.setCaffeine(Caffeine.newBuilder() .initialCapacity(100) // 初始容量 .maximumSize(1000) // 最大缓存条目数 .expireAfterWrite(10, TimeUnit.MINUTES)); // 写入后10分钟过期 // 可以指定允许创建的缓存名称 // cacheManager.setCacheNames(Arrays.asList("users", "products")); return cacheManager; }
}
``
application.properties
Spring Boot 对 Caffeine 有很好的自动配置支持,通常你只需要添加依赖并在或
application.yml中配置即可,无需手动创建
CaffeineCacheManager` Bean。
四、核心缓存注解详解与示例
接下来详细介绍 Spring Cache 的核心注解及其常用属性。
4.1 @Cacheable
用于方法级别的读缓存。
常用属性:
value
(或cacheNames
): 必须指定,一个字符串数组,表示方法返回值要存入的缓存名称。例如{"users", "people"}
表示同时存入 “users” 和 “people” 两个缓存。key
: 可选,用于指定生成缓存 key 的 SpEL (Spring Expression Language) 表达式。默认的 key 生成策略会使用方法的所有参数哈希值作为 key。condition
: 可选,一个 SpEL 表达式,只有当表达式计算结果为 true 时,方法才会被缓存。常用于根据方法参数或返回值动态决定是否缓存。unless
: 可选,一个 SpEL 表达式,只有当表达式计算结果为 false 时,方法返回值才会被缓存。常用于排除某些不适合缓存的结果,例如 null 或空集合。sync
: 可选,布尔值,默认为false
。如果设置为true
,则在多个线程同时请求同一个 key 时,只有一个线程会执行方法体并写入缓存,其他线程会阻塞等待结果。这有助于解决缓存击穿(”thundering herd”)问题。仅适用于@Cacheable
。cacheManager
: 可选,指定使用的 CacheManager Bean 名称。keyGenerator
: 可选,指定使用的 KeyGenerator Bean 名称。
示例:
“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
// 简单的根据ID获取用户,缓存到名为"users"的缓存中
@Cacheable(value = "users")
public User getUserById(Long userId) {
System.out.println("Executing getUserById for userId: " + userId + " - Not from cache");
// 模拟数据库查询延迟
try { Thread.sleep(500); } catch (InterruptedException e) {}
return new User(userId, "User " + userId);
}
// 使用SpEL指定key,根据用户名和年龄生成key
@Cacheable(value = "users", key = "#username + ':' + #age")
public User getUserByUsernameAndAge(String username, int age) {
System.out.println("Executing getUserByUsernameAndAge for username: " + username + ", age: " + age + " - Not from cache");
// 模拟数据库查询
return new User(null, username); // 简化示例
}
// 使用condition,只缓存ID小于100的用户
@Cacheable(value = "users", condition = "#userId < 100")
public User getUserIfIdLessThan100(Long userId) {
System.out.println("Executing getUserIfIdLessThan100 for userId: " + userId + " - Not from cache");
return new User(userId, "User " + userId);
}
// 使用unless,不缓存返回值为null的用户
@Cacheable(value = "users", unless = "#result == null")
public User getUserAllowNull(Long userId) {
System.out.println("Executing getUserAllowNull for userId: " + userId + " - Not from cache");
if (userId % 2 == 0) {
return new User(userId, "User " + userId);
} else {
return null; // 模拟有时返回null
}
}
// 结合key和unless,只缓存非空列表且列表不为空的情况
@Cacheable(value = "products", key = "'allProducts'")
public List<Product> getAllProducts() {
System.out.println("Executing getAllProducts - Not from cache");
// 模拟数据库查询
// return null; // 如果返回null,不会缓存 (除非unelss条件允许)
// return Collections.emptyList(); // 如果返回空列表,可以通过unless过滤
return List.of(new Product(1L, "Product A"), new Product(2L, "Product B"));
}
}
// 示例用的简单类
class User {
private Long id;
private String name;
// … getters/setters
public User(Long id, String name) { this.id = id; this.name = name; }
public Long getId() { return id; }
public String getName() { return name; }
@Override public String toString() { return “User{” + “id=” + id + “, name='” + name + ‘\” + ‘}’; }
}
class Product {
private Long id;
private String name;
public Product(Long id, String name) { this.id = id; this.name = name; }
public Long getId() { return id; }
public String getName() { return name; }
@Override public String toString() { return “Product{” + “id=” + id + “, name='” + name + ‘\” + ‘}’; }
}
“`
SpEL 表达式中的可用对象:
在 SpEL 表达式中,可以使用 #
开头的变量来访问方法相关的信息:
#root
: 根对象,包含方法信息、目标对象等。#root.method
: 当前被调用的方法。#root.target
: 目标对象(Service 实例)。#root.args
: 方法参数数组。
#result
: 方法的返回值(仅在unless
属性和@CachePut
/@CacheEvict
的condition
/unless
属性中可用)。#p<index>
或#a<index>
: 方法的参数,按索引访问 (例如#p0
或#a0
表示第一个参数)。#p<name>
: 方法的参数,按名称访问 (需要使用-parameters
编译选项,或者在接口/抽象类中声明参数名称)。
4.2 @CachePut
用于方法级别的写缓存。它 总会 执行被标注的方法,然后将方法的返回值放入缓存中。
常用属性:
与 @Cacheable
类似,包括 value
(或 cacheNames
)、key
、condition
、unless
、cacheManager
、keyGenerator
。
关键区别:
@Cacheable
:执行前 查缓存,如果命中则 不执行 方法。@CachePut
:总是 执行方法,然后 执行后 更新缓存。
应用场景:
- 数据更新操作:例如更新用户信息后,确保缓存中的该用户信息是最新的。
- 创建新数据并放入缓存:例如创建新用户后,直接将新用户对象放入缓存。
示例:
“`java
import org.springframework.cache.annotation.CachePut;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// ... (getUserById 等方法)
// 更新用户,并更新"users"缓存中对应ID的条目
@CachePut(value = "users", key = "#user.id") // key使用更新后的用户的ID
public User updateUser(User user) {
System.out.println("Executing updateUser for userId: " + user.getId() + " - Always executes");
// 模拟更新数据库操作
user.setName(user.getName() + "_updated"); // 修改用户属性作为示例
return user; // 返回更新后的用户对象,存入缓存
}
// 创建用户,并将新用户对象放入"users"缓存
// 假设createNewUser返回一个包含新ID的用户对象
@CachePut(value = "users", key = "#result.id") // key使用返回值(新用户)的ID
public User createNewUser(String name) {
System.out.println("Executing createNewUser for name: " + name + " - Always executes");
// 模拟创建数据库记录并获取生成的ID
Long newId = System.currentTimeMillis(); // 模拟生成ID
User newUser = new User(newId, name);
return newUser; // 返回新用户,存入缓存
}
}
``
updateUser
在示例中,
key = “#user.id”是为了确保更新的是缓存中 ID 与入参
user` 相同的条目。
4.3 @CacheEvict
用于方法级别的缓存淘汰/移除。
常用属性:
value
(或cacheNames
): 必须指定,一个字符串数组,表示要从中移除数据的缓存名称。key
: 可选,一个 SpEL 表达式,用于指定要移除的缓存 key。condition
: 可选,一个 SpEL 表达式,只有当表达式计算结果为 true 时,缓存才会被移除。beforeInvocation
: 可选,布尔值,默认为false
。如果设置为true
,则在方法 执行前 移除缓存;如果设置为false
,则在方法 执行后 移除缓存。allEntries
: 可选,布尔值,默认为false
。如果设置为true
,则清除指定缓存中的所有条目,忽略key
属性。cacheManager
: 可选,指定使用的 CacheManager Bean 名称。keyGenerator
: 可选,指定使用的 KeyGenerator Bean 名称。
应用场景:
- 删除数据:删除某个对象后,清除缓存中对应的条目。
- 批量更新/操作:一次操作可能影响多个缓存条目,或者需要清除整个缓存区域。
示例:
“`java
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable; // 引入Cacheable用于验证
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class UserService {
// ... (getUserById 等方法, getAllProducts 方法)
// 根据ID删除用户,并移除"users"缓存中对应ID的条目
@CacheEvict(value = "users", key = "#userId")
public void deleteUser(Long userId) {
System.out.println("Executing deleteUser for userId: " + userId + " - Evicting from cache");
// 模拟数据库删除操作
}
// 清除所有用户缓存
@CacheEvict(value = "users", allEntries = true)
public void deleteAllUsers() {
System.out.println("Executing deleteAllUsers - Evicting all entries from 'users' cache");
// 模拟数据库删除所有用户
}
// 清除所有产品缓存,在方法执行前清除
@CacheEvict(value = "products", allEntries = true, beforeInvocation = true)
public void refreshAllProductsCache() {
System.out.println("Executing refreshAllProductsCache - Evicting 'products' cache BEFORE execution");
// 可以在这里重新加载数据并放入缓存,例如调用一个被@Cacheable修饰的方法
// 或者让其他地方触发加载
// 示例:假设这里只是一个触发器,清除缓存后由后续调用重新加载
}
// 使用 condition,只在用户ID大于100时移除缓存
@CacheEvict(value = "users", key = "#userId", condition = "#userId > 100")
public void deleteUserConditional(Long userId) {
System.out.println("Executing deleteUserConditional for userId: " + userId);
// 模拟数据库删除操作
}
}
``
beforeInvocation = true在某些场景下很有用,例如在一个事务方法中使用
@CacheEvict。如果在方法执行后清除缓存 (
beforeInvocation = false),而方法执行过程中发生异常导致事务回滚,但缓存已经被清除了,可能导致数据与缓存不一致。将
beforeInvocation设置为
true` 可以避免这种情况,但如果在方法执行前清除缓存,而方法因其他原因未能成功执行,缓存也会被错误地清除。选择哪种方式取决于具体的业务需求和风险权衡。
4.4 @Caching
用于在一个方法上组合多个 Spring Cache 注解。
常用属性:
cacheable
: 一个@Cacheable
数组。put
: 一个@CachePut
数组。evict
: 一个@CacheEvict
数组。
示例:
假设有一个操作既要更新某个对象(需要 @CachePut
),又要清除另一个相关的列表缓存(需要 @CacheEvict
)。
“`java
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Caching;
import org.springframework.stereotype.Service;
@Service
public class ProductService {
// ... (getProductById, getAllProducts)
// 更新产品信息,同时更新该产品的缓存,并清除所有产品列表缓存
@Caching(
put = {
@CachePut(value = "products", key = "#product.id") // 更新单个产品缓存
},
evict = {
@CacheEvict(value = "allProducts", allEntries = true) // 清除产品列表缓存
}
)
public Product updateProduct(Product product) {
System.out.println("Executing updateProduct for id: " + product.getId());
// 模拟数据库更新操作
product.setName(product.getName() + "_updated");
return product; // 返回更新后的产品对象
}
// 注意:allProducts 缓存和 products 缓存通常是不同的缓存名称,或者至少 key 不同
// @Cacheable(value = "allProducts", key = "'all'") // 假设 getAllProducts 方法是这样缓存的
// public List<Product> getAllProducts() { ... }
}
“`
五、高级特性
5.1 自定义 KeyGenerator
默认的 KeyGenerator
对于简单的参数组合通常是够用的。但如果需要更复杂的 key 生成逻辑,例如只使用部分参数、参数对象中的特定属性,或者结合 Spring Security 的用户信息等,可以实现 org.springframework.cache.interceptor.KeyGenerator
接口,并将其注册为一个 Bean。
“`java
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.cache.interceptor.SimpleKeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
@Configuration
public class CustomKeyGeneratorConfig {
@Bean("customUserKeyGenerator") // 指定Bean名称,用于@Cacheable等的keyGenerator属性
public KeyGenerator customUserKeyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
// 假设方法签名是 getUserByIdAndTenantId(Long userId, String tenantId)
// 我们想生成 key 为 "user:userId:tenantId"
if (method.getName().equals("getUserByIdAndTenantId") && params.length == 2) {
return "user:" + params[0] + ":" + params[1];
}
// 对于其他方法,回退到默认的 KeyGenerator
return SimpleKeyGenerator.generateKey(params);
}
};
}
}
``
@Cacheable
然后在等注解中使用
keyGenerator` 属性引用这个 Bean:
“`java
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
// 使用自定义KeyGenerator
@Cacheable(value = "users", keyGenerator = "customUserKeyGenerator")
public User getUserByIdAndTenantId(Long userId, String tenantId) {
System.out.println("Executing getUserByIdAndTenantId for userId: " + userId + ", tenantId: " + tenantId + " - Not from cache");
// 模拟查询
return new User(userId, "User " + userId + " under tenant " + tenantId);
}
// ... 其他方法继续使用默认KeyGenerator或通过key属性指定SpEL
}
“`
5.2 CacheErrorHandler
在分布式缓存环境下,缓存操作可能会因为网络、服务器故障等原因失败。默认情况下,Spring Cache 的异常处理器 SimpleCacheErrorHandler
会捕获异常并记录日志,但不会重新抛出异常。对于 @Cacheable
,这意味着即使缓存查找失败,方法体仍然会执行。对于 @CachePut
或 @CacheEvict
,这意味着缓存更新或删除可能会默默失败。
如果你需要更复杂的错误处理逻辑(例如,在缓存读取失败时抛出自定义异常,或者在缓存写入失败时执行回退逻辑),可以实现 org.springframework.cache.interceptor.CacheErrorHandler
接口,并将其注册为一个 Bean。
“`java
import org.springframework.cache.Cache;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.CacheOperationInvocationContext;
import org.springframework.stereotype.Component;
@Component
public class CustomCacheErrorHandler implements CacheErrorHandler {
@Override
public void handleCacheGetError(RuntimeException exception, Cache cache, Object key) {
System.err.println("Cache get error! Cache: " + cache.getName() + ", Key: " + key + ", Error: " + exception.getMessage());
// 可以在这里记录日志,或者抛出自定义异常
// throw new CustomCacheException("Failed to get data from cache", exception);
// 默认行为是忽略,这里也选择忽略让方法继续执行
}
@Override
public void handleCachePutError(RuntimeException exception, Cache cache, Object key, Object value) {
System.err.println("Cache put error! Cache: " + cache.getName() + ", Key: " + key + ", Value: " + value + ", Error: " + exception.getMessage());
// 默认行为是忽略
}
@Override
public void handleCacheEvictError(RuntimeException exception, Cache cache, Object key) {
System.err.println("Cache evict error! Cache: " + cache.getName() + ", Key: " + key + ", Error: " + exception.getMessage());
// 默认行为是忽略
}
@Override
public void handleCacheClearError(RuntimeException exception, Cache cache) {
System.err.println("Cache clear error! Cache: " + cache.getName() + ", Error: " + exception.getMessage());
// 默认行为是忽略
}
}
``
CacheErrorHandler` Bean。
Spring 会自动检测并使用这个
5.3 缓存与事务的交互
在一个同时使用 @Transactional
和 Spring Cache 注解的方法中,它们的执行顺序和相互影响是一个容易产生问题的地方。
-
@Cacheable
和@Transactional
: 如果@Cacheable
在事务方法内部,并且缓存命中,方法体(包括事务代码)将不会执行。如果缓存未命中,方法体执行,事务会生效。潜在的问题是,如果方法执行成功,数据写入数据库,并成功写入缓存;但如果方法执行过程中或结束后发生异常导致事务回滚,数据库的数据并未提交,而缓存却已经写入了成功的数据,造成数据不一致。- 解决方案: 通常建议将缓存放在事务的 外部。即,调用一个非事务方法,该方法负责先查缓存;如果缓存未命中,再调用内部的事务方法去查询数据库。或者使用
beforeInvocation = true
的@CacheEvict
在事务开始前清除缓存(但如前所述有其自身的风险)。Spring Cache 的事务感知模式 (RedisCacheManager.transactionAware()
) 可以让缓存操作在事务提交后才执行,但这需要底层缓存提供者和 CacheManager 的支持。
- 解决方案: 通常建议将缓存放在事务的 外部。即,调用一个非事务方法,该方法负责先查缓存;如果缓存未命中,再调用内部的事务方法去查询数据库。或者使用
-
@CachePut
和@Transactional
: 类似地,如果@CachePut
方法在事务内部,方法体总是执行,然后缓存更新。如果事务回滚,数据库更新被撤销,但缓存可能已经被更新,再次造成不一致。- 解决方案: 同样建议将
@CachePut
放在事务外部,或者使用事务感知模式。
- 解决方案: 同样建议将
-
@CacheEvict
和@Transactional
: 如果@CacheEvict
方法在事务内部:beforeInvocation = false
(默认): 在方法执行后、事务提交前(取决于具体实现和顺序)尝试清除缓存。如果事务回滚,方法执行被撤销,但缓存清除可能已经发生(或未发生),取决于清除操作是否受事务控制以及异常发生时机。beforeInvocation = true
: 在方法执行前、事务开始前清除缓存。如果方法执行失败或事务回滚,缓存已被清除,可能导致不一致。- 解决方案: 最安全的方式是让
@CacheEvict
在事务 提交后 才执行。这可以通过事务感知模式实现,或者将@CacheEvict
放在一个独立的、在事务提交后才调用的方法中。
总结:在使用 Spring Cache 和 @Transactional
时,需要仔细考虑缓存操作和事务提交/回滚的顺序,避免数据不一致问题。事务感知缓存(Transaction-Aware Caches)是 Spring 提供的解决方案之一,它确保缓存操作(特别是写操作,如 put 和 evict)仅在相应的 Spring 事务成功提交后才实际执行。
六、集成特定的缓存提供者
Spring Cache 抽象层本身不提供实际的缓存存储,需要依赖于具体的缓存库。集成特定的缓存提供者通常只需要两步:
- 添加相应的依赖。
- 配置对应的
CacheManager
Bean。
Spring Boot 的自动配置简化了这一过程,对于常见的提供者(如 Redis、Caffeine),只需添加依赖并在配置文件中进行少量设置,Spring Boot 就会自动配置好相应的 CacheManager
。
例如,使用 Redis:
* 添加 spring-boot-starter-data-redis
和 spring-boot-starter-cache
依赖。
* 在 application.properties
或 application.yml
中配置 Redis 连接信息(host, port, password, etc.)。
yaml
spring:
redis:
host: localhost
port: 6379
cache:
type: redis # 明确指定缓存类型,虽然有 data-redis 依赖通常会自动选择
* 如果需要自定义序列化、TTL等,才需要手动配置 RedisCacheConfiguration
和 RedisCacheManager
Bean,如前面示例所示。
集成其他提供者(如 EhCache, JCache/JSR-107 based caches like Hazelcast, Infinispan)的模式是类似的:添加依赖,配置对应的 CacheManager
。具体配置方式请参考 Spring Boot 官方文档或对应缓存提供者的 Spring 集成文档。
七、实际应用中的最佳实践与注意事项
-
选择合适的缓存策略 (
@Cacheable
vs@CachePut
vs@CacheEvict
):- 读取热点数据,方法结果稳定不变或不常变动:
@Cacheable
- 更新或创建数据后,需要确保缓存最新:
@CachePut
- 删除数据或发生某些事件导致缓存失效:
@CacheEvict
- 组合多种操作:
@Caching
- 读取热点数据,方法结果稳定不变或不常变动:
-
设计有效的 Key:
- Key 必须能够唯一标识缓存中的数据。
- Key 应该是稳定不变的(对于同一个逻辑数据),不应依赖于方法调用时的上下文(除非是业务需要)。
- Key 对象需要实现
equals()
和hashCode()
方法,以便缓存系统正确比较和查找。对于分布式缓存,Key 和 Value 对象还需要是可序列化的。 - 避免使用复杂对象作为 Key,尤其是分布式缓存,序列化成本高且可能出现兼容性问题。使用简单类型或由简单类型组成的字符串作为 Key 通常更安全高效。
-
理解缓存一致性问题: 缓存是数据库或其他数据源的副本。数据的更新/删除操作可能导致缓存中的数据与源数据不一致(缓存过期、缓存失效但未及时清除等)。
- 对于强一致性要求高的场景,可能需要更复杂的缓存策略(如读写穿透、失效策略、双写一致性方案等),或者考虑放弃缓存或降低缓存粒度。
- Spring Cache 主要提供的是最终一致性(Eventually Consistency)。通过
@CacheEvict
等手段手动管理失效是常见的做法。
-
处理 Null 值: 某些缓存实现(如 Caffeine、JSR-107)默认不允许缓存 null 值。Redis 通常允许缓存 null (以特定的方式存储)。Spring Cache 默认是支持缓存 null 值的,但可以通过配置
CacheManager
来禁用(例如RedisCacheConfiguration.disableCachingNullValues()
)。根据业务需求决定是否缓存 null 值。 -
缓存粒度: 决定缓存是方法级别、对象级别还是更细粒度。Spring Cache 主要工作在方法级别。缓存整个方法返回值通常更简单,但如果返回值是一个大对象且只有部分属性经常访问,可能需要考虑更细粒度的缓存或手动缓存部分数据。
-
序列化 (分布式缓存): 使用 Redis 等分布式缓存时,缓存的 Key 和 Value 都需要在网络上传输和存储,因此必须是可序列化的。Java 自带的序列化 (
java.io.Serializable
) 效率和兼容性较差,通常推荐使用 JSON (Jackson
,Gson
)、Kryo 或 Protocol Buffers 等。确保配置的序列化器与你的数据类型兼容。 -
监控缓存性能: 关注缓存的命中率、存取延迟、缓存大小等指标,以便及时发现和解决性能问题。许多缓存提供者都提供了监控工具或暴露 JMX Metrics。
-
谨慎使用
allEntries = true
: 清除整个缓存区域是一个代价较高的操作,可能会导致短时间内大量请求穿透到后端数据源,造成雪崩效应。只有在确实需要全局失效的场景下谨慎使用。
八、总结
Spring Cache 抽象层提供了一种声明式的、与底层实现无关的缓存使用方式,极大地简化了 Java 应用中缓存的开发和维护。通过 @Cacheable
、@CachePut
、@CacheEvict
等注解,开发者可以轻松地在方法级别应用缓存策略。配合灵活的 KeyGenerator、CacheErrorHandler 以及 SpEL 表达式,可以实现各种复杂的缓存需求。
虽然 Spring Cache 提供了便利,但在实际应用中,理解缓存的工作原理、底层提供者的特性、以及缓存一致性、事务交互等问题依然至关重要。合理地设计缓存策略和 Key,选择合适的缓存提供者,并进行必要的监控,才能真正发挥缓存的作用,构建高性能、可扩展的应用系统。
希望本文的详细介绍能帮助你更好地理解和使用 Spring Cache,为你的应用带来显著的性能提升!