JohnShen's Blog.

Spring Data Redis 实践 (v2.1.7)

字数统计: 1.2k阅读时长: 5 min
2019/08/15 Share

最近重写一个项目,在 SpringBoot 用的是最新的 GA 版本 2.1.7,缓存层使用了 Spring Data Redis。其实使用它是因为上一个项目也是用的这个框架,了解的程度还行,但不算细致。此外,因为以前直接使用 Redisson 的时候出现了内存泄漏(实际上也不是人家的锅,算是依赖的 Netty 版本的 bug),对 Redisson 总会心有余悸。但我真的得承认,Redisson 非常好用,如果项目中需要使用一些分布式的 API,比如分布式锁、优先级阻塞队列等,Redisson 是不二之选。其实与Redisson 做横向对比的应该是 Jedis,Spring Data Redis 是在 Jedis 上架了一层(boot 1.x)。但是这个项目对 Redis 的API 操作比较简单,所以就当仔细学习一下 Spring Data Redis了。

POM

因为 Boot 版本升了 2.X,Spring Data Redis 的默认框架从 Jedis 换成了 Lettuce,后者主要突出基于 Netty 的事件驱动,容易发挥异步优势。但是由于真不了解以及学习成本的考量,还是使用了 Jedis。

POM文件中需移除 lettuce 的依赖,引入 Jedis。具体版本在 2.1.7.RELEASEspring-boot-dependencies 文件中(lettuce 版本 5.1.8.RELEASE,jedis 版本 2.9.3)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
<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>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>

Config

RedisConnectionFactory

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
@Bean
public RedisConnectionFactory redisConnectionFactory() {
redisConf = ...;
RedisStandaloneConfiguration standaloneConf = new RedisStandaloneConfiguration(
redisConf.getHost(), redisConf.getPort());
if (!Strings.isNullOrEmpty(redisConf.getPassword())) {
standaloneConf.setPassword(redisConf.getPassword());
}
JedisConnectionFactory jedisConnectionFactory = new JedisConnectionFactory(standaloneConf);
GenericObjectPoolConfig poolConfig = jedisConnectionFactory.getPoolConfig();
poolConfig.setMaxTotal(30);
poolConfig.setMinIdle(0);
poolConfig.setMaxIdle(10);
poolConfig.setMaxWaitMillis(3000);
return jedisConnectionFactory;
}

2.x 的配置也和之前有所区别,JedisConnectionFactorysetXxx方法大多已是 Deprecated,如:setHostNamesetPortsetPassword等。文档的提示内容是:

since 2.0, configure the password using {@link RedisStandaloneConfiguration}, {@link RedisSentinelConfiguration} or {@link RedisClusterConfiguration}.

所以,在 JedisConnectionFactory 构造参数中设置RedisStandaloneConfiguration。而 pool 信息可以从 factory 中获取,从而进行设置。从 JedisConnectionFactory 构造函数中,我们发现它构造了MutableJedisClientConfiguration对象。该对象的 poolConfig 在 变量声名中就 new 出了JedisPoolConfig,我们可以对这个对象进行连接池设置。

1
2
3
public JedisConnectionFactory(RedisStandaloneConfiguration standaloneConfig) {
this(standaloneConfig, new MutableJedisClientConfiguration());
}

当然设置 pool 信息不单只有这一种,MutableJedisClientConfigurationJedisClientConfiguration接口的实现类,该接口下面还有接口JedisPoolingClientConfigurationBuilder,也可以使用它的实现类DefaultJedisClientConfigurationBuilderJedisClientConfiguration.builder()返回的就是这个类实例。

感觉到了 2.x,手动建 Factory 配置有点麻烦啊…

RedisTemplate

RedisTemplate 的 配置比较简单,注入 RedisConnectionFactory Bean 对象即可,但是要注意的序列化方案。

1
2
3
4
5
6
7
8
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory factory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
redisTemplate.setConnectionFactory(factory);
setSerializer(redisTemplate);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}

序列化

1
2
3
4
5
6
7
8
private void setSerializer(RedisTemplate<String, Object> template) {
GenericJackson2JsonRedisSerializer genericJackson2JsonRedisSerializer =
new GenericJackson2JsonRedisSerializer();
template.setKeySerializer(new StringRedisSerializer());
template.setValueSerializer(genericJackson2JsonRedisSerializer);
template.setHashKeySerializer(new StringRedisSerializer());
template.setHashValueSerializer(genericJackson2JsonRedisSerializer);
}

redisTemplate.afterPropertiesSet()方法中,显示默认的序列化方式是JdkSerializationRedisSerializer。使用的是 JDK 序列化方式,数据以字节流的形式存储。这种方式会造成可读性很差。

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
if (defaultSerializer == null) {

defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}

if (enableDefaultSerializer) {

if (keySerializer == null) {
keySerializer = defaultSerializer;
defaultUsed = true;
}
if (valueSerializer == null) {
valueSerializer = defaultSerializer;
defaultUsed = true;
}
if (hashKeySerializer == null) {
hashKeySerializer = defaultSerializer;
defaultUsed = true;
}
if (hashValueSerializer == null) {
hashValueSerializer = defaultSerializer;
defaultUsed = true;
}
}

如果项目中 key value 都只要使用字符串即可的话,StringRedisSerializer也是一种选项。这里我使用的是 Jackson 方式进行序列化操作。我事先使用的是Jackson2JsonRedisSerializer方式,该方式会将对象完全展为 JSON,但是在反序列的过程中报错:LinkedHashMap cannot be cast to ...,所以改成了GenericJackson2JsonRedisSerializer,该方案会在 JSON 中添加 @class类信息 field,这样在处理集合类泛型信息时,都能够正确处理。

使用 GenericJackson2JsonRedisSerializer 的时候,一开始是使用了自己定义的 ObjectMapper,结果发现无用,后来选择了无参构造器的实现才不出错。究其原因不难发现,该构造器中会对 objectMapper 设置 enableDefaultTyping(ObjectMapper.DefaultTyping.NON_FINAL),正是该字段使 Jackson 生成了类信息字段,这才是允许反序列化的基础。而类的前面出现 @class 是因为 NullValueSerializer 里 设置了classIdentifier 为 @class(即 JsonTypeInfo.Id#CLASS)。

如果使用 Jackson2JsonRedisSerializer 时里面的 objectMapper 配置了 DefaultTyping.NON_FINAL,简单试验了一下发现序列化/反序列化操作会成功(没配 @class 前缀也是可以的)。

1
2
3
4
5
6
7
8
9
10
11
12
13
14
15
16
public GenericJackson2JsonRedisSerializer() {
this((String) null);
}

public GenericJackson2JsonRedisSerializer(@Nullable String classPropertyTypeName) {

this(new ObjectMapper());

mapper.registerModule(new SimpleModule().addSerializer(new NullValueSerializer(classPropertyTypeName)));

if (StringUtils.hasText(classPropertyTypeName)) {
mapper.enableDefaultTypingAsProperty(DefaultTyping.NON_FINAL, classPropertyTypeName);
} else {
mapper.enableDefaultTyping(DefaultTyping.NON_FINAL, As.PROPERTY);
}
}

感觉一个完整 JSON 工具包的实现还是很难的,有机会可以详细过一遍 Jackson…


官方 Reference

CATALOG
  1. 1. POM
  2. 2. Config
    1. 2.1. RedisConnectionFactory
    2. 2.2. RedisTemplate
    3. 2.3. 序列化