Redis篇--常见问题篇8--缓存一致性3(注解式缓存Spring Cache)
1、概述
Spring Cache是Spring框架提供的一个缓存抽象层,旨在简化应用程序中的缓存管理。通过使用Spring Cache,开发者可以轻松地将缓存机制集成到业务逻辑中,而无需关心具体的缓存实现细节。
Spring Cache支持多种缓存提供者(如EhCache、Caffeine、Redis、Hazelcast等),并且提供了统一的API来操作缓存。
Spring Cache的主要目标是减少对后端数据源(如数据库、远程服务等)的访问次数,从而提高应用的性能和响应速度。它通过缓存计算结果或查询结果,避免重复执行相同的业务逻辑或数据库查询。
2、Spring Cache的核心概念
(1)、CacheManager
CacheManager是Spring Cache的核心接口之一,负责管理多个Cache实例。每个Cache实例对应一个缓存区域(cache region),用于存储特定类型的缓存数据。
CacheManager负责创建、获取和管理这些缓存区域。
常见的 CacheManager 实现包括:
- ConcurrentMapCacheManager:基于内存的缓存,默认使用ConcurrentHashMap。
- EhCacheCacheManager:基于EhCache的缓存。
- CaffeineCacheManager:基于Caffeine的缓存。
- RedisCacheManager:基于Redis的分布式缓存。
- HazelcastCacheManager:基于Hazelcast的分布式缓存。
(2)、Cache
Cache接口表示一个具体的缓存区域,负责存储和检索缓存数据。每个Cache实例通常与一个命名空间相关联,例如user-cache或product-cache。
Cache提供了基本的缓存操作,如get()、put()和evict()。
(3)、@Cacheable
@Cacheable注解用于标记需要缓存的方法。当方法被调用时,Spring Cache会首先检查缓存中是否存在相同参数的结果。如果存在,则直接返回缓存中的结果,此时不会执行方法中的代码;如果不存在,则执行方法并将结果存入缓存。
示例:
@Cacheable(value = "user-cache", key = "id")
public User getUserById(Long id) {
// 查询数据库或远程服务
return userRepository.findById(id).orElse(null);
}
解释:
- value:指定缓存的名称,即缓存区域的名称。你可以为不同的业务逻辑配置不同的缓存区域。
- key:指定缓存的键生成策略。默认情况下,Spring Cache会根据方法参数自动生成缓存键。你也可以使用SpEL(Spring Expression Language)来定义更复杂的键生成规则。
- condition:指定缓存条件。只有当条件为true时,才会将结果存入缓存。
- unless:指定不缓存的条件。即使方法执行成功,只有当条件为false时,才会将结果存入缓存。
@Cacheable 注意的点:
1、当方法中抛出异常时,不会缓存任何数据。
2、当方法中返回null时,默认情况下不会缓存null,但是可以通过配置cache-null-values属性实现缓存null。
例如:
@Cacheable(value = "user-cache", key = "id", cache-null-values = true)
public User getUserById(Long id) {
// 查询数据库或远程服务
return userRepository.findById(id).orElse(null);
}
3、当方法中存在不确定因素时,不建议使用@Cacheable注解。
如:方法中你返回了对象,同时也打印数据信息到日志中。第一次请求会按照你的预期执行。但第二次请求会直接走缓存,不会走方法,从而造成无法打印日志。
所以在使用@Cacheable注解时,方法内建议使用最少且确定的查询操作。可以mapper文件中执行,如果有其他操作建议放在之前的service中去完成。
4、假设我们只想缓存非空的用户信息,使用unless示例。
注意unless是指定不缓存的条件,这里设置result==null,实际会缓存result!=null的数据。
示例:
@Cacheable(value = "user-cache", key = "id", unless = "result == null")
public User getUserById(Long id) {
// 查询数据库或远程服务
return userRepository.findById(id).orElse(null);
}
5、带条件的缓存condition
假设我们只在用户年龄大于18岁时缓存用户信息
示例:
@Cacheable(value = "user-cache", key = "id", condition = "id > 18")
public User getUserById(Long id) {
// 查询数据库或远程服务
return userRepository.findById(id).orElse(null);
}
6、指定缓存key的格式
可以使用SpEL表达式来组合多个参数,生成更复杂的缓存键
示例:
@Cacheable(value = "user-cache", key = "id + '_' + username")
public User getUserByIdAndUsername(Long id, String username) {
// 查询数据库或远程服务
return userRepository.findByIdAndUsername(id, username).orElse(null);
}
(4)、@CachePut
@CachePut注解用于更新缓存中的数据。与@Cacheable不同,@CachePut总是会执行方法,并将结果存入缓存。它通常用于更新缓存中的数据,而不影响方法的正常执行。
示例:
@CachePut(value = "user-cache", key = "user.id")
public User updateUser(User user) {
// 更新数据库或远程服务
userRepository.save(user);
return user;
}
解释:
- value:指定缓存的名称。
- key:指定缓存的键生成策略。
- condition:指定缓存条件。
(5)、@CacheEvict
@CacheEvict注解用于清除缓存中的数据。它可以用于删除单个缓存条目,也可以清除整个缓存区域。它通常用于在数据发生变更时清理过期的缓存数据。
示例:
@CacheEvict(value = "user-cache", key = "id")
public void deleteUser(Long id) {
// 删除数据库或远程服务中的数据
userRepository.deleteById(id);
}
解释:
- value:指定缓存的名称。
- key:指定要清除的缓存条目的键。
- allEntries:如果设置为true,则清除整个缓存区域中的所有条目。
- beforeInvocation:如果设置为 true,则在方法执行之前清除缓存;否则,在方法执行之后清除缓存。
(6)、@Caching
@Caching注解允许你组合多个缓存注解(如@Cacheable、@CachePut和@CacheEvict),以便在同一方法上应用多个缓存策略。
示例1:
保存user-cache中的键,同时删除user-stats-cache中的键
@Caching(
cacheable = @Cacheable(value = "user-cache", key = "id"),
evict = @CacheEvict(value = "user-stats-cache", key = "id")
)
public User getUserById(Long id) {
// 查询数据库或远程服务
return userRepository.findById(id).orElse(null);
}
示例2:
一条信息保存到多个缓存区域。
@Caching(
cacheable = {
@Cacheable(value = "user-cache", key = "id"), // 缓存到 user-cache
@Cacheable(value = "admin-cache", key = "id", condition = "user.isAdmin()")}
)
public User getUserById(Long id) {
return userRepository.findById(id).orElse(null);
}
3、Spring Cache的工作原理
1、拦截方法调用:当调用带有缓存注解的方法时,Spring AOP会拦截该方法的执行。
2、检查缓存:Spring Cache会根据注解中的配置(如@Cacheable)检查缓存中是否存在相同参数的结果。
- 如果存在缓存结果,则直接返回缓存中的数据,跳过方法的实际执行。
- 如果不存在缓存结果,则继续执行方法。
3、执行方法:如果缓存中没有找到结果,Spring Cache会执行方法,并将结果存入缓存。
4、更新或清除缓存:根据注解的配置(如@CachePut或@CacheEvict),Spring Cache可能会在方法执行前后更新或清除缓存。
4、集成Spring Cache
(1)、启用缓存支持
要在Spring Boot项目中启用缓存支持,首先需要在主类或配置类上添加@EnableCaching注解。
示例:
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
@SpringBootApplication
@EnableCaching
public class CacheApplication {
public static void main(String[] args) {
SpringApplication.run(CacheApplication.class, args);
}
}
(2)、选择缓存提供者
Spring Cache支持多种缓存提供者。你可以根据需求选择合适的缓存实现。这里以Redis为例。
首先,添加 Redis 的依赖:
<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>
</dependency>
然后,配置Redis缓存管理器(application.yml):
spring:
cache:
type: redis // 指定spring Cache的方法为Redis(重点)
redis:
host: localhost
port: 6379
timeout: 5000 连接超时时间
lettuce:
pool:
max-active: 8
max-idle: 8
min-idle: 0
redis:
cache:
time-to-live: 60000 默认缓存过期时间为 60 秒(毫秒)
(3)、编写配置类
这里配置类主要是为了设置合理的序列化,以及针对不同域的key设置不同的过期时间。
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.parser.ParserConfig;
import com.alibaba.fastjson.serializer.SerializerFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
import org.springframework.boot.autoconfigure.data.redis.RedisProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.Cache;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.CacheErrorHandler;
import org.springframework.cache.interceptor.KeyGenerator;
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.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.*;
import java.lang.reflect.Method;
import java.nio.charset.Charset;
import java.time.Duration;
@Slf4j
@Configuration
@EnableCaching
@EnableConfigurationProperties(RedisProperties.class)
public class CacheConfig extends CachingConfigurerSupport {
private static final FastJsonRedisSerializer<Object> fastJsonRedisSerializer = new FastJsonRedisSerializer<>(Object.class);
//缓存管理器。可以管理多个缓存
//只有CacheManger才能扫描到cacheable注解
//spring提供了缓存支持Cache接口,实现了很多个缓存类,其中包括RedisCache。但是我们需要对其进行配置,这里就是配置RedisCache
@Bean
public CacheManager cacheManager(RedisConnectionFactory connectionFactory) {
RedisCacheManager cacheManager = RedisCacheManager.RedisCacheManagerBuilder
//Redis链接工厂
.fromConnectionFactory(connectionFactory)
//缓存配置 通用配置 默认存储一小时
.cacheDefaults(getCacheConfigurationWithTtl(Duration.ofHours(1)))
//配置同步修改或删除 put/evict
.transactionAware()
//对于不同的cacheName我们可以设置不同的过期时间
// .withCacheConfiguration("app:",getCacheConfigurationWithTtl(Duration.ofHours(5)))
.withCacheConfiguration("user:",getCacheConfigurationWithTtl(Duration.ofHours(2)))
.build();
return cacheManager;
}
//缓存的基本配置对象
private RedisCacheConfiguration getCacheConfigurationWithTtl(Duration duration) {
return RedisCacheConfiguration
.defaultCacheConfig()
//设置key value的序列化方式
// 设置key为String
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()))
// 设置value 为自动转Json的Object
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(fastJsonRedisSerializer))
// 不缓存null
.disableCachingNullValues()
// 设置缓存的过期时间
.entryTtl(duration);
}
//缓存的异常处理
@Bean
@Override
public CacheErrorHandler errorHandler() {
// 异常处理,当Redis发生异常时,打印日志,但是程序正常走
log.info("初始化 -> [{}]", "Redis CacheErrorHandler");
return new CacheErrorHandler() {
@Override
public void handleCacheGetError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheGetError:key -> [{}]", key, e);
}
@Override
public void handleCachePutError(RuntimeException e, Cache cache, Object key, Object value) {
log.error("Redis occur handleCachePutError:key -> [{}];value -> [{}]", key, value, e);
}
@Override
public void handleCacheEvictError(RuntimeException e, Cache cache, Object key) {
log.error("Redis occur handleCacheEvictError:key -> [{}]", key, e);
}
@Override
public void handleCacheClearError(RuntimeException e, Cache cache) {
log.error("Redis occur handleCacheClearError:", e);
}
};
}
/** @Override
@Bean("myKeyGenerator") // 自定义key的生成策略
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object target, Method method, Object... params) {
StringBuffer sb = new StringBuffer();
sb.append(target.getClass().getName());
sb.append(method.getName());
for (Object obj : params) {
sb.append(obj.toString());
}
return sb.toString();
}
};
}
**/
}
(4)、编写业务,使用注解
在业务需要的mapper文件中定义方法,参考上诉中的注解即使用示例就可以了。
5、Spring Cache实践建议
- 合理选择缓存提供者:根据应用场景选择合适的缓存提供者。对于单机应用,可以选择内存缓存(如Caffeine);对于分布式应用,可以选择分布式缓存(如Redis)。
- 避免缓存雪崩:缓存雪崩是指大量缓存条目同时失效,导致短时间内大量请求直接打到后端数据源。可以通过设置不同的TTL或使用缓存预热机制来避免缓存雪崩。
- 避免缓存穿透:缓存穿透是指查询的缓存键不存在,且后端数据源也没有对应的数据。可以通过设置默认值或使用布隆过滤器来避免缓存穿透。
- 避免缓存击穿:缓存击穿是指某个热点数据恰好在缓存失效时,大量请求同时访问该数据,导致后端数据源压力过大。可以通过加锁或使用互斥锁机制来避免缓存击穿。
- 定期清理无效缓存:定期清理不再使用的缓存条目,避免缓存占用过多内存资源。
6、总结
Spring Cache是一个强大且灵活的缓存框架,能够显著提升应用的性能和响应速度。通过使用@Cacheable、@CachePut和@CacheEvict等注解,开发者可以轻松地将缓存机制集成到业务逻辑中,而无需关心具体的缓存实现细节。Spring Cache支持多种缓存提供者,开发者可以根据实际需求选择合适的缓存方案。