在 Spring Boot 中使用 Redis

在 Spring Boot 中 使用 Redis

本文中的代码见 spring-boot-redis

Redis 本身的一些概念

Redis 支持的数据结构

  • String 字符串
  • Hash 字典
  • List 列表
  • Set 集合
  • Sorted Set 有序集合

String 和 Hash 的对比

String 实际是就是一个 Key - Value 的映射;

Hash 就是一个 Key - (Key - Value) 的两层映射。

1
2
3
4
5
6
7
8
9
10
11
12
13
# redis-cli
# Redis 中命令不区分大小写。这里命令使用小写,仅在特别的地方用大写。
# 参数使用“大写+下划线”的方式。

# String
set KEY VALUE
get KEY

# Hash
hset HASH_NAME KEY VALUE
hget HASH_NAME KEY
hMset HASH_NAME KEY0 VALUE0 KEY1 VALUE1 ...
hMget HASH_NAME KEY0 KEY1 ...

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
    18
    spring:
    # 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
    130
    package 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
    */
    @Configuration
    @EnableCaching
    @Slf4j
    public class RedisConfig extends CachingConfigurerSupport {
    @Bean
    public KeyGenerator wiselyKeyGenerator() {
    return new KeyGenerator() {
    private static final String SEPARATE = ":";

    @Override
    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 注解进行扩展:注解失效时间 + 主动刷新缓存
    */
    @Bean
    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/
    */
    @Bean
    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。

by Spring-data-redis: serializer实例

我们设置了 serializer 后,读写 Redis 要使用同一种 serizlizer,否则会读不出之前用不同 serializer 写入的数据。

也就是设置 valueSerializer 为GenericJackson2JsonRedisSerializer,然后写入了数据。
后面要读数据的时候,如果将 valueSerializer 又设置成了 Jackson2JsonRedisSerializer,那么读取数据时就会报错。

通常情况下,我们只需要在 RedisConfig 中统一设置好 4 个 serializer 即可。

Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 的对比

  • 两者都是将对象的数据序列化成 JSON 格式的字符串。
  • Jackson2JsonRedisSerializer 需要自己指定 ObjectMaper 或某个特定的类型。
  • GenericJackson2JsonRedisSerializer 是 Jackson2JsonRedisSerializer 的一个特例,默认支持所有类型。
  • 两者序列化时,都会将原始对象的类名和包名写入 JSON 字符串中。以便反序列化时,确认要将 JSON 转成何种格式。
可用如下方式来获得通用的 Jackson2JsonRedisSerializer
1
2
3
4
5
6
7
8
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);

Jackson2JsonRedisSerializer 与 GenericJackson2JsonRedisSerializer 生成 JSON 的对比

1
2
3
4
5
# Jackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"[\"zz.domain.User\",{\"id\":123,\"name\":\"name\"}]"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}],[\"zz.domain.User\",{\"id\":233,\"name\":\"new\"}]]]"
1
2
3
4
5
# GenericJackson2JsonRedisSerializer 序列化的效果
127.0.0.1:6379> get 123
"{\"@class\":\"zz.domain.User\",\"id\":123,\"name\":\"name\"}"
127.0.0.1:6379> get userList
"[\"java.util.LinkedList\",[{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"},{\"@class\":\"zz.domain.User\",\"id\":233,\"name\":\"new\"}]]"

如何使用

使用注解来缓存函数的结果

在要缓存的方法上使用注解 @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
60
package 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
*/
@Service
@Slf4j
public class UserService {
public final String DEFAULT_NAME = "def";

@Cacheable(cacheNames = "user", key = "'id_'+#userId")
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;
}

@CachePut(cacheNames = "user", key = "'id_'+#user.getId()")
public User update(User user) {
// save to db
​ log.debug("[++] update user=" + user);
return user;
}

@CacheEvict(cacheNames = "user", key = "'id_'+#userId")
public void delete(int userId) {
// delete from db
​ log.debug("[++] delete userId=" + userId);
}

@CachePut(cacheNames = "user", key = "'id_'+#userId")
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,可以使用 RedisTemplateStringRedisTemplate

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
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
130
131
132
133
134
135
136
137
138
139
140
141
142
143
144
145
146
147
148
149
150
151
152
153
154
155
156
157
158
159
160
package zz;

import lombok.extern.slf4j.Slf4j;
import org.junit.Assert;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.data.redis.serializer.RedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.test.context.junit4.SpringRunner;
import zz.domain.User;
import zz.service.UserService;

import java.util.LinkedList;
import java.util.List;

/**
* zz.TestRedis
*
* @author zz
* @date 2018/5/7
*/
@SpringBootTest
@RunWith(SpringRunner.class)
@Slf4j
public class TestRedis {

@Autowired
StringRedisTemplate stringRedisTemplate;

@Autowired
RedisTemplate redisTemplate;

@Autowired
UserService userService;

@Test
public void testSerializer() {
// 1.
// 这里的 opsForValue().get() 的参数必须转成 String 类型。
// 除非在 RedisConfig 中 将 keySerializer 设置成 GenericJackson2JsonRedisSerializer 等能将其他类型转换成 String 的。

// 2.
// 如果切换了 RedisConfig 中的 ValueSerializer,要先用 redis-cli 将其中的旧数据删除。
// 不同 Serializer 格式之间的转换可能存在问题。

final int ID = 123;
User oldUser;
oldUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
log.debug("oldUser=" + oldUser);

User user = new User();
user.setId(ID);
user.setName("name");
log.debug("user=" + user);

redisTemplate.opsForValue().set(String.valueOf(user.getId()), user);

User newUser;
newUser = (User) redisTemplate.opsForValue().get(String.valueOf(ID));
log.debug("newUser=" + newUser);

Assert.assertEquals(user.getId(), newUser.getId());
Assert.assertEquals(user.getName(), newUser.getName());


List<User> userList = new LinkedList<>();
userList.add(user);
user.setId(233);
user.setName("new");
userList.add(user);

redisTemplate.opsForValue().set("userList", userList);
List<User> newUserList;
newUserList = (List<User>) redisTemplate.opsForValue().get("userList");

Assert.assertEquals(userList, newUserList);
}

@Test
public void testSerizlizer2() {
// 保存用于恢复,以免影响其他部分
RedisSerializer oldKeySerializer = redisTemplate.getKeySerializer();
RedisSerializer oldValueSerializer = redisTemplate.getValueSerializer();

RedisSerializer redisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(redisSerializer);
redisTemplate.setValueSerializer(redisSerializer);

final String KEY = "key";
String VALUE = "value";

redisTemplate.opsForValue().set(KEY, VALUE);
Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));
Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));


VALUE = "Val2";
stringRedisTemplate.opsForValue().set(KEY, VALUE);
Assert.assertEquals(VALUE, stringRedisTemplate.opsForValue().get(KEY));
Assert.assertEquals(VALUE, redisTemplate.opsForValue().get(KEY));


// 恢复原本设置
redisTemplate.setKeySerializer(oldKeySerializer);
redisTemplate.setValueSerializer(oldValueSerializer);
}


@Test
public void testCache() {
final int USER_ID = 1;

User user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(userService.DEFAULT_NAME, user.getName());

// 这次会直接返回 cache
user = userService.get(USER_ID);
log.debug("user=" + user);

// 获得修改过的 cache
final String ANOTHER_NAME = "another user";
user.setName(ANOTHER_NAME);
userService.update(user);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(ANOTHER_NAME, user.getName());

// 直接调用 get 会走缓存,通过 innerCall 来调用 get 不会走缓存
log.debug("------ before");
userService.get(USER_ID);
log.debug("------ middle");
userService.innerCall(USER_ID);
log.debug("------ after");

// 另一种修改的方式
final String NEW_NAME = "updated";
userService.updateName(USER_ID, NEW_NAME);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(NEW_NAME, user.getName());


// 删除后,cache 中的数据会被删除,name 会变成初始值
userService.delete(USER_ID);
user = userService.get(USER_ID);
log.debug("user=" + user);
Assert.assertEquals(userService.DEFAULT_NAME, user.getName());

// 即使 cache 中没有该数据,也会执行 delete 中的逻辑
userService.delete(USER_ID);
userService.delete(USER_ID);

}

}