缓存 redis 缓存失效 分布式锁 Redisson SpringCache

1、缓存

1.1 缓存背景

      在系统性能测试3.3.2这一节中,已经发现了频繁读写数据库,非常影响整个系统的性能指标,比如系统的TPS,QPS,吞吐量等等都很低。为了提高系统整体性能,在有限的硬件条件下,尽量能够让更多的用户同时访问微服务,而且还有较高的稳定性,就要尽量避免频繁读写数据库。解决办法就是把常用的数据放入缓存中,这样web请求过来,就可直接从缓存中给数据,不用频繁查询数据库了,从而达到整体系统性能提升。

1.2 缓存的使用

        为了系统性能的提升,我们一般都会将部分数据放入缓存中,加速访问。而 db 承担数据落盘工作。

1.2.1 哪些数据适合放入缓存

(1)即时性、数据一致性要求不高的数据

(2)访问量大且更新频率不高的数据(读多,写少)

举例:电商类应用,商品分类,商品列表等适合缓存并加一个失效时间(根据数据更新频率 来定),后台如果发布一个商品,买家需要 5 分钟才能看到新的商品一般还是可以接受的。

1.2.2 缓存一般使用流程

缓存 redis 缓存失效 分布式锁 Redisson SpringCache_第1张图片

 注意:在开发中,凡是放入缓存中的数据我们都应该指定过期时间,使其可以在系统即使没 有主动更新数据也能自动触发数据加载进缓存的流程。避免业务崩溃导致的数据永久不一致 问题

2、缓存基本功—Redis    

     现在主流承担缓存职能的是Redis,是一种非关系型数据库(NoSQL),比起关系型数据库(比如Mysql,Oracle)的优点就是只用键值对存储,读写,查询效率都很高。缺点自然就是关系型数据库能处理的字段关系,NoSQL又不好处理。总之,知道Redis适合做缓存就对了,至于非关系数据库和Redis想深入了解的,请阅读

2.1 简介

     Tip:本文介绍和缓存相关的Redis的使用,想深入了解非关系型数据库(NoSQL)以及Redis,请阅读https://blog.csdn.net/u011863024/article/details/107476187,写得非常非常全面。

2.2 Redis五大数据类型及常用命令

缓存 redis 缓存失效 分布式锁 Redisson SpringCache_第2张图片

 以上只是大纲,具体细化,还得花时间挨个百度。

2.3 jedis

    Redis的java客户端,java程序员通过jedis来操作Redis数据库。了解一下即可,主要操作的就是下面注意事项中的依赖替换,jedis的操作都封装在springcache里了,至少在缓存方面,我们也没啥机会用这个jedis

特别注意:

     springboot2.0以后默认使用lettuce作为操作redis的客户端。它使用netty进行网络通信,效率确实比Jedis更好,但是有bug会导致netty堆外内存溢出,暂时没有更好的解决办法(其实可能已经解决了,我学这个的时候,老师还用的spring2.1.8,现在都spring5.x了),因此只能Lettuce新版本看能不能解决此bug,或者切换使用jedis。

    我们要操作的就是从springboot redis的starter中排除掉lettuce,然后补上jedis依赖,其他操作交给后面的SpringCache框架。


    org.springframework.boot
    spring-boot-starter-data-redis
        
           
                io.lettuce
                lettuce-core
            
        



     redis.clients
     jedis

3、缓存失效问题

      本节讲的缓存失效问题,是你自己去写一个缓存,需要避免的问题。缓存框架springcache都已经基本解决了这些问题(说基本是因为还有一类复杂问题没有解决),我们只需要配置springcache即可,但是首先还是需要了解下缓存失效问题。

3.1 缓存穿透

缓存 redis 缓存失效 分布式锁 Redisson SpringCache_第3张图片

(1)简介

    缓存穿透是指查询一个一定不存在的数据,由于缓存是不命中,将去查询数据库,但是数 据库也无此记录,我们没有将这次查询的 null 写入缓存,这将导致这个不存在的数据每次请求都要到存储层(Mysql)去查询,失去了缓存的意义。

(2)风险

 黑客利用不存在的数据进行攻击,数据库瞬时压力增大,最终导致崩溃

(3)解决办法

解决: 把空结果null也加入缓存、并且设置短的过期时间。(代码先不慌,后文springcache一个注解就解决)

3.2 缓存雪崩

缓存 redis 缓存失效 分布式锁 Redisson SpringCache_第4张图片

     缓存雪崩是指在我们设置缓存时key采用了相同的过期时间, 导致缓存在某一时刻同时失效,请求全部转发到DB,DB瞬时压力过重导致雪崩雪崩

  • 解决办法:

      原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件(代码先不慌,后文springcache一个注解就解决)

3.3 缓存击穿

缓存 redis 缓存失效 分布式锁 Redisson SpringCache_第5张图片

     对于一些设置了过期时间的key,如果这些key可能会在某些时间点被超高并发地访问,成为非常“热点”的数据,比如“加拿大电鳗”,突然就变热点了。

    如果这个key在大量请求同时进来前正好失效,那么所有对这个key的数据查询都落到db,我们称为缓存击穿。

  • 解决办法:

加锁:大量并发只让一个去查,其他人等待,查到以后释放锁,其他 人获取到锁,先查缓存,就会有数据,不用去db。(代码先不慌,后文springcache一个注解就解决)

4、分布式锁框架 Redisson

    分布式锁,和普通单体应用的锁用法都一样的,只是分布式锁用Redisson就是了

官方文档:目录 · redisson/redisson Wiki · GitHub

    官方文档非常不友好,打开就一堆锁,全部学完是不可能的。我这里就介绍项目中,最常用的读写锁,

4.1 Redisson的使用

4.1.1 引入依赖



     org.redisson
     redisson
     3.12.0

4.1.2 配置Redisson

就是创建一个RedissonCliet,加入Spring容器

@Configuration
public class MyRedissonConfig {

    /**
     * 所有对 Redisson 的使用都是通过 RedissonClient
     *
     * @return
     * @throws IOException
     */
    @Bean(destroyMethod = "shutdown")
    public RedissonClient redisson() throws IOException {
        // 1、创建配置
        Config config = new Config();
        // Redis url should start with redis:// or rediss://
        config.useSingleServer().setAddress("redis://127.0.0.1:6379");

        // 2、根据 Config 创建出 RedissonClient 实例
        return Redisson.create(config);
    }

}

更多配置方法:参考官网https://github.com/redisson/redisson/wiki,的程序化方式(代码)配置,可自行研究配置文件方式配置

4.2 读写锁

Redisson框架提供了  writeValue() 和 readValue()方法  来加读写锁


    为了达成以下效果,读写锁是一把锁“rw-lock"
    读写锁:确保正在更新数据的时候,读不到旧数据,一直等待直到数据更新完成。 有点像mysql的禁止不可重复读
    读写锁最终效果:一定能读到最新数据

    写锁:排它锁(互斥锁,独享锁),不准其他线程同时进来修改数据,只要写锁没释放,其他线程就必须一直等待
    读锁:共享锁,其他线程可以一起读,但是不准修改

   一个线程在调用读方法readValue(),同时另一个线程也在调用读方法readValue():web浏览器中,两个页面同时访问xxx/read
    读 + 读 :相当于无锁,并发读,只会在Redis中记录好,所有当前的读锁。他们都会同时加锁成功

    同理 一个线程在调用写方法writeValue(),同时另一个线程调用读方法readValue():web浏览器中,一个页面访问xxx/write,另一个访问xxx/read
    写 + 读 :必须等待写锁释放

    写 + 写 :阻塞方式
    读 + 写 :有读锁。写也需要等待

     规律:只要有写的存在,就必须等待
 

//写锁
    @GetMapping(value = "/write")
    @ResponseBody
    public String writeValue() {
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        String s = "";
        RLock rLock = readWriteLock.writeLock();
        try {
            //1、改数据加写锁
            rLock.lock();
            s = UUID.randomUUID().toString();
            ValueOperations ops = stringRedisTemplate.opsForValue();
            ops.set("writeValue",s); //给redis存入数据
            TimeUnit.SECONDS.sleep(10);
        } catch (InterruptedException e) {
            e.printStackTrace();
        } finally {
            rLock.unlock(); //释放写锁,然后readValue()方法,才可以操作(读写方法用的同一把锁)
        }

        return s;
    }


    //读锁
    @GetMapping(value = "/read")
    @ResponseBody
    public String readValue() {
        String s = "";
        RReadWriteLock readWriteLock = redisson.getReadWriteLock("rw-lock");
        //加读锁,拿到锁加锁成功后再执行业务代码
        RLock rLock = readWriteLock.readLock();
        try {
            rLock.lock();
            ValueOperations ops = stringRedisTemplate.opsForValue();
            s = ops.get("writeValue");
            try { TimeUnit.SECONDS.sleep(10); } catch (InterruptedException e) { e.printStackTrace(); }
        } catch (Exception e) {
            e.printStackTrace();
        } finally {
            rLock.unlock();
        }

        return s;
    }

5、Spring Cache(重点,项目中就是用它来缓存了)

5.1 简介 

   SpringCache的作用就是将从DB(一般指Mysql)查到的数据,添加进缓存(一般用Redis)。

5.2 使用方法

5.2.1 spring配置文件中

配置文件中,主要完成个性化定制功能

#本项目使用的是redis来缓存
spring.cache.type=redis


#设置缓存过期时间TTL,单位为毫秒,3600000就是1个小时
spring.cache.redis.time-to-live=3600000


#一般规则就是,开启前缀,但是不指定缓存前缀,就用分区名作为前缀
#开启使用前缀,默认也是开启。只有false的时候,才需要写配置
#比如本项目的redis中,存储的名字就是db0/category下的 category::getCatalogJson
spring.cache.redis.use-key-prefix=true 
#如果指定了前缀就用我们指定的前缀(给redis的key的前缀),如果没有指定,就默认使用缓存的名字(分区名字)作为前缀(@Cacheable注解的value属性)。
#spring.cache.redis.key-prefix=CACHE_


#是否缓存空值,防止缓存穿透,一定要用
spring.cache.redis.cache-null-values=true

5.2.2 标注解

(1)启动类注解(标记在配置类或主启动类上,一般放在配置类上)

① @EnableConfigurationProperties(CacheProperties.class) :主要用于将SpringCache的默认存储数据类型修改后(一般都要改成Json),让spring配置文件里的配置生效(看后面案例,也很难文字解释)

② @EnableCaching: 开启使用缓存

@EnableConfigurationProperties(CacheProperties.class) //让application.yml中的缓存相关配置生效
@EnableCaching //开启使用缓存
@Configuration//这个是spring注解,跟缓存无关
public class MyCacheConfig {

    /**
     * 配置文件的配置没有用上
     * 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
     *                                public class CacheProperties
     * 

* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class) */ @Bean //返回一个RedisCacheConfiguration 加入spring容器,即可完成配置 public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { //.... }

(2)业务类注解(一般标记在方法上)

方法上主要就是完成业务功能。即对缓存Redis的增、删、改、查

① @Cacheable:将当前方法的返回值保存到缓存中;(如果缓存中有该结果,就不调用该方法) 逻辑:查找缓存 - 有就返回 -没有就执行方法体 - 将结果缓存起来;
② @CachePut :和@Cacheable一样,都是将方法的返回值添加缓存。但差异是,要先执行方法体。有结果就缓存,没有就算了 逻辑:执行方法体 - 将结果缓存起来
总结: @Cacheable 适用于查询数据的方法,@CachePut 适用于更新数据的方法。

③ @CacheEvict  : 触发将数据从缓存删除的操作,但是只删除一个或者所有;
④ @Cacheing:组合以上多个操作,比如可以弥补@CacheEvict只能删除一个的不足,删除多个,就用这个注解组合,详情见后面的案例;
⑤ @CacheConfig:在类级别共享缓存的相同配置

 5.3 SpringCache使用案例

5.3.1 引入依赖(SpringCache+Redis)



   org.springframework.boot
   spring-boot-starter-cache



   org.springframework.boot
   spring-boot-starter-data-redis
   
       
          io.lettuce
          lettuce-core
       
   



   redis.clients
   jedis

5.3.2 配置类

配置类主要是有两个任务

(1)基本任务:配置创建缓存的Bean组件,加入spring容器内

(2)进阶任务

      springCache将DB中的数据存入Redis,默认是存的值是java序列化后的格式。为了能更好的跨语言,跨平台,我们需要通过配置让SpringCache向Redis中存的值是Json。

    但是修改默认配置以后,又会导致SpringCache没法读取到spring配置文件application.propertis,因此在修改默认配置的同时,还要解决这个问题。

/**
 * @author: Taoji
 * @create: 2021-08-5 15:19
 * 单独写配置: 配置给redis中存放json,默认是存放java序列化的一堆看不懂的乱码,不适合跨语言,跨平台使用
 *
 * 配置原理
 *   CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager
 *   ->初始化所有的缓存 -> 每个缓存决定使用什么配置
 *   如果redisCacheConfiguration有自定义配置,就使用自定义配置 (下面的配置类,就是配这个)
 *   如果没有自定义配置,才会使用默认配置
 *   想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可,就会应用到当前的RedisCacheManager管理的所有缓存分区中
 *
 */

@EnableConfigurationProperties(CacheProperties.class) //让application.yml中的缓存相关配置生效
@Configuration
@EnableCaching
public class MyCacheConfig {

    /**
     * 配置文件的配置没有用上
     * 1. 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache")
     *                                public class CacheProperties
     * 

* 2. 要让他生效,要加上 @EnableConfigurationProperties(CacheProperties.class) */ @Bean //返回一个RedisCacheConfiguration 加入spring容器,即可完成配置 public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) { RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig(); // config = config.entryTtl(); config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer())); config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer())); /**只写以上代码,配置文件不会生效的(我们在spring配置文件中写的ttl时间就不生效) * 原来和配置文件绑定的配置类为:@ConfigurationProperties(prefix = "spring.cache") * public class CacheProperties * 现在要让配置文件中所有的配置都生效 要加上 @EnableConfigurationProperties(CacheProperties.class) 同时加入以下代码: */ CacheProperties.Redis redisProperties = cacheProperties.getRedis(); //如果配置文件不为空,就去配置文件中获取数据 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; } }

5.3.3 改配置文件,application.properties

特别注意:

设置缓存过期时间,就是在解决3.2中的缓存雪崩

设置缓存空值,就是在解决3.1中的缓存穿透

(不要慌,缓存击穿问题的解决方法在5.3.4中)

#本项目使用的是redis来缓存
spring.cache.type=redis


#设置缓存过期时间TTL,单位为毫秒,3600000就是1个小时
spring.cache.redis.time-to-live=3600000


#一般规则就是,开启前缀,但是不指定缓存前缀,就用分区名作为前缀
#开启使用前缀,默认也是开启。只有false的时候,才需要写配置
spring.cache.redis.use-key-prefix=true 
#如果指定了前缀就用我们指定的前缀(给redis的key的前缀),如果没有指定,就默认使用缓存的名字(分区名字)作为前缀(@Cacheable注解的value属性)。
#spring.cache.redis.key-prefix=CACHE_


#是否缓存空值,防止缓存穿透,一定要用
spring.cache.redis.cache-null-values=true

小细节:spring.cache.redis.use-key-prefix=true ,允许使用前缀,但是不指定前缀,这样缓存的键就会统一用@Cacheable(value="xx")中的值xx作为前缀,这样数据在redis中更规范

5.3.4 编写业务逻辑

(1)向缓存Redis添加数据

方法做的事情就是从Mysql查询数据,并封装成javaBean。由于有了@Cacheable注解,自然就存到缓存Redis中去了,存在redis的category目录下,key就是方法名,getLevel1Categories,由于前面5.3.3配置文件中配置了使用用前缀,又未指定前缀,因此默认前缀就是category,因此该数据再redis中key为category::getLevel1Categories

特别注意:sync=true

给微服务加锁,防止多个相同为服务同时进来查询一个正好过期的数据,造成缓存击穿,解决3.3中的缓存击穿问题

/ @Cacheable:将当前方法的返回值保存到缓存中;(如果缓存中有该结果,IndexController再调用该方法,也不会执行)
    //value属性:缓存的分区(按照业务类型分)】,就是在redis下创建一个名为 category 文件夹
    //key属性:就是redis的key,用spel表达式取值,参考官网#root.method.name 代表用方法名getLevel1Categories作为 key
    //sync属性:给微服务加锁,防止多个相同为服务同时进来查询一个正好过期的数据,造成缓存击穿
    //特别注意:key中的是spel表达式,""里面的还是变量  如果写普通字符串,一定要“‘’”
    //找1级分类,1级分类就是pms_category数据表中 cat_level为1 或者 parent_cid为0 的项目就是1级分类
    @Cacheable(value = {"category"}, key = "#root.methodName", sync = true)
    @Override
    public List getLevel1Categories() {
        
        //去数据库查数据
        List categoryEntities = this.baseMapper.selectList(
                new QueryWrapper().eq("parent_cid", 0));
       
        return categoryEntities;
    }

(2)删除缓存中的多个过期值(由于是多个,所以不能只用@CacheEvict)

下面代码演示了,SpringCache对缓存数据的删除操作。

    方法中的两句代码说明该方法从数据库中重新更新了数据,自然会导致缓存中的数据已经过期了,是错误的。又由于存在多个过期值,因此才用@Caching 组合多个@CacheEvict,将缓存中的过期值删了。

说明:不用担心,删除缓存后,下次前端请求没缓存了。下次请求进来会先查缓存,发现没缓存了,就会用(1)中的 @Cacheable添加好最新版缓存。

//@CacheEvict(value = "category",key = "'getLevel1Categories'") //只能删除一个key,或者所有key,删除多个key的话,用@Caching
//@CacheEvict(value = "category",allEntries = true) //删除category目录里(分区)的所有key
@Caching(evict = {  //就是组合使用 缓存的哪几种注解,比如@Cacheable,@CacheEvict,@CachePut,@CacheConfig等等。这里是删除多个key,就组合@CacheEvict即可
            @CacheEvict(value = "category",key = "'getLevel1Categories'"),
            @CacheEvict(value = "category",key = "getCatalogJson")
    })
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
        
  
  this.updateById(category);
       
 categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());

        //修改过数据库以后,数据库和缓存中数据已经不一致了,就删除缓存中的数据   相关搜索失效模式
        //实践中,通过springcache的@CacheEvict即可达到以下代码的效果即删除缓存中的数据,完成失效模式
    }

你可能感兴趣的:(缓存,SpringCache,缓存,redis)