Loading

spring cache

至于那些真正的意气用事之外,注定有很多盘根交错的利益之争,有人投石问路,有人煽风点火,有人浑水摸鱼。

基本概念

spring从3.1开始定义了CacheCacheManager接口来统一不同的缓存技术,并使用JCache(JSR-107)注解来简化开发。

cache接口的实现包括RedisCacheEhCahceCacheConcurrentMapCache等。

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

缓存注解的说明

  • @Cacheable:触发将数据保存到缓存的操作

  • @CacheEvict:触发将数据从缓存中删除的操作

  • @CachePut:不影响方法执行更新缓存

  • @Caching:组合以上多个操作

  • @CacheConfig:在类级别共享缓存的相同配置

项目整合cache步骤

  1. 引入spring-boot-start-cachespring-boot-starter-redis依赖

  2. 使用@EnableCaching注解开启缓存功能

  3. 编写配置文件

spring.cache.type=redis
# 过期时间。毫秒为单位,设置为1小时
spring.cache.redis.time-to-live=3600000
# key的前缀,如果指定了前缀就用我们指定的前缀,如果没有就默认使用缓存的名字作为前缀
#spring.cache.redis.key-prefix=CACHE_
#是否使用前缀
spring.cache.redis.use-key-prefix=true
# 是否缓存空值。防止缓存穿透
spring.cache.redis.cache-null-values=true

默认使用jdk进行序列化(可读性差),默认ttl为-1永不过期,自定义序列化方式需要编写配置类。

@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()));
       config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericFastJsonRedisSerializer()));

       // 将配置文件中的所有配置都生效
       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;
  }
}

使用示例

/**
* 更新本表及关联表,保证冗余字段的数据一致性
* @CacheEvict:失效模式
* 1. 同时进行多种缓存操作:@Caching
* 2. 指定删除某个分区下的所有数据:@CacheEvict(value = "category",allEntries = true)
* 3. 存储同一个类型的数据,都可以指定成同一个分区。分区名默认就是缓存的前缀。
*/
//   @Caching(evict = {
//       @CacheEvict(value = "category",key = "'getLevel1Categorys'"),
//       @CacheEvict(value = "category",key = "'getCatalogJson'")
//   })
@CacheEvict(value = "category",allEntries = true)//失效模式
//   @CachePut()//双写模式
@Transactional
@Override
public void updateCascade(CategoryEntity category) {
   this.updateById(category);
   if (!StringUtils.isEmpty(category.getName())) {
       // 同步更新其他关联表中的数据
       categoryBrandRelationService.updateCategory(category.getCatId(),category.getName());
       // TODO 更新其他关联表
  }
}

/**
* 查询所有一级分类
* 1. 每一个需要缓存的数据我们都来指定要放到那个名字的缓存。【缓存的分区(按照业务划分)】
* 2. @Cacheable({"category"}):表示当前方法的结果需要缓存,如果缓存中有,方法不用调用。如果缓存中没有,会调用方法,并将方法的结果放入缓存。
* 3. 默认行为
*     1)如果缓存中有,方法不用调用
*     2)key默认自动生成:缓存的名字::SimpleKey [](自动生成key的值)
*     3)缓存的value的值:默认使用jdk序列换机制。将序列化后的数据存到redis。
*     4)默认ttl时间是-1。
* 4. 自定义
*     1)指定生成缓存使用的key: key属性指定,接收一个SpEL表达式
*         SpEL语法详细:https://docs.spring.io/spring-framework/docs/5.3.19-SNAPSHOT/reference/html/integration.html#cache
*     2)指定缓存数据的存活时间: 配置文件中修改ttl
*     3)将数据保存为json格式:
*         查看源码,自定义RedisCacheConfiguration配置类进行修改
*/
@Cacheable(value = {"category"},key = "#root.method.name")
@Override
public List<CategoryEntity> getLevel1Categorys() {
   System.out.println("getLevel1Categorys....");
   List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
   return categoryEntities;
}

@Cacheable(value = {"category"},key = "#root.methodName")
@Override
public Map<String, List<Catelog2Vo>> getCatalogJson(){
   System.out.println("查询了数据库.....");
   List<CategoryEntity> selectList = baseMapper.selectList(null);
   // 查询所有一级分类
   List<CategoryEntity> level1Categorys = getParent_cid(selectList,0L);
   // 封装数据
   Map<String, List<Catelog2Vo>> listMap = level1Categorys.stream().collect(Collectors.toMap(k -> k.getCatId().toString(), v -> {
       // 每一个一级分类,查到这个一级分类的二级分类
       List<CategoryEntity> level2Catelog = getParent_cid(selectList,v.getCatId());
       // 封装上面的结果集
       List<Catelog2Vo> catelog2Vos = null;
       if (level2Catelog != null) {
           catelog2Vos = level2Catelog.stream().map(l2 -> {
               Catelog2Vo catelog2Vo = new Catelog2Vo(v.getCatId().toString(), null, l2.getCatId().toString(), l2.getName());

               // 找到当前二级分类的三级分类封装成vo
               List<CategoryEntity> level3Catelog = getParent_cid(selectList,l2.getCatId());
               if (level3Catelog != null) {
                   List<Catelog2Vo.catelog3Vo> collect = level3Catelog.stream().map(l3 -> {
                       // 封装成指定格式
                       Catelog2Vo.catelog3Vo catelog3Vo = new Catelog2Vo.catelog3Vo(l2.getCatId().toString(), l3.getCatId().toString(), l3.getName());
                       return catelog3Vo;
                  }).collect(Collectors.toList());
                   catelog2Vo.setCatalog3List(collect);
              }
               return catelog2Vo;
          }).collect(Collectors.toList());
      }
       return catelog2Vos;
  }));
   return listMap;
}

SpringCache原理与不足

  1. 读模式:

  • 缓存穿透:查询一个null数据。解决方案:缓存空数据,可通过spring.cache.redis.cache-null-calues=true

  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。

    • 使用sync=true来解决击穿问题

  • 缓存雪崩:大量的key同时过期。解决:加随机时间,可通过spring.cache.redis.time-to-live=3600000

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

  • 读写加锁(适用于读多写少)

  • 引入Canal,感知到mysql的更新去更新redis

  • 读多写多,直接去数据库查询就行

总结

  • 常规数据(读多写少,及时性,一致性要求不高的数据,完全可以使用Spring-cache)

  • 写模式(只要缓存的数据有过期时间就够了)

  • 特殊模式:特殊设计

  •  
posted @ 2022-10-12 10:27  你比从前快乐;  阅读(185)  评论(0编辑  收藏  举报