当前位置: 移动技术网 > IT编程>开发语言>Java > 荐 SpringBoot缓存详解并整合Redis

荐 SpringBoot缓存详解并整合Redis

2020年07月03日  | 移动技术网IT编程  | 我要评论

一.简述

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注解,表示开启基于注解的缓存

启动类,开启基于注解的缓存

  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. 几个核心注解的属性

在这里插入图片描述

  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
在这里插入图片描述

四.编写测试接口

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

测试@Cacheable注解

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

测试@CachePut注解

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

测试@CacheEvict注解

五.Spring Cache 总结

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

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

Spring Cache 自动配置类
②定义了多个缓存组件的配置类
在这里插入图片描述
③系统如何选择使用哪个配置类

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

配置类判断条件

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

SimpleCacheConfiguration
RedisCacheConfiguration

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

ConcurrentMapCacheManager

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

在这里插入图片描述

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

在这里插入图片描述

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

在这里插入图片描述

在这里插入图片描述

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

执行目标方法

将目标结果存入缓存中

五.整合Redis

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

②修改配置文件

在这里插入图片描述

六.Redis简单介绍

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

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

	@Resource
    private RedisTemplate redisTemplate;
	@Resource
    private StringRedisTemplate stringRedisTemplate;    
  1. 一些模板方法,redis提供的命令api中都有,具体可以查看
方法名称 作用
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操作字符串是不会出现这种问题的,但是操作其他数据类型则会报错

在这里插入图片描述

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

  1. 什么是序列化和反序列化
  • 序列化:将对象写到IO流中
  • 反序列化:从IO流中恢复对象
  • 序列化的意义:序列化机制允许将实现序列化的Java对象转换位字节序列,这些字节序列可以保存在磁盘上,或通过网络传输,以达到以后恢复成原来的对象。序列化机制使得对象可以脱离程序的运行而独立存在。
  1. 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");


    }

}

测试结果

在这里插入图片描述

  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序列化的结果

在这里插入图片描述
使用FastJson序列化的结果

在这里插入图片描述

八.最后

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

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

如对本文有疑问, 点击进行留言回复!!

相关文章:

验证码:
移动技术网