• springcache的使用详解(使用redis做分布式缓存)


    写在前面

    使用springcache做缓存,需要首先明白几个概念:缓存穿透、缓存击穿、缓存雪崩,以及缓存一致性。
    缓存一致性解决方案——改数据时如何保证缓存和数据库中数据的一致性
    从查询数据库性能优化谈到redis缓存-谈一谈缓存的穿透、雪崩、击穿

    简介

    Spring 从 3.1 开始定义了 org.springframework.cache.Cache 和 org.springframework.cache.CacheManager 接口来统一不同的缓存技术; 并支持使用 JCache(JSR-107)注解简化我们开发;

    Cache 接口为缓存的组件规范定义,包含缓存的各种操作集合; Cache 接 口 下 Spring 提 供 了 各 种 xxxCache 的 实 现 ; 如 RedisCache , EhCacheCache , ConcurrentMapCache 等;

    每次调用需要缓存功能的方法时,Spring 会检查检查指定参数的指定的目标方法是否已 经被调用过;如果有就直接从缓存中获取方法调用后的结果,如果没有就调用方法并缓 存结果后返回给用户。下次调用直接从缓存中获取。

    在这里插入图片描述
    Cache:缓存接口,定义缓存操作。实现有RedisCache、EhCacheCache、ConcurrentMapCache等。
    CacheManager:缓存管理器,管理各种缓存(Cache)组件。

    使用springcache

    1.引入依赖

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-cache</artifactId>
    </dependency>
    
    <!-- 引入redis,默认的letture有内存溢出的bug,这里用jedis -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <exclusions>
            <exclusion>
                <groupId>io.lettuce</groupId>
                <artifactId>lettuce-core</artifactId>
            </exclusion>
        </exclusions>
    </dependency>
    <dependency>
        <groupId>redis.clients</groupId>
        <artifactId>jedis</artifactId>
    </dependency>
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20

    2.写配置

    (1)自动配置
    CacheAutoConfiguration自动配置类(会导入RedisCacheConfiguration),
    RedisCacheConfiguration自动配好了RedisCacheManager缓存管理器,
    RedisCacheManager初始化所有的缓存,每个缓存决定使用什么配置,如果RedisCacheConfiguration有就用已有的,没有就用redis的配置文件中的配置,或者默认的配置defaultCacheConfig。
    想修改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可,就会应用到当前的RedisCacheManager缓存管理器管理的所有缓存分区中。

    (2)我们需要做的配置

    spring.cache.type=redis
    
    #使用默认的即可
    #spring.cache.cache-names=qq
    
    #毫秒为单位,缓存过期时间
    spring.cache.redis.time-to-live=3600000
    
    #如果指定了key前缀就用我们指定的前缀,如果没有就默认使用缓存的名字(value)作为前缀,最好不指定前缀
    #spring.cache.redis.key-prefix=CACHE_
    #是否使用前缀,默认为true,不写即可
    #spring.cache.redis.use-key-prefix=true
    
    #是否缓存空值,防止缓存穿透
    spring.cache.redis.cache-null-values=true
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15

    3.测试使用缓存

    (1)开启缓存功能

    在配置类加上注解:

    @EnableCaching      //开启缓存功能
    
    • 1

    (2)只需要使用注解就能使用缓存操作
    @Cacheable:触发将数据保存到缓存的操作
    @CacheEvict:触发将数据从缓存删除的操作
    @CachePut:不影响方法执行更新缓存
    @Caching:组合以上多个操作
    @CacheConfig:在类级别共享缓存的相同配置

    // 代表当前方法的结果需要缓存,如果缓存中有,方法都不用调用,如果缓存中没有,会调用方法。最后将方法的结果放入缓存
    @Cacheable({"category"}) 
    public Category getCategory(){
    
    • 1
    • 2
    • 3

    进阶

    解决缓存穿透问题

    springcache默认就是缓存null值的,默认就是解决缓存穿透的。
    我们也可以在配置文件中加入该配置,让其缓存null值。

    #是否缓存空值,防止缓存穿透
    spring.cache.redis.cache-null-values=true
    
    • 1
    • 2

    缓存的分区

    每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务类型分)】

    @Cacheable(value = {"category"})
    public List<CategoryEntity> getLevel1Categorys() {
    
    • 1
    • 2

    @Cacheable的value属性,可以指定缓存的分区,这个分区category就是缓存key 的前缀,作为key的其中一部分。
    缓存的分区很重要,后续删除缓存时,可以按照分区删除,最好是按照业务进行分区,这样数据有修改的话,可以将整个业务的缓存都删掉,处理起来更方便。

    在redis中存储的key的设置

    springcache在redis存储的key是可以设置的。

    @Cacheable(value = {"category"},key = "#root.method.name")
    public List<CategoryEntity> getLevel1Categorys() {
    
    • 1
    • 2

    默认的key的存储的值是自动生成的:缓存的名字::SimpleKey::[] (自动生成key值)

    其中缓存的名字就是分区category,也可以在配置文件中指定key的前缀(最好是不指定,使用分区前缀即可,方便区分)

    #如果指定了key前缀就用我们指定的前缀,如果没有就默认使用缓存的名字(value)作为前缀,最好不指定前缀
    #spring.cache.redis.key-prefix=CACHE_
    #是否使用前缀,默认为true,不写即可
    #spring.cache.redis.use-key-prefix=true
    
    • 1
    • 2
    • 3
    • 4

    指定了@Cacheable的key属性之后,就会将key的后缀替换为key属性。
    其中@Cacheable的key属性是可以使用spel表达式的,具体使用如下:

    • #root.methodName :被调用的方法的名称
    • #root.method.name:被调用的方法的名称,其中root.method可以获取被调用的方法,下面很多属性
    • #root.target:正在调用的目标对象
    • #root.targetClass:正在调用的目标类
    • #root.args[0]:用于调用目标的参数(作为数组)
    • #root.caches[0].name:对当前方法执行的缓存的集合
    • #iban or #a0 (也可以用 #p0或 #p<#arg> 符号作为别名):任何方法参数的名称。如果名称不可用(可能是因为没有调试信息),参数名称也可以在#a<#arg>下使用,其中#arg代表参数索引(从0开始)。
    • #result:方法调用的结果(缓存的值)。
    • ‘xxxxxxx’:用单引号引起来的表示写死的字符串。

    设置过期时间

    默认过期时间为-1,也就是永久,可以在配置文件中指定过期时间

    #毫秒为单位,缓存过期时间
    spring.cache.redis.time-to-live=3600000
    
    • 1
    • 2

    将value值保存为json格式

    默认是使用jdk的序列化器,存储的是jdk序列化后的数据,我们如果想要保存为json数据可以自己来设置配置类

    @EnableConfigurationProperties(CacheProperties.class)
    @Configuration
    @EnableCaching
    public class MyCacheConfig {
    
        /**
         * 用上配置文件的配置
         */
        @Bean
        public RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
    
            RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
    
            config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
            // 使用json序列化器,用fastjson的也可以
            config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
    
            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;
        }
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21
    • 22
    • 23
    • 24
    • 25
    • 26
    • 27
    • 28
    • 29
    • 30
    • 31
    • 32
    • 33
    • 34
    • 35

    解决缓存击穿问题

    加上sync = true属性,默认是false。

    @Cacheable(value = {"category"},key = "#root.method.name",sync = true)
    public List<CategoryEntity> getLevel1Categorys() {
    
    • 1
    • 2

    我们正常解决缓存击穿是使用分布式锁来对数据库的操作加一把分布式锁,springcache解决的方式是使用synchronize加本地锁,虽然不能锁全部服务器,但是锁住当前服务器也是可以大大减少了并发量,多查几次数据库其实也没什么问题,只要不是大量请求来查数据库就可以。

    springcache只有查询的时候才有锁,解决缓存击穿的问题,是加了synchronize本地锁,并不是分布式锁。

    缓存更新

    缓存一致性解决方案——改数据时如何保证缓存和数据库中数据的一致性

    缓存更新就涉及缓存一致性问题。

    /**
    * 级联更新所有关联的数据
    *
    * @CacheEvict:失效模式(此处使用的是失效模式)
    * @CachePut:双写模式,需要有返回值(用起来不方便)
    * 1、同时进行多种缓存操作:@Caching
    * 2、指定删除某个分区下的所有数据 @CacheEvict(value = "category",allEntries = true)
    * 3、由2可知,存储同一类型的数据,都可以指定为同一分区,方便删除数据
    * @param category
    */
    // @Caching(evict = {
    //         @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
    //         @CacheEvict(value = "category",key = "'getCatalogJson'")
    // })
    @CacheEvict(value = "category",allEntries = true)       //删除某个分区下的所有数据
    public void updateCascade(CategoryEntity category) {
    
    	// 方法执行完之后,springcache进行的操作:
        //同时修改缓存中的数据
        //删除缓存,等待下一次主动查询进行更新
    }
    
    • 1
    • 2
    • 3
    • 4
    • 5
    • 6
    • 7
    • 8
    • 9
    • 10
    • 11
    • 12
    • 13
    • 14
    • 15
    • 16
    • 17
    • 18
    • 19
    • 20
    • 21

    缓存一致性问题

    springcache是做不到缓存与数据库强一致性的问题的,我们可以使用特殊手段来做到强一致性(例如:在方法调用之前加锁,然后再调用目标方法)。
    常规数据(读多写少,即时性,一致性要求不高的数据,完全可以使用Spring-Cache):写模式(只要缓存的数据有过期时间就足够了)。
    特殊数据需要进行特殊设计。

    总结

    springcache极大的简化了我们对缓存的操作,常规数据完全可以使用springcache。

    但是,springcache无法保证缓存与数据库的强一致性,如果对数据实时性要求不是很高的话,可以使用其失效模式,缓存过期后自动刷新最新的数据。

    如果是对缓存与数据一致性要求高的话,就需要考虑自己来设计,加上锁了。

  • 相关阅读:
    我要涨知识——TypeScript 经典高频面试题(二)
    浮动元素的特点(2)
    用wcferry部署vx机器人遇到问题,求解答
    微型操作系统内核源码详解系列五(1):arm cortex m3架构
    【Python抽奖系统】好消息:史上最强商场抽奖活动来啦,超优惠,攻略快拿好啦~(超牛)
    PMP考生必读,7月30日考试防疫要求都在这里
    中国象棋棋盘识别
    第99天:权限提升-数据库提权&口令获取&MYSQL&MSSQL&Oracle&MSF
    cesium 鹰眼图2
    工业交换机常见的硬件故障有哪些?
  • 原文地址:https://blog.csdn.net/A_art_xiang/article/details/125580962