在 Spring Boot 中 使用 Redis
本文中的代码见 spring-boot-redis。
Redis 本身的一些概念
Redis 支持的数据结构
- String 字符串
- Hash 字典
- List 列表
- Set 集合
- Sorted Set 有序集合
String 和 Hash 的对比
String 实际是就是一个 Key - Value
的映射;
Hash 就是一个 Key - (Key - Value)
的两层映射。
1 | # redis-cli |
STACK OVERFLOW 上一个对 String 和 Hash 的讨论
对于一个对象是把本身的数据序列化后用 String 存储,还是使用 Hash 来分别存储对象的各个属性:
- 如果在大多数时候要访问对象的大部分数据:使用 String
- 如果在大多数时候只要访问对象的小部分数据:使用 Hash
- 如果对象里面还有对象这种结构复杂的,最好用 String。否则最外层用 Hash,里面又将对象序列化,两者混用可能导致混乱。
Spring Boot 添加 Redis 的配置
以 gradle 为例。
修改
build.gradle
1
compile("org.springframework.boot:spring-boot-starter-data-redis")
修改
application.yml
1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
17
18spring:
# redis
redis:
host: 127.0.0.1
# 数据库索引(默认为0)
database: 0
port: 6379
password: PASSWORD
# 连接池中的最大空闲连接
pool.max-idle: 8
# 连接池中的最小空闲连接
pool.min-idle: 0
# 连接池最大连接数(使用负值表示没有限制)
pool.max-active: 8
# 连接池最大阻塞等待时间(使用负值表示没有限制)
pool.max-wait: -1
# 连接超时时间(毫秒)
timeout: 0添加
RedisConfig
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
118
119
120
121
122
123
124
125
126
127
128
129
130package zz.config;
import com.fasterxml.jackson.annotation.JsonAutoDetect;
import com.fasterxml.jackson.annotation.PropertyAccessor;
import com.fasterxml.jackson.databind.ObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.CacheManager;
import org.springframework.cache.annotation.CachingConfigurerSupport;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.cache.interceptor.KeyGenerator;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
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.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.Jackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.lang.reflect.Method;
import java.util.HashMap;
import java.util.Map;
/**
* RedisConfig
*
* @author zz
* @date 2018/5/7
*/
4j
public class RedisConfig extends CachingConfigurerSupport {
public KeyGenerator wiselyKeyGenerator() {
return new KeyGenerator() {
private static final String SEPARATE = ":";
public Object generate(Object target, Method method, Object... params) {
log.debug("+++++generate");
StringBuilder sb = new StringBuilder();
sb.append(target.getClass().getName());
sb.append(SEPARATE).append(method);
for (Object obj : params) {
sb.append(SEPARATE).append(obj);
}
return sb.toString();
}
};
}
/**
* https://www.jianshu.com/p/9255b2484818
*
* TODO: 对 Spring @CacheXXX 注解进行扩展:注解失效时间 + 主动刷新缓存
*/
public CacheManager cacheManager(@SuppressWarnings("rawtypes") RedisTemplate redisTemplate) {
log.debug("++++cacheManager");
RedisCacheManager redisCacheManager =new RedisCacheManager(redisTemplate);
redisCacheManager.setTransactionAware(true);
redisCacheManager.setLoadRemoteCachesOnStartup(true);
// 最终在 Redis 中的 key = @Cacheable 注解中 'cacheNames' + 'key'
redisCacheManager.setUsePrefix(true);
// 所有 key 的默认过期时间,不设置则永不过期
// redisCacheManager.setDefaultExpiration(6000L);
// 对某些 key 单独设置过期时间
// 这里的 key 是 @Cacheable 注解中的 'cacheNames'
Map<String, Long> expires = new HashMap<>(10);
// expires.put("feedCategoryDto", 5000L);
// expires.put("feedDto", 5000L);
redisCacheManager.setExpires(expires);
return redisCacheManager;
}
// value serializer
private Jackson2JsonRedisSerializer getJackson2JsonRedisSerializer() {
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);
return jackson2JsonRedisSerializer;
}
private GenericJackson2JsonRedisSerializer getGenericJackson2JsonRedisSerializer() {
return new GenericJackson2JsonRedisSerializer();
}
/**
*
* Once configured, the template is thread-safe and can be reused across multiple instances.
* -- https://docs.spring.io/spring-data/data-redis/docs/current/reference/html/
*/
public RedisTemplate<String, String> redisTemplate(RedisConnectionFactory factory) {
log.debug("++++redisTemplate");
StringRedisTemplate template = new StringRedisTemplate(factory);
// key serializer
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
RedisSerializer valueRedisSerializer;
// -- 1 Jackson2JsonRedisSerializer
// valueRedisSerializer = getJackson2JsonRedisSerializer();
// -- 2 GenericJackson2JsonRedisSerializer
valueRedisSerializer = getGenericJackson2JsonRedisSerializer();
// set serializer
template.setKeySerializer(stringRedisSerializer);
template.setValueSerializer(valueRedisSerializer);
template.setHashKeySerializer(stringRedisSerializer);
template.setHashValueSerializer(valueRedisSerializer);
template.afterPropertiesSet();
return template;
}
}
RedisConfig 中定义了三个函数,主要作用如下:
- wiselyKeyGenerator:定义了一个生成 Redis 的 key 的方法。如下文使用了 @Cacheable 注解的地方,可以指定 key 的生成方法使用我们这个函数。
- cacheManager:定义了对 Redis 的一些基本设置。
- redisTemplate:对我们要使用的 RedisTemplate 做一些设置。主要是确定序列化方法。
RedisTemplate 设置序列化器
Spring Redis 虽然提供了对 list、set、hash 等数据类型的支持,但是没有提供对 POJO 对象的支持,底层都是把对象序列化后再以字节的方式存储的。
因此,Spring Data Redis 提供了若干个 Serializer,主要包括:
- JdkSerializationRedisSerializer: 默认的序列化器。序列化速度快,生成的字节长度较大。
- OxmSerializer: 生成 XML 格式的字节。
- StringSerializer: 只能对 String 类型进行序列化。
- JacksonJsonRedisSerializer:以 JSON 格式进行序列化。
- Jackson2JsonRedisSerializer:JacksonJsonRedisSerializer 的升级版。
- GenericJackson2JsonRedisSerializer:Jackson2JsonRedisSerializer 的泛型版。
RedisTemplate 中需要声明 4 种 serializer(默认使用的是 JdkSerializationRedisSerializer
):
- keySerializer :对于普通 K-V 操作时,key 采取的序列化策略
- valueSerializer:value 采取的序列化策略
- hashKeySerializer: 在 hash 数据结构中,hash-key 的序列化策略
- hashValueSerializer:hash-value 的序列化策略
无论如何,建议 key/hashKey 采用 StringRedisSerializer。
我们设置了 serializer 后,读写 Redis 要使用同一种 serizlizer,否则会读不出之前用不同 serializer 写入的数据。
也就是设置 valueSerializer 为GenericJackson2JsonRedisSerializer,然后写入了数据。
后面要读数据的时候,如果将 valueSerializer 又设置成了 Jackson2JsonRedisSerializer,那么读取数据时就会报错。
通常情况下,我们只需要在 RedisConfig 中统一设置好 4 个 serializer 即可。
Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 的对比
- 两者都是将对象的数据序列化成 JSON 格式的字符串。
- Jackson2JsonRedisSerializer 需要自己指定 ObjectMaper 或某个特定的类型。
- GenericJackson2JsonRedisSerializer 是 Jackson2JsonRedisSerializer 的一个特例,默认支持所有类型。
- 两者序列化时,都会将原始对象的类名和包名写入 JSON 字符串中。以便反序列化时,确认要将 JSON 转成何种格式。
可用如下方式来获得通用的 Jackson2JsonRedisSerializer
1 | Jackson2JsonRedisSerializer jackson2JsonRedisSerializer = |
Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 生成 JSON 的对比
1 | # Jackson2JsonRedisSerializer 序列化的效果 |
1 | # GenericJackson2JsonRedisSerializer 序列化的效果 |
如何使用
使用注解来缓存函数的结果
在要缓存的方法上使用注解 @Cacheable
、@CachePut
、@CacheEvict
分别用于缓存返回数据、更新缓存数据、删除缓存数据。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
60package zz.service;
import lombok.extern.slf4j.Slf4j;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
import zz.domain.User;
/**
* UserService
*
* @author zz
* @date 2018/5/7
*/
4j
public class UserService {
public final String DEFAULT_NAME = "def";
"user", key = "'id_'+#userId") (cacheNames =
public User get(int userId) {
// get from db
log.debug("[++] get userId=" + userId);
User user = new User();
user.setId(userId);
user.setName(DEFAULT_NAME);
log.debug("[++] create default user=" + user);
return user;
}
"user", key = "'id_'+#user.getId()") (cacheNames =
public User update(User user) {
// save to db
log.debug("[++] update user=" + user);
return user;
}
"user", key = "'id_'+#userId") (cacheNames =
public void delete(int userId) {
// delete from db
log.debug("[++] delete userId=" + userId);
}
"user", key = "'id_'+#userId") (cacheNames =
public User updateName(int userId, String name) {
// update to db
log.debug("[++] updateName userId=" + userId + ", name=" + name);
User user = get(userId);
user.setName(name);
return user;
}
public void innerCall(int userId) {
log.debug("[++] innerCall");
get(userId);
}
}
- 对函数的缓存是通过代理来实现的 :
类内部的某个函数对其他函数(即便被调用函数有@CacheXXX
注解)的调用是不会走代理的,也就没有缓存。(比如innerCall
调用get
时不会使用缓存) 。 - 注解可以放到 Service、Dao 或 Controller 层。
@CacheXXX
会缓存函数的返回值。比如increaseComment
会缓存更新后的FeedCount
。- 当缓存中有数据时,
@Cacheable
注解的函数不会执行,直接返回缓存中的数据。 @CachePut
、@CacheEvit
注解的函数,无论如何都会执行。
自定义缓存
如果要更细粒度地控制 Redis,可以使用 RedisTemplate
、StringRedisTemplate
StringRedisTemplate 是 RedisTemplate 的一个特例:key 和 value 都是 String 类型。
- RedisTemplate 默认使用 JDK 对 key 和 value 进行序列化,转成字节存入 Redis。
- StringRedisTemplate 的 key、value 本身就是 String,使用 StringRedisSerializer 将 String 转成字节存入 Redis。
当我们将 RedisTemplate 的 keySerializer 和 valueSerializer 都设置成了 StringRedisSerializer,则 RedisTemplate 和 StringRedisTemplate 的效果是相同的,就像下面的样例所示。
RedisTemplate 对 Redis 中各个数据结构的操作
- redisTemplate.opsForValue();//操作字符串
- redisTemplate.opsForHash();//操作hash
- redisTemplate.opsForList();//操作list
- redisTemplate.opsForSet();//操作set
- redisTemplate.opsForZSet();//操作有序set
1 | package zz; |