RedisTemplate混用带来的序列化问题

最近在工作中发现一个现象,项目中使用了不同的 RedisTemplate 来操作redis,有的同事用默认的 RedisTemplate ,有的同事用 StringRedisTemplate。这就导致了我本次遇到的问题:

在一次需求中,我需要从 redis 中取值,并且这个值是之前就有的,而我要加代码的那个类里也早早存在了 RedisTemplate 的引用

java 复制代码
@Autowired
RedisTemplate redisTemplate;

于是直接用这个类里的 RedisTemplate 去获取 key,结果取到了 null,而翻一翻 redis,这个key 也确实是存在的,但是死活获取不到。翻看这个key 往 redis 中放值的逻辑,发现是使用的 StringRedisTemplate

java 复制代码
@Autowired
StringRedisTemplate stringRedisTemplate;

难道是这两个 RedisTemplate 的原因?搜查了一下后发现果然是这样,这是因为两个 RedisTemplate 使用了不同的序列化方式造成的。之前一直没关注过 RedisTemplate 的序列化方式,借着本次机会也重新了解一下。

先来看下常见的自定义配置RedisTemplate的方式

java 复制代码
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 的部分源码

java 复制代码
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 的部分源码

java 复制代码
public class StringRedisTemplate extends RedisTemplate<String, String> {

    // 给四个属性都赋值为字符串序列化器 StringRedisSerializer
	public StringRedisTemplate() {
		setKeySerializer(RedisSerializer.string());
		setValueSerializer(RedisSerializer.string());
		setHashKeySerializer(RedisSerializer.string());
		setHashValueSerializer(RedisSerializer.string());
	}

}

RedisSerializer.string()的值为

java 复制代码
public static final StringRedisSerializer UTF_8 = new StringRedisSerializer(StandardCharsets.UTF_8);

可见 StringRedisTemplate 的所有 key、value都是使用的字符串序列化器 StringRedisSerializer。

综上所述,RedisTemplate 与 StringRedisTemplate 使用的是完全不同的两种序列化方式,理论上他们存入 redis 的内容是不能被交叉读取的,即 RedisTemplate 存的 key,StringRedisTemplate 读不到;StringRedisTemplate存的 key,RedisTemplate读不到。

这里来做个实验验证一下

java 复制代码
@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;
}

运行结果

bash 复制代码
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 值

java 复制代码
@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);
}

运行结果:

bash 复制代码
stringRedisTemplate获取到值:{"age":20,"name":"李四"}
redisTemplate获取到值:null

redis中存放的键值如下

可以看到这种情况下,redis中信息的可读性要好了不少,StringRedisTemplate 往 redis 中存放的 key就是纯净的字符串,value就是我们程序中提前转化好的"User对象的 json 串"这个字符串,StringRedisTemplate 从 redis 中取值自然没问题,正常拿到了字符串; 而 RedisTemplate 就无法从 redis 中正常取出值,通过上一个例子可以知道: RedisTemplate 要找的 key 不是程序中的那个简单的字符串,而是附加了其他的信息的(乱码的前缀信息), 所以RedisTemplate 自然也就找不到指定的 key。

所以说,一个项目中的公共组件,大家最好提前定义好,都用同一个,否则的话 五花八门的用法极易出现问题,且程序扩展性很差。

相关推荐
考虑考虑9 小时前
Jpa使用union all
java·spring boot·后端
阿杆18 小时前
同事嫌参数校验太丑,我直接掏出了更优雅的 SpEL Validator
java·spring boot·后端
AAA修煤气灶刘哥1 天前
别让Redis「歪脖子」!一次搞定数据倾斜与请求倾斜的捉妖记
redis·分布式·后端
昵称为空C1 天前
SpringBoot3 http接口调用新方式RestClient + @HttpExchange像使用Feign一样调用
spring boot·后端
christine-rr2 天前
linux常用命令(4)——压缩命令
linux·服务器·redis
麦兜*2 天前
MongoDB Atlas 云数据库实战:从零搭建全球多节点集群
java·数据库·spring boot·mongodb·spring·spring cloud
麦兜*2 天前
MongoDB 在物联网(IoT)中的应用:海量时序数据处理方案
java·数据库·spring boot·物联网·mongodb·spring
汤姆yu2 天前
基于springboot的毕业旅游一站式定制系统
spring boot·后端·旅游
计算机毕业设计木哥2 天前
计算机毕设选题推荐:基于Java+SpringBoot物品租赁管理系统【源码+文档+调试】
java·vue.js·spring boot·mysql·spark·毕业设计·课程设计
凯子坚持 c2 天前
精通 Redis list:使用 redis-plus-plus 的现代 C++ 实践深度解析
c++·redis·list