4.商城业务-Spring Cache解析及应用

173 阅读6分钟

本文我们将介绍一下SpringCache的相关内容

学习网址:docs.spring.io/spring-fram…

1.介绍

  • 就像 Spring Framework 中的其他服务一样,缓存服务是一个抽象(不是缓存实现)并且需要使用实际存储来存储缓存数据——也就是说,抽象使开发人员不必编写缓存逻辑并且不用提供真正的存储。这种抽象是由org.springframework.cache.Cacheorg.springframework.cache.CacheManager接口具体化的。

  • Cache接口为缓存的组件规范定义,包含缓存的各种操作集合;有一些开箱即用的抽象实现:基于ConcurrentMap的缓存、 EhCache、RedisCache等。

  • 通俗来讲,Cache接口就是定义了对缓存数据的增删改查

image-20230106161301252.png

  • 有很多缓存产品可以用作后备存储,但需要实现CacheManager接口。该接口只有两个方法:

    // 根据指定的名称得到对应缓存
    @Nullable
    Cache getCache(String name);
    
    // 得到此缓存管理器中所有的缓存名称
    Collection<String> getCacheNames();
    
  • 实现了CacheManager接口的各个缓存产品

image-20230106162457537.png

整体结构

image-20230106162519428.png

总结:CacheManager实现类中包含了多个cache(需要指定名称【我自己将其理解为文件夹】),而这些cache是由Cache来负责读写的。

2.介绍一下RedisCacheManager

可以看到在这个类中声明了一个集合用来存储缓存数据

private final Map<String, RedisCacheConfiguration> initialCacheConfiguration;

像ConcurrentMapCacheManager也有类似集合private final ConcurrentMap<String, Cache> cacheMap = new ConcurrentHashMap<>(16);

两个重要的方法---下面讲解自配配置的时候会再次提到

initialCacheNames()

public RedisCacheManagerBuilder initialCacheNames(Set<String> cacheNames) {

    Assert.notNull(cacheNames, "CacheNames must not be null!");

    cacheNames.forEach(it -> withCacheConfiguration(it, defaultCacheConfiguration));
    return this;
}

withCacheConfiguration()----将cacheName以及对应和的cacheConfiguration(包含了一些基础配置ttl、cacheNullValues、keyPrefix、usePrefix、keySerializationPair等)存放进集合中

public RedisCacheManagerBuilder withCacheConfiguration(String cacheName,
      RedisCacheConfiguration cacheConfiguration) {

   Assert.notNull(cacheName, "CacheName must not be null!");
   Assert.notNull(cacheConfiguration, "CacheConfiguration must not be null!");

   this.initialCaches.put(cacheName, cacheConfiguration);
   return this;
}

3.实操

1)引入cache、redis的依赖

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-cache</artifactId>
</dependency>

2)配置相关内容

让我们先看一下CacheAutoConfiguration中自动配置了哪些内容

CacheAutoConfiguration中将所有类型的缓存保存起来

static class CacheConfigurationImportSelector implements ImportSelector {
    @Override
    public String[] selectImports(AnnotationMetadata importingClassMetadata) {
        CacheType[] types = CacheType.values();
        String[] imports = new String[types.length];
        for (int i = 0; i < types.length; i++) {
            imports[i] = CacheConfigurations.getConfigurationClass(types[i]);
        }
        return imports;
    }
}

CacheConfigurations类的静态代码块中保存着各个缓存类型

static {
   Map<CacheType, Class<?>> mappings = new EnumMap<>(CacheType.class);
   mappings.put(CacheType.GENERIC, GenericCacheConfiguration.class);
   mappings.put(CacheType.EHCACHE, EhCacheCacheConfiguration.class);
   mappings.put(CacheType.HAZELCAST, HazelcastCacheConfiguration.class);
   mappings.put(CacheType.INFINISPAN, InfinispanCacheConfiguration.class);
   mappings.put(CacheType.JCACHE, JCacheCacheConfiguration.class);
   mappings.put(CacheType.COUCHBASE, CouchbaseCacheConfiguration.class);
   mappings.put(CacheType.REDIS, RedisCacheConfiguration.class);
   mappings.put(CacheType.CAFFEINE, CaffeineCacheConfiguration.class);
   mappings.put(CacheType.SIMPLE, SimpleCacheConfiguration.class);
   mappings.put(CacheType.NONE, NoOpCacheConfiguration.class);
   MAPPINGS = Collections.unmodifiableMap(mappings);
}

如果使用的是Redis缓存【需要我们在配置文件中指明spring.cache.type=redis】,自然会导入RedisCacheConfiguration配置,而RedisCacheConfiguration中就引入了RedisCacheManager

@Bean
RedisCacheManager cacheManager(CacheProperties cacheProperties, CacheManagerCustomizers cacheManagerCustomizers,
      ObjectProvider<org.springframework.data.redis.cache.RedisCacheConfiguration> redisCacheConfiguration,
      ObjectProvider<RedisCacheManagerBuilderCustomizer> redisCacheManagerBuilderCustomizers,
      RedisConnectionFactory redisConnectionFactory, ResourceLoader resourceLoader) {
   RedisCacheManagerBuilder builder = RedisCacheManager.builder(redisConnectionFactory).cacheDefaults(
         determineConfiguration(cacheProperties, redisCacheConfiguration, resourceLoader.getClassLoader()));
   List<String> cacheNames = cacheProperties.getCacheNames();
   if (!cacheNames.isEmpty()) {
      builder.initialCacheNames(new LinkedHashSet<>(cacheNames));
   }
   redisCacheManagerBuilderCustomizers.orderedStream().forEach((customizer) -> customizer.customize(builder));
   return cacheManagerCustomizers.customize(builder.build());
}

上面就用到了我们之前提到的RedisCacheManager中的两个方法,将缓存名字及其配置【缓存规则】存入集合中,在定义缓存规则的时候,使用的是默认缓存配置

private org.springframework.data.redis.cache.RedisCacheConfiguration createConfiguration(
      CacheProperties cacheProperties, ClassLoader classLoader) {
   Redis redisProperties = cacheProperties.getRedis();
    // 拿到默认缓存配置的对象
   org.springframework.data.redis.cache.RedisCacheConfiguration config = org.springframework.data.redis.cache.RedisCacheConfiguration
         .defaultCacheConfig();
   config = config.serializeValuesWith(
         SerializationPair.fromSerializer(new JdkSerializationRedisSerializer(classLoader)));
   // redisProperties就是从配置文件中拿到对应的配置信息
    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;
}

4.缓存注解

  • @Cacheable:触发将数据保存到缓存的操作
  • @CachePut:不影响方法执行更新缓存
  • @CacheEvict:触发将数据从缓存删除的操作
  • @Caching:组合以上多种缓存操作
  • @CacheConfig:在类(class)级别共享缓存的相同配置
  • @EnableCaching:用于开启缓存功能

4.1.@Cacheable注解

用在方法上,代表当前方法的结果需要缓存。如果缓存中有,方法不再调用;如果缓存中没有,会调用方法,最终将方法的结果放入缓存;用在类上表示:表示该类的所有方法都支持该注解。

属性说明

  • value:每一个需要缓存的数据都指定放到哪个名字中的缓存(可放多个),这里名字最好是按照具体的业务类型划分。如@Cacheable({"category"})

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

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

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

  • sync:读取数据时,使用加了synchronized的get方法。

默认配置

  • redis中的key如果不指定会自动生成,格式:缓存的名字::SimpleKey []
  • 缓存的value,默认使用的是jdk序列化机制,将序列化后的数据存到redis;
  • 默认ttl时间-1----永不过期;

自定义配置

  • 自定义key

  • 指定缓存数据存活时间:@Cacheable没有相关属性,需要在配置文件中进行配置spring.cache.redis.time-to-live

  • 为了兼容,将数据保存为json格式,修改序列化配置。

在2小节我们提到了将缓存名字存放近缓存集合中时,加入的配置是默认缓存配置defaultCacheConfiguration;如果我们在容器中存放了自己的RedisCacheConfiguration,该配置就会应用到当前RedisCacheManager管理的所有缓存分区中

image-20230106181946069.png

注意

  • 如果我们只配置了Key和Value的序列化器,运行之后发现在配置文件中配置的ttl不生效了,为-1。

  • 因此我们需要将默认缓存配置中的内容拿到

    配置文件所使用的CacheProperties,没有放到容器中;因此我们需要使用注解 @EnableConfigurationProperties

    @ConfigurationProperties(prefix = "spring.cache")    没有放在容器中
    public class CacheProperties {
    

自定义配置编写

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

//    @Autowired
//    CacheProperties cacheProperties;

    @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;
    }
}

4.2.@CacheEvict注解

  • 利用失效模式保证缓存一致性。
  • 用来标注在需要清除缓存元素的方法或类上的。
  • 当数据库中的数据有更新时,会先清除缓存。在下次访问页面时再调用方法将数据存放到缓存中。
  • 当标记在一个类上时表示其中所有的方法的执行都会触发缓存的清除操作。

属性说明:

  • allEntries:清除当前value值空间下的所有缓存数据
  • beforeInvocation:执行这个方法之前执行清除缓存的操作;以免后续代码抛出异常导致未能清除缓存,下次查询时依旧从缓存中去读取,这时查询到的结果值是删除操作之前的值。

4.3.@CachePut

  • 利用双写模式保证缓存一致性。
  • 标注的方法(如果标注在类上,就表示类中所有的方法)在执行前不会去检查缓存中是否存在之前执行过的结果,而是每次都会执行该方法,并将执行结果以键值对的形式存入指定的缓存中。

5.Spring Cache的不足&总结

在读模式下,会出现缓存穿透缓存击穿以及缓存雪崩的问题

  • 缓存穿透:缓存空数据。spring.cache.redis.cache-null-values=true
  • 缓存击穿:Spring Cache默认是无加锁的,但@Cacheable中有一个sync(默认时false)属性,可同步读取数据
  • 缓存雪崩:加上过期时间。spring.cache.redis.time-to-live=3600000

写模式下,如何保证缓存和数据库数据一致呢?

  • 读多写少,可利用读写锁
  • 引入Canal,感知到MySQL更新会去更新缓存。
  • 读多写多,直接读取数据库就可以。

总结:缓存适合存放读多写少,即时性不高,一致性要求不高的数据使用Spring Cache完全足够。针对写模式,保证缓存的数据有过期时间一般就足够了。对于特殊数据特殊处理即可。分布式锁相对来说影响性能。

声明

  • 本文仅仅作为个人学习笔记;
  • 文章内容主要来自尚硅谷项目视频;