赞
踩
众所周知,缓存一般在内存中存储,cpu对内存的调度效率要优于硬盘(时间快)
我们通常使用空间换取时间,达到某种程度的时空平衡
于是一般来讲使用缓存可以提高效率—>提升性能
我这次使用缓存是目的是将数据库字典类查询的结果放入缓存,以便于提高字典类使用时查询的效率
(注:我所用到的字典类一般不会更新,数据量也不大,占用内存不多,大多用于查询的操作,适合用做缓存,可以大大减少数据库的压力,在一定程度上提升性能)
为什么要使用SpringBoot Cache 而不是使用Mybatis缓存
首先,Mybatis 缓存分为一级和二级缓存
一级缓存默认开启,是SqlSession级别的(一般是指程序对数据库的一次会话,与@Transactional对应),默认在一个会话中有效
一般来讲,一个service服务中连续查询两次相同的sql情况很少,故不常用到。
二级缓存默认关闭,是SqlSessionFactory级别的,多个SqlSession会话共享,缓存是以namespace为单位的
但是通常使用会存在一些问题,不同的namespace完全是有可能操作同一张表的,那么会导致一个namespace的数据修改了一张表,但是另一个namespace的那张表的数据缓存没被修改,这样就导致了缓存和数据库不一致的问题,如果是集群的状态下,则产生的问题会更多,故也不常用到。
所以使用缓存一般会用到缓存中间件如Redis来处理,SpringBoot 很早就为我们提供好了Cache的操作规范,使用简洁方便,故简单使用的话一般不用自己重复造轮子。
SpringBoot Cache 是 SpringBoot为了简化缓存的的开发,提供的一整套的缓存解决方案。
提供一整套的接口和代码规范、配置、注解等,用于整合各种缓存方案。
主要是定义了统一的接口和不同的实现类来统一使用不同的缓存技术。
官方文档:https://springdoc.cn/spring-boot/io.html#io
功能简介:Cache是作用于方法上的,方法配置了缓存,则第一次执行完方法后会将结果数据存入缓存,第二次执行则直接从缓存读取结果返回,不会执行方法
其中主要包含接口:Cache、CacheManager
Cache
缓存的接口,用来存储缓存的key和value,如有Redis的实现,RedisCache
主要方法
方法名 | 描述 |
---|---|
getName() | 获取到缓存的名称 |
get(key) | 获取这个缓存中某个 key映射的值 |
put(key,value) | 保存或者更新这个缓存中某个key 映射的值 |
evict(key) | 从这个缓存中删除某个key ,即删除缓存中的某个条目 |
clear() 方法 | 清空缓存中的所有条目 |
CacheManager
缓存管理的接口,用来管理多个Cache对象
主要方法
方法名 | 描述 |
---|---|
getCache(String) | Cache 根据缓存的名称得到缓存对象 |
getCacheNames() | Collection 获取管理器管理范围内的所有cache名称 |
其中主要包含注解如下:
Annotation | 作用 |
---|---|
@Cacheable | 查询常用。将方法的结果缓存起来,下一次方法执行参数相同时,将不执行方法,返回缓存中的结果。@Cacheable 会进行缓存检查 |
@CacheEvict | 删除常用。移除指定缓存,如果向让cache 中所有的 key-value 都失效,即清空cache中所有的数据,可以使用 allEntries=true |
@CachePut | 更新常用。标记该注解的方法总会执行,根据注解的配置将结果缓存。一般用于更新数据的时候,方法更新了数据,就将更新后的数据返回,如果有这个Annotation,那么这个数据就立即被缓存了。 |
@Caching | 可以组合使用@Cacheable,@CacheEvict,@CachePut |
@CacheConfig | 类级别注解,可以设置一些共通的配置,@CacheConfig(cacheNames=“user”), 代表该类下的方法均使用这个cacheNames |
这些缓存操作Annotation 中常用属性的解释:
总结:
在支持 Spring Cache 的环境下,对于使用 @Cacheable 标注的方法,Spring 在每次执行前都会检查 Cache 中是否存在相同 key 的缓存元素,如果存在就不再执行该方法,而是直接从缓存中获取结果进行返回,否则才会执行并将返回结果存入指定的缓存中。@CachePut 也可以声明一个方法支持缓存功能。与 @Cacheable 不同的是使用 @CachePut 标注的方法在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。
本文只介绍了@Cacheable的使用,@CacheEvict、@CachePut这两个见名知意,就不介绍了
SpringBoot项目maven构建情况下pom需要包含spring-boot-starter-cache和spring-boot-starter-data-redis依赖,序列化可以使用fastjson
<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>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>fastjson</artifactId>
<version> 1.2.70</version>
</dependency>
spring:
redis:
host: 127.0.0.1
port: 6379
password: xxx
database: 1
# 等等
在入口处加入 @EnableCaching
打开缓存配置:
@SpringBootApplication
@EnableCaching
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
一般情况下,引入了
spring-boot-starter-data-redis
依赖后,就可以直接使用了, 业务类上无需做任何改动。CacheAutoConfiguration会自动判断加载Redis的实现RedisCacheManager等
@Cacheable
注解@Cacheable(cacheNames = {"cache:sp_data"},key="#root.methodName")
public List<Data> findAll() {
TimeInterval timer = DateUtil.timer();
List<Data> = dataMapper.findAll();
log.info("数据库访问:findAll方法");
System.out.println(timer.interval());//花费毫秒数
return data;
}
cacheNames : 可以指定多个cache名称,是一个数组。
key : cache中的key,可以使用SpringEL表达式获取当前方法上下文信息,比如方法名称,参数的值等。
Caching SpringEL Evaluation Context说明:
属性名称 | 描述 | 示例 |
---|---|---|
methodName | 当前方法名 | #root.methodName |
method | 当前方法 | #root.method.name |
target | 当前被调用的对象 | #root.target |
targetClass | 当前被调用的对象的class | #root.targetClass |
args | 当前方法参数组成的数组 | #root.args[0] |
caches | 当前被调用的方法使用的Cache | #root.caches[0].name |
要使用 root 对象的属性作为 key 时,也可以将“#root”省略,因为 Spring 默认使用的就是 root 对象的属性。
如果要直接使用方法参数传递的值,可以用
#参数名称
来取出方法调用的时候传递的实参值,比如上面的#id
如果key的生成规则比较复杂,无法用 SpringEL来生成,可以自定义一个 KeyGenerator, 分为三个步骤来实现:
1.定义一个类,实现 org.springframework.cache.interceptor.KeyGenerator 接口。
2.将自定义的KeyGenerator注册到容器中
3.在@Cacheable 中使用keyGenerator
属性
注:一旦使用了 keyGenerator
,就不要再使用 key属性了。
/** * @author xxx * @version 1.0 * @description: 缓存配置类 * @date 2023/8/26 18:18 */ @Configuration public class CacheConfig { /** * 缓存key生成策略 (cacheNames:方法名:[参数列表]) * @return KeyGenerator */ @Bean("cacheKeyGenerator") public KeyGenerator keyGenerator() { return (target, method, params) -> (method.getName() + ":" + Arrays.asList(params)); }
@Cacheable(cacheNames = {"cache:xxx_data"}, keyGenerator = "cacheKeyGenerator")
RedisCacheConfiguration的配置:
/** * @author xxx * @version 1.0 * @description: 缓存配置类 * @date 2023/8/26 18:18 */ @Configuration public class CacheConfig { // 设置key过期时间为24小时 private static final Duration DEAFULT_TTL= Duration.ofHours(24); /** * redis缓存全局配置 * @return redisCacheConfiguration */ @Bean public RedisCacheConfiguration redisCacheConfiguration(){ // 使用fastJson来序列化数据 GenericFastJsonRedisSerializer genericFastJsonRedisSerializer = new GenericFastJsonRedisSerializer(); // 构造RedisCacheConfiguration RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); configuration = configuration // 设置cacheNames和key之间默认的双冒号为单冒号 .computePrefixWith(name -> name + ":") // 禁用缓存空值 禁用后如果返回空值会抛出异常,建议搭配@Cacheable的condition、unless等规则使用 .disableCachingNullValues() // 设置value序列化器 .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(genericFastJsonRedisSerializer)) // 设置 key的过期时间(设置的是全局过期时间) .entryTtl(DEAFULT_TTL); // // 指定前缀 // .prefixCacheNameWith(REDIS_CACHE_PREFIX); return configuration; }
之前的全局配置 设置 key的过期时间导致所有的key都是相同的过期时间,在业务上来讲不够灵活
如果要自定义缓存的过期时间,网上有两种实现方案:
全局设置通过不同的cacheNames来区分设置不同的过期时间
这种方案需要在自定义缓存管理器中进行判断cacheNames,在设置相应的过期时间,
// 参数1:缓存写入是否开启锁 // 参数2:默认缓存配置 // 参数3:已知缓存名称的映射以及用于这些缓存的配置。 @Bean public CacheManager cacheManager(RedisConnectionFactory factory) { return new RedisCacheManager( RedisCacheWriter.lockingRedisCacheWriter(factory), this.getRedisCacheConfigurationWithTtl(1), this.getRedisCacheConfigurationMap() ); } /** * 默认失效时间配置 * * @param days 未设置失效事件的key 默认days失效 */ private RedisCacheConfiguration getRedisCacheConfigurationWithTtl(Integer days) { Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper om = new ObjectMapper(); om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL); jackson2JsonRedisSerializer.setObjectMapper(om); return RedisCacheConfiguration.defaultCacheConfig().serializeValuesWith( RedisSerializationContext .SerializationPair .fromSerializer(jackson2JsonRedisSerializer)).entryTtl(Duration.ofDays(days)); } public static final String CACHE_NAME_1 = "cache:name1"; public static final String CACHE_NAME_2 = "cache:name2"; /** * 已知缓存名称的映射以及用于这些缓存的配置 */ private Map<String, RedisCacheConfiguration> getRedisCacheConfigurationMap() { Map<String, RedisCacheConfiguration> redisCacheConfigurationMap = new HashMap<>(); // 自定义缓存名称对应的配置 redisCacheConfigurationMap.put(CACHE_NAME_1, this.getRedisCacheConfigurationWithTtl(5)); redisCacheConfigurationMap.put(CACHE_NAME_2, this.getRedisCacheConfigurationWithTtl(10)); return redisCacheConfigurationMap; }
写法大致如上,在配置中传入缓存名称和过期时间的对应关系,在灵活一些的话可以自定义规则如从cacheNames中设置名称包含过期时间或自自定义cacheNames与过期时间的动态配置类。
但是不可以细分到设置每个key的过期时间,所以有如下第二种办法
在缓存AOP之前执行缓存的处理,增加自定义注解,从注解中传入过期时间
扩展了CacheResolver后,就相当于拦截了 Cache的解析,即能获取到 Cache对象,又能获取到被拦截的Method,这样就可以通过method 的反射 获取到
@CacheExpire
对象了。这样就能替换掉 RedisCache中的RedisCacheConfiguration
对象了。
自定义注解CacheExpire
/** * @author :xxx * @date :Created in 2023/8/26 * @description: 缓存过期时间注解(结合 cacheResolver = "redisExpireCacheResolver" 使用) * @version: 1.0.0 */ @Target(ElementType.METHOD) @Retention(RetentionPolicy.RUNTIME) public @interface CacheExpire { /** * 过期时间,默认是24小时 */ public long expire() default 24L; /** * 单位,默认是小时 */ public TimeUnit timeUnit() default TimeUnit.HOURS; }
自定义缓存解析类RedisExpireCacheResolver
/** * @author xxx * @version 1.0 * @description: 自定义缓存解析类 * @date 2023/8/26 19:05 */ @Slf4j public class RedisExpireCacheResolver extends SimpleCacheResolver { public RedisExpireCacheResolver(CacheManager cacheManager){ super(cacheManager); } @Override public Collection<? extends Cache> resolveCaches(CacheOperationInvocationContext<?> context) { Collection<String> cacheNames = getCacheNames(context); if (cacheNames == null) { return Collections.emptyList(); } Collection<Cache> result = new ArrayList<>(cacheNames.size()); for (String cacheName : cacheNames) { Cache cache = getCacheManager().getCache(cacheName); if (cache == null) { throw new IllegalArgumentException("Cannot find cache named '" + cacheName + "' for " + context.getOperation()); } // 获取到缓存对象后,解析并设置过期时间 parseCacheExpire(cache,context); result.add(cache); } return result; } /** * 解析缓存和上下文对象 * @param cache * @param context */ private void parseCacheExpire(Cache cache,CacheOperationInvocationContext<?> context){ // 获取方法类 Method method= context.getMethod(); // 获取缓存实现类 RedisCache RedisCache redisCache=(RedisCache) cache; // 方法上是否标注了CacheExpire if(AnnotatedElementUtils.isAnnotated(method, CacheExpire.class)){ // 获取对象 CacheExpire cacheExpire= AnnotationUtils.getAnnotation(method,CacheExpire.class); log.info("redisCache解析,CacheExpire expire:{}, CacheExpire timeUnit:{}",cacheExpire.expire(), cacheExpire.timeUnit()); Duration duration= Duration.ofMillis(cacheExpire.timeUnit().toMillis(cacheExpire.expire())); // 替换RedisCacheConfiguration 对象 setRedisCacheConfiguration(redisCache,duration); } else { // 未设置过期时间注解处理 // ...... } } /** * 替换RedisCacheConfiguration 对象 * @param redisCache * @param duration */ private void setRedisCacheConfiguration(RedisCache redisCache, Duration duration){ RedisCacheConfiguration defaultConfiguration=redisCache.getCacheConfiguration(); RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig(); // 通过反射查找并修改缓存配置中过期时间的字段 Field expireField = ReflectionUtils.findField(RedisCacheConfiguration.class, "ttl", Duration.class); expireField.setAccessible(true); ReflectionUtils.setField(expireField, defaultConfiguration, duration); } }
使用方式
@CacheExpire(expire = 10000L, timeUnit = TimeUnit.MILLISECONDS)
@Cacheable(cacheNames = "cache:xxx_data", keyGenerator = "cacheKeyGenerator", cacheResolver = "redisExpireCacheResolver")
注:存在1个问题,因为RedisCacheConfiguration是全局配置的,通过反射修改会影响所有的过期时间
所以需要都配置cacheResolver、@CacheExpire
为了方便给每个key设置过期时间,使用第二种方式反射设置RedisCacheConfiguration,如果忘记使用cacheResolver可能就会导致过期时间使用的是上个cacheResolver设置的RedisCacheConfiguration,而非默认值,导致管理混乱。
建议使用第一种自定义RedisCacheManager,规范化让每个cacheName都是相同的过期时间,官方的api的示例也是这样
如果想使用第二方式,则可以针对问题在进行措施解决,如下:
定义切面来进行处理更新RedisCacheConfiguration配置为默认过期时间,这样就算未使用cacheResolver,也会初始化过期时间
/** * @author xxx * @version 1.0 * @description: 缓存过期时间切面类 * @date 2023/8/27 16:00 */ @Component @Aspect @Slf4j @Order(value = -1) public class CacheExpireAspect { private final RedisCacheConfiguration defaultRedisCacheConfiguration; public CacheExpireAspect(RedisCacheConfiguration defaultRedisCacheConfiguration) { this.defaultRedisCacheConfiguration = defaultRedisCacheConfiguration; } @Pointcut("@annotation(com.xxx.common.anno.CacheExpire)") private void cacheExpire() {} @After("cacheExpire()") public void cacheExpireAspectAfter() { // 更新RedisCacheConfiguration配置 设置过期时间为24小时 Field expireField = ReflectionUtils.findField(RedisCacheConfiguration.class, "ttl", Duration.class); expireField.setAccessible(true); ReflectionUtils.setField(expireField, defaultRedisCacheConfiguration, Duration.ofHours(24)); } }
当然,还存在一些问题,比如缓存一致性,缓存双删等策略未实现,后续有完善优化的空间……
Copyright © 2003-2013 www.wpsshop.cn 版权所有,并保留所有权利。