日本免费高清视频-国产福利视频导航-黄色在线播放国产-天天操天天操天天操天天操|www.shdianci.com

學無先后,達者為師

網站首頁 編程語言 正文

SpringBoot整合SpringCache詳解

作者:IT-老牛 更新時間: 2022-07-21 編程語言

文章目錄

    • 前言
    • 1.為什么要使用緩存
    • 2.為什么要使用SpringCache
    • 3.SpringBoot整合SpringCache
      • 3.1.說明
      • 3.2.原理梳理
        • 3.2.1.比較重要的源碼類
        • 3.2.2.原理說明
      • 3.3.默認緩存的數據類型
      • 3.3.整合
      • 3.4.使用
        • 3.4.1 @Cacheable(開啟緩存功能)
        • 3.4.2 @CacheEvict(失效模式)
        • 3.4.3 @Caching(組合使用)
    • 4.簡單實戰案例
    • 5.Spring-Cache的不足
    • 6.Spring-Cache小結

在這里插入圖片描述

前言

明明我們項目中使用最多的緩存技術就是Redis,用Redis就完全就可以搞定緩存的問題了,為什么還有一個SpringCache,以及SpringCacheRedis之間的區別。

1.為什么要使用緩存

  • 緩存是將數據直接存入內容中,讀取效率比數據庫的更高
  • 緩存可以有效地降低數據庫壓力,為數據庫減輕負擔

2.為什么要使用SpringCache

先看一下我們使用緩存步驟:

  1. 查尋緩存中是否存在數據,如果存在則直接返回結果
  2. 如果不存在則查詢數據庫,查詢出結果后將結果存入緩存并返回結果
  3. 數據更新時,先更新數據庫
  4. 然后更新緩存,或者直接刪除緩存

此時我們會發現一個問題,所有我們需要使用緩存的地方都必須按照這個步驟去書寫,這樣就會出現很多邏輯上相似的代碼。并且我們程序里面也需要顯示的去調用第三方的緩存中間件的API,如此一來就大大的增加了我們項目和第三方中間件的耦合度。就以Redis為列,如下圖所示:
在這里插入圖片描述
圖中代碼所示,就是我們上面描述的使用Redis作為緩存中間件來進行緩存的實列,我們不難發現,我們的查詢和存儲時都是使用到了SpringBoot整合Redis后的相關API的,并且項目中所有的使用緩存的地方都會如此使用,這樣子提升了代碼的復雜度,我們程序員更應該關注的是業務代碼,因此我們需要將查詢緩存和存入緩存這類似的代碼封裝起來用框架來替我們實現,讓我們更好的去處理業務邏輯。
那么我們如何讓框架去幫我們自動處理呢,這不就是典型的AOP思想嗎?

是的,Spring Cache就是一個這樣的框架。它利用了AOP,實現了基于注解的緩存功能,并且進行了合理的抽象,業務代碼不用關心底層是使用了什么緩存框架,只需要簡單地加一個注解,就能實現緩存功能了。而且Spring Cache也提供了很多默認的配置,用戶可以3秒鐘就使用上一個很不錯的緩存功能。

使用了Spring Cache框架后使用緩存實列,如下圖所示:
在這里插入圖片描述我們只需要將我們的方法添加一個注解就可以將方法返回結果直接存入緩存,并不需要手動去進行設置,是不是大大的簡化了代碼。

3.SpringBoot整合SpringCache

3.1.說明

spring cache官方文檔

spEl語法說明==>官方文檔

官網springcache介紹目錄,官網的注解一共有5個,如圖:
在這里插入圖片描述

注解 說明
@Cacheable 觸發將數據保存到緩存的操作(啟動緩存)
@CacheEvict 觸發將數據從緩存刪除的操縱(失效模式)
@CachePut 不影響方法執行更新緩存(雙寫模式)
@Caching 組合以上多個操作(點擊注解看源碼就知道了,組合注解))
@CacheConfig 在類級別共享緩存的相同配置

3.2.原理梳理

3.2.1.比較重要的源碼類

  1. CacheAutoConfiguration 緩存的自動配置
  2. 用的類型是redis所以看 RedisCacheConfiguration
  3. CacheManager 緩存管理者
  4. 類型是redis所以看 RedisCacheManager
  5. CacheProperties 緩存默認配置
  6. idea搜索的方法 雙擊shift或者 ctrl n

3.2.2.原理說明

流程說明:
 CacheAutoConfiguration  =>  RedisCacheConfiguration =>
 自動配置了RedisCacheManager =>  初始化所有的緩存 => 
 每個緩存決定使用什么配置=>
 =>如果RredisCacheConfiguration有就用已有的,沒有就用默認配置(CacheProperties)
 =>想改緩存的配置,只要給容器中放一個RredisCacheConfiguration即可
 =>就會應用到當前RedisCacheManager管理的所有緩存分區中

3.3.默認緩存的數據類型

在默認配置下,springcache給我們緩存的試用jdk序列化過的數據

我們通常是緩存Json字符串,因為使用Json能跨語言,跨平臺進行交互, 所以我們也可以修改他的默認配置,包括ttl(過期時間)、存儲格式、等…

3.3.整合

引入依賴

<!--spring-boot-starter-data-redis-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--spring cache-->
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

先看下配置源碼是怎么樣的 RedisCacheConfiguration
在這里插入圖片描述
創建配置類(照貓畫虎)
注意事項:要讓原本配置文件的一些配置生效

開啟屬性綁定配置的功能
@EnableConfigurationProperties(CacheProperties.class)
package com.bruce.config;

import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;


@EnableConfigurationProperties(CacheProperties.class)//開啟屬性綁定配置的功能
@Configuration //配置類
@EnableCaching //開啟緩存功能
public class MyCacheConfig {

//    第一種、從容器里面拿
//    @Autowired
//    CacheProperties cacheProperties;

    /**
     * 配置文件中的很多東西沒用上
     *      1、原來和配置文件綁定的配置類是這個樣子的
     *          @ConfigurationProperties(
     *              prefix = "spring.cache"
     *          )
     *          public class CacheProperties
     *
     *      2、要讓他生效
     *          @EnableConfigurationProperties(CacheProperties.class)//開啟屬性綁定配置的功能
     *
     */

    //第二種、因為注入了容器,參數屬性spring會自己去容器里面找 (CacheProperties cacheProperties)
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
//        config=config.entryTtl();
        config= config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        /**
         * GenericFastJsonRedisSerializer   fastjson家族的
         * GenericJackson2JsonRedisSerializer   spring自帶的 package org.springframework.data.redis.serializer;
         */
        //指定序列化-Jackson
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        //指定序列化-fastjson
        //config= config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));


        //從所以配置中取出redis的配置
        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        //將配置文件中所有的配置都生效(之間從源碼里面拷 RedisCacheConfiguration)
        if (redisProperties.getTimeToLive() != null) {
            config = config.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            config = config.prefixKeysWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            config = config.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            config = config.disableKeyPrefix();
        }
        return config;

    }
}



配置文件(application.properties)

#類型指定redis
spring.cache.type=redis
#一個小時,以毫秒為單位
spring.cache.redis.time-to-live=3600000
#給緩存的建都起一個前綴。  如果指定了前綴就用我們指定的,如果沒有就默認使用緩存的名字作為前綴,一般不指定
#spring.cache.redis.key-prefix=CACHE_
#指定是否使用前綴
spring.cache.redis.use-key-prefix=true
#是否緩存空值,防止緩存穿透
spring.cache.redis.cache-null-values=true

3.4.使用

在方法上標注注解就可以

3.4.1 @Cacheable(開啟緩存功能)

將查詢到的結果存入緩存

注意事項

1、 有對應的緩存就不進入方法 [需要返回值,沒有返回值緩存空值]
2、 @Cacheable并沒有單獨的失效時間的方法。
3、 但是可以在CacheManager配置,在加上自動刷新的功能,但是這樣的的操作比較繁瑣。如果不設置,只有統一的過期時間很容易導致緩存雪崩的問題

01、有返回緩存

   /**
     * TODO @Cacheable并沒有單獨的失效時間的方法。
     *      但是可以在CacheManager配置,在+上自動刷新的功能,但是這樣的的操作比較繁瑣。
     *      如果不設置,只有統一的過期時間很容易導致緩存雪崩的問題。
     * @Cacheable開啟緩存功能 有對應的緩存就不進入方法 [需要返回值,沒有返回值緩存空值]
     * value = "student", 【key ="#root.methodName" 或 key = "'名稱'" 或 key = "#傳入的參數" 或  key = "#接收參數的實體.屬性"
     * 更多方式看spEl語法 】
     * <p>
     * student是分區名字
     * #root.methodName是spEl語法 也就是方法名 testCache
     * <p>
     * 在redis里面 他的存儲就是 student::testCache
     * 同一個業務類型是數據放在同一個分區,樹形結構,
     * 類如:a包里面有b,c。  b和c就是具體緩存。a就是名稱空間
     * @Cacheable(value = {"student"},key ="#root.method.name" ,sync = true)
     * sync = true	這個屬性的意思是加鎖,解決緩存擊穿問題
     */
    //localhost:8080/testCache
    @Cacheable(value = "student", key = "#root.method.name")
    @GetMapping("/saveCache01")
    public HashMap<String, List<Users>> saveCache01() {
        System.out.println("方法saveCache01執行");
        HashMap<String, List<Users>> map = new HashMap<String, List<Users>>();
        List<Users> studentList = new ArrayList<Users>();
        studentList.add(new Users(1,"ssm", 11));
        studentList.add(new Users(2,"boot", 22));
        studentList.add(new Users(3,"cloud", 33));
        map.put("studentList", studentList);
        System.out.println("緩存成功");
        return map;
    }

02、無返回值,或者返回空,緩存空值

 /**
     * 1、返回值為void 緩存空值
     * 2、返回null 緩存空值
     * TODO 【NullValue】
     *  sr+org.springframework.cache . support.NullValue xp
     */
    @Cacheable(value = "student", key = "#root.method.name")
    @GetMapping("/saveCache02")
    public void saveCache02() {
        System.out.println("方法saveCache02執行");
        HashMap<String, List<Users>> map = new HashMap<String, List<Users>>();
        List<Users> studentList = new ArrayList<Users>();
        studentList.add(new Users(1,"ssm", 11));
        studentList.add(new Users(2,"boot", 22));
        studentList.add(new Users(3,"cloud", 33));
        map.put("studentList", studentList);
        System.out.println("緩存成功");
    }

3.4.2 @CacheEvict(失效模式)

簡單的說:就是你執行了修改/刪除的操作,他會將緩存里面數據給清除

第一種、刪除單個

	 /**
     * 失效模式(可以叫刪除模式)
     * value = "student",key = "'saveCache01'" 注意單引號
     * student是分區名字
     * saveCache01是緩存的key值。使用@Cacheable緩存的時候spEl我們指定的方法名
     * todo @CacheEvict(value = "student",allEntries = true)  allEntries = true表示刪除student分區下所有數據
     */
    @CacheEvict(value = "student", key = "'saveCache01'")//緩存 失效模式
    @GetMapping("/updateData")
    public void updateData() {
        System.out.println("執行失效模式,刪除緩存");
    }

第二種、刪除多個,將整個分區的緩存都清除

好比說 a下面有b和c 。將b和c一起刪除

所以:同一業務\同一類型緩存的數據要放在同一的分區下面

 //1、失效模式
 //2、allEntries = true 刪除分區所有的數據
 @CacheEvict(value = "student",allEntries = true)
 @GetMapping("/updateCascade")
 public void updateCascade(Users users) {
     //service的業務代碼
     System.out.println("更新分區所有的數據");
 }

3.4.3 @Caching(組合使用)

在這里插入圖片描述
比如說要讓哪個分區下面的哪個緩存失效(刪除)

   /**
     * TODO @Caching 組合注解 允許在同一方法上使用多個嵌套的 @Cacheable、@CachePut和@CacheEvict
     * value ==> student分區
     * key   ==> saveCache01 緩存的key名稱
     * 個人感覺還是使用@CacheEvict的刪除分區里面全部的緩存方便點
     */
    @Caching(evict = {
            @CacheEvict(value = "student", key = "'saveCache01'"),
            @CacheEvict(value = "student", key = "'saveCache02'")
    })
    @GetMapping("/selectEvict")
    public void selectEvict() {
        System.out.println("組合注解=>指定分區下失效的key");
    }

4.簡單實戰案例

實體類:

public class User {

    private String id;//主鍵
    private String userName;//用戶名
    private String password;//密碼

	//get /set /構造、tostring...省略
}
package com.bruce.controller;

import com.bruce.pojo.User;
import org.springframework.cache.annotation.*;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import java.io.IOException;

/**
 * @author suqinyi
 * @Date 2022/1/21
 * springCache的實戰使用【sqy測】--- TODO 緩存數據不要去數據庫直接改數據!!!
 */
@RestController
@CacheConfig(cacheNames = "user")//指定緩存空間的名稱
public class UserCacheController {

    /**
     * todo 說明
     *
     * @Cacheable 必須要有返回【實體、list、map】-- 用于 查詢
     * @CachePut 必須要有返回【實體、list、map】-- 用于 新增、修改
     * @CacheEvict 返回值為void--用于 刪除
     * @CacheConfig 配置      --通常用于指定緩存空間名稱較多
     * @Cacheable 組合注解  [ cacheable() 、put()、evict() ] 存、加、刪
     */
//    private static List<User> list = new ArrayList<>();

    //模擬從數據庫獲取到數據
    private User getUserData01() {
        User user = new User("001", "userOO1", "123456");
        return user;
    }
    //模擬從數據庫獲取到數據
    private User getUserData02() {
        User user = new User("002", "userOO2", "789456");
        return user;
    }




    /**
     * 主鍵查詢--這個緩存是在service做,測試案例我就之間在controller寫了
     * 名稱空間value 在controller統一指定了
     * 緩存key為 名稱空間::id
     *
     * @Cacheable(key = "#qvo.id",unless = "#result!=null" )
     * unless = "#result!=null" 返回的結果不為空才緩存
     * 這個方法不緩存空值
     * localhost:8080/findById
     * post  json  {"id":"1"}
     */
    @PostMapping("/findById")
    @Cacheable(key = "#qvo.id")
    public User findById(@RequestBody User qvo) {
        System.out.println("執行方法-findById");
        //查到數據
        if ("001".equals(qvo.getId())) {
            User user = getUserData01();
            return user;
        } else {
            return null;
        }
    }


    /**
     * 用戶名查詢
     * 名稱空間value 在controller統一指定了
     * 緩存key為 名稱空間::id
     * 這個查詢緩存空值  sr+org.springframework.cache . support.NullValue xp
     * localhost:8080/findByName
     * post json {"userName":"userOO1"}
     */
    @PostMapping("/findByName")
    @Cacheable(key = "#qvo.userName")
    public User findByName(@RequestBody User qvo) {
        System.out.println("執行方法-findByName");
        //查到數據
        if ("userOO1".equals(qvo.getUserName())) {
            User user = getUserData01();
            return user;
        } else {
            return null;
        }
    }


    /**
     * 新增數據-測試 @Caching組合注解
     * 緩存新增的id和用戶名
     * condition = "#result != null" 當結果不為空時緩存
     * localhost:8080/userSave
     * post json  {"id":"002","userName":"user002"}
     */
    @PostMapping("/userSave")
    @Caching(put = {
            @CachePut(key = "#result.id", condition = "#result != null"),
            @CachePut(key = "#result.userName", condition = "#result != null")
    })
    public User userSave(@RequestBody User vo) throws IOException {
        if ("002".equals(vo.getId()) && "user002".equals(vo.getUserName())) {
            //1、存入數據庫 2、查詢數據返回
            System.out.println(vo);
            return vo;
        } else {
            return null;
        }
    }

    /**
     * 修改數據-測試 @Caching組合注解---
     * 【有雙寫模式@CachePut 和 失效模式@CacheEvict 】
     * 緩存新增的id和用戶名
     * condition = "#result != null" 當結果不為空時緩存
     *
     *  localhost:8080/userUpdate
     *  post json  {"id":"002","userName":"user003"}
     */
    @PostMapping("/userUpdate")
    @Caching(put = {
            @CachePut(key = "#result.id", condition = "#result != null"),
            @CachePut(key = "#result.userName", condition = "#result != null")
    })
    public User userUpdate(@RequestBody User vo) {
        //將原本2號數據user002改成user003
        if ("002".equals(vo.getId()) && "user003".equals(vo.getUserName())) {
            //查數據
            User user = getUserData02();
            //更新
            user.setUserName(vo.getUserName());
            user.setPassword(vo.getPassword());
            return user;
        } else {
            return null;
        }
    }


    /**
     * 刪除數據
     * 緩存新增的id和用戶名
     * condition = "#result != null" 當結果不為空時緩存
     * localhost:8080/userDel
     * post json {"id":"001","userName":"user001"}
     *
     */
    @PostMapping("/userDel")
    @Caching(evict = {
            @CacheEvict(key = "#vo.id"),
            @CacheEvict(key = "#vo.userName")
    })
    public void userDel(@RequestBody User vo) throws Exception {
        //刪除1號數據
        if ("001".equals(vo.getId()) && "user001".equals(vo.getUserName())) {
            //1、查數據
            User user = getUserData01();
            System.out.println(user);
            //2、刪除  ...
        } else {
            throw new Exception("id不是1,不能刪");
        }
    }

}


效果:
在這里插入圖片描述

5.Spring-Cache的不足

SpringCache對讀模式都進行處理,解決了緩存擊穿,緩存穿透,緩存雪崩的問題,但是對寫模式并沒有去處理

讀模式(SpringCache都處理了)

緩存穿透:查詢一個null數據。 解決方法:緩存空數據。 spring.cache.redis.cache-null-values=true
緩存擊穿:大量并發進來,查詢一個正好過期的數據。 解決方法:加鎖 :默認是無加鎖的; @Cacheable(sync = true),加鎖`(加鎖,解決緩存擊穿)

緩存雪崩:大量的key同時過期 解決方法:加隨機時間 (很容易弄巧成拙,要注意) spring.cache.redis.time-to-live=3600000 以ms為單位 3600000為1小時

寫模式(SpringCache沒有管)

我們該如何解決(3種方式)

  • 引入中間件Canal,感知到mysql的更新去更新
  • 讀多寫多的,直接去數據庫查詢

6.Spring-Cache小結

1、對于常規數據(讀多寫少,及時性、一致性要求不高的數據)完全可以使用 Spring Cache
2、對于特殊數據(比如要求高一致性)則需要特殊處理

原文鏈接:https://blog.csdn.net/BruceLiu_code/article/details/125869665

欄目分類
最近更新