IT数码 购物 网址 头条 软件 日历 阅读 图书馆
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
图片批量下载器
↓批量下载图片,美女图库↓
图片自动播放器
↓图片自动播放器↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁
 
   -> Java知识库 -> SpringBoot整合Spring Cache,简化分布式缓存开发 -> 正文阅读

[Java知识库]SpringBoot整合Spring Cache,简化分布式缓存开发

前言

上篇博文,我们深入的介绍了SpringBoot整合Redis的相关内容,处理缓存我们使用RedisTemplate或者StringRedisTemplate结合场景选择不同的数据结构,会造成缓存代码和业务代码会紧耦合在一起。有没有更加简便的方式呢?

答案:有,SpringCache。

这篇博文,我们介绍,SpringCache,以及SpringCache是如何来统一不同的缓存技术以高效便捷的方式接入到项目中,最后,深入讲解SpringCache是如何解决缓存击穿,缓存穿透,缓存雪崩的,还有哪些不足。

Spring Cache介绍

Spring Data Redis对Redis底层开发包(Jedis, JRedis, and RJC)进行了高度封装,RedisTemplate提供了redis各种操作、异常处理及序列化,支持发布订阅,并对spring 3.1 Cache进行了实现。

SpringCache并非某一种Cache实现的技术,SpringCache是一种缓存实现的通用技术,基于Spring提供的Cache框架,让开发者更容易将自己的缓存实现高效便捷的嵌入到自己的项目中。当然,SpringCache也提供了本身的简单实现NoOpCacheManager、ConcurrentMapCacheManager 等。通过SpringCache,可以快速嵌入自己的Cache实现。

  • Spring从3.1开始定义了org.springframework.cache.Cache和org.springframework.cache.CacheManager接口来统一不同的缓存技术;并支持使用JCache(JSR-107)注解简化开发;
  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;Cache接口下Spring提供了各种xxxCache的实现;如RedisCache,EhCacheCache,ConcurrentMapCache等;
  • 每次调用需要缓存的功能的方法时,Spring会检查指定参数、指定的目标方法是否已经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有,就调用方法并缓存结果后返回给用户。下次调用直接从缓存中获取。
  • 使用Spring缓存抽象时我们需要关注以下两点:
    • 缓存声明:确定方法需要被缓存以及他们的缓存策略
    • 缓存配置:从缓存中读取之前缓存存储的数据

整合SpringCache,简化开发

SpringCache是缓存的上层封装,RedisCache是底层实现,这篇博文,我们就结合Redis来实现分布式缓存。我们以缓存用户数据为例,来实现我们的案例。建表语句以及mybatis的相关内容在源码中都有,我们就一一展示了,大家可以在源码中查看,项目整体目录如下图所示:

在这里插入图片描述

引入依赖

<!--引入缓存场景-->
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-cache</artifactId>
</dependency>
<!--使用redis作为缓存中间件-->
<dependency>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

使用步骤

使用SpringCache其实特别简单,就跟把大象装进冰箱一样,就两步。

1、开启缓存功能@EnableCaching

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class CacheConfig {
  //其他内容,暂时略过
}

2、使用注解完成缓存操作

@Repository
@CacheConfig(cacheNames = "users")
public class UserDao implements IUserDao{
    @Autowired
    private UserMapper userMapper;
    @Cacheable(key = "'getTotalCount'")
    @Override
    public int getTotalCount(){
        int totalCount = userMapper.getTotalCount();
        return totalCount;
    }

    @Cacheable(key = "#userId")
    @Override
    public User getUser(Integer userId){
      return  userMapper.getUser(userId);
    }
    @Caching(evict = {
            @CacheEvict(key = "'getUsers'"),
            @CacheEvict(key = "'getTotalCount'")
    })
    @Override
    public void insertUser(User u){

        userMapper.insertUser(u);
    }
    @Cacheable(key = "'getUsers'")
    @Override
    public List<User> getUsers(){

       return userMapper.getUsers();
    }
    @Caching(evict = {
            @CacheEvict(key = "'getUsers'")
    })
    @Override
    public void updateUserNameById(Integer userId, String name){

        userMapper.updateUserNameById(userId, name);
    }
    @Caching(evict = {
            @CacheEvict(key = "'getUsers'"),
            @CacheEvict(key = "'getTotalCount'"),
            @CacheEvict(key = "#userId")
    })
    @Override
    public void deleteUser(Integer userId){
        userMapper.deleteUser(userId);
    }

    /**
     * 调用方法,有更新缓存的数据 修改数据库的数据同时更新新缓存。
     */
    @Caching(evict = {
            @CacheEvict(key="'getUsers'")
    },put = {@CachePut(key = "#result.id")})
    @Override
    public User updateUser(User user){
        userMapper.updateUser(user);
        return user;
    }
}

测试用例,都在源码示例。

添加配置

server:
  port: 8084

spring:
  application:
    name: springboot-cache
  datasource:
    url: jdbc:mysql://localhost:3306/user_db_test
    username: root
    password: admin123
    type: com.zaxxer.hikari.HikariDataSource
    driver-class-name: com.mysql.cj.jdbc.Driver
  redis:
    # Redis服务器地址
    host: localhost
    # Redis服务器连接端口
    port: 6379
    # Redis服务器连接密码(默认为空)
    password:
    # Redis数据库索引(默认为0)
    database: 0
  # 连接超时时间(毫秒)
    timeout : 300
    client-type: lettuce #切换jedis客户端,改成jedis
    lettuce: #切换jedis客户端,改成jedis
      pool:
        # 连接池最大连接数(使用负值表示没有限制)
        max-active: 8
        # 连接池最大阻塞等待时间(使用负值表示没有限制)
        max-wait: -1
        # 连接池中的最大空闲连接
        max-idle: 8
        # 连接池中的最小空闲连接
        min-idle: 0
  cache:
    type: redis
    redis:
      #是否缓存空值,防止缓存穿透
      cache-null-values: true
      #缓存过期时间(单位为毫秒)
      time-to-live: 100000
      #缓存前缀,用于区分其他缓存,不指定前缀,默认使用缓存的名字作为前缀
#      key-prefix: CACHE_
      #是否使用缓存前缀,false不使用任何缓存前缀
#      use-key-prefix: false

# 配置mybatis规则
mybatis:
  config-location: classpath:mybatis/mybatis-config.xml  #全局配置文件位置
  mapper-locations: classpath:mybatis/mapper/*.xml  #sql映射文件位置

注解详解

  • @Cacheable: Triggers cache population.(将数据保存到缓存操作)
  • @CacheEvict: Triggers cache eviction.(将数据从缓存删除操作;失效模式)
  • @CachePut: Updates the cache without interfering with the method execution.(不影响方法执行更新缓存;双写模式)
  • @Caching: Regroups multiple cache operations to be applied on a method.(组合以上多个缓存操作)
  • @CacheConfig: Shares some common cache-related settings at class-level.(在类级别共享缓存的相同配置)

注解参数

  • @Cacheable

    代表当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法最后将方法的结果放入缓存。

    value:每一个需要缓存的数据我们都来指定要放到哪个名字的缓存。【缓存的分区(按照业务类型分)】。如果指定缓存前缀 spring.cache.redis.key-prefix=CACHE_@Cacheable(value={"user"}) 中的 value会失效!

    key:缓存对象存储在Map集合中的key值,缺省按照函数的所有参数组合作为key值,若自己配置需使用SpEL表达式;注意:使用的SpEL表达式,字符串一定要加单引号。

    condition:额外添加缓存的条件,满足条件的数据才会被缓存。语法为SpEL。

    unless:配置哪些条件下的记录不缓存。语法为SpEL。

    sync:加同步锁的同步获取,更新操作。

    默认行为:

    1. 缓存中有数据,方法不调用
    2. 缓存的value值,默认使用jdk序列化机制,将序列化的数据存到redis中;
    3. key是默认生成的,如果不指定,默认user::SimpleKey [];可以通过key属性指定,接收一个SpEL表达式的值;
    4. 默认时间是 -1,永不过期,可以在配置文件中配置过期时间;
  • @CacheEvict

    ? 删除缓存,【失效模式】

    ? allEntries:表示是否需要清除缓存中的所有元素。默认为false,表示不需要。当指定了allEntries为true时,Spring Cache将忽略指定的key。有的时候我们需要Cache一下清除所有的元素,这比一个一个清除元素更有效率。

    ? befareInvocation:清除操作默认是在对应方法成功执行之后触发的,即方法如果因为抛出异常而未能成功返回时也不会触发清除操作。使用beforeInvocation可以改变触发清除操作的时间,当我们指定该属性值为true时,Spring会在调用该方法之前清除缓存中的指定元素。

  • @CachePut

    ? 根据返回值更新缓存,【双写模式】

  • @Caching

    ? 组合多个缓存操作;

    ? @Caching允许在同一方法上使用多个嵌套的 @Cacheable@CachePut@CacheEvict注释

可使用的SpEL表达式

每个SpEL表达式都有一个专门的context。除了采用参数构建表达式,框架提供了专门的与caching相关的元数据,比如参数名。下表列出了在context中可用的参数,你可以用来当做key和conditional 处理。

NameLocationDescriptionExample
methodNameroot object被执行的method的名字#root.methodName
methodroot object被执行的method#root.method.name
targetroot object执行的对象#root.target
targetClassroot object执行对象的class#root.targetClass
argsroot object执行对象的参数们(数组)#root.args[0]
cachesroot object当前method对应的缓存集合#root.caches[0].name
argument nameevaluation context任意method的参数。如果特殊情况下参数还没有被赋值(e.g. 没有debug信息),参数可以使用#a<#arg>来表示,其中#arg代表参数顺序,从0开始#iban或者#a0(也可以使用#p0或者#p<#arg>注解来启用别名)
resultevaluation contextmethod执行的结果(要缓存的对象),仅仅在unless表达式中可以使用,或者cache put(用来计算key),或者cache evict表达式(当beforeInvocation=false). 为了支持wrapper,比如Optional,#result指向世纪的对象,不是wrapper.#result

自定义缓存配置

自定义序列化方式,缓存的前缀,默认使用分区名,缓存的过期时间,是否缓存空值等。

@EnableConfigurationProperties(CacheProperties.class)//开启属性配置绑定功能
@Configuration
@EnableCaching //开启缓存启动类的注解从启动类移到这里
public class CacheConfig {


    /**
     * 配置文件中的东西没有用上;
     * 1、原来文件中的东西没有用上
     * @ConfigurationProperties(prefix = "spring.cache")
     * public class CacheProperties {
     *
     * 2、要让他生效
     * @return
     */
    @Bean
    public CacheManager cacheManager(RedisConnectionFactory redisConnectionFactory,CacheProperties cacheProperties){
         //缓存配置对象
         RedisCacheConfiguration redisCacheConfiguration = RedisCacheConfiguration.defaultCacheConfig();
         redisCacheConfiguration = redisCacheConfiguration
         //序列化方式:new GenericJackson2JsonRedisSerializer();
        .serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new FastJsonRedisSerializer<>(Object.class)));

        CacheProperties.Redis redisProperties = cacheProperties.getRedis();
        if (redisProperties.getTimeToLive() != null) {
            redisCacheConfiguration = redisCacheConfiguration.entryTtl(redisProperties.getTimeToLive());
        }
        if (redisProperties.getKeyPrefix() != null) {
            redisCacheConfiguration = redisCacheConfiguration.prefixCacheNameWith(redisProperties.getKeyPrefix());
        }
        if (!redisProperties.isCacheNullValues()) {
            redisCacheConfiguration = redisCacheConfiguration.disableCachingNullValues();
        }
        if (!redisProperties.isUseKeyPrefix()) {
            redisCacheConfiguration = redisCacheConfiguration.disableKeyPrefix();
        }

        return RedisCacheManager
                .builder(RedisCacheWriter.nonLockingRedisCacheWriter(redisConnectionFactory))
                 .cacheDefaults(redisCacheConfiguration).build();
    }
}

原理

1、自动配置

CacheAutoConfiguration会导入CacheProperties。CacheProperties用于配置缓存的基本属性。通过Import导入CacheConfigurationImportSelector,通过用户设置缓存类型,导入响应的缓存配置。

在这里插入图片描述

2、配置使用Redis作为缓存

会自动导入RedisCacheConfiguration;RedisCacheConfiguration自动配好了缓存管理器RedisCacheManager,RedisProperties。

在这里插入图片描述

缓存作用

为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问,而db承担数据落盘工作。但是在现实业务中,缓存场景按照读写分,可以分成读环境场景和写缓存场景,各自又有需要注意的问题。

读缓存场景

哪些数据适合放入缓存?

  • 即时性,数据一致性要求不高的。
  • 访问量大且更新频率不高的数据(读多,写少)
  • 读场景的基本流程如下图:

在这里插入图片描述

读缓存问题

缓存穿透

描述:

缓存穿透是指缓存和数据库中都没有的数据,而用户不断发起请求,请求会直接打到数据库上,并且查不到数据,没法写缓存,所以下一次同样会打到数据库上。

此时,缓存起不到作用,请求每次都会走到数据库,流量大时数据库可能会被打挂。此时缓存就好像被“穿透”了一样,起不到任何作用。

解决方案:

  1. 接口校验。接口层增加校验,如用户鉴权校验,数据合法性校验等;
  2. 缓存空值。从缓存取不到的数据,在数据库中也没有取到,这时也可以将key-value对,写为key-null,缓存有效时间可以设置短点,如30秒(设置太长会导致正常情况也没法使用)。这样可以防止攻击用户反复用同一个id暴力攻击。
  3. 布隆过滤器。使用布隆过滤器存储所有可能访问的 key,不存在的 key 直接被过滤,存在的 key 则再进一步查询缓存和数据库。

缓存击穿

描述:

缓存击穿是指某一个热点key,缓存中没有但数据库中有的数据(一般是缓存时间到期),这时由于并发用户特别多,同时读缓存没读到数据,又同时去数据库去取数据,引起数据库压力瞬间增大,造成过大压力

解决方案:

  1. 设置热点数据永远不过期。直接将缓存设置为不过期,然后由定时任务去异步加载数据,更新缓存。

    这种方式适用于比较极端的场景,例如流量特别特别大的场景,使用时需要考虑业务能接受数据不一致的时间,还有就是异常情况的处理,不要到时候缓存刷新不上,一直是脏数据,那就凉了。

  2. 加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。。在并发的多个请求中,只有第一个请求线程能拿到锁并执行数据库查询操作,其他的线程拿不到锁就阻塞等着,等到第一个线程将数据写入缓存后,直接走缓存。

缓存雪崩

描述:

大量的热点 key 设置了相同的过期时间,导在缓存在同一时刻全部失效,造成瞬时数据库请求量大、压力骤增,引起雪崩,甚至导致数据库被打挂。

缓存雪崩其实有点像“升级版的缓存击穿”,缓存击穿是一个热点 key,缓存雪崩是一组热点 key。

解决方案:

  1. 过期时间打散。缓存数据的过期时间设置随机,防止同一时间大量数据过期现象发生。
  2. 缓存分布式部署。如果缓存数据库是分布式部署,将热点数据均匀分布在不同的缓存数据库中。
  3. 热点数据不过期。设置热点数据永远不过期。
  4. 加互斥锁。该方式和缓存击穿一样,按 key 维度加锁,对于同一个 key,只允许一个线程去计算,其他线程原地阻塞等待第一个线程的计算结果,然后直接走缓存即可。

写缓存场景

对于读取缓存步骤一般没有什么问题,但是一旦涉及到数据更新:数据库和缓存更新,就容易出现缓存(Redis)和数据库(MySQL)间的数据一致性问题。缓存不一致有两种模式:双写模式、失效模式。

双写模式

写数据库的,同时写缓存。

问题1:单线程,更新数据成功,更新缓存失败,导致数据出现不一致。

问题2:多线程,由于卡顿问题,导致写缓存2在最前,写缓存1在后面出现不一致。

如下图:

在这里插入图片描述

失效模式

不管是先写MySQL数据库,再删除Redis缓存;还是先删除缓存,再写库,都有可能出现数据不一致的情况。

问题1:数据发生了变更,先删除缓存,然后要修改数据库,此时还没有修改。一个请求过来,去读缓存,发现缓存为空了,去查询数据库,查到了修改前的旧数据,放到缓存中。随后数据变更的程序完成了数据库的修改。此时出现数据不一致的情况。

如下图:
在这里插入图片描述

无论是双写模式,还是失效模式,都会导致缓存的不一致问题。类似的问题,如何处理呢?

1、缓存数据本就不应该是实时性,一致性要求超高的。所以缓存数据的时候加上过期时间,保证每天拿到当前最新数据即可。

2、遇到实时性,一致性要求高的数据,就应该查数据库,即使慢点。

3、通过加锁保证并发读写,写写的时候按顺序排好队,读读无所谓。所以适合使用读写锁。

4、如果现实业务场景中确实有需要,可以参考终极解决方案。

终极解决方案

异步更新缓存(基于订阅binlog的同步机制)

技术整体思路:

MySQL binlog增量订阅消费+消息队列+增量数据更新到redis

1)读Redis:热数据基本都在Redis

2)写MySQL:增删改都是操作MySQL

3)更新Redis数据:MySQ的数据操作binlog,来更新到Redis

这样一旦MySQL中产生了新的写入、更新、删除等操作,读取binlog后分析 ,利用消息队列,推送更新各台的redis缓存数据。

在这里插入图片描述

Spring-Cache的不足之处

读模式

  • 缓存穿透:查询一个null数据;spring.cache.redis.cache-null-values: true
    解决方案:缓存空数据;
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据;
    解决方案:默认是无加锁的;使用@Cacheable(sync = true)来解决击穿问题;
  • 缓存雪崩:大量的key同时过期;
    解决方案:加过期时间;spring.cache.redis.time-to-live: 100000
    读模式的3个问题spring Cache都考虑到了;

写模式:(缓存与数据库一致)

  • 读写加锁;
  • 引入Canal,感知到MySQL的更新去更新Redis;
  • 读多写多,直接去数据库查询就行;
    写模式spring Cache 没做特殊处理,根据特殊业务进行特殊处理!

总结

  • 常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache)
  • 写模式(只要缓存的数据有过期时间就足够了,业务允许短暂不一致);
  • 特殊数据:特殊设计,脱离业务的设计都是耍流氓。

代码示例

本文示例读者可以通过查看下面仓库中的项目,如下所示:

<module>springboot-cache</module>
  • CodeChina: https://codechina.csdn.net/jiuqiyuliang/springboot-learning

如果您对这些感兴趣,欢迎star、follow、收藏、转发给予支持!


作者:程序猿小亮

博主写作不易,加个关注呗

求关注、求点赞,加个关注不迷路,感谢

点赞是对我最大的鼓励
↓↓↓↓↓↓

  Java知识库 最新文章
计算距离春节还有多长时间
系统开发系列 之WebService(spring框架+ma
springBoot+Cache(自定义有效时间配置)
SpringBoot整合mybatis实现增删改查、分页查
spring教程
SpringBoot+Vue实现美食交流网站的设计与实
虚拟机内存结构以及虚拟机中销毁和新建对象
SpringMVC---原理
小李同学: Java如何按多个字段分组
打印票据--java
上一篇文章      下一篇文章      查看所有文章
加:2021-07-23 10:35:34  更:2021-07-23 10:36:08 
 
开发: C++知识库 Java知识库 JavaScript Python PHP知识库 人工智能 区块链 大数据 移动开发 嵌入式 开发工具 数据结构与算法 开发测试 游戏开发 网络协议 系统运维
教程: HTML教程 CSS教程 JavaScript教程 Go语言教程 JQuery教程 VUE教程 VUE3教程 Bootstrap教程 SQL数据库教程 C语言教程 C++教程 Java教程 Python教程 Python3教程 C#教程
数码: 电脑 笔记本 显卡 显示器 固态硬盘 硬盘 耳机 手机 iphone vivo oppo 小米 华为 单反 装机 图拉丁

360图书馆 购物 三丰科技 阅读网 日历 万年历 2024年11日历 -2024/11/17 20:21:24-

图片自动播放器
↓图片自动播放器↓
TxT小说阅读器
↓语音阅读,小说下载,古典文学↓
一键清除垃圾
↓轻轻一点,清除系统垃圾↓
图片批量下载器
↓批量下载图片,美女图库↓
  网站联系: qq:121756557 email:121756557@qq.com  IT数码