欢迎您访问程序员文章站本站旨在为大家提供分享程序员计算机编程知识!
您现在的位置是: 首页  >  IT编程

SpringBoot缓存详解并整合Redis架构

程序员文章站 2023-12-10 19:51:40
整合使用序列化配置...

一.简述

Spring从3.1开始定义了
org.springframework.cache.Cache 和
org.springframework.cache.CacheManager接口来统一不同的缓存技术
自然SpringBoot 也提供了支持

二.环境搭建

  1. 创建一个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>
  1. 在启动类上加上@EnableCaching注解,表示开启基于注解的缓存

SpringBoot缓存详解并整合Redis架构

  1. 编写配置文件
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
  1. 创建测试用的表
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;
  1. 编写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);
}

三.缓存相关的注解

  1. SpringBoot 提供的有关缓存的注解,这些注解既可以作用在方法上也可以作用在类上
注解名称 注解作用 说明
@Cacheable 添加缓存 将方法的返回值存到缓存中,方法执行之前先去查询是否存在缓存,若存在则不执行方法,反之执行方法
@CacheEvict 清除缓存 根据指定的key去清除缓存,也可以清除所有的缓存
@CachePut 更新缓存 每次执行都会执行方法,并且修改缓存中的数据
@CacheConfig 缓存的全局配置,抽取公共的配置信息 将一些相同的配置信息写在类上
@Caching 复杂缓存 可以配置多个缓存信息
  1. 几个核心注解的属性

SpringBoot缓存详解并整合Redis架构

  1. 核心属性详解
属性名称 属性作用 用法
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
SpringBoot缓存详解并整合Redis架构

四.编写测试接口

  1. 测试@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时,每一次请求都会去访问数据库

SpringBoot缓存详解并整合Redis架构

  1. 测试@CachePut注解
	/**
     * 使用返回结果对象的id值作为key值
     */
    @CachePut(value = "user", key = "#result.id")
    @GetMapping("/modUser")
    public User modUser(User user) {
        userMapper.modUser(user);
        return user;
    }
  • 每次执行更新操作都会访问一次数据库,因为@CachePut每次执行都会访问数据库,并且修改缓存,执行更新操作之后调用查询接口不再访问数据库

SpringBoot缓存详解并整合Redis架构

  1. 测试@CacheEvict注解
	@CacheEvict(value = "user", key = "#id")
    @GetMapping("/delUser/{id}")
    public void delUser(@PathVariable int id) {
        System.out.println("删除用户缓存");
    }
  • 执行查询操作后,执行删除操作,由于缓存中的数据被清除,所以再次执行查询操作将会访问数据库

SpringBoot缓存详解并整合Redis架构

五.Spring Cache 总结

  1. Spring Boot 缓存的结构图
    SpringBoot缓存详解并整合Redis架构
名称 说明
CacheingProvider 缓存提供者:用于控制、管理、创建、配置、获取多个CacheManager
CacheManager 缓存管理者:用于控制、管理、创建、配置、获取多个唯一命名的Cache
Cache 类似于一个命名空间,用于区分不同的缓存
Entry 存储在Cache中的数据,以key-value的形式存储
Expiry 缓存有效期
  1. 缓存部分源码流程(这里学习了尚硅谷的SpringBoot Cache教程,这里附上链接

①Spring Cache 的自动配置类是:CacheAutoConfiguration

SpringBoot缓存详解并整合Redis架构
②定义了多个缓存组件的配置类
SpringBoot缓存详解并整合Redis架构
③系统如何选择使用哪个配置类

  1. 通过类头做的判断来决定使用哪个配置类

SpringBoot缓存详解并整合Redis架构

  1. 在配置文件中加上debug:
    true
    这个配置,在控制台查看SpringBoot自动配置了哪些服务,可以看一下默认情况下,SpringBoot
    到底使用了哪个缓存配置类,可以发现 SimpleCacheConfiguration匹配上了

SpringBoot缓存详解并整合Redis架构
SpringBoot缓存详解并整合Redis架构

  1. SimpleCacheConfiguration配置往容器中注入了一个ConcurrentMapCacheManager缓存管理器

SpringBoot缓存详解并整合Redis架构

  1. ConcurrentMapCacheManager实现了CacheManager接口,通过名称获取到一个缓存组件,如果没有获取到就自己创建一个ConcurrentMapCache缓存组件,并将数据存储在ConcurrentMap中

SpringBoot缓存详解并整合Redis架构

  1. 最后再完整的梳理一下缓存的执行流程
  • 第一步:在方法执行之前,先进入到ConcurrentMapCacheManager中的getCache这个方法,获取到名称为user的Cache缓存组件,第一次进来的时候没有名称叫user的Cache缓存组件,这时候会走到createConcurrentMapCache这里去创建一个名叫user的Cache缓存组件

SpringBoot缓存详解并整合Redis架构

  • 第二步:去刚才创建的叫user的Cache缓存组件中,查找内容,查找的key值就是在@Cahceable中设置的key值,这里是1,由于是第一次进来所以自然是查不到数据的

SpringBoot缓存详解并整合Redis架构

SpringBoot缓存详解并整合Redis架构

  • 第三步:没有查到缓存结果,就会执行目标方法,并将结果放进缓存中

SpringBoot缓存详解并整合Redis架构

SpringBoot缓存详解并整合Redis架构

五.整合Redis

  1. Cache缓存接口,提供了8种缓存实现,只需要配置对应的缓存组件,Spring在自动装配的时候的时候就会自动匹配,并注入容器
    SpringBoot缓存详解并整合Redis架构
  2. 配置redis
    ①引入redis依赖
		<dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>

②修改配置文件

SpringBoot缓存详解并整合Redis架构

六.Redis简单介绍

  1. Redis是一个高性能的key-value数据库,可以存储一下这些数据类型
    String(字符串),List(列表),Set(集合),Hash(散列),ZSet(有序集合)

SpringBoot Rerdis 提供了两种模板去操作redis

	@Resource
    private RedisTemplate redisTemplate;
	@Resource
    private StringRedisTemplate stringRedisTemplate;    
  1. 一些模板方法,redis提供的命令api中都有,具体可以查看Redis官网
方法名称 作用
opsForValue 用于操作字符串
opsForList 用于操作列表
opsForSet 用于操作集合
opsForHash 用于操作散列
ZSet 用于操作有序集合
  1. 安装redis desktop manager,Redis可视化工具
  2. 编写测试类操作redis
	@Test
    void addMsg() {
        redisTemplate.opsForValue().set("msg", "Hello");
    }

    @Test
    void appendMsg() {
        redisTemplate.opsForValue().append("msg", "Java");
    }
  1. 这里可以看到存进去的数据是一些奇怪的字符和乱码,这是由于我使用的是redisTemplate需要进行序列化配置,如果仅仅使用StringRedisTemplate操作字符串是不会出现这种问题的,但是操作其他数据类型则会报错

SpringBoot缓存详解并整合Redis架构

七.最后再聊一聊Redis序列化

  1. 什么是序列化和反序列化
  • 序列化:将对象写到IO流中
  • 反序列化:从IO流中恢复对象
  • 序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
  1. Redis提供了多种序列化的手段,当然也可以使用一些外部的序列化工具

SpringBoot缓存详解并整合Redis架构

  1. 只需要配置一下,就可以解决刚才出现的问题,但是这么多序列化的手段如何挑选呢,我比较好奇,所以我又稍微深挖了一下
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 简单的字符串序列化
  1. 比较几种常见序列化手段的差异

测试代码

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");


    }

}

测试结果

SpringBoot缓存详解并整合Redis架构

  1. 总结
名称 序列化效率 反序列化效率 占用空间 是否推荐使用
StringRedisSerializer 很高 很高 推荐给kye进行序列化
Jackson2JsonRedisSerializer 较高 偏高 推荐给value进行序列化
GenericFastJsonRedisSerializer 较低 较低 推荐给value进行序列化
  1. 附上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序列化的结果

SpringBoot缓存详解并整合Redis架构
使用FastJson序列化的结果

SpringBoot缓存详解并整合Redis架构

八.最后

才疏学浅,可能有些地方说的不准确,如果有些的不对的地方感谢各位老哥指正。

本文地址:https://blog.csdn.net/progammer10086/article/details/107040457