22.缓存-SpringCache

1 简介

文档地址: https://docs.spring.io/spring-framework/docs/5.2.22.RELEASE/spring-framework-reference/integration.html#cache

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

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

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

  • 使用Spring缓存抽象时我们需要关注以下两点

    1. 确定方法需要被缓存以及他们的缓存策略
    2. 从缓存中读取之前缓存存储的数据

22.缓存-SpringCache_第1张图片

2 整合&体验

整合SpringCache简化缓存开发步骤:

  1. 引入依赖spring-boot-starter-cachespring-boot-starter-data-redis
  2. 写配置,指定缓存类型,CacheAutoConfiguration会导入RedisCacheConfiguration
  3. 开启缓存功能,@EnableCaching
  4. 使用注解完成缓存操作

主要注解操作:

  • @Cacheable:触发将数据保存到缓存的操作
  • @CacheEvict:触发将数据从缓存删除的操作
  • @CachePut:不影响方法执行更新缓存
  • @Caching:组合以上多个操作
  • CacheConfig:在类级别共享缓存的配置

1、引入依赖spring-boot-starter-cachespring-boot-starter-data-redis

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-cacheartifactId>
dependency>

<dependency>
    <groupId>org.springframework.bootgroupId>
    <artifactId>spring-boot-starter-data-redisartifactId>
dependency>

2、写配置,路径:product模块下的application.properties

spring.cache.type=redis

3、在启动类上开启缓存注解@EnableCaching,路径:com/atguigu/gulimall/product/GulimallProductApplication.java

4、修改获取一级分类方法,路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java

@Cacheable("category") // 代表当前方法的结果需要缓存,如果缓存中有,方法不调用,如果缓存中没有则会调用方法
@Override
public List<CategoryEntity> getLevel_1_Categorys() {
    System.out.println("获取一级缓存方法执行");
    List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntityList;
}

5、启动product服务测试,多次访问http://localhost:10000/,查看控制台打印信息和redis缓存。

3 @Cacheable细节配置

SpEL表达式文档:https://docs.spring.io/spring-framework/docs/5.2.22.RELEASE/spring-framework-reference/integration.html#cache-spel-context

默认的行为:

  1. key自动生成,格式为:缓存的名字::simplekey[]
  2. 缓存的value值,默认使用jdk序列化机制,将序列化后的数据存储到redis
  3. 默认ttl时间为-1

自定义:

  1. 指定生成key
  2. 指定缓存的ttl
  3. 指定数据保存为JSON格式

1、指定生成key

// @Cacheable(value = {"category"}, key = "#root.method.name")
@Cacheable(value = {"category"}, key = "'getLevel_1_Categorys'") // 代表当前方法的结果需要缓存,如果缓存中有,方法不调用,如果缓存中没有则会调用方法
@Override
public List<CategoryEntity> getLevel_1_Categorys() {
    System.out.println("获取一级缓存方法执行");
    List<CategoryEntity> categoryEntityList = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
    return categoryEntityList;
}

2、指定缓存的ttl

# 单位为毫秒
spring.cache.redis.time-to-live=3600000

4 自定义缓存配置

原理:

CacheAutoConfiguration -> RedisCacheConfiguration -> 自动配置了RedisCacheManager-> 初始化所有的缓存 -> 每个缓存决定使用什么配置 -> 如果RedisCacheConfiguration有就用,没有就用默认配置 -> 想改缓存的配置,只需要给容器中放一个RedisCacheConfiguration即可 -> 就会应用到当前RedisCacheManager管理的所有缓存分区中

如果自己配置了RedisCacheConfiguration,那么在application.properties里的配置就失效了,需要将application.properties里的配置放到自己配置的RedisCacheConfiguration中开启

RedisCacheConfiguration源码:

private org.springframework.data.redis.cache.RedisCacheConfiguration determineConfiguration(
      ClassLoader classLoader) {
    // 如果自己配置了redisCacheConfiguration,就使用自己的,直接返回了,下面的redisProperties不在生效
   if (this.redisCacheConfiguration != null) {
      return this.redisCacheConfiguration;
   }
   Redis redisProperties = this.cacheProperties.getRedis();
   org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
         .defaultCacheConfig();
   config = config.serializeValuesWith(SerializationPair
         .fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
   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、编写MyCacheConfig,路径:com/atguigu/gulimall/product/config/MyCacheConfig.java

@EnableConfigurationProperties(CacheProperties.class)
@Configuration
@EnableCaching
public class MyCacheConfig {

    @Bean
    RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {

        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();

        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        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;
    }
}

2、修改redis-cache配置,路径:application.properties

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

5 @CacheEvict

@CacheEvict:失效模式

  1. 同时进行多种缓存操作 @Caching
  2. 指定删除某个分区下的所有数据,@CacheEvict(value = "category", allEntries = true)
  3. 存储同一类型的数据,都可以指定成同一个分区
  4. 推荐使用默认的缓存前缀,删除spring.cache.redis.key-prefix的配置

1、修改getCatalogJson方法。路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java

@Cacheable(value = "category", key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson() {
    System.out.println("查询了数据库......");
    // 查询出表pms_category所有的记录实体
    List<CategoryEntity> categoryEntityList = baseMapper.selectList(null);

    // 查出所有的一次分类
    List<CategoryEntity> level_1_categorys = getParent_cid(categoryEntityList, 0L);

    // 封装数据,构造一个以1级id为键,2级分类列表为值的map
    Map<String, List<Catelog2Vo>> collect = level_1_categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), l1 -> {
        // 根据一级分类id查找二级分类
        List<CategoryEntity> level_2_categorys = getParent_cid(categoryEntityList, l1.getCatId());

        // 封装结果为Catelog2Vo的集合
        List<Catelog2Vo> catelog2Vos = null;

        if (level_2_categorys != null) {

            // 把 level_2_categorys 封装为 catelog2Vos
            catelog2Vos = level_2_categorys.stream().map(l2 -> {
                Catelog2Vo catelog2Vo = new Catelog2Vo(l1.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

                // 根据二级分类id查找三级分类
                List<CategoryEntity> level_3_categorys = getParent_cid(categoryEntityList, l2.getCatId());

                // 将 level_3_categorys 封装为 catelog3Vos
                if (level_3_categorys != null) {
                    List<Catelog2Vo.Catelog3Vo> catelog3Vos = level_3_categorys.stream().map(l3 -> {
                        Catelog2Vo.Catelog3Vo catelog3Vo = new Catelog2Vo.Catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                        return catelog3Vo;
                    }).collect(Collectors.toList());

                    catelog2Vo.setCatalog3List(catelog3Vos);
                }

                return catelog2Vo;

            }).collect(Collectors.toList());

        }

        return catelog2Vos;
    }));

    return collect;
}

2、修改updateDetail方法,添加@CacheEvict注解。路径:com/atguigu/gulimall/product/service/impl/CategoryServiceImpl.java

// @Caching(evict = {
//         @CacheEvict(value = "category", key = "'getLevel_1_Categorys'"),
//         @CacheEvict(value = "category", key = "'getCatalogJson'")
// })
@CacheEvict(value = "category", allEntries = true)
@Transactional
@Override
public void updateDetail(CategoryEntity category) {
    this.updateById(category);

    categoryBrandRelationService.updateCategory(category.getCatId(), category.getName());
}

3、启动服务进行测试

6 SpringCache的不足

读模式:

  • 缓存穿透:查询一个null数据。解决:缓存空数据;ache-null-values=true
  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁。SpringCache默认是无加锁的,需要加锁可以在注解@Cacheable上添加sync = true
  • 缓存雪崩:大量的key同时过期。解决:加随机时间。加上过期时间 spring.cache.redis.time-to-live

写模式:

  • 读写加锁。
  • 引入Canal,感知到MysQL的更新去更新数据库
  • 读多写多,直接去数据库查询就行

总结:

  • 常规数据(读多写少,即时性,一致性要求不高的数据)完全可以使用Spring-Cache,只要缓存的数据有过期时间就行
  • 特殊数据:特殊处理

你可能感兴趣的:(谷粒商城笔记,缓存,redis,java)