Spring Cache的坑入坑出
最近的项目用到了Spring Cache做Redis的序列化配置,做完业务后整理一份学习记录供大家参考
写在最前面
缓存是一种在实际的开发中经常使用的提高性能的方法,提到缓存第一个想到的当然就是Redis了。在spring中,有一种基于redis序列化的cache配置。本文将介绍,如何在少的配置下可以给既有代码提供缓存能力。
一、Spring Cache的简单介绍
Spring 3.1引入了基于注解的缓存技术,它本质上不是一个具体的缓存实现方案(比如EHCache或OSCache),而是一个对缓存使用的抽象。通过在既有代码中加入少量的注解和配置,既能够实现缓存,提供缓存能力。
Spring的缓存技术还具备相当的灵活性。不仅能够用SpEL(Spring Expression Language)来定义缓存的key和各种condition,还提供开箱即用的缓存暂时存储方案,也支持主流的专业缓存比如EHCache集成。
其特点总结例如下:
- 通过少量的配置 annotation 凝视就可以使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件就可以使用缓存
- 支持 Spring Express Language,能使用对象的不论什么属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过事实上现不论什么方法的缓存支持
- 支持自己定义 key 和自己定义缓存管理者,具有相当的灵活性和扩展性
下面,通过一步步实现的过程,逐步介绍其内容。
二、开发测试环境和代码结构
工具:IDEA
项目类型:MAVEN项目
JDK版本:1.8
代码结构:
结构简单,就是基本的springboot+mysql+mybatis,依次说明一下:
config包是用来存放配置代码,比如Redis的配置,还有一些自定义的configuration。
controller包存放控制器(很基础,主要写给需要的萌新)。
entity存放实体类,也就是常说的POJO。
贴一下代码作参考:
package com.guanzhi.entity;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
/**
* @author guan.zhi
* @date 2021/3/8
*/
@TableName("user")
public class User {
@TableId
private Long id;
@TableField("name")
private String name;
@TableField("pwd")
private String pwd;
public Long getId() {
return id;
}
public void setId(Long id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getPwd() {
return pwd;
}
public void setPwd(String pwd) {
this.pwd = pwd;
}
}
mapper放mybatis的映射mapper类,用来和对应的xml做映射,关联SQL。
service是我们的业务层的代码,主要是放业务实现代码,service包下放接口,impl下放实现类。DTO顾名思义是数据传输对象,用来接收前端传递的参数,放在对象中做处理。
toolkit存放工具类,本文中主要使用到了统一API返回接口,做接口返回处理。
maven就不多说了吧,就是基本的springboot、mysql驱动、mybatis等依赖,不做赘述。需要了解的小伙伴可以参考这个:
https://blog.csdn.net/S_L__/article/details/104197511
和我不一样的是,他直接建springboot项目,我是以maven项目为基础的。
三、Cache实现的关键步骤
1.CacheConfiguration(自定义缓存读写机制)
CacheConfiguration的主要作用是继承CachingConfigurerSupport来进行部分配置,本文中主要重写了keyGenerator,用来生成自定义的key。还有errorHandler,缓存仅仅是为了业务更快地查询而存在的,如果因为缓存操作失败导致正常的业务流程失败,有点得不偿失了。因此需要开发者自定义CacheErrorHandler
处理缓存读写的异常,当redis异常时,打印日志但程序继续运行。其次,使用redis进行缓存需要配置其为序列化方式,以此替换默认的jdk序列化。关于springcache的序列化方式参考这个博客:
https://blog.csdn.net/wabiaozia/article/details/107134081
我测试的代码如下:
package com.guanzhi.config;
import ...//篇幅原因省略
/**
* @author guan.zhi
* @date 2021/3/8
*/
@EnableCaching
@Configuration(proxyBeanMethods = false)
public class CacheConfiguration extends CachingConfigurerSupport {
public static final Logger log = LoggerFactory.getLogger(CacheConfiguration.class);
private final static String CLASS = "class";
private final static String METHOD_NAME = "methodName";
private final static String PACKAGE = "package";
@Bean
public RedisCacheConfiguration redisCacheConfiguration() {
GenericJackson2JsonRedisSerializer serializer = new GenericJackson2JsonRedisSerializer();
RedisCacheConfiguration configuration = RedisCacheConfiguration.defaultCacheConfig();
configuration = configuration.serializeValuesWith(RedisSerializationContext
.SerializationPair
.fromSerializer(serializer))
.entryTtl(Duration.ofHours(2));
return configuration;
}
@Override
public KeyGenerator keyGenerator() {
return (target, method, params) -> {
Map<String, Object> container = new HashMap<>(3);
Class<?> targetClassClass = target.getClass();
container.put(CLASS, targetClassClass.toGenericString());
container.put(METHOD_NAME, method.getName());
container.put(PACKAGE, targetClassClass.getPackage());
for (int i = 0; i < params.length; i++) {
container.put(String.valueOf(i),params[i]);
}
ObjectMapper objectMapper = new ObjectMapper();
String jsonString = null;
try {
jsonString = objectMapper.writeValueAsString(container);
} catch (JsonProcessingException e) {
e.printStackTrace();
}
return DigestUtils.sha256Hex(jsonString);
};
}
@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, @Nullable 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);
}
};
}
}
2.yml配置
#只展示重要部分
spring:
application:
name: demo
datasource:
type: com.mysql.cj.jdbc.MysqlDataSource
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/mybatis?serverTimezone=Asia/Shanghai&characterEncoding=utf8&
useSSL=false
username: root
password: 123456
cache:
type: redis
redis:
cache-null-values: false
time-to-live: 3600000ms
jackson:
date-format: yyyy-MM-dd HH:mm:ss
redis:
host: localhost
这样SpringCache将会自动配置redis,注入相关bean,我们就可以在代码中使用注解了。
3.代码注解
springcache的注解主要有四种:@Cacheable
,@CacheEvict
,@CachePut
。
我在开发的过程中常用的是前两个,以此介绍一下主角们:
@Cacheable
注解在方法上,标识方法可以进行缓存,可以将数据返回进行缓存。当我们第一次访问的时候,会先把查出来的数据根据key缓存到redis中,当再次发起访问请求的时候,会先去缓存中查看时候有相同的key,如果有就调用缓存。它有这几个参数:
名称 | 解释 | 例子 |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | 例如: @Cacheable(value=”mycache”) @Cacheable(value={”cache1”,”cache2”} |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @Cacheable(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @Cacheable(value=”testcache”,condition=”#userName.length()>2”) |
我的测试代码实例:
@Cacheable(value = "user", key = "'list:' + #queryCondition.current", unless = "#result == null")
public Page<User> getList(QueryCondition queryCondition) {
Page page = new Page(queryCondition.getCurrent(), queryCondition.getSize());
Page<User> userPage = userMapper.getList(page);
return userPage;
}
在这个方法中,首先用@Cacheable注解,写了三个参数,value、key和缓存的条件设置。
@CachePut
这个注解的作用就其实有点类似于上面的,只是他的作用相当于我们数据库中的update的作用,加上这个注解后,我们每次访问数据都不会从缓存中拿取数据,而是直接通过去数据库查询并将数据缓存到redis中。可能有人会问,这样有什么作用呢?其实如果你是结合到cachable就有作用了,因为我们更新后数据是不是变化了,所以我们就需要将之前的数据的给清空掉,否则的话就会产生脏数据。
同样他也有几个参数需要填写,其实就和cachable一样的。
@CacheEvict
我们上面的put的注解说了他是用于更新的,那么如果我们增加了数据或者删除了数据怎么办,比如通过用户id来查询用户所购买的所有东西的数据,如果用户又买了新东西或者删除了某个物品的信息,那么我们怎么办,理论上我们是可以使用上面的注解,可是我们删除和增加其实没有必要去查询新数据。所以我们这个CacheEvict注解的作用就出现了。他的作用其实就是帮我们清空指定的缓存,他可以清空value,也可以清空value.key的数据。这样的话我们可以针对性的去处理数据。主要参数有:
参数 | 解释 | example |
---|---|---|
value | 缓存的名称,在 spring 配置文件中定义,必须指定至少一个 | @CacheEvict(value=”my cache”) |
key | 缓存的 key,可以为空,如果指定要按照 SpEL 表达式编写,如果不指定,则缺省按照方法的所有参数进行组合 | @CacheEvict(value=”testcache”,key=”#userName”) |
condition | 缓存的条件,可以为空,使用 SpEL 编写,返回 true 或者 false,只有为 true 才进行缓存 | @CacheEvict(value=”testcache”,condition=”#userName.length()>2”) |
allEntries | 是否清空所有缓存内容,缺省为 false,如果指定为 true,则方法调用后将立即清空所有缓存 | @CachEvict(value=”testcache”,allEntries=true) |
beforeInvocation | 是否在方法执行前就清空,缺省为 false,如果指定为 true,则在方法还没有执行的时候就清空缓存,缺省情况下,如果方法执行抛出异常,则不会清空缓存 | @CachEvict(value=”testcache”,beforeInvocation=true) |
我的测试代码实例:
@CacheEvict(value = "user", allEntries = true)
public int insertSelective(User record) {
return userMapper.insert(record);
}
上面的代码作用插入新的用户信息,但是插入了数据以后导致缓存中的部分数据失效,所以需要清除所有缓存。
四、一些坑
-
配置一定要写好,在yml中cache:redis一定要写出来,其次redis的基本配置也要写好,不然注解会报错。
-
注解中的value参数,尽可能写上,避免因为key的读取问题不能存或者读缓存。
-
上述的一些代码写的不是很严谨,只是参考和测试使用,在实际的开发中一定要对key的设置,再细节一点。比如,如果是查询方法,需要把当前这个Query的key唯一标识出来,主要的目的是方便更新数据后@CacheEvict来准确的清理所有脏数据。
-
最开始做的时候,没有注意@Cacheable和@CachePut的区别,字面意思以为Put是存数据,默认会读取缓存,读了文档和源码后,才发现其实不然。所以希望大家在做开发的时候,一定要知其然也知其所以然,这样才能写出健壮的代码,保证系统整体的稳定性。
如果卷不可避免,那也要让自己时刻保持清醒。加油打工人们~