Spring Cache 是什么?
首先我们需要明确一点:Spring Cache 不是一个具体的缓存实现方案,而是一个对缓存使用的抽象,类似Java的JDBC。是定义了一套规范。第三方缓存需要实现这套规范,才能通过Spring API使用缓存功能。它利用了AOP,实现了基于注解的缓存功能,并且进行了合理的抽象,业务代码不用关心底层是使用了什么缓存框架,只需要简单的加一个注解,就能实现缓存功能了。
为什么要使用SpringCache
我们使用缓存一般有以下步骤:
- 查询缓存中是否存在数据,如果存在则直接返回结果
- 如果不存在则查询数据库,查询出结果后将结果存入缓存并返回结果
- 数据更新时,先更新数据库然后更新缓存或者直接删除缓存
此时,我们会发现一个问题,所有我们需要使用缓存的地方都按照这个步骤去书写,这样就会出现很多逻辑上相似的代码。并且我们程序里面也需要去调用第三方的缓存中间件的API,如此一来就大大增加了我们项目和第三方中间件的耦合度。
在Spring Boot项目中使用Spring ache 的操作步骤(使用 Redis 缓存技术)
导入maven坐标
spring-boot-starter-data-redis、spring-boot-starter-cache
配置application.yml
spring: cache: redis: time-to-live: 1800000 #设置缓存有效期,单位是毫秒
在启动类上加入@EnableCaching注解,开启注解缓存功能
在Controller的方法上加入@Cacheable、@CacheEvict等注解,进行缓存操作
Spring Cache 常用注解
- @EnableCaching: 开启缓存功能,一般使用在SpringBoot的启动类或配置类上
- @Cacheable: 使用缓存。在方法执行前Spring先查看缓存中是否有数据,如果有数据,则直接返回缓存数据;没有则调用方法并将方法返回值放进缓存。
@Cacheable属性名 | 用途 | 备注 |
---|---|---|
cacheNames或value | 指定缓存的名称,不同缓存空间的数据是彼此隔离的 | |
key | 同一个缓存空间中通过key区别不同的缓存,如果指定要按照SpEL表达式编写,如果不指定,则缺省按照方法的所有参数进行组合,如:@CachePut(value = “demo”, key = " ‘user_’ + #user.id"),字符串中Spring表达式以外的字符串部分需要用单引号。 | |
condition | condition是在调用方法之前进行条件判断,满足条件才缓存 | @Cacheable(value = “book”, condition = “#name.length() < 32”) |
unless | unless 是在调用方法之后判断条件,如果条件成立,则不缓存 | |
sync | 缓存过期之后,如果多个线程同时请求对某个数据的访问,会同时去到数据库,导致数据库瞬间负荷增高。Spring4.3为@Cacheable 注解提供了一个新的参数"sync"(boolean类型,默认为false)。当设置它为true时,只有一个线程的请求会到数据库,其他线程都会等待直到缓存可用。这个设置可以减少对数据库的瞬间并发访问。 |
@CachePut: 更新缓存,将方法的返回值放到缓存中
@CacheEvict: 清空缓存,一般用在更新或者删除的方法上
如果想删除多个缓存,可以使用allEntries属性,并设置为true
@Caching: 组合定义多种缓存功能
Java注解的机制决定了,一个方法上只能有一个相同的注解失效。那有时候可能一个方法会操作多个缓存(这个在删除缓存操作中比较常见,在添加操作中不太常见)。
@CacheConfig: 用于标注在类上,可以存放该类中所有缓存的公有属性,比如设置缓存空间的名字value、key的生成策略keyGenerator等。
Spring Cache的使用注意事项
@CacheEvict注解中的allEntries = true 属性会将当前缓存空间的所有缓存数据全部清楚,请谨慎使用
@CacheEviet 注解适用于更新完数据库数据后删除缓存
@CachePut 注解适用于双写模式,更新完数据库后写入缓存中
配置文件中
cache: redis: time-to-live: 1800000 #设置缓存有效期,单位是毫秒 cache-null-values: true #设置缓存null值,可以有效防止缓存穿透
读模式下的缓存失效处理方案:
- 缓存穿透:cache-null-values: true,允许写入空值
- 缓存击穿:@Cacheable(sync = true),加锁
- 缓存雪崩:time-to-live: xxxx ,设置不同的过期时间
双写不一致:
使用缓存会带来许多问题,尤其是高并发下,双写不一致,这是一个比较常见的问题,其中一个常用的解决方案是,更新的时候,先删除缓存再更新数据库。所以Spring Cache 的@CacheEvict 会有一个beforeInvocation属性的设置,当这个值为true时,无论方法结果如何(即它是否抛出异常)都会导致删除缓存事件发生。默认为 false,这意味着缓存驱逐操作将在成功调用方法后发生(即仅当调用未引发异常时)。
如果你去看了Cacheable中sync属性上方的注释,你会发现它写到:当使用sync为true,会有这些限制
- 不支持unless()
- 只能指定一个缓存
- 不能支持其他缓存相关操作,只支持Cacheable
提示:
1、对于常规数据(读多写少,及时性、一致性要求不高的数据)完全可以使用 Spring Cache
2、对于特殊数据(比如要求高一致性)则需要特殊处理使用Spring Cache也有不好之处,就是屏蔽了底层缓存的特性。比如,很难做到不同的场景有不同的过期时间(但并不是做不到,也可以通过配置不同的cacheManager来实现)。但整体上来看,还是利大于弊的,大家仁者见仁智者见智,适合自己就好。
添加配置类,设置序列化方式
当我们从普通的RedisTemplate这种调用第三方的API的方式去实现缓存功能 转变到 使用注解的方式实现缓存,我们应该要清晰地认识到我们之前配置的序列化配置类已经不起作用了,我们此时需要额外添加一个适用于注解的序列化的配置类
@Configuration
@EnableConfigurationProperties(CacheProperties.class) // 加载缓存配置类
public class MyRedisCacheConfig {
@Bean
public 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());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixKeysWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}
return config;
}
}