RedisTemplate混用带来的序列化问题

ops/2024/9/23 2:48:14/

最近在工作中发现一个现象,项目中使用了不同的 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 {@Beanpublic 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;@Overridepublic 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> {// 给四个属性都赋值为字符串序列化器 StringRedisSerializerpublic 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。

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


http://www.ppmy.cn/ops/114547.html

相关文章

搭建微服务注册中心(命令行简易版,不使用IDE)

目录 1、微服务注册中心Eureka简介 2、安装Maven 3、配置Maven 4、配置环境变量和Java 5、创建Eureka 6、启动项目 1、微服务注册中心Eureka简介 微服务注册中心Eureka是Netflix开发的一个开源服务注册与发现组件,也是Spring Cloud体系中的核心组件之一。 Eureka主要应…

富文本编辑器wangEdittor使用入门

一、wangEdittor介绍 富文本编辑器为开源产品wangEditor。wangEditor是一款轻量级Web富文本编辑器&#xff0c;配置方便&#xff0c;使用简单。读者可在其官网和GitHub仓库进行更多了解。主要有以下功能&#xff1a; 1&#xff09;图文混排 wangEditor可以编辑的内容比较丰富…

gin基本使用

中文文档:https://gin-gonic.com/zh-cn/docs/ 下载和安装gin模块 go get -u github.com/gin-gonic/gin简单接口demo package mainimport "github.com/gin-gonic/gin"func main() {r := gin.Default() // 创建一个默认的路由引擎r.GET("/pin…

Flutter启动无法运行热重载

当出现这种报错时&#xff0c;大概率是flutter的NO_Proxy出问题。 请忽略上面的Android报错因为我做的是windows开发这个也就不管了哈&#xff0c;解决下面也有解决报错的命令大家执行一下就行。 着重说一下Proxy的问题&#xff0c; 我们看到提示NO_PROXY 没有设置。 这个时候我…

Mac系统Docker中SQLserver数据库文件恢复记录

Mac系统Docker中SQLserver数据库文件恢复记录 Mac想要安装SQLsever&#xff0c;通过docker去拉去镜像是最简单方法。 一、下载Docker Docker 下载安装&#xff1a; 需要‘科学上网’ 才能访问到docker官网。&#xff08; https://docs.docker.com/desktop/install/mac-ins…

如何在 CentOS 中管理用户、组和服务状态

如何在 CentOS 中管理用户、组和服务状态 在 CentOS 系统中&#xff0c;用户管理、文件权限设置以及服务的启动与管理是系统管理的重要组成部分。本文将通过实际案例&#xff0c;逐步展示如何新建用户组、创建用户、修改文件权限以及使用 systemctl 来管理系统服务。让我们开始…

数模方法论-整数规划

一、基本概念 非线性规划的应用包括工程设计、资源分配、经济模型等。在求解过程中&#xff0c;由于非线性特性&#xff0c;常用的方法有梯度法、牛顿法、启发式算法等。求解非线性规划问题时&#xff0c;解的存在性和唯一性通常较难保证&#xff0c;且可能存在多个局部最优解…

第十一章 【后端】商品分类管理微服务(11.5)——增强响应

11.5 增强响应 在前后端分离的开发模式下,我们一般会统一后端的响应格式,比如自定义 Response 结构,但每个开发者可能会封装各自的 Response 结构,造成不一致,因此我们需要将响应格式统一起来,定义一个统一的标准响应格式。 11.5.1 创建响应模块 新建 yumi-etms-respon…