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。

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

相关推荐
Hello.Reader1 小时前
Redis大Key问题全解析
数据库·redis·bootstrap
B1nna4 小时前
Redis学习(三)缓存
redis·学习·缓存
Q_19284999064 小时前
基于Spring Boot的电影售票系统
java·spring boot·后端
陈无左耳、5 小时前
Spring Boot应用开发实战:从入门到精通
spring boot
烟波人长安吖~5 小时前
【目标跟踪+人流计数+人流热图(Web界面)】基于YOLOV11+Vue+SpringBoot+Flask+MySQL
vue.js·pytorch·spring boot·深度学习·yolo·目标跟踪
A22749 小时前
Redis——缓存雪崩
java·redis·缓存
weisian1519 小时前
Redis篇--应用篇3--数据统计(排行榜,计数器)
数据库·redis·缓存
言之。9 小时前
Redis单线程快的原因
数据库·redis·缓存
顽疲10 小时前
从零用java实现 小红书 springboot vue uniapp (6)用户登录鉴权及发布笔记
java·vue.js·spring boot·uni-app
编程洪同学11 小时前
Spring Boot 中实现自定义注解记录接口日志功能
android·java·spring boot·后端