RedisTemplate混用带来的序列化问题
最近在工作中发现一个现象,项目中使用了不同的 RedisTemplate 来操作redis,有的同事用默认的 RedisTemplate ,有的同事用 StringRedisTemplate。这就导致了我本次遇到的问题:
在一次需求中,我需要从 redis 中取值,并且这个值是之前就有的,而我要加代码的那个类里也早早存在了 RedisTemplate 的引用
@Autowired
RedisTemplate redisTemplate;
于是直接用这个类里的 RedisTemplate 去获取 key,结果取到了 null,而翻一翻 redis,这个key 也确实是存在的,但是死活获取不到。翻看这个key 往 redis 中放值的逻辑,发现是使用的 StringRedisTemplate
@Autowired
StringRedisTemplate stringRedisTemplate;
难道是这两个 RedisTemplate 的原因?搜查了一下后发现果然是这样,这是因为两个 RedisTemplate 使用了不同的序列化方式造成的。之前一直没关注过 RedisTemplate 的序列化方式,借着本次机会也重新了解一下。
先来看下常见的自定义配置RedisTemplate的方式
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
@Configuration
public class RedisConfig {
@Bean
public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) {
RedisTemplate<String, Object> redisTemplate = new RedisTemplate<>();
// 设置连接工厂
redisTemplate.setConnectionFactory(connectionFactory);
// 使用String序列化器来序列化和反序列化redis的key值
StringRedisSerializer stringRedisSerializer = new StringRedisSerializer();
redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setHashKeySerializer(stringRedisSerializer);
// 使用Jackson2JsonRedisSerializer来序列化和反序列化redis的value值(默认使用Jackson序列化为JSON)
GenericJackson2JsonRedisSerializer jsonRedisSerializer = new GenericJackson2JsonRedisSerializer();
redisTemplate.setValueSerializer(jsonRedisSerializer);
redisTemplate.setHashValueSerializer(jsonRedisSerializer);
// 开启事务支持
redisTemplate.setEnableTransactionSupport(true);
redisTemplate.afterPropertiesSet();
return redisTemplate;
}
}
可以看到,在配置方法里需要对四个属性设置序列化与反序列化的方式,分别是 Key 与 HashKey,Value 与 HashValue。
在RedisTemplate的源码中也能看到,设置这四种值的序列化方式即可完成对 RedisTemplate 的序列化配置
看到这你可能会有疑问,redis支持五种基本类型 String、List、Set、Hash、Zset,为什么只设置了两种值的序列化配置呢?
其实 String、List、Set、Zset 序列化方式统一被 keySerializer 与 valueSerializer 两个属性设置了,这几种数据类型都是 key、value形式。而 Redis 的 Hash 数据结构的特性与其他数据结构有所不同。Hash 数据结构存储的是键值对集合,每个 Hash相当于一个小型的 key-value 存储,因此它的 hashKey 和 hashValue 序列化方式要单独配置。
什么是序列化?
序列化(Serialization)是将对象的状态转换为可以存储或传输的格式的过程。通过序列化,一个复杂的对象可以被转换成字节序列或字符串,然后存储到文件、数据库,或者通过网络传输到另一个系统。相应的,反序列化(Deserialization)是将存储或传输的字节序列转换回原来的对象的过程。
所以序列化与反序列化方式必须是配对使用的,A序列化方式 序列化的数据必须由 A反序列化方式来正确转化,B反序列化方式 极大可能是不能将数据正常转化回来的。
现在我们已经知道了 RedisTemplate 需要配置四个序列化相关的属性值。那么默认的 RedisTemplate 与 StringRedisTemplate(RedisTemplate的子类)是怎么配置这四个属性值的呢?
先看看 RedisTemplate 的部分源码
public class RedisTemplate<K, V> extends RedisAccessor implements RedisOperations<K, V>, BeanClassLoaderAware {
private boolean initialized = false;
private @Nullable RedisSerializer<?> defaultSerializer;
private boolean enableDefaultSerializer = true;
private @Nullable RedisSerializer keySerializer = null;
private @Nullable RedisSerializer valueSerializer = null;
private @Nullable RedisSerializer hashKeySerializer = null;
private @Nullable RedisSerializer hashValueSerializer = null;
@Override
public void afterPropertiesSet() {
super.afterPropertiesSet();
boolean defaultUsed = false;
// 使用JDK自带的序列化方式作为redis的默认序列化器
if (defaultSerializer == null) {
defaultSerializer = new JdkSerializationRedisSerializer(
classLoader != null ? classLoader : this.getClass().getClassLoader());
}
// 默认情况下 RedisTemplate 是使用 JdkSerializationRedisSerializer 来作为所有 key value 的序列化器的
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;
}
}
if (enableDefaultSerializer && defaultUsed) {
Assert.notNull(defaultSerializer, "default serializer null and not all serializers initialized");
}
if (scriptExecutor == null) {
this.scriptExecutor = new DefaultScriptExecutor<>(this);
}
initialized = true;
}
}
可以看到默认情况下 RedisTemplate 是使用 JdkSerializationRedisSerializer 来作为所有 key、value 的序列化器的,四个属性的值都是 JdkSerializationRedisSerializer;
再来看看 StringRedisTemplate 的部分源码
public class StringRedisTemplate extends RedisTemplate<String, String> {
// 给四个属性都赋值为字符串序列化器 StringRedisSerializer
public StringRedisTemplate() {
setKeySerializer(RedisSerializer.string());
setValueSerializer(RedisSerializer.string());
setHashKeySerializer(RedisSerializer.string());
setHashValueSerializer(RedisSerializer.string());
}
}
RedisSerializer.string()的值为
public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);
可见 StringRedisTemplate 的所有 key、value都是使用的字符串序列化器 StringRedisSerializer。
综上所述,RedisTemplate 与 StringRedisTemplate 使用的是完全不同的两种序列化方式,理论上他们存入 redis 的内容是不能被交叉读取的,即 RedisTemplate 存的 key,StringRedisTemplate 读不到;StringRedisTemplate存的 key,RedisTemplate读不到。
这里来做个实验验证一下
@Autowired
RedisTemplate redisTemplate;
@Autowired
StringRedisTemplate stringRedisTemplate;
@Test
public void testRedisTemplate01() {
String keyA = "r_set_aaa";
User valueA = new User("张三",18);
redisTemplate.opsForValue().set(keyA,valueA);
Object value01 = redisTemplate.opsForValue().get(keyA);
log.info("redisTemplate获取到值:{}",value01);
User user = (User) value01;
log.info("user.getName():{}, user.getAge():{}", user.getName(),user.getAge());
String value02 = stringRedisTemplate.opsForValue().get(keyA);
log.info("stringRedisTemplate获取到值:{}",value02);
}
@Data
@NoArgsConstructor
@AllArgsConstructor
// 注意要使用jdk的序列化方式的话,需要实现 Serializable 接口
private static class User implements Serializable {
private String name;
private Integer age;
}
运行结果
redisTemplate获取到值:RedisTemplateTest.User(name=张三, age=18)
user.getName():张三, user.getAge():18
stringRedisTemplate获取到值:null
redis中存放的键值如下
可以看到默认的 RedisTemplate 往 redis 中存入的 key 和 value 的可读性很差,redis客户端可以看到有很多乱码,但是仍可以看到其 value 值中带有对象信息;RedisTemplate 从 redis 中取出来的值直接就是一个对象,可以强转为指定对象。
这种情况下 StringRedisTemplate 就无法从 redis 中正常取出值,因为 StringRedisTemplate 在 redis 寻找的 key 是 "r_set_aaa" 这个纯净的字符串,但是显然 RedisTemplate 往 redis 中存入的 key 并不是那么纯净,所以 StringRedisTemplate 压根找不到它想要找的 key。
再来个例子,这次让 StringRedisTemplate 来往 redis 中 set 值
@Test
public void testRedisTemplate02() {
String keyB = "sr_set_aaa";
User valueB = new User("李四",20);
stringRedisTemplate.opsForValue().set(keyB,JSON.toJSONString(valueB));
String value03 = stringRedisTemplate.opsForValue().get(keyB);
log.info("stringRedisTemplate获取到值:{}",value03);
Object value04 = redisTemplate.opsForValue().get(keyB);
log.info("redisTemplate获取到值:{}",value04);
}
运行结果:
stringRedisTemplate获取到值:{"age":20,"name":"李四"}
redisTemplate获取到值:null
redis中存放的键值如下
可以看到这种情况下,redis中信息的可读性要好了不少,StringRedisTemplate 往 redis 中存放的 key就是纯净的字符串,value就是我们程序中提前转化好的“User对象的 json 串”这个字符串,StringRedisTemplate 从 redis 中取值自然没问题,正常拿到了字符串; 而 RedisTemplate 就无法从 redis 中正常取出值,通过上一个例子可以知道: RedisTemplate 要找的 key 不是程序中的那个简单的字符串,而是附加了其他的信息的(乱码的前缀信息), 所以RedisTemplate 自然也就找不到指定的 key。
所以说,一个项目中的公共组件,大家最好提前定义好,都用同一个,否则的话 五花八门的用法极易出现问题,且程序扩展性很差。