0%

SpringCache 使用

1. 简介

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

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

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

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

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

2. 简单使用

下面我们使用底层实现采用 Redis 的 Cache 缓存技术

2.1 导入依赖

1
2
3
4
5
6
7
8
<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.2 application.yml 配置缓存

1) 配置 Redis 连接信息:

1
2
3
4
spring:
redis:
host: 192.168.160.129
port: 6379

2) 配置 cache 使用 redis:

1
2
3
spring:
cache:
type: redis

2.3 开启缓存

在启动类上加上缓存开启注解 @EnableCaching

1
2
3
4
5
6
7
8
9
@EnableCaching
@SpringBootApplication
public class GulimallProductApplication {

public static void main(String[] args) {
SpringApplication.run(GulimallProductApplication.class, args);
}

}

2.4 加缓存注解 @Cacheable

如下代码所示,在方法上加上 @Cacheable 注解后,会将该方法的返回值存储缓存中。当下一次请求调用该方法时,会先查询缓存是否有数据,有则直接将缓存中的数据进行返回。其中注解中的值 “category” 为缓存分区名称。该注解也可以指明 key 值,代表具体的缓存名称(可以这么理解吧~)

1
2
3
4
5
6
7
8
9
10
11
/**
* 使用 SpringCache 来实现缓存
* @return
*/
@Cacheable("category")
@Override
public List<CategoryEntity> getLevel1Categorys() {
System.out.println("getLevel1Categorys.....");
List<CategoryEntity> categoryEntities = baseMapper.selectList(new QueryWrapper<CategoryEntity>().eq("parent_cid", 0));
return categoryEntities;
}

执行后,redis 中的数据如下所示,其存在如下默认行为:

  1. key 值 “SimpleKey []” 是默认生成的;
  2. TTL 的值为 -1,表示该缓存永不过期;
  3. 缓存的数据在为乱码数据,因为底层默认使用 JDK 进行序列化(源码有体现)

图1-默认的缓存数据

针对上诉的默认行为,可以有针对解决方法:

1) 可以在注解 @Cacheable()中指定 key 的值,比如下述代码即可指定 key 的值为 categoryKey,因为 key 支持使用表达式,所以字符串时需要加单引号:

1
@Cacheable(value = "category", key = "'categoryKey'")

2) TTL 的值可以在配置文件中指定,比如下述代码配置:

1
2
## 设置 36s 后过期
spring.cache.redis.time-to-live=36000

重新执行后的效果如下:

针对缓存的数据在为乱码的问题,我们需要自定义配置类并更换底层的序列化机制,可以查看下文自定义缓存配置。

3. 自定义缓存配置

查看如下代码,可以使用自定义配置类 MyCacheConfig 实现自定义缓存配置:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

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

/**
* 自定义缓存配置类
* @param cacheProperties SpringCache 默认注入到 IOC 容器中的配置类对象
* @return
*/
@Bean
RedisCacheConfiguration redisCacheConfiguration(CacheProperties cacheProperties) {
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig();
// key 采用字符串序列化
config = config.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(new StringRedisSerializer()));
// value 采用 json 序列化
config = config.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(new GenericJackson2JsonRedisSerializer()));
CacheProperties.Redis redisProperties = cacheProperties.getRedis();
// 将配置文件中的所有配置都生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
// @Cacheable 的 key 前缀
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
// 是否缓存 null,防止缓存穿透
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
// @Cacheable 的 key 前缀是否开启
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}

查看第 27 行代码,这里我们指定缓存中的值采用 json 序列化器进行序列化,加上以上配置后 getLevel1Categorys 方法代码(查看 2.4)执行结果如下所示:

指定 json 序列化器

另外,查看 MyCacheConfig 类中的第 38 - 40 行代码,其用来读取配置文件是否配置允许缓存空值,默认为 true

4. @CacheEvict

与 @Cacheable 相反,@CacheEvict 则是从缓存中删除数据。注解 @CacheEvict 中的 value 表示要删除的缓存分区的名称,key 表示要删除的缓存名称,示例如下:

1
2
3
4
5
6
7
8
9
10
11
12
/**
* 更新分类和品牌分类关联表的分类名称
* @CacheEvict 采用失效模式来解决缓存一致性问题
* @param category
*/
@CacheEvict(value = "category", key = "'categoryKey'")
@Transactional(rollbackFor = Exception.class)
@Override
public void updateDetail(CategoryEntity category) {
this.updateById(category);
categoryBrandRelationService.updateCategoryName(category.getCatId(), category.getName());
}

@CacheEvict(value = "category", key = "'categoryKey'") 的作用就是当调用 updateDetail 时,会将缓存分区 category 下名为 categoryKey 缓存删掉。我们同样可以删除整个分区下的所有缓存分区,注解写法为:@CacheEvict(value = "category", allEntries = true)

5. @Caching

@Caching 注解支持组合多步操作(多步删除缓存、多步添加缓存等),比如可以使用 @Caching 组合多个删除缓存 @CacheEvict 操作,示例如下所示:

1
2
3
4
@Caching(evict = {
@CacheEvict(value = "category", key = "'categoryKey'"),
@CacheEvict(value = "category", key = "'getCatalogJson'")
})

6. @CachePut

该注解的作用就是在更新完成数据库时,其会自动将更新方法返回的最新数据添加进缓存中,相当于采用双写模式来解决缓存一致性问题,比如下述方法:

1
2
3
4
5
6
7
8
/**
* 更新完成数据库数据之后,返回最新数据。@CachePut 会将最新数据加入到缓存中
*/
@CachePut(value = "category", key = "'categoryKey'")
public List<CategoryEntity> updateDetail(CategoryEntity category) {
this.updateById(category);
this.list();
}

7. SpringCache 使用总结

7.1 不足之处

1)读模式:

  • 缓存穿透:查询一个 null 数据。解决:缓存空数据,使用 spring.cache.redis.cache-null-values=true

  • 缓存击穿:大量并发进来同时查询一个正好过期的数据。解决:加锁;SpringCache 默认是不加分布式锁的,但是 @Cacheable 注解支持使用 sync = true 来实现本地锁,可以解决一定程度的并发缓存击穿问题

  • 缓存雪崩:大量的 key 同时过期。解决:加随机时间。SpringCache 仅支持过期时间,例如 spring.cache.redis.time-to-live=3600000,因为随机过期时间也会出现 key 同时过期的情况。

2)写模式:(缓存与数据库一致)

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

总结:

  • 常规数据:读多写少,即时性和一致性要求不高的数据,完全可以使用Spring-Cache;对于写模式,只要缓存的数据有过期时间就足够了。
  • 特殊数据:特殊设计
------ 本文结束------