官方文档

https://docs.spring.io/spring-framework/docs/5.3.25-SNAPSHOT/reference/html/integration.html#cache

版本

该文章基于SpringBoot 2.5.7编写

引入依赖

1
2
3
4
5
6
7
8
9
10
11
<!-- spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

<!--jackson-datatype-jsr310用于解决LocalDateTime日期序列化问题-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

如果报错,则引入下面的依赖,删除上面引入的依赖

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
<!-- spring-boot-starter-data-redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
<exclusions>
<exclusion>
<groupId>io.lettuce</groupId>
<artifactId>lettuce-core</artifactId>
</exclusion>
</exclusions>
</dependency>

<!--jackson-datatype-jsr310用于解决LocalDateTime日期序列化问题-->
<dependency>
<groupId>com.fasterxml.jackson.datatype</groupId>
<artifactId>jackson-datatype-jsr310</artifactId>
</dependency>

<!-- spring-boot-starter-cache -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-cache</artifactId>
</dependency>

配置

Redis配置类

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
48
49
50
51
52
53
54
55
56
57
58
59
60
61
62
63
64
65
66
67
68
69
70
71
72
73
74
75
76
77
78
79
80
81
82
83
84
85
86
87
88
89
90
91
92
93
94
95
96
97
98
99
100
101
102
103
104
105
106
107
108
109
110
111
112
113
114
115
116
117
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.JsonInclude;
import com.fasterxml.jackson.annotation.JsonTypeInfo;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.SerializationFeature;
import com.fasterxml.jackson.databind.jsontype.impl.LaissezFaireSubTypeValidator;
import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.AutoConfigureBefore;
import org.springframework.boot.autoconfigure.cache.CacheProperties;
import org.springframework.boot.autoconfigure.data.redis.RedisAutoConfiguration;
import org.springframework.boot.context.properties.EnableConfigurationProperties;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
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.cache.RedisCacheManager;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;

import java.time.Duration;

/**
* redis配置
*/
@Configuration
@EnableCaching
@AutoConfigureBefore(RedisAutoConfiguration.class)
@EnableConfigurationProperties(CacheProperties.class)
public class RedisConfig extends CachingConfigurerSupport {

private StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();

/**
* Jackson2JsonRedisSerializer 序列化和反序列化效率高
*/
private Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = new Jackson2JsonRedisSerializer(Object.class);

/**
* 由于原生的redis自动装配,在存储key和value时,没有设置序列化方式,故自己创建redisTemplate实例
*/
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();

// 使用StringRedisSerializer来序列化和反序列化redis的key值
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);

// Hash的key也采用StringRedisSerializer的序列化方式
redisTemplate.setValueSerializer(jackson2JsonRedisSerializer);
redisTemplate.setHashValueSerializer(jackson2JsonRedisSerializer);

redisTemplate.setConnectionFactory(factory);

redisTemplate.afterPropertiesSet();

return redisTemplate;

}

@Bean
public CacheManager cacheManager(RedisConnectionFactory factory, CacheProperties cacheProperties) {
// 解决查询缓存转换异常的问题
ObjectMapper om = new ObjectMapper();
//POJO无public的属性或方法时,不报错
om.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS);
// null值字段不显示
om.setSerializationInclusion(JsonInclude.Include.NON_NULL);
om.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY);
// 序列化JSON串时,在值上打印出对象类型
// om.enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL);
// 替换上方 过期的enableDefaultTyping
om.activateDefaultTyping(LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.WRAPPER_ARRAY);
jackson2JsonRedisSerializer.setObjectMapper(om);

// 解决jackson2无法反序列化LocalDateTime的问题
om.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS);
om.registerModule(new JavaTimeModule());

// 配置序列化(解决乱码的问题)
RedisCacheConfiguration config = RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofDays(30))//设置缓存过期时间,如果spring.cache.time-to-live设置了值,将使用配置文件中的值
.serializeKeysWith(RedisSerializationContext.SerializationPair.fromSerializer(stringRedisSerializer))
.serializeValuesWith(RedisSerializationContext.SerializationPair.fromSerializer(jackson2JsonRedisSerializer))
//缓存前缀重新设置,使用spring-data-redis2.x版本时,@Cacheable缓存key值时默认会给value或cacheNames后加上双引号
.computePrefixWith(cacheName -> cacheName + ":")
.disableCachingNullValues();

CacheProperties.Redis redisProperties = cacheProperties.getRedis();

//将配置文件中 spring.cache 的所有配置都生效
if (redisProperties.getTimeToLive() != null) {
config = config.entryTtl(redisProperties.getTimeToLive());
}
if (redisProperties.getKeyPrefix() != null) {
config = config.prefixCacheNameWith(redisProperties.getKeyPrefix());
}
if (!redisProperties.isCacheNullValues()) {
config = config.disableCachingNullValues();
}
if (!redisProperties.isUseKeyPrefix()) {
config = config.disableKeyPrefix();
}

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

yml配置文件

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
spring:
cache:
type: redis
redis:
#time-to-live: 100000 # 单位为毫秒 1:1000
# 如果指定缓存前缀,则使用下面配置的名字作为缓存前缀(不建议配置),如果没有配置,则使用 @Cacheable 中的 value / cacheNames 配置的值作为缓存前缀
# key-prefix: user_
# 是否使用缓存前缀(key-prefix)。true:使用。false:不使用
use-key-prefix: true
# 是否缓存空值。防止缓存穿透
cache-null-values: true
redis:
host: 127.0.0.1
port: 6379
password: 123456
database: 0
# lettuce redis数据库连接池和jedis二选一配置,如果用lettuce,还需要再引入org.apache.commons下的commons-pool2
# lettuce:
# pool:
# max-active: 100 # 连接池最大连接数(使用负值表示没有限制)
# max-idle: 100 # 连接池中的最大空闲连接
# min-idle: 10 # 连接池中的最小空闲连接
# max-wait: 5000ms # 连接池最大阻塞等待时间(使用负值表示没有限制)
#jedis数据库连接池,和lettuce二选一配置
jedis:
pool:
max-active: 100
max-idle: 100
max-wait: 50
min-idle: 10

测试类

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
    /**
* 根据id批量删除
*
* @param ids id集合
* @return true:删除成功。false:删除失败
*/
//@CacheEvict(cacheNames = "user", key = "'all1'")
/*@Caching(
evict = {
@CacheEvict(cacheNames = "user", key = "'all1'"),
@CacheEvict(cacheNames = "user", key = "'all2'")
}
)*/
@CacheEvict(cacheNames = "user", allEntries = true)
@Override
public Boolean delete(List<Long> ids) {
return removeByIds(ids);
}

/**
* 查询全部1
*
* @return 全部数据
*/
@Cacheable(cacheNames = "user", key = "'all1'")
@Override
public List<UserEntity> selectAll1() {
return this.list();
}

/**
* 查询全部2
*
* @return 全部数据
*/
@Cacheable(cacheNames = "user", key = "'all2'")
@Override
public List<UserEntity> selectAll2() {
return this.list();
}
}

SpringCache常用注解

使用org.springframework.cache.annotation包下面的注解

  • @Cacheable:添加缓存
  • @CacheEvict:删除缓存
  • @CachePut:更新缓存
  • @Caching:组合以上@Cacheable、@CacheEvict、@CachePut等操作
  • @CacheConfig:在类级别共享缓存的相同配置

@Cacheable

  • 默认属性
    • 缓存中有:获取缓存。
    • 缓存中没有:先查询,再缓存,然后返回
    • 缓存key:默认自动生成。缓存名字::SimpleKey []
    • 缓存value值,默认使用jdk序列化机制,将序列化的值存入redis中
    • 默认过期时间:不过期
  • 自定义
    • 指定生成的缓存使用的key:key属性指定。接收一个SpEL,如果需要传一个字符串,使用''包裹【@Cacheable(key = "'all'")】
    • 指定缓存的存活时间:指定spring.cache.time-to-live的值,单位为毫秒 1:1000
    • 将数据保存为JSON:

指定缓存名称和缓存key

指定生成的缓存使用的key:key属性指定。接收一个SpEL,如果需要传一个字符串,使用''包裹【@Cacheable(key = "'all'")】

字符串缓存key

1
@Cacheable(cacheNames = "user", key = "'all'")

SpEL生成缓存key

官方文档:https://docs.spring.io/spring-framework/docs/5.3.25-SNAPSHOT/reference/html/integration.html#cache-spel-context

1
@Cacheable(cacheNames = "user", key = "#root.methodName")

指定缓存时间

指定缓存的存活时间:指定spring.cache.time-to-live的值,单位为毫秒 1:1000

1
2
3
4
5
spring:
cache:
type: redis
redis:
time-to-live: 100000 # 单位为毫秒 1:1000

@CacheEvict

删除单个缓存

指定缓存名称和缓存key

1
@CacheEvict(cacheNames = "user", key = "'all1'")

删除多个缓存

使用@Caching

使用@Caching,指定多个@CacheEvict,对缓存一一删除

1
2
3
4
5
6
@Caching(
evict = {
@CacheEvict(cacheNames = "user", key = "'all1'"),
@CacheEvict(cacheNames = "user", key = "'all2'")
}
)

使用@CacheEvict的allEntries

将allEntries设置为true后,只需要@Cacheable中cacheNames相同的将会被全部删除

1
@CacheEvict(cacheNames = "user", allEntries = true)

SpringCache 原理与不足

原理:CacheManager(RedisCacheManager)——>Cache(RedisCache)——>Cache负责缓存的读写

  • 读模式

    • 缓存穿透:查询一个null数据。
      • 解决方案:缓存空数据,可通过spring.cache.redis.cache-null-values=true
    • 缓存击穿:大量并发进来同时查询一个正好过期的数据。
      • 解决方案:加锁 ? 默认是无加锁的;使用sync = true来解决击穿问题
    • 缓存雪崩:大量的key同时过期。
      • 解决方案:加随机时间。加上过期时间:spring.cache.redis.time-to-live=3600000
  • 写模式

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

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