SpringBoot緩存詳解並整合Redis

一.簡述

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中都有,具體可以查看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操作字符串是不會出現這種問題的,但是操作其他數據類型則會報錯

在這裏插入圖片描述

七.最後再聊一聊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序列化的結果

在這裏插入圖片描述

八.最後

才疏學淺,可能有些地方說的不準確,如果有些的不對的地方感謝各位老哥指正。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章