15158 字
31 分钟

SpringBoot整合SpringCache详解

文章摘要
DeepSeek R1
Spring Cache是一个基于AOP的缓存框架,通过注解简化缓存操作,底层可集成Redis。它减少了代码复杂度和耦合,让开发者更关注业务逻辑,但对写模式和高一致性数据支持有限,需额外处理。

SpringCache使用详解及与Redis的整合

前言

明明我们项目中使用最多的缓存技术就是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.4. 整合

引入依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<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 {

    /**
     * 配置文件中的很多东西没用上
     *      1、原来和配置文件绑定的配置类是这个样子的
     *          @ConfigurationProperties(
     *              prefix = "spring.cache"
     *          )
     *          public class CacheProperties
     *
     *      2、要让他生效
     *          @EnableConfigurationProperties(CacheProperties.class)//开启属性绑定配置的功能
     */
    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties){
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        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.5. 使用

在方法上标注注解就可以

3.5.1 @Cacheable(开启缓存功能)

将查询到的结果存入缓存

注意事项

  1. 有对应的缓存就不进入方法 [需要返回值,没有返回值缓存空值]
  2. @Cacheable并没有单独的失效时间的方法。
  3. 但是可以在CacheManager配置,在加上自动刷新的功能,但是这样的的操作比较繁琐。如果不设置,只有统一的过期时间很容易导致缓存雪崩的问题

01、有返回缓存

/**
 * TODO @Cacheable并没有单独的失效时间的方法。
 *      但是可以在CacheManager配置,在+上自动刷新的功能,但是这样的的操作比较繁琐。
 *      如果不设置,只有统一的过期时间很容易导致缓存雪崩的问题。
 * @Cacheable开启缓存功能 有对应的缓存就不进入方法 [需要返回值,没有返回值缓存空值]
 * value = "student", 【key ="#root.methodName" 或 key = "'名称'" 或 key = "#传入的参数" 或  key = "#接收参数的实体.属性"
 * 更多方式看spEl语法 】
 *
 * student是分区名字
 * #root.methodName是spEl语法 也就是方法名 testCache
 *
 * 在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> saveCache01() {
    System.out.println("方法saveCache01执行");
    HashMap<String, List> map = new HashMap<String, List>();
    List studentList = new ArrayList();
    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> map = new HashMap<String, List>();
    List studentList = new ArrayList();
    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.5.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.5.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 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 种方式)

  1. 引入中间件Canal,感知到mysql的更新去更新
  2. 读多写多的,直接去数据库查询

6. Spring-Cache 小结

1、对于常规数据(读多写少,及时性、一致性要求不高的数据)完全可以使用 Spring Cache

2、对于特殊数据(比如要求高一致性)则需要特殊处理


✨ 感谢您的耐心阅读!!!! ✨ 文章仅限学习使用~ ✨ 感谢耐心阅读!!❤ ✨ 文章转载于: ,如有侵权,请联系删除。

Firefly
Firefly
Hello, I'm Firefly.
公告
欢迎体验 Firefly 主题复刻版,壁纸与布局已全面同步。
查看文档