前言
在实际的开发中,缓存的使用已经是随处可见了,就目前来看,普遍使用的比较多的大概就是redis了吧,但从编码的角度,纯粹使用redis去操作缓存,似乎并不是一个很好的选择
我们不妨来看下面这段代码(细节请暂时忽略)
@Autowired
private RedisTemplate<String,DbUser> redisTemplate;
public DbUser getUserById(String id) {
DbUser dbUser = redisTemplate.opsForValue().get("user:" + id);
if(dbUser != null){
return dbUser;
}
dbUser = dbUserMapper.getByUserId(id);
if(dbUser != null){
redisTemplate.opsForValue().set("user:"+id,dbUser);
}
return dbUser;
}
上面这段代码展现的是一个常规的使用redis缓存数据的做法,看完后,是不是觉得这样写挺麻烦的,如果程序中需要缓存的数据比较多,这么写不仅给编码带来了较大的工作量,而且实在是不方便对缓存key的管理,一旦需要缓存的数据多了,最后可能自己都整不清哪些key是需要删的
基于上面这个小小的痛点,在实际开发中,涉及到缓存比较多的项目,我们并不推荐直接使用上面这种方式操作缓存,而是引入springcache
SpringCache 特点
- 通过少量的配置 annotation 注释即可使得既有代码支持缓存
- 支持开箱即用 Out-Of-The-Box,即不用安装和部署额外第三方组件即可使用缓存
- 支持 Spring Express Language,能使用对象的任何属性或者方法来定义缓存的 key 和 condition
- 支持 AspectJ,并通过其实现任何方法的缓存支持
- 支持自定义 key 和自定义缓存管理者,具有相当的灵活性和扩展性
- 支持各种缓存实现,如对象,对象列表,默认基于ConcurrentMap实现的ConcurrentMapCache,同时支持其他缓存实现
综合来说,springCache并不像正常缓存那样存储数据,而是在我们调用一个缓存方法时,会把该方法参数和返回结果作为一个键值对存放在缓存中,等到下次利用同样的参数来调用该方法时将不再调用该方法,而是直接从缓存中获取结果进行返回,从而实现缓存的效果
SpringCache 管理缓存的几个重要注解
1、@Cacheable注解
- 该注解用于标记缓存,就是对使用注解的位置进行缓存
- 该注解可以在方法或者类上进行标记,在类上标记时,该类所有方法都支持缓存
@Cacable使用时通常搭配三个属性使用
- value,用来指定Cache的名称,就是存储于哪个Cache上 ,简单理解是cache的命名空间或者大的前缀
- key,用于指定生成缓存对应的key,如果没指定,则会使用默认策略生成key,也可以使用springEL编写,默认是方法参数组合
@Cacheable(value="users", key="#user.id")
public User findUser(User user){
return user;
}
@Cacheable(value="users", key="#root.args[0]")
public User findUser(String id){
return user;
}
- condition,用来指定当前缓存的触发条件,可以使用springEL编写,如下代码,则当user.id为偶数时才会触发缓存
@Cacheable(value="users", key="#user.id",condition="#user.id%2==0")
public User findUser(User user){
return user;
}
- cacheManager ,用于指定当前方法或类使用缓存时的缓存管理器,通过cacheManager 的配置,可以为不同的方法使用不同的缓存策略,比如有的对象缓存的时间短,有的缓存的长,可以通过自定义配置cacheManager 来实现
2、@CachePut注解
该注解将标记的方法的返回值放入缓存中,使用方法与@Cacheable一样,通常@CachePut放在数据更新的操作上,举例来说,当 getByUserId这样一个方法上使用了以 userId为key的缓存时,如果更新了这条数据,该key对应的数据是不是要同步变更呢?
答案是肯定的,于是,我们就需要在更新数据的地方添加@CachePut注解,当updateByUserId触发之后,getByUserId上面的key对应的缓存对象数据也能同步变更
3、@CacheEvict注解
- 该注解用于清理缓存数据
- 使用在类上时,清除该类所有方法的缓存
- @CacheEvict同样拥有@Cacheable三个属性,同时还有一个allEntries属性,该属性默认为false,当为true时,删除该value所有缓存
@CacheEvict在实际使用中需要重点关注,比如一开始我们给用户组,角色,部门等与用户查询相关的业务上面添加了key的时候,当一个userId对应的这条数据被清理的时候,那么关联的key,即所说的用户组,部门,角色等关联的用户数据都需要一同清理
4、caching
- 组合多个cache注解一起使用
- 允许在同一个方法上使用 以上3个注解的组合
补充说明
以上简单介绍了springcache中的几个重要注解,以及各自的作用,通常来讲,在开发过程中,使用springcache也就是在和这些注解打交道,里面有一个点值得注意就是,关于方法级别上 key的使用规范和命名规范问题,这里可以关注和参考下springEL的写法规范
与springboot的整合
下面我们来通过实例演示如何在springboot中整合springcache
1、添加pom依赖
需要说明的是,springcache提供了多种缓存的实现,其中与redis的整合比较符合大家对redis的使用习惯,同时也更进一步了解springcache在redis中存储的结构,因此这里需引入springboot-redis的依赖
<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>
2、配置文件做简单的配置
spring:
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://IP:3306/bank1?autoReconnect=true&useUnicode=true&characterEncoding=utf8&serverTimezone=GMT%2B8&useSSL=false
username: root
password: 123456
redis:
host: 127.0.0.1
port: 6379
database: 1
cache:
type: redis
重点是 spring.redis.cache.type 这个配置,可以看到,springcache是提供了多种实现方式的,redis只是其中一种,大家结合自己的情况合理选择一种
3、自定义cacheManager
在上文,提到了cacheManager这个组件,它是作为@Cacheable这个注解中的一个属性搭配使用的,为了方便开发中,根据不同的业务需求对不同类型的key做个性化配置管理,比如有的数据需要缓存分钟级别,有的key需要缓存小时级别,这里我们就可以通过自定义配置cacheManager的方式来达到这个目的
提供一个配置类RedisConfig ,这种类实际开发中只需要一次配置保存即可,具体的细节配置参数可参考 spring官网,重点关注对cacheManager的配置,我们分别设定了分钟级,小时级和天级的3个cacheManager ,以bean的方式注入到spring容器中
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.MapperFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Primary;
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.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.util.StringUtils;
import java.lang.reflect.Method;
import java.time.Duration;
@Configuration
public class RedisConfig extends CachingConfigurerSupport {
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
template.setConnectionFactory(connectionFactory);
//使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);
ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
mapper.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(mapper);
template.setValueSerializer(jackson2JsonRedisSerializer);
//使用StringRedisSerializer来序列化和反序列化redis的key值
template.setKeySerializer(new StringRedisSerializer());
template.afterPropertiesSet();
return template;
}
/**
* 分钟级别
* @param connectionFactory
* @return
*/
@Bean("cacheManagerMinutes")
public RedisCacheManager cacheManagerMinutes(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration configuration = instanceConfig(60L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(configuration)
.transactionAware()
.build();
}
/**
* 小时级别
* @param connectionFactory
* @return
*/
@Bean("cacheManagerHour")
@Primary
public RedisCacheManager cacheManagerHour(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration configuration = instanceConfig(3600L);
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(configuration)
.transactionAware()
.build();
}
/**
* 天级别
* @param connectionFactory
* @return
*/
@Bean("cacheManagerDay")
public RedisCacheManager cacheManagerDay(RedisConnectionFactory connectionFactory){
RedisCacheConfiguration configuration = instanceConfig(3600 * 24L);;
return RedisCacheManager.builder(connectionFactory)
.cacheDefaults(configuration)
.transactionAware()
.build();
}
private RedisCacheConfiguration instanceConfig(long ttl){
Jackson2JsonRedisSerializer<Object> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<Object>(Object.class);
ObjectMapper objectMapper = new ObjectMapper();
objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
objectMapper.registerModule(new JavaTimeModule());
objectMapper.configure(MapperFeature.USE_ANNOTATIONS,false);
//只针对非空的值进行序列化
objectMapper.setSerializationInclusion(JsonInclude.Include.NON_NULL);
//将类型序列化到属性的json字符串
objectMapper.activateDefaultTyping(LaissezFaireSubTypeValidator.instance,ObjectMapper.DefaultTyping.NON_FINAL,
JsonTypeInfo.As.PROPERTY);
jackson2JsonRedisSerializer.setObjectMapper(objectMapper);
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofSeconds(ttl))
.disableCachingNullValues()
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer));
}
/**
* 自定义key生成策略
* @return
*/
@Bean("defaultSpringKeyGenerator")
public KeyGenerator defaultSpringKeyGenerator(){
return new KeyGenerator() {
@Override
public Object generate(Object o, Method method, Object... objects) {
String key = o.getClass().getSimpleName() + "_"
+ method.getName() +"_"
+ StringUtils.arrayToDelimitedString(objects,"_");
System.out.println("key :" + key);
return key;
}
};
}
}
在该配置类中,有一个KeyGenerator的bean需要稍加说明,即我们在给方法上声明key的时候,key的生成规则配置可以使用springEL的方式,也可以使用自定义key的生成规则,这个需要结合实际情况,搭配使用
经验来说,那么缓存的数据和key的结构关系非常紧密的,建议采用springEL的方式,做到精准控制,而那些比较通用的缓存数据,则可以考虑自定义key的规则,减少编码的工作量
以上所有的前置准备工作就完成了,下面通过几个增删改查接口来实际体验下springcache的使用
1、查询单条数据
接口:
@GetMapping("/getById")
public DbUser getById(String id){
return dbUserService.getById(id);
}
业务实现:
@Override
@Cacheable(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
public DbUser getById(String id) {
System.out.println("查询数据库");
DbUser dbUser = dbUserMapper.getByUserId(id);
return dbUser;
}
关于key的规则使用,springcache提供了丰富的选择,通常情况下,使用的比较多的是以参数作为key,或者以方法名称为key这里我们以方法参数为例做演示 前置准备:数据库中初始化了一批数据
下面启动项目,并启动本地redis,测试一下这个方法是否好使,接口访问:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5
接口数据返回: 接口连续刷几次,发现除了第一次之后没有再走数据库查询,说明缓存生效了 redis中key的存储结构如下:
如果换成自定义的keyGenerator,对方法上的注解做一下改造即可,然后再次请求接口,这时候发现缓存的key就是我们自定义的格式了
2、修改数据
修改数据涉及到@CachePut注解的使用,当修改之后,添加该注解,可以使得原来查询数据的接口中的缓存数据同步发生变更
接口:
@GetMapping("/updateById")
public DbUser updateById(String id,String name){
return dbUserService.updateById(id,name);
}
业务实现:
@Override
@CachePut(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
public DbUser updateById(String id,String name) {
DbUser dbUser = dbUserMapper.getByUserId(id);
dbUser.setRealname(name);
dbUserMapper.updateUserName(id,name);
return dbUser;
}
启动项目,仍然使用上面这条数据的user_id,我们修改下名称,依次调用修改的接口,和查询的接口,看看数据如何变化,
1、首先调用:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5 2、调用修改数据接口:http://localhost:8083/updateById?id=000ef60318254a768ed14b31514848a5&name=邓聪国2 3、再次调用查询接口:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5
redis中的数据已经近实时发生了修改,但是key仍然未发生变化
3、删除数据
接口:
@GetMapping("/deleteById")
public String deleteById(String id){
return dbUserService.deleteById(id);
}
业务实现:
@Override
@CacheEvict(value = {"dbUser"},key = "#root.args[0]",cacheManager = "cacheManagerMinutes")
public String deleteById(String id) {
dbUserMapper.deleteByUserId(id);
return "delete success";
}
启动项目,按照以下步骤做测试,
1、首先调用:http://localhost:8083/getById?id=000ef60318254a768ed14b31514848a5
2、调用删除数据接口:http://localhost:8083/deleteById?id=000ef60318254a768ed14b31514848a5
redis中的key被同步清理
通过以上的几个接口演示,我们基本上弄清了springcache的几个注解配合缓存的实际使用,更复杂的场景有兴趣的同学可以在此基础上做进一步的深究,比如最后那个 caching组合注解的使用,这里限于篇幅就不再继续展开了
本篇到此结束,最后感谢观看!
|