Spring Cache的基本使用與分析

農夫三拳有點疼~發表於2020-05-16

概述

使用 Spring Cache 可以極大的簡化我們對資料的快取,並且它封裝了多種快取,本文基於 redis 來說明。

基本使用

1、所需依賴

<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、配置檔案

spring:
  # redis連線資訊
  redis:
    host: 192.168.56.10
    port: 6379
  cache:
    # 指定使用的快取型別
    type: redis
    # 過期時間
    redis:
      time-to-live: 3600000
      # 是否開啟字首,預設為true
      use-key-prefix: true
      # 鍵的字首,如果不配置,預設就是快取名cacheNames
      key-prefix: CACHE_
      # 是否快取空置,防止快取穿透,預設為true
      cache-null-values: true

3、Spring Cache 提供的註解如下,使用方法參見:官方文件,通過這些註解,我們可以方便的操作快取資料。

  • @Cacheable:觸發快取寫入的操作
  • @CacheEvict:觸發快取刪除的操作
  • @CachePut:更新快取,而不會影響方法的執行
  • @Caching:重新組合要應用於一個方法的多個快取操作,即對一個方法新增多個快取操作
  • @CacheConfig:在類級別共享一些與快取有關的常見設定

例如,如果需要對返回結果進行快取,直接在方法上標註 @Cacheable 註解

@Cacheable(cacheNames = "userList") //指定快取的名字,便於區分不同快取
public List<User> getUserList() {
	...
} 

4、redis 預設使用 jdk 序列化,需要我們配置序列化機制,自定義一個配置類,否則存入的資料顯示亂碼

@EnableCaching //開啟快取
@Configuration
public class MyCacheConfig {
    @Bean
    public RedisCacheConfiguration redisCacheConfiguration(){
        RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
        //指定鍵和值的序列化機制
        config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
        config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
        return config;
    }
}

5、使用以上配置後,雖然亂碼的問題解決了,但配置檔案又不生效了,比如過期時間等,這是因為在初始化時會判斷使用者是否自定義了配置檔案,如果自定義了,原來的就不會生效,原始碼如下:

private org.springframework.data.redis.cache.RedisCacheConfiguration
    determineConfiguration(ClassLoader classLoader) {
    //如果配置了,就返回自定義的配置
    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;
}

6、所以,我們也需要手動獲取 ttl、prefix 等屬性,直接仿照原始碼就行,將配置類修改為如下:

@EnableCaching //開啟快取
@Configuration
@EnableConfigurationProperties(CacheProperties.class) //快取的所有配置屬性都在這個類裡
public class MyCacheConfig {

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

原理分析

在 Spring 中 CacheManager 負責建立管理 Cache,Cache 負責快取的讀寫,因此使用 redis 作為快取對應的就有 RedisCacheManager 和 RedisCache。

開啟 RedisCache 原始碼,我們需要注意這兩個方法:

1、讀取資料,未加鎖

@Override
protected Object lookup(Object key) {
   byte[] value = cacheWriter.get(name, createAndConvertCacheKey(key));
    
   if (value == null) {
      return null;
   }
    
   return deserializeCacheValue(value);
}

2、讀取資料,加鎖,這是 RedisCache 中唯一一個同步方法

@Override
public synchronized <T> T get(Object key, Callable<T> valueLoader) {
   ValueWrapper result = get(key);
    
   if (result != null) {
      return (T) result.get();
   }
    
   T value = valueFromLoader(key, valueLoader);
   put(key, value);
   return value;
}

通過打斷點的方式可以知道 RedisCache 預設呼叫的是 lookup(),因此不能應對快取穿透,如果有相關需求,可以這樣配置:@Cacheable(sync = true),開啟同步模式,此配置只在 @Cacheable 中才有。

總結

Spring Cache 對於讀模式下快取失效的解決方案:

  • 快取穿透:cache-null-values: true,允許寫入空值
  • 快取擊穿:@Cacheable(sync = true),加鎖
  • 快取雪崩:time-to-live:xxx,設定不同的過期時間

而對於寫模式,Spring Cache 並沒有相應處理,我們需要使用其它方式處理。


總的來說:

1、對於常規資料(讀多寫少,及時性、一致性要求不高的資料)完全可以使用 Spring Cache

2、對於特殊資料(比如要求高一致性)則需要特殊處理

相關文章