概述
使用 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 注解(在主配置類上需要標注上 @EnableCaching)
@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、對於特殊數據(比如要求高一致性)則需要特殊處理
