《知识点扫盲 · Redis 序列化器》-LMLPHP

写在前面的话

博主所在公司近期线上环境,某天遇到某服务大批量异常,查看异常堆栈指向 StringRedisSerializer#serialize,具体错误如下:java.lang.ClassCastException: class com.alibaba.fastjson.JSONArray cannot be cast to class java.lang.String
经排查发现,问题起因是该服务的开发人员,在某段业务代码修改了 redisTemplate 的序列化器导致该服务全局操作缓存异常,在解决问题并故障复盘后,这边也分享一下缓存序列化器的一些知识。


缓存序列化器

技术简介

Redis 数据序列化器,用于将数据在存储到 Redis 中时进行序列化(编码)和反序列化(解码)。
通俗来说,存数据的时候,需要先进行序列化,例如 Java 对象转为 JSON 字符串,再存到 Redis。
反过来,取数据的时候,需要先进行反序列化,例如 JSON 字符串转为 Java 对象,得到结果。


RedisSerializer 接口

Spring-Redis 把上述逻辑封闭在redis序列化器中,接口为 RedisSerializer,完整类路径:

该接口内容主要如下,主要关注序列化 serialize 和反序列化 deserialize 两个接口;

//**把对象序列化为byte数组
byte[] serialize(@Nullable T t) throws SerializationException;

//**把byte数组反序列化为对象
T deserialize(@Nullable byte[] bytes) throws SerializationException;

//**静态方法,java格式序列化器
static RedisSerializer<Object> java() {
	return java(null);
}

//**静态方法,java格式序列化器
static RedisSerializer<Object> java(@Nullable ClassLoader classLoader) {
	return new JdkSerializationRedisSerializer(classLoader);
}

//**静态方法,json格式序列化器
static RedisSerializer<Object> json() {
	return new GenericJackson2JsonRedisSerializer();
}

//**静态方法,string数据类型序列化器
static RedisSerializer<String> string() {
	return StringRedisSerializer.UTF_8;
}

//**静态方法,byte数据类型序列化器
static RedisSerializer<byte[]> byteArray() {
	return ByteArrayRedisSerializer.INSTANCE;
}

//**检查是此序列化器否能够序列化指定数据类型
default boolean canSerialize(Class<?> type) {
	return ClassUtils.isAssignable(getTargetType(), type);
}

//**此序列化器针对的数据类型,默认object
default Class<?> getTargetType() {
	return Object.class;
}

常用序列化器

常用的序列化器,包含但不限于如下:

  • JdkSerializationRedisSerializer
  • StringRedisSerializer
  • JacksonJsonRedisSerializer
  • GenericFastJsonRedisSerializer

另外按用途序列化器又分两种,Key 和 Value的,用代码表示比较清楚:

redisTemplate.setKeySerializer(stringRedisSerializer);
redisTemplate.setValueSerializer(jdkSerializationRedisSerializer);

方案比较

这四种 Redis 序列化器各有其优势和劣势,适用于不同的场景。
1、JdkSerializationRedisSerializer
优势:默认方案,不需要特别配置;可以序列化 Java 的任何对象,只要这些对象实现了 Serializable 接口。
劣势:性能相对较慢,序列化后的数据是二进制格式,可读性差。
过程:序列化是利用 ByteArrayOutputStream 将 Java 对象转换为 byte,反序列化就不赘述了。
2、StringRedisSerializer
优势:简单高效,适合处理字符串数据,序列化和反序列化速度快,序列化后的数据是字符串格式,易于调试和查看。
劣势:仅支持字符串类型,不适合复杂对象,在读取时需要手动转换为正确的类型,容易出错。
过程:序列化是使用指定的字符集直接把 String 类型转换为 byte。
3、JacksonJsonRedisSerializer
优势:序列化后的数据是 JSON 格式,易于调试和查看,兼容性强:支持多种 Java 对象,能够处理复杂对象和集合类型,也支持不同的配置选项,可以自定义序列化和反序列化过程。
劣势:需要引入 Jackson 库,增加了项目的依赖,速度低于 GenericFastJsonRedisSerializer。
过程:序列化时使用 jackson#ObjectMapper 把对象转换为 JSON 字符串,再转换为 byte。
4、GenericFastJsonRedisSerializer
优势:基于 FastJSON 实现,序列化和反序列化速度较快,适合处理大数据量,同时序列化后的数据为 JSON 格式,易于查看和调试,也支持泛型,能够处理不同类型的对象。
劣势:需要引入 FastJSON 库,增加了项目的依赖。
过程:序列化时使用 fastjson的JSON.toJSONBytes 把对象转换为 JSON 字符串,再转换为 byte。


使用示例

使用 JdkSerializationRedisSerializer
博主所在公司的旧框架封装的RedisTemplate采用JdkSerializationRedisSerializer作为值序列化器。
它的特点就是存的值如下所示,通过缓存客户端工具,难以阅读和直接修改。
《知识点扫盲 · Redis 序列化器》-LMLPHP

使用 GenericFastJsonRedisSerializer
博主所在公司最新框架封装的RedisTemplate采用GenericFastJsonRedisSerializer作为值序列化器。
采用它的主要目的应该是解决可读性差的问题,如下所示,同时性能和兼容性方面都不错。
当存入的是一个对象,能将Java对象自动的序列化为JSON字符串,并且查询时能自动把JSON反序列化为Java对象。
《知识点扫盲 · Redis 序列化器》-LMLPHP
《知识点扫盲 · Redis 序列化器》-LMLPHP

手动序列化
这种方案仅供参考,就是自己用代码实现序列化和反序列化,就挺麻烦的。

private static final ObjectMapper mapper = new ObjectMapper();
 
@Resource
private StringRedisTemplate stringRedisTemplate;

@Test
void testSaveUser() throws JsonProcessingException {

    // 创建对象
    Student student = new Student("李白",28);

    // 手动序列化
    String json = mapper.writeValueAsString(student);

    // 写入数据
    stringRedisTemplate.opsForValue().set("student", json);

    // 获取数据
    String jsonUser = stringRedisTemplate.opsForValue().get("user:200");

    // 手动反序列化
    Student user1 = mapper.readValue(jsonUser, Student.class);

    System.out.println("user1 = " + user1);
}

故障说明

开发人员在新框架公用服务,编写了如下代码,凭一己之力修改了全局的 redisTemplate 的缓存策略。
最终可能实现了功能,但却本末倒置、因小失大了,替换回了不太合适的StringRedisSerializer,也带来了更多的影响。

@Transactional
public List<Map<String,String>> getSCHData(BasicDict dict) throws Exception {
    redisTemplate.setHashValueSerializer(new StringRedisSerializer());
    。。。省略业务代码
}

模拟异常

参考下方代码,可以看到,通过 setValueSerializer 修改序列化器之后,再次操作设置缓存的语句,就出现了异常。

try {
    ResultVO a = new ResultVO();
    a.setMessage("message");
    a.setCode("123");
    redisTemplate.opsForValue().set("PARAMS:PORTAL:0", a);
    Object o = redisTemplate.opsForValue().get("PARAMS:PORTAL:0");
    System.out.println(o);
    redisTemplate.setValueSerializer(new StringRedisSerializer());
    Object o2 = redisTemplate.opsForValue().get("PARAMS:PORTAL:0");

    //这句出现异常
    redisTemplate.opsForValue().set("PARAMS:PORTAL:0", a);
} catch (Exception e) {
    //异常信息如下:
    //java.lang.ClassCastException: class com.zoe.onelink.common.entity.ResultVO cannot be cast to class java.lang.String (com.zoe.onelink.common.entity.ResultVO is in unnamed module of loader 'app'; java.lang.String is in module java.base of loader 'bootstrap')
	//at org.springframework.data.redis.serializer.StringRedisSerializer.serialize(StringRedisSerializer.java:36)
    e.printStackTrace();
}

《知识点扫盲 · Redis 序列化器》-LMLPHP


总结陈词

此篇文章介绍了@Value在项目中得常见用法,仅供学习参考。
💗 后续会逐步分享企业实际开发中的实战经验,有需要交流的可以联系博主。

《知识点扫盲 · Redis 序列化器》-LMLPHP

07-29 21:42