Spring Cache

学习Spring Cache的使用,结合Redis,完成对数据的缓存

1.添加依赖

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

此外,Spring Cache支持缓存品种多,常见缓存Redis、EhCache、Caffeine均支持。它们之间既能独立使用,也能组合使用。

这里使用最常用的Redis作为缓存数据库

1
2
3
4
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

确保Redis配置正确,能正常连接后即可使用SpringCache

2.常用注解

@EnableCaching

@EnableCaching开启缓存注解功能,通常加在启动类上

@Cacheable

@Cacheble注解表示这个方法有了缓存的功能,方法的返回值会被缓存下来,下一次调用该方法前,会去检查是否缓存中已经有值,如果有就直接返回,不调用方法。如果没有,就调用方法,然后把结果缓存起来。这个注解一般用在查询方法上

@CachePut

加了@CachePut注解的方法,会把方法的返回值put到缓存里面缓存起来,供其它地方使用。它通常用在新增方法上

@CacheEvict

使用了CacheEvict注解的方法,会清空指定缓存。一般用在更新或者删除的方法上

3.开启缓存

在启动类加上@EnableCaching注解即可开启使用缓存。

4.加缓存注解

根据情况为接口加上@Cacheable@CachePut@CacheEvict 注解

注解内的参数cacheNameskey共同拼接为存入的缓存的名字,其中key以SpEL的方式调用传入的参数

如下面这个(cacheNames = "userCache",key = "#user.id"),传入id=1,存入后缓存的key为userCache::1

@CachePut

通常用在新增方法上

通过调试后,可以在mysql和redis中看到对应的数据,说明缓存成功

@Cacheable

通常用在查询方法上

调用接口后,通过断点调试可以发现,当发送/user的GET请求时,若缓存已经存在,则不会调用到userMapper.getById(id)

@CacheEvict

通常用在更新或者删除方法上

如第一个接口,当请求删除id为3的用户时,redis中相对应的userCache::3也会被删除

第二个接口中,使用参数allEntries=true,那么当调用该接口时,所有的userCache都会被删除

5.常用配置项

1
2
3
4
5
6
spring:
cache:
type: redis #指定使用redis
redis:
time-to-live: 3600000 #单位ms,3600000是一小时
cache-null-values: true #是否缓存空值,缓存空值可以解决缓存穿透问题

6.概念补充

6.1缓存穿透

缓存穿透简单来说是一种恶意攻击,当客户端通过访问接口,故意大量地去查询一个不存在的数据时,会经过以下步骤:

​ ①访问缓存,发现没有该数据

​ ②访问数据库进行查询,查询不到数据,自然也不会缓存数据

由于这种不存在的数据没有缓存,所以每一次访问接口都会去数据库查询,当客户端恶意地多次访问该接口就会对数据库造成压力

解决方案

①缓存空值:springcache中默认会实现这种方式

②使用布隆过滤器

….

6.2缓存击穿

而且缓存击穿是一个热点的Key,有大并发集中对其进行访问,突然间这个Key失效了,导致大并发全部打在数据库上,导致数据库压力剧增。这种现象就叫做缓存击穿。

解决方案

①如果业务允许的话,对于热点的key可以设置永不过期的key。

②使用互斥锁。如果缓存失效的情况,只有拿到锁才可以查询数据库,降低了在同一时刻打在数据库上的请求。当然这样会导致系统的性能变差。在SpringCache中的实现方法为 加一个属性“sync=true”,如下

@Cacheable(cacheNames = “shop”,key = “#id”,sync = true)

区别:缓存穿透是请求缓存和数据库都没有的数据,而缓存击穿是请求缓存中没有,而数据库中有的数据

6.3缓存雪崩

缓存雪崩是,当某一个时刻出现大规模的缓存失效的情况,那么就会导致大量的请求直接打在数据库上面,导致数据库压力巨大,如果在高并发的情况下,可能瞬间就会导致数据库宕机。这时候如果运维马上又重启数据库,马上又会有新的流量把数据库打死。

解决方案

①在设置key的时候,使用随机的TTL,避免大量key同时失效。

②可以搭建Redis集群,提高Redis的容灾性

③给业务添加多级缓存

7.后续问题解决

报错:对象无法序列化

在新项目中发现,当使用Redis作为缓存时,执行以下方法会报错:

2023-11-30 10:25:08.043 ERROR 14556 — [nio-8081-exec-1] com.hmdp.config.WebExceptionAdvice : org.springframework.data.redis.serializer.SerializationException: Cannot serialize; nested exception is org.springframework.core.serializer.support.SerializationFailedException: Failed to serialize object using DefaultSerializer; nested exception is java.lang.IllegalArgumentException: DefaultSerializer requires a Serializable payload but received an object of type [com.hmdp.dto.Result]

这个错误是说,使用Redis作为缓存时,尝试序列化一个不支持的Java对象类型(Result)。

查看Result类,发现是由于没有实现序列化的接口

只要添加上 implements Serializable 即可解决这个问题

查看存储对象为乱码

当存储一个自定义的对象时,进入redis查看,发现他的数据保存格式是Hex,导致内容不可见。

这个问题可以通过在配置类中添加以下配置解决,将value存入时的JDK序列化改为json格式

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
@SuppressWarnings("rawtypes")
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair
.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues();

RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}

time-to-live不生效

可能是由于在部分Springboot的版本中不支持通过配置文件设置Redis缓存的TTL。

可以通过代码的形式配置,类似上面的 “查看存储对象为乱码” 中的配置,直接在上面代码中添加即可

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
@SuppressWarnings("rawtypes")
@Bean
public CacheManager cacheManager(RedisConnectionFactory factory) {
RedisSerializer<String> redisSerializer = new StringRedisSerializer();
Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new
Jackson2JsonRedisSerializer(Object.class);

//解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
jackson2JsonRedisSerializer.setObjectMapper(om);

// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration
.defaultCacheConfig()
.serializeKeysWith(RedisSerializationContext
.SerializationPair
.fromSerializer(redisSerializer))
.serializeValuesWith(RedisSerializationContext
.SerializationPair
.fromSerializer(jackson2JsonRedisSerializer))
.disableCachingNullValues()
//只需要在这里添加一句
//下面这一句是设置ttl为1个小时
.entryTtl(Duration.ofHours(1L));

RedisCacheManager cacheManager = RedisCacheManager.builder(factory)
.cacheDefaults(config)
.build();
return cacheManager;
}