Springboot整合redis实现缓存及其缓存运行原理浅析
声明:小白,学习阶段,主要目的是为了记录学习过程,本文仅供参考,如有不足的地方欢迎指出讨论交流
本文基于Springboot2.1.3版本开发:
准备阶段
首先是pom.xml文件所需的依赖:
<dependencies>
<!--redis依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<version>2.1.3.RELEASE</version>
</dependency>
<!--缓存模块所需依赖-->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
</dependencies>
接着就是application.properties,这里使用的是yum格式的配置文件:
spring:
datasource:
#配置mysql连接信息
url: jdbc:mysql://localhost:3306/enterprisetalentmanagement?serverTimezone=UTC
username: root
password: 1234
driver-class-name: com.mysql.cj.jdbc.Driver
thymeleaf:
cache: false
redis:
host: 192.168.0.120 #配置redis的IP地址
port: 6379 #该属性默认使用的是6379,如果你redis端口映射的不是6379可以通过这个属性来改
logging:
level:
#打印SQL信息
com.demo02.demo.Dao: debug
mybatis:
configuration:
#开启下划线到驼峰命名法的自动转换,将数据库字段根据驼峰规则自动注入到对象属性
map-underscore-to-camel-case: true
server:
port: 8080
debug: true
这个地方我们开启debug:true,这样我们就可以在程序运行的时候很方便的看到哪些配置类生效,结果会打印到控制台中,接着是springboot的启动类:
@SpringBootApplication //声明启动类
@MapperScan("com.demo02.demo.Dao.Mapper") //mapper文件扫描
@EnableCaching //开启注解
public class DemoApplication {
public static void main(String[] args) {
SpringApplication.run(DemoApplication.class, args);
}
}
接下来在Service中我们来尝试一下缓存最基本的用法:
import com.demo02.demo.Dao.Mapper;
import com.demo02.demo.POJO.department;
import com.demo02.demo.POJO.enterprisetalent;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Service;
import java.util.List;
@Service
public class serviceimpl implements service{
@Autowired
private Mapper mapper;
//将方法的运行结果进行缓存,以后再要相同的数据,可以直接从缓存中获取不需要调用方法
@Cacheable(cacheNames = "emp")
@Override
public enterprisetalent getempByid(Integer id) {
System.out.println("查询:"+id+"号员工");
return mapper.selectempById(id);
}
}
常用的缓存注解
注解名 | 作用 |
---|---|
@Cacheable | 主要针对方法配置,能够根据方法的请求参数对其结果进行缓存 |
@CacheEvict | 清空缓存 |
@CachePut | 保证方法被调用,又希望结果被缓存 |
@EnableCaching | 开启基于注解的缓存 |
我们可以点击@Cacheable注解进入它的源码,来看看它有些什么属性:
@Target({ElementType.METHOD, ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
public @interface Cacheable {
/**
* Alias for {@link #cacheNames}.
*/
@AliasFor("cacheNames")
String[] value() default {};
@AliasFor("value")
String[] cacheNames() default {};
String key() default "";
String keyGenerator() default "";
String cacheManager() default "";
String cacheResolver() default "";
String unless() default "";
boolean sync() default false;
可以看到有一个叫做value的属性和它的作用其实是和cacheNames一样的,用于指定缓存组件的名字,是数组的方式,可以指定多个缓存。
cacheNames | 用于指定缓存存储的集合名,value作用与 cacheNames相同 |
key | 缓存数据使用的key;可以用它来指定。默认是使用方法参数的值 |
keyGenerator | key的生成器;可以自己指定key的生成器的组件id 。key/keyGenerator:二选一使用 |
cacheManager | 指定缓存管理器;或者cacheResolver指定获取解析器 |
unless | 否定缓存;当unless指定的条件为true,方法的返回值就不会被缓存;可以获取到结果进行判断 |
这里先只介绍这五个常用属性
其他类和文件就是普通的三层架构dao,service,controller大家应该都会我先就不写着占位置了,
我们可以先运行代码一遍,执行getempByid()方法:
可以看到,我们第一次执行查询是调用了方法去连接数据库的获取数据的,查询的是id为2的员工信息,打开RedisDesktopManager我们可以看见redis中多了一个名为emp::2,但它的value是序列化后的结果,因为我们要将对象数据存储到redis中实体类必须序列化,这个问题我们会放到后面说。
缓存运行原理浅析
说完了基本的文件配置接下来说我们应该怎么去实现springboot整合redis作为缓存,
首先我们先屡一下缓存的运行原理,既然我们引入了缓存模块那我们就应该可以想到肯定会有一个类叫做xxxAutoConfiguration的自动配置类,不然我们还用什么springboot。全局搜索可以找到一个叫做CacheAutoConfiguration的类,点进去源码可以看到它给我们注入了一个叫做CacheConfigurationImportSelector的class。
我们点击定位到该类所在位置
可以看到里面有一个方法,这个方法有什么作用呢,我们可以直接在return上打个断点,然后以debug模式运行程序:
我们可以看到,这个方法返回的都是一些xxxCacheConfiguration之类的组件,是一些缓存的配置类bean,所以这个方法的作用就是为我们的容器中添加一些缓存组件,接着我们把它放行,因为之前我们在配置文件中配置了debug=true,我们可以在控制台中看到这些bean的输出:
可以发现,在刚才selectImports方法为我们添加的组件中只有SimpleCacheConfiguration是matched匹配上的,通过打印日志可以看出SimpleCacheConfiguration配置类默认生效,我们可以全局搜索SimpleCacheConfiguration类进入源码看看它做了什么:
首先可以看到该类的上边有个注解叫做@ConditionalOnMissingBean(CacheManager.class),这个注解的意思是只有在容器中没有CacheManager也就是缓存组件的时候才会给该类装配,如果有的话该类就不会生效。
接下来看到下面的cacheManager方法,该方法先是直接创建了一个ConcurrentMapCacheManager的对象,点击进入ConcurrentMapCacheManager可以看到里面声明了一个叫做ConcurrentMap<String, Cache>的HashMap,这个map的key是一个字符串,value是对应的cache缓存对象,可以看出来该对象是用来存储缓存对象的:
往下拉找到getCache(String name)的方法:
@Override
@Nullable
public Cache getCache(String name) {
Cache cache = this.cacheMap.get(name);
if (cache == null && this.dynamic) {
synchronized (this.cacheMap) {
cache = this.cacheMap.get(name);
if (cache == null) {
cache = createConcurrentMapCache(name);
this.cacheMap.put(name, cache);
}
}
}
return cache;
}
它会通过传进来的name也就是我们在Service方法的注解中设置的cacheNames去cacheMap中获取对应的cache,如果获取到的cache为null,则会给cacheMap上锁后再去获取,如果还是为空就会调用createConcurrentMapCache方法,我们可以进入createConcurrentMapCache看看它又做了什么:
可以看到它会去帮去我们创建一个缓存对象,该方法返回了一个叫做ConcurrentMapCache对象,该对象将cacheNames给传入了进去,我们进入ConcurrentMapCache,可以看到其中有两个方法,一个叫做put,还有一个叫做lookup,我们为这两个方法打上断点来看看他们的运行时机:
可以看到我们传入了一个不存在的cacheNames,它会通过之前的getCache方法进入到createConcurrentMapCache方法,然后createConcurrentMapCache方法会去new一个ConcurrentMapCache方法,进入到ConcurrentMapCache后首先会调用lookup方法,lookup方法会以某种策略生成一个key,当不指定缓存的key时,SpringBoot会默认使用keyGenerator()方法生成key,然后会调用目标方法也就是getempByid(),方法得到返回值后ConcurrentMapCache会调用put()方法拿到目标方法返回值,再将目标方法返回值放入新创建的缓存中。
整合RedisTemplate
在我们引入了redis模块的依赖后,redis的starter场景后自动配置就会生效,这时候我们再来启动一次程序,观察控制台输出,可以看到RedisCacheConfiguration这个组件就被matched了:
而先前SpringBoot给我们默认的SimpleCacheConfiguration组件就不会生效,因为我们引入redis的场景就开启了RedisCacheConfiguration的自动配置,redis的缓存组件会被注册,而SimpleCacheConfiguration中标注了@ConditionalOnMissingBean(CacheManager.class),现在CacheManager已经有了redis的缓存组件了所以SimpleCacheConfiguration就不会生效。
然后我们来说一下为什么前面我们存入redis的数据会是那样的字节对象呢?原因是因为CacheManager中value的序列化方式默认使用的是JdkSerializationRedisSerializer,也就是jdk自带的序列化器,所以我们之前才会说需要被序列化的实体类必须要实现Serializable接口,那我们一般喜欢用json的方式存储数据,我们可以去先去看一下它源码中是怎么实现的,我们找到redis的缓存组件RedisCacheConfiguration进入源码:
** **
先说明一下,SpringBoot1.x和2.x版本的cacheManager略有不同,2.x版本之前RedisCacheManager会自带一个单参构造函数,而2.x后换成了builder构造,我们可以参照源码中的方法自定义一些规则:
@Bean
public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory){
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer 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);
// 配置序列化
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofHours(1)) //我们这里设置缓存失效时间为一个小时
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();
RedisCacheManager cacheManager = RedisCacheManager.builder(redisConnectionFactory)
.cacheDefaults(config)
.build();
return cacheManager;
}
我们这里给CacheManager设置的序列化规则用的是Jackson2JsonRedisSerializer,它实现了RedisSerializer接口,找到RedisSerializer接口ctrl+h可以查看有哪些类实现了它:
可以看到我们之前默认的JdkSerializationRedisSerializer,还有一些其他的类,我们点进我们现在所使用的Jackson2JsonRedisSerializer:
可以看到类中注释了这个该类可以用来读取和写入JSON,“ {@link RedisSerializer} that can read and write JSON using”,注意,定制了CacheManager 后方法上一定要记得加上@Bean注解,这样我们写的CacheManager 就会注册进Spring容器中取代原先的CacheManager,我们再启动一遍程序调用一遍方法:
可以看到现在数据就变成了json的形式;
StringRedisTemplate与RedisTemplate
两者的关系是StringRedisTemplate继承RedisTemplate。
两者的数据是不共通的;也就是说StringRedisTemplate只能管理StringRedisTemplate里面的数据,RedisTemplate只能管理RedisTemplate中的数据。
SDR默认采用的序列化策略有两种,一种是String的序列化策略,一种是JDK的序列化策略。
StringRedisTemplate默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。
RedisTemplate默认采用的是JDK的序列化策略,保存的key和value都是采用此策略序列化保存的。
StringRedisTemplate常用方法:
这里给大家一个链接,StringRedisTemplate常用方法都在里面我就不一一列出来了 https://blog.****.net/awhip9/article/details/71425041
RedisTemplate常用方法
RedisTemplate中定义了对5种数据结构操作
redisTemplate.opsForValue();//操作字符串
redisTemplate.opsForHash();//操作hash
redisTemplate.opsForList();//操作list
redisTemplate.opsForSet();//操作set
redisTemplate.opsForZSet();//操作有序set
这里我们来简单演示一个demo:
@Autowired
private RedisTemplate<Object, Object> redisTemplate;
@Autowired
private StringRedisTemplate stringRedisTemplate;
@Override
public void SRTemplateTest(){
stringRedisTemplate.opsForValue().set("hhh","hhh");
}
@Override
public department getdeptByid(Integer id) {
System.out.println("查询"+id+"号部门");
department department = mapper.selectdeptByid(id);
redisTemplate.opsForValue().set("dept",department);
return department;
}
如果想要存储String类型的数据推荐使用StringRedisTemplate ,因为StringRedisTemplate 默认采用的是String的序列化策略,保存的key和value都是采用此策略序列化保存的。而RedisTemplate默认采用的JDK的序列化策略,会出现一种情况,我们下面会介绍及该如何处理这种情况,StringRedisTemplate 主要作用是用来操作字符串,我们重点介绍RedisTemplate的使用及如何去定制它的序列化机制,现在调用getdeptByid()方法:
会发现我们的dept已经存入redis中了,但显示方式却是序列化后的字节对象,我们前面说过RedisTemplate默认使用的JDK的序列化策略,我们点击进入RedisTemplate源码:
这边可以看到RedisTemplate中RedisSerializer<?> defaultSerializer也就是redis的序列化机制如果是空的话,他会默认采用JdkSerializationRedisSerializer来进行序列化,我们找到redis的自动配置类RedisAutoConfiguration:
进入源码可以看到,里面有两个方法,这两个方法正是我们前面所使用的RedisTemplate和StringRedisTemplate ,我们可以参照源码的代码来自己重写一个新方法,起名就叫做deptRedisTemplate():
@Bean
public RedisTemplate<Object, department> deptRedisTemplate(
RedisConnectionFactory redisConnectionFactory) throws UnknownHostException {
RedisTemplate<Object, department> template = new RedisTemplate<Object, department>();
template.setConnectionFactory(redisConnectionFactory);
Jackson2JsonRedisSerializer<department> jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer<department>(department.class);
template.setDefaultSerializer(jackson2JsonRedisSerializer);
return template;
}
和前面一样我们想要把数据序列化成JSON形式,声明Jackson2JsonRedisSerializer对象:
接下来我们和源码中一样实例一个RedisTemplate对象,在它的源码中可以看到里面声明了一个RedisSerializer<?> defaultSerializer,这个正是前面做判断的条件,它是RedisTemplate
的序列化机制,如果我们没有主动赋值它就会默认使用JdkSerializationRedisSerializer,回到代码我们给RedisTemplate对象setDefaultSerializer()我们自己的序列化机制,把上面声明的jackson2JsonRedisSerializer入参,然后返回RedisTemplate对象,和之前CacheManager 方法一样我们一定要在方法上加上@Bean注解,这样我们定义的RedisTemplate就会替换容器中的bean。
最后在Service中声明我们自己的deptRedisTemplate()方法再次调用就解决了序列化的问题了。