Spring Cache 操作详解
在现代的应用程序中,缓存是提升性能、减少数据源压力的重要手段之一。Spring Boot 提供了一个简洁的缓存抽象框架——spring-boot-starter-cache
,而 Redis 作为一个高性能的内存数据库,是缓存实现的理想选择。本文将介绍如何结合 Spring Boot Starter Cache 与 Redis 来实现缓存管理。
1. 添加依赖
首先,在 Spring Boot 项目中集成 Redis 和缓存功能,需要在 pom.xml
中添加相关依赖。
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
spring-boot-starter-cache
提供了 Spring 的缓存抽象,而 spring-boot-starter-data-redis
使我们能够使用 Redis 作为缓存存储。
2. 配置 Redis 连接
在 application.yml
或 application.properties
中配置 Redis 连接信息。以下是使用 application.yml
的示例:
spring:
redis:
host: localhost
port: 6379
password: yourpassword # 如果没有密码可以省略
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
cache:
type: redis # 将缓存类型指定为 redis
以上配置告诉 Spring Boot 使用 Redis 作为缓存存储,并配置了 Redis 连接的主机、端口和连接池。
3. 启用缓存功能
在主启动类或配置类中,添加 @EnableCaching
注解来开启 Spring Boot 的缓存支持:
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
@EnableCaching
将启用缓存机制,允许在应用程序中使用缓存注解。
4. 使用缓存注解
Spring 提供了一系列注解用于缓存操作,其中最常用的是 @Cacheable
、@CachePut
和 @CacheEvict
。下面通过示例介绍如何使用这些注解。
@Cacheable
@Cacheable
注解用于将方法的返回值缓存起来,之后调用相同参数时将直接返回缓存的内容,而不是再次执行方法。
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class UserService {
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟耗时操作
try {
Thread.sleep(3000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return new User(id, "John Doe");
}
}
上面的代码中,getUserById
方法被 @Cacheable
注解标注,缓存名称为 users
,缓存的键为方法参数 id
。当第一次调用该方法时,结果将被缓存起来,之后使用相同的 id
调用时将直接从缓存中获取数据,而不会再次执行方法逻辑。
@CachePut
@CachePut
用于更新缓存,不会跳过方法执行。它确保方法执行并将结果存入缓存。
@CachePut(value = "users", key = "#user.id")
public User updateUser(User user) {
// 更新用户信息
return userRepository.save(user);
}
即使缓存中已经存在相应的 user
,@CachePut
也会执行方法并更新缓存。
@CacheEvict
@CacheEvict
用于移除缓存中的某些数据或清空整个缓存。
@CacheEvict(value = "users", key = "#id")
public void deleteUser(Long id) {
userRepository.deleteById(id);
}
在这个例子中,当用户被删除时,缓存中的数据也会被移除。
5. 缓存注解与 Redis Key-Value 映射
在使用 Spring Boot 的缓存注解时,缓存的具体存储介质可以是内存、数据库或像 Redis 这样的缓存系统。结合 Redis 时,Spring Boot 的缓存注解将以键值对的形式将缓存数据存入 Redis。以下是这些注解与 Redis 中的 key-value 映射过程的详细说明。
5.1 与 Redis 的映射
当使用 @Cacheable
注解时,方法返回的值会存储在 Redis 中,键值对的形式如下:
-
Key(键): Spring Cache 会根据
value
和key
属性构造 Redis 的键。value
表示缓存的名称,也可以视为 Redis 中的命名空间或前缀,key
通常由方法参数或自定义的 SpEL 表达式决定。默认情况下,key
是参数的字符串形式。 -
Value(值): 方法返回的对象会被序列化后存入 Redis。默认情况下,Spring Boot 使用
JdkSerializationRedisSerializer
,但可以通过配置来指定不同的序列化方式,如 JSON 序列化。
例如,在如下的 @Cacheable
注解使用中:
@Cacheable(value = "users", key = "#id")
public User getUserById(Long id) {
// 模拟耗时操作
return new User(id, "John Doe");
}
- Redis Key:
users::1
(假设id=1
)users
是缓存名称value
,1
是参数id
的值。
- Redis Value:
User
对象的序列化形式。
通过 Redis 客户端可以看到类似的键值对:
> GET users::1
"{\"id\":1,\"name\":\"John Doe\"}"
5.2 自定义 Redis Key 和 Value 的序列化
默认情况下,Spring Boot 使用 JdkSerializationRedisSerializer
序列化对象为字节流存储在 Redis 中。如果我们希望以更人性化的方式查看 Redis 中的缓存内容,可以通过配置来使用 JSON 序列化。例如:
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 设置缓存过期时间
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())) // 使用字符串序列化key
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); // 使用JSON序列化value
}
}
在上述配置中,键将使用 StringRedisSerializer
序列化,值将使用 GenericJackson2JsonRedisSerializer
以 JSON 格式存储。
例如,对于上述的 @Cacheable
注解,如果使用该配置,Redis 中的键值对将如下显示:
- Redis Key:
users::1
- Redis Value:
{"id":1,"name":"John Doe"}
通过 Redis 客户端查看:
> GET users::1
"{\"id\":1,\"name\":\"John Doe\"}"
6. Redis 缓存配置
Spring Boot 提供了默认的 Redis 缓存管理器,但我们可以通过自定义缓存配置来实现更多功能,例如设置缓存过期时间。
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
public class RedisConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(10)) // 设置缓存过期时间为10分钟
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
}
}
通过自定义 RedisCacheConfiguration
,可以控制缓存的过期时间、序列化方式等。
7. 效果演示
当你第一次调用 getUserById
方法时,应用会执行耗时操作(如访问数据库)。而当第二次调用相同方法时,结果将直接从 Redis 中获取,显著减少响应时间。
可以通过 Redis 客户端或 Redis 图形化工具(如 RedisInsight)来查看缓存的内容。
8. @Cacheable 高级用法
@Cacheable
注解是Spring Cache中最常用的注解之一,它用于将方法的返回值缓存起来,以便后续调用时直接从缓存中获取,而不是再次执行方法。
指定缓存的Key
默认情况下,@Cacheable
会使用方法参数作为缓存的Key。可以通过key
属性指定自定义的Key:
@Cacheable(value = "exampleCache", key = "#param")
public String getData(String param) {
// ...
}
条件缓存
可以使用condition
属性指定条件,满足条件时才进行缓存:
@Cacheable(value = "exampleCache", condition = "#param.length() > 3")
public String getData(String param) {
// ...
}
缓存同步
在并发环境中,可以使用sync
属性启用缓存同步,防止缓存击穿:
@Cacheable(value = "exampleCache", sync = true)
public String getData(String param) {
// 模拟耗时操作
try {
Thread.sleep(2000);
} catch (InterruptedException e) {
e.printStackTrace();
}
return "Data for " + param;
}
当多个线程同时请求缓存中没有的数据时,只有一个线程会执行方法,其他线程等待结果。这种机制能够有效避免多个线程同时执行相同的方法,从而减少资源消耗,提高系统性能。
例如,当大量并发请求同时查询某个缓存中没有的数据时,如果没有同步机制,每个请求都会触发一次方法执行,这不仅浪费资源,还可能导致数据库或其他底层服务过载。而启用sync
属性后,只有一个请求会执行方法,其他请求等待结果,执行完毕后,结果会被缓存,后续请求直接从缓存中获取数据。
自定义的Key生成器keyGenerator
keyGenerator
参数用于指定一个自定义的Key生成器,覆盖默认的Key生成方式。需要实现org.springframework.cache.interceptor.KeyGenerator
接口,并在配置类或服务类中进行注册。
自定义Key生成器示例:
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
@Component("customKeyGenerator")
public class CustomKeyGenerator implements KeyGenerator {
@Override
public Object generate(Object target, Method method, Object... params) {
return method.getName() + "_" + Arrays.toString(params);
}
}
在使用@Cacheable
时指定自定义Key生成器:
@Cacheable(value = "exampleCache", keyGenerator = "customKeyGenerator")
public String getData(String param) {
// ...
}
详细介绍 cacheManager
cacheManager
参数用于指定一个自定义的缓存管理器。如果没有指定,使用默认的缓存管理器。缓存管理器负责管理应用程序中的所有缓存实例。
自定义缓存管理器示例:
import org.springframework.cache.CacheManager;
import org.springframework.cache.concurrent.ConcurrentMapCacheManager;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
public class CacheConfig {
@Bean
public CacheManager customCacheManager() {
return new ConcurrentMapCacheManager("exampleCache");
}
}
在使用@Cacheable
时指定自定义缓存管理器:
@Cacheable(value = "exampleCache", cacheManager = "customCacheManager")
public String getData(String param) {
// ...
}
自定义的缓存解析器cacheResolver
cacheResolver
参数用于指定一个自定义的缓存解析器。需要实现org.springframework.cache.interceptor.CacheResolver
接口,缓存解析器负责根据某些逻辑来解析和提供缓存实例。
自定义缓存解析器示例:
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.interceptor.CacheResolver;
import org.springframework.stereotype.Component;
import java.util.Collection;
import java.util.Collections;
@Component("customCacheResolver")
public class CustomCacheResolver implements CacheResolver {
private final CacheManager cacheManager;
public CustomCacheResolver(CacheManager cacheManager) {
this.cacheManager = cacheManager;
}
@Override
public Collection<? extends Cache> resolveCaches(org.springframework.cache.interceptor.CacheOperationInvocationContext<?> context) {
// 自定义缓存解析逻辑
return Collections.singleton(cacheManager.getCache("exampleCache"));
}
}
在使用@Cacheable
时指定自定义缓存解析器:
@Cacheable(value = "exampleCache", cacheResolver = "customCacheResolver")
public String getData(String param) {
// ...
}
unless
unless
参数是一个SpEL表达式,只有当表达式为false时才缓存方法的返回值。它的优先级高于condition
,可以用于在某些条件下禁用缓存。
例如,可以在结果长度大于10时不缓存:
@Cacheable(value = "exampleCache", unless = "#result.length() > 10")
public String getData(String param) {
// ...
}
通过unless
参数,可以在不改变方法逻辑的情况下,灵活控制缓存行为,避免不必要的缓存操作,提高系统性能。
9. 缓存一致性问题
在使用缓存时,一个常见的问题是缓存一致性问题。当数据源(如数据库)中的数据发生变化时,缓存中的数据可能不会及时更新,导致缓存中的数据与实际数据不一致。为了解决这个问题,可以使用以下几种策略:
- 缓存失效策略:在更新或删除数据时,同时让相关缓存失效。
- 缓存刷新:定期刷新缓存,确保缓存中的数据是最新的。
- 消息队列:使用消息队列,在数据变化时通知缓存进行更新。
例如,在数据更新时手动清除缓存:
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.stereotype.Service;
@Service
public class ExampleService {
@CacheEvict(value = "exampleCache", key = "#param")
public void updateData(String param, String newData) {
// 更新数据库中的数据
}
}
参考链接
- Spring Cache 官方文档:https://docs.spring.io/spring-framework/docs/current/reference/html/integration.html#cache
- Spring Data Redis 官方文档:https://docs.spring.io/spring-data/redis/docs/current/reference/html/
- Redis 官方文档:https://redis.io/documentation
- SpEL 表达式官方文档 https://docs.spring.io/spring-framework/docs/current/reference/html/core.html#expressions