在我们的后端项目中的性能瓶颈往往就是IO操作了,用户对数据库的查询往往存在许多重复性的查询,因此有许多针对数据库的查询其实是不必要的,我们可以将查询结果缓存起来,下一次用户想查询同样的内容时就不必再去访问数据库了,而是直接从缓存中获取,这样速度就快很多了。
SpringBoot已经自带了缓存机制,默认用的是ConcurrentMapCacheManager,使用ConcurrentMap来缓存数据,不过这样项目一重启数据就没了,并且不支持设置失效时间,为了更有效地管理缓存,我们可以借助第三方,比如JCache、EhCache、Redis等等,SpringBoot支持我们自定义缓存配置来配置合适的缓存管理器,在我做项目中,我选择了Redis来做缓存,当然,相比如EhCache或者默认的ConcurrentMap这种直接在jvm中缓存的机制,速度相比较起来还是比较慢的,但是也比直接访问数据库来得快,下面就来说说我是怎么配置的。
配置参考了这位大佬的文章,做了些完善和修改https://www.jianshu.com/p/b1d6f0c53362
1、需求
对于项目的缓存,我的需求有如下几点:
- 通过注解,利用AOP实现缓存管理
- 自定义缓存key的生成策略,为全限定类名+方法名+方法参数,如
com.hzhang.service.impl.CommentServiceImpl.findCommentListByBlogId:19
,由于使用的Cacheable
注解要求指定缓存区块名,所以最终缓存名前面还加了一句自己指定的“cache::”,存储在redis中的缓存如下所示:
- 在某些方法上通过注解指定批量删除某些缓存
2、代码实现
代码的实现分为五步:
- 1、引入依赖
- 2、编写缓存配置类
- 3、自定义注解
- 4、缓存存入的实现
- 5、缓存删除的实现
2.1、引入依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-aop</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
2.2、编写Redis缓存配置类
需要注入三个Bean,首先是key的生成策略:
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(method.getDeclaringClass().getName()).append(".").append(method.getName()).append(":"); //先将类的全限定名和方法名拼装在 key 中
if (params.length == 0) {
return key.append("").toString();
}
for (Object param : params) { //通过遍历参数,将参数也拼装在 key 中,保证每次获取key 的唯一性
key.append(param.toString());
}
return key.toString();
}
};
}
其次是定义redis缓存管理器:
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
// 定义缓存配置
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig() // 使用默认配置
.entryTtl(Duration.ofHours(12)); // 缓存过期时间
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
其中,默认配置的规则如下:
缓存过时时间为永不过时,允许缓存空值,使用实际缓存名称做前缀(比如我之后在@Cacheable
中指定的前缀cache::
),key的序列化采用StringRedisSerializer
(只允许序列化字符串),值的序列化使用JdkSerializationRedisSerializer
。
如此我们就可以将结果序列化之后缓存到Redis中去了,我们读取的话则需要做反序列化(所有Bean都应该实现Serializable接口),Spring中操作Redis用的是RedisTemplate
类,如果我们不自己指定序列化规则,这个类默认采用的序列化器将是JdkSerializationRedisSerializer
,到时读出来的数据会包含一些奇怪的字符,因此需要配置一下序列化规则,我使用的阿里巴巴的jackson
(听说有安全漏洞?github一直提醒我换了。。。也可以使用谷歌的Gson),配置规则如下:
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 配置具体序列化方式
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
// 使用om格式化输出
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectJackson2JsonRedisSerializer.setObjectMapper(om);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化
template.setHashKeySerializer(stringRedisSerializer);
// value采用jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
// hash的value也采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
// 执行afterPropertiesSet方法,让配置设置进去
template.afterPropertiesSet();
return template;
}
完整代码如下:
package com.hzhang.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
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.cache.RedisCacheWriter;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.net.UnknownHostException;
import java.time.Duration;
/**
* @author :Hzhang
* @date :Created in 2020/6/1 16:07
* @description:redis配置
* @modified By:
* @version: $
*/
@Configuration
@EnableCaching
public class RedisConfig extends CachingConfigurerSupport {
/**
* 自定义key的生成策略:包名+方法名+方法参数
*
* @return
*/
@Override
@Bean
public KeyGenerator keyGenerator() {
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... params) {
StringBuilder key = new StringBuilder();
key.append(method.getDeclaringClass().getName()).append(".").append(method.getName()).append(":"); //先将类的全限定名和方法名拼装在 key 中
if (params.length == 0) {
return key.append("").toString();
}
for (Object param : params) { //通过遍历参数,将参数也拼装在 key 中,保证每次获取key 的唯一性
key.append(param.toString());
}
return key.toString();
}
};
}
/**
* 配置缓存过期时间
*
* @param redisConnectionFactory
* @return
*/
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory) {
RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(12));
return RedisCacheManager
.builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
.cacheDefaults(redisCacheConfiguration).build();
}
/**
* 定义序列化方式
*
* @param redisConnectionFactory
* @return
* @throws UnknownHostException
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<String, Object> template = new RedisTemplate();
template.setConnectionFactory(redisConnectionFactory);
// 配置具体序列化方式
Jackson2JsonRedisSerializer<Object> objectJackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
// 使用om格式化输出
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
objectJackson2JsonRedisSerializer.setObjectMapper(om);
// String的序列化
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
// key采用String的序列化
template.setKeySerializer(stringRedisSerializer);
// hash的key也采用String的序列化
template.setHashKeySerializer(stringRedisSerializer);
// value采用jackson
template.setValueSerializer(objectJackson2JsonRedisSerializer);
// hash的value也采用jackson
template.setHashValueSerializer(objectJackson2JsonRedisSerializer);
// 执行afterPropertiesSet方法,让配置设置进去
template.afterPropertiesSet();
return template;
}
}
2.3、自定义注解
接下来定义一个ClearRedisCache
注解用以在增删改方法执行后对某些缓存进行删除,由于数据表关联关系的存在,因此还需要指定需要级联删除的缓存:
package com.hzhang.annotation;
import java.lang.annotation.*;
/**
* @author :Hzhang
* @date :Created in 2020/6/1 22:13
* @description:调用方法前清空Redis中对应的缓存
* @modified By:
* @version: $
*/
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface ClearRedisCache {
/**
* 需要级联删除的缓存
* @return
*/
Class<?>[] cascade() default {};
}
删除的缓存将为前缀为所在的全限定类名对应的key,以及指定的级联删除的key。
2.4、缓存存入的实现
这里使用的是Spring框架的Cacheable注解,该注解的作用是执行方法前先判断缓存中有没有数据,如果有的话就直接从缓存中取出结果并返回,没有的话再去执行对应的方法,除了这个注解外,还有CachePut、Caching、CacheEvict、CacheConfig这几个常用的缓存相关的注解,到网上搜可以看到很多介绍。
/**
* 查询首页博客分页列表
* @param currentPage
* @param pageSize
* @return
*/
@Cacheable(cacheNames = "cache")
PageInfo<Blog> findHomeBlogList(Integer currentPage, Integer pageSize);
其中cacheNames指定的就是缓存的前缀名。
2.5、缓存删除的实现
缓存的删除在org.springframework.cache.annotation
中有一个CacheEvict就是用来删除指定缓存的,不过只能删除一个确定的key,我们需要的是批量的删除,因此才定义了一个ClearRedisCache
注解,将这个注解加在增删改方法上,同时指定需要级联删除的缓存,如下:
/**
* 根据id删除博客
* @param id
*/
@ClearRedisCache(cascade = {TagServiceImpl.class, TypeServiceImpl.class})
void deleteBlog(Long id);
在执行deleteBlog方法之后,我们需要清空关于这个BlogService相关的缓存,同时由于Blog与Tag及Type都有关联关系,因此需要做级联删除,这一步通过AOP来实现,实际执行的是***Impl类中的方法,因此需要指定的类也应该是实现类。AOP代码的实现如下:
package com.hzhang.aspect;
import com.hzhang.annotation.ClearRedisCache;
import com.hzhang.utils.RedisUtil;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.core.annotation.AnnotationUtils;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.util.Arrays;
import java.util.stream.Stream;
/**
* @author :Hzhang
* @date :Created in 2020/6/1 22:00
* @description:调用Service层方法前清空缓存
* @modified By:
* @version: $
*/
@Component
@Aspect
public class ClearCacheAspect {
@Autowired
private RedisUtil redisUtil;
@Pointcut("execution(* com.hzhang.service.*.*(..))")
public void clearCache() {
}
@Before("clearCache()")
public void doBefore(JoinPoint joinPoint) {
// 找到被调用的方法并判断是否加上了清空缓存的注解
Class targetClass = joinPoint.getTarget().getClass();
boolean isClear = false;
Method[] methods = targetClass.getMethods();
Class<?>[] cascade = null;
for (Method method : methods) {
if (method.getName().equals(joinPoint.getSignature().getName())) {
if (AnnotationUtils.findAnnotation(method, ClearRedisCache.class) != null) {
ClearRedisCache annotation = AnnotationUtils.findAnnotation(method, ClearRedisCache.class);
cascade = annotation.cascade();
isClear = true;
break;
}
}
}
// 如果需要清空缓存
if (isClear) {
// 获取key
String[] key = {"cache::" + joinPoint.getSignature().getDeclaringTypeName() + "*"};
// 需要级联删除的key
String[] cascadeKeys = Arrays.stream(cascade)
.map(cls -> "cache::" + cls.getName() + "*")
.toArray(len -> new String[len]);
// 模糊删除对应的key
Stream.of(key, cascadeKeys)
.flatMap(Arrays::stream)
.forEach(k -> {
String[] keys = redisUtil.keys(k);
if (keys != null && keys.length > 0) {
redisUtil.del(keys);
}
});
}
}
}
逻辑就是对com.hzhang.service包中的所有方法做切面,在方法执行前先判断有没有加上ClearRedisCache的注解,如果有的话就设置isClear为true,同时获取注解中的casche参数。
接下需要按照key的生成策略构造key,先获取方法本身对应的key,再去获取需要级联删除的key数组,利用flatMap将两个数组对应的stream流拼接成一个stream流,然后从redis中模糊查询出需要删除的key列表,然后进行批量的删除。
这里用到的redisUtil是自定义的对redisTemplate做一层封装的工具类,差不多就是把对redis的操作方法都换成redis的操作命令同名的方法,网上有许多类似的实现,可以再根据自己的需要加入一些命令的封装。
好了,到这里SpringBoot整合Redis缓存的工作就完成啦,我们只需要在需要缓存的方法上加上@Cacheable
注解,在需要删除缓存的方法上加上@ClearRedisCache
注解就可以了,一般都是在Service层上做。
3、题外话
这里插一个题外话,就是持久层框架使用的是Mybatis,开启了懒加载,然后Redis的序列化就出问题,会报类似这样的错误:
解决方法有两个,第一个就是关闭懒加载,第二个就是在所有实体类上面加上下面这个注解,忽略掉Json序列化的错误。我采用的是加注解的方法,对结果没有什么影响。
@JsonIgnoreProperties(value = {"handler"})