SpringBoot缓存详解并整合Redis架构
程序员文章站
2023-12-10 19:51:40
整合使用序列化配置...
一.简述
Spring从3.1开始定义了
org.springframework.cache.Cache 和
org.springframework.cache.CacheManager接口来统一不同的缓存技术
自然SpringBoot 也提供了支持
二.环境搭建
- 创建一个SpringBoot 项目,引入下面这些依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.1.3</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
- 在启动类上加上@EnableCaching注解,表示开启基于注解的缓存
- 编写配置文件
spring:
datasource:
url: jdbc:mysql://localhost:3306/spring_cache?useUnicode=true&characterEncoding=utf-8&serverTimezone=GMT%2B8
username: root
password: root
#mybatis:
# configuration:
# # 打印sql日志
# log-impl: org.apache.ibatis.logging.stdout.StdOutImpl
# 打印sql日志
logging:
level:
com.xx.mapper: debug
- 创建测试用的表
CREATE TABLE `tb_user` (
`id` int(10) NOT NULL AUTO_INCREMENT,
`username` varchar(10) DEFAULT NULL,
`password` varchar(10) DEFAULT NULL,
PRIMARY KEY (`id`)
) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8;
- 编写Mapper层接口代码,这里不做解释,直接拿去用就好
package com.xx.mapper;
import com.xx.entity.User;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import org.apache.ibatis.annotations.Update;
/**
* @author aqi
* DateTime: 2020/6/30 1:40 下午
* Description: No Description
*/
@Mapper
public interface UserMapper {
@Select("SELECT * FROM tb_user WHERE id = #{id}")
User getUserById(int id);
@Insert("INSERT INTO tb_user (username, password) VALUES (#{username}, #{password})")
void addUser(User user);
@Update("UPDATE tb_user SET username = #{username}, password = #{password} where id = #{id}")
void modUser(User user);
}
三.缓存相关的注解
- SpringBoot 提供的有关缓存的注解,这些注解既可以作用在方法上也可以作用在类上
注解名称 | 注解作用 | 说明 |
---|---|---|
@Cacheable | 添加缓存 | 将方法的返回值存到缓存中,方法执行之前先去查询是否存在缓存,若存在则不执行方法,反之执行方法 |
@CacheEvict | 清除缓存 | 根据指定的key去清除缓存,也可以清除所有的缓存 |
@CachePut | 更新缓存 | 每次执行都会执行方法,并且修改缓存中的数据 |
@CacheConfig | 缓存的全局配置,抽取公共的配置信息 | 将一些相同的配置信息写在类上 |
@Caching | 复杂缓存 | 可以配置多个缓存信息 |
- 几个核心注解的属性
- 核心属性详解
属性名称 | 属性作用 | 用法 |
---|---|---|
value | 缓存的名称,相当于命名空间,必须指定 | @Cacheable(value = “user”) @Cacheable(value = {“user”, “people”}) |
cacheNames | 和value一样,二选一 | |
key | 缓存的key,如果不指定则按照方法的所有参数进行组合,可以使用SpEL进行指定 | @Cacheable(value = “user”, key = “#id”) |
keyGenerator | 自定义缓存key生成器,和key二选一 | @Cacheable(value = “user”, keyGenerator = “myKeyGenerator”) |
cacheManager | 缓存管理器,默认采用的是SimpleCacheConfiguration | 只要引入相应地配置,SpringBoot就会自动的切换成对应的缓存管理器 |
cacheResolver | 缓存解析器,自定义缓存解析器 | |
condition | 缓存的条件,使用SpEL编写,只有条件为true时才进行缓存操作,在方法的调用之后之后都可以进行判断 | @Cacheable(value = “user”, key = “#id”, condition = “#id > 0 and #result != null”) |
unless | 与condition相反,条件为false时才会缓存,并且只在方法执行之后判断 | 用法和condition一样 |
sync | 异步 @Cacheable 特有的 | |
allEntries | 是否在方法执行之后清空缓存,默认为false @CacheEvict特有的 |
@CacheEvict(value = “user”, allEntries = true) |
beforeInvocation | 是否在方法执行之前清空缓存,默认为false @CacheEvict 特有 |
@CacheEvict(value = “user”, beforeInvocation = true) |
自定义缓存key生成器
package com.xx.config;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import java.lang.reflect.Method;
import java.util.Arrays;
/**
* @author aqi
* DateTime: 2020/6/29 5:00 下午
* Description: 自定义缓存key生成器
*/
@Configuration
public class MyCacheConfig {
@Bean("myKeyGenerator")
public KeyGenerator keyGenerator() {
return new KeyGenerator(){
@Override
public Object generate(Object o, Method method, Object... objects) {
// 自定义缓存key的样式
return method.getName() + "[" + Arrays.asList(objects).toString() + "]";
}
};
}
}
SpringBoot默认提供的缓存管理器,如果要使用Redis只需要引入Redis的POM和配置文件,就会默认切换到RedisCacheManager
四.编写测试接口
- 测试@Cacheable注解
/**
* 这里如果不写key,则使用id作为key,也就是id = result
*/
@Cacheable(value = "user", key = "#id", condition = "#id == 1")
@GetMapping("/getUser/{id}")
public User getUser(@PathVariable Integer id) {
System.out.println("请求的id:" + id);
return userMapper.getUserById(id);
}
- 当请求的id为1时,第一次请求去访问数据库,后续则不再访问数据库
- 当请求的id为2时,每一次请求都会去访问数据库
- 测试@CachePut注解
/**
* 使用返回结果对象的id值作为key值
*/
@CachePut(value = "user", key = "#result.id")
@GetMapping("/modUser")
public User modUser(User user) {
userMapper.modUser(user);
return user;
}
- 每次执行更新操作都会访问一次数据库,因为@CachePut每次执行都会访问数据库,并且修改缓存,执行更新操作之后调用查询接口不再访问数据库
- 测试@CacheEvict注解
@CacheEvict(value = "user", key = "#id")
@GetMapping("/delUser/{id}")
public void delUser(@PathVariable int id) {
System.out.println("删除用户缓存");
}
- 执行查询操作后,执行删除操作,由于缓存中的数据被清除,所以再次执行查询操作将会访问数据库
五.Spring Cache 总结
- Spring Boot 缓存的结构图
名称 | 说明 |
---|---|
CacheingProvider | 缓存提供者:用于控制、管理、创建、配置、获取多个CacheManager |
CacheManager | 缓存管理者:用于控制、管理、创建、配置、获取多个唯一命名的Cache |
Cache | 类似于一个命名空间,用于区分不同的缓存 |
Entry | 存储在Cache中的数据,以key-value的形式存储 |
Expiry | 缓存有效期 |
- 缓存部分源码流程(这里学习了尚硅谷的SpringBoot Cache教程,这里附上链接)
①Spring Cache 的自动配置类是:CacheAutoConfiguration
②定义了多个缓存组件的配置类
③系统如何选择使用哪个配置类
- 通过类头做的判断来决定使用哪个配置类
- 在配置文件中加上debug:
true这个配置,在控制台查看SpringBoot自动配置了哪些服务,可以看一下默认情况下,SpringBoot
到底使用了哪个缓存配置类,可以发现 SimpleCacheConfiguration匹配上了
- SimpleCacheConfiguration配置往容器中注入了一个ConcurrentMapCacheManager缓存管理器
- ConcurrentMapCacheManager实现了CacheManager接口,通过名称获取到一个缓存组件,如果没有获取到就自己创建一个ConcurrentMapCache缓存组件,并将数据存储在ConcurrentMap中
- 最后再完整的梳理一下缓存的执行流程
- 第一步:在方法执行之前,先进入到ConcurrentMapCacheManager中的getCache这个方法,获取到名称为user的Cache缓存组件,第一次进来的时候没有名称叫user的Cache缓存组件,这时候会走到createConcurrentMapCache这里去创建一个名叫user的Cache缓存组件
- 第二步:去刚才创建的叫user的Cache缓存组件中,查找内容,查找的key值就是在@Cahceable中设置的key值,这里是1,由于是第一次进来所以自然是查不到数据的
- 第三步:没有查到缓存结果,就会执行目标方法,并将结果放进缓存中
五.整合Redis
- Cache缓存接口,提供了8种缓存实现,只需要配置对应的缓存组件,Spring在自动装配的时候的时候就会自动匹配,并注入容器
- 配置redis
①引入redis依赖
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
②修改配置文件
六.Redis简单介绍
- Redis是一个高性能的key-value数据库,可以存储一下这些数据类型
String(字符串),List(列表),Set(集合),Hash(散列),ZSet(有序集合)
SpringBoot Rerdis 提供了两种模板去操作redis
@Resource
private RedisTemplate redisTemplate;
@Resource
private StringRedisTemplate stringRedisTemplate;
- 一些模板方法,redis提供的命令api中都有,具体可以查看Redis官网
方法名称 | 作用 |
---|---|
opsForValue | 用于操作字符串 |
opsForList | 用于操作列表 |
opsForSet | 用于操作集合 |
opsForHash | 用于操作散列 |
ZSet | 用于操作有序集合 |
- 安装redis desktop manager,Redis可视化工具
- 编写测试类操作redis
@Test
void addMsg() {
redisTemplate.opsForValue().set("msg", "Hello");
}
@Test
void appendMsg() {
redisTemplate.opsForValue().append("msg", "Java");
}
- 这里可以看到存进去的数据是一些奇怪的字符和乱码,这是由于我使用的是redisTemplate需要进行序列化配置,如果仅仅使用StringRedisTemplate操作字符串是不会出现这种问题的,但是操作其他数据类型则会报错
七.最后再聊一聊Redis序列化
- 什么是序列化和反序列化
- 序列化:将对象写到IO流中
- 反序列化:从IO流中恢复对象
- 序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
- Redis提供了多种序列化的手段,当然也可以使用一些外部的序列化工具
- 只需要配置一下,就可以解决刚才出现的问题,但是这么多序列化的手段如何挑选呢,我比较好奇,所以我又稍微深挖了一下
package com.xx.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* @author aqi
* DateTime: 2020/6/30 10:56 上午
* Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* 序列化的几种方式:
* OxmSerializer
* ByteArrayRedisSerializer
* GenericJackson2JsonRedisSerializer
* GenericToStringSerializer
* StringRedisSerializer
* JdkSerializationRedisSerializer
* Jackson2JsonRedisSerializer
* @param redisConnectionFactory redis连接工厂
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
// 设置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 设置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<Object>(Object.class));
return template;
}
}
名称 | 说明 |
---|---|
ByteArrayRedisSerializer | 数组序列化 |
GenericJackson2JsonRedisSerializer | 使用Jackson进行序列化 |
GenericToStringSerializer | 将对象泛化成字符串并序列化,和StringRedisSerializer差不多 |
Jackson2JsonRedisSerializer | 使用Jackson序列化对象为json |
JdkSerializationRedisSerializer | jdk自带的序列化方式,需要实现Serializable接口 |
OxmSerializer | 用xml格式存储 |
StringRedisSerializer | 简单的字符串序列化 |
- 比较几种常见序列化手段的差异
测试代码
package com.xx;
import com.alibaba.fastjson.support.spring.FastJsonRedisSerializer;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import com.xx.entity.User;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.serializer.*;
import java.util.ArrayList;
import java.util.List;
@SpringBootTest
class CacheApplicationTests {
/**
* 测试几种序列化手段的效率
*/
@Test
void test() {
User user = new User();
user.setId(1);
user.setUsername("张三");
user.setPassword("123");
List<Object> list = new ArrayList<>();
for (int i = 0; i < 2000; i++) {
list.add(user);
}
// 使用GenericJackson2JsonRedisSerializer做序列化(效率太低,不推荐使用)
GenericJackson2JsonRedisSerializer g2 = new GenericJackson2JsonRedisSerializer();
long g2s = System.currentTimeMillis();
byte[] byteG2 = g2.serialize(list);
long g2l = System.currentTimeMillis();
System.out.println("GenericJackson2JsonRedisSerializer序列化消耗的时间:" + (g2l - g2s) + "ms,序列化之后的长度:" + byteG2.length);
g2.deserialize(byteG2);
System.out.println("GenericJackson2JsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - g2l) + "ms");
// 使用GenericToStringSerializer做序列化(和StringRedisSerializer差不多,效率没有StringRedisSerializer高,不推荐使用)
GenericToStringSerializer g = new GenericToStringSerializer(Object.class);
long gs = System.currentTimeMillis();
byte[] byteG = g.serialize(list.toString());
long gl = System.currentTimeMillis();
System.out.println("GenericToStringSerializer序列化消耗的时间:" + (gl - gs) + "ms,序列化之后的长度:" + byteG.length);
g.deserialize(byteG);
System.out.println("GenericToStringSerializer反序列化的时间:" + (System.currentTimeMillis() - gl) + "ms");
// 使用Jackson2JsonRedisSerializer做序列化(效率高,适合value值的序列化)
Jackson2JsonRedisSerializer j2 = new Jackson2JsonRedisSerializer(Object.class);
long j2s = System.currentTimeMillis();
byte[] byteJ2 = j2.serialize(list);
long j2l = System.currentTimeMillis();
System.out.println("Jackson2JsonRedisSerializer序列化消耗的时间:" + (j2l - j2s) + "ms,序列化之后的长度:" + byteJ2.length);
j2.deserialize(byteJ2);
System.out.println("Jackson2JsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - j2l) + "ms");
// 使用JdkSerializationRedisSerializer,实体类必须实现序列化接口(不推荐使用)
JdkSerializationRedisSerializer j = new JdkSerializationRedisSerializer();
long js = System.currentTimeMillis();
byte[] byteJ = j.serialize(list);
long jl = System.currentTimeMillis();
System.out.println("JdkSerializationRedisSerializer序列化消耗的时间:" + (jl - js) + "ms,序列化之后的长度:" + byteJ.length);
j.deserialize(byteJ);
System.out.println("JdkSerializationRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - jl) + "ms");
// 使用StringRedisSerializer做序列化(效率非常的高,但是比较占空间,只能对字符串序列化,适合key值的序列化)
StringRedisSerializer s = new StringRedisSerializer();
long ss = System.currentTimeMillis();
byte[] byteS = s.serialize(list.toString());
long sl = System.currentTimeMillis();
System.out.println("StringRedisSerializer序列化消耗的时间:" + (sl - ss) + "ms,序列化之后的长度:" + byteS.length);
s.deserialize(byteS);
System.out.println("StringRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - sl) + "ms");
// 使用FastJson做序列化,这个表现为什么这么差我也不是很明白
FastJsonRedisSerializer<Object> f = new FastJsonRedisSerializer<>(Object.class);
long fs = System.currentTimeMillis();
byte[] byteF = f.serialize(list);
long fl = System.currentTimeMillis();
System.out.println("FastJsonRedisSerializer序列化消耗的时间:" + (fl - fs) + "ms,序列化之后的长度:" + byteF.length);
f.deserialize(byteF);
System.out.println("FastJsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - fl) + "ms");
// 使用FastJson(效率高,序列化后占空间也很小,推荐使用)
GenericFastJsonRedisSerializer gf = new GenericFastJsonRedisSerializer();
long gfs = System.currentTimeMillis();
byte[] byteGf = gf.serialize(list);
long gfl = System.currentTimeMillis();
System.out.println("GenericFastJsonRedisSerializer序列化消耗的时间:" + (gfl - gfs) + "ms,序列化之后的长度:" + byteGf.length);
gf.deserialize(byteGf);
System.out.println("GenericFastJsonRedisSerializer反序列化的时间:" + (System.currentTimeMillis() - gfl) + "ms");
}
}
测试结果
- 总结
名称 | 序列化效率 | 反序列化效率 | 占用空间 | 是否推荐使用 |
---|---|---|---|---|
StringRedisSerializer | 很高 | 很高 | 高 | 推荐给kye进行序列化 |
Jackson2JsonRedisSerializer | 高 | 较高 | 偏高 | 推荐给value进行序列化 |
GenericFastJsonRedisSerializer | 高 | 较低 | 较低 | 推荐给value进行序列化 |
- 附上Redis序列化配置文件
package com.xx.config;
import com.alibaba.fastjson.support.spring.GenericFastJsonRedisSerializer;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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;
/**
* @author aqi
* DateTime: 2020/6/30 10:56 上午
* Description: Redis配置
*/
@Configuration
public class MyRedisConfig {
/**
* redisTemplate配置
* @param redisConnectionFactory redis连接工厂
* @return RedisTemplate
*/
@Bean
public RedisTemplate<Object, Object> redisTemplate(RedisConnectionFactory redisConnectionFactory) {
RedisTemplate<Object, Object> template = new RedisTemplate<>();
// 配置连接工厂
template.setConnectionFactory(redisConnectionFactory);
// 配置key的序列化方式
template.setKeySerializer(new StringRedisSerializer());
// 使用Jackson2JsonRedisSerializer配置value的序列化方式
template.setValueSerializer(new Jackson2JsonRedisSerializer<>(Object.class));
// 使用FastJson配置value的序列化方式
// template.setValueSerializer(new GenericFastJsonRedisSerializer());
return template;
}
}
使用Jackson2JsonRedisSerializer序列化的结果
使用FastJson序列化的结果
八.最后
才疏学浅,可能有些地方说的不准确,如果有些的不对的地方感谢各位老哥指正。
本文地址:https://blog.csdn.net/progammer10086/article/details/107040457