之前同事反馈说线上遇到Redis反序列化异常问题,异常如下:

XxxClass1 cannot be cast to XxxClass2

已知信息如下:

  • 该异常不是必现的,偶尔才会出现;
  • 出现该异常后重启应用或者过一会就好了;
  • 序列化协议使用了hessian。

因为偶尔出现,首先看了报异常那块业务逻辑是不是有问题,看了一遍也发现什么问题。看了下对应日志,发现是在Redis读超时之后才出现的该异常,因此怀疑redis client操作逻辑那块导致的(公司架构组对redis做了一层封装),发现获取/释放redis连接如下代码:

 try {
jedis = jedisPool.getResource();
// jedis业务读写操作
} catch (Exception e) {
// 异常处理
} finally {
if (jedis != null) {
// 归还给连接池
jedisPool.returnResourceObject(jedis);
}
}

初步认定原因为:发生了读写超时的连接,直接归还给连接池,下次使用该连接时读取到了上一次Redis返回的数据。因此本地验证下,示例代码如下:

 @Data
@NoArgsConstructor
@AllArgsConstructor
static class Person implements Serializable {
private String name;
private int age;
}
@Data
@NoArgsConstructor
@AllArgsConstructor
static class Dog implements Serializable {
private String name;
} public static void main(String[] args) throws Exception {
JedisPoolConfig config = new JedisPoolConfig();
config.setMaxTotal(1);
JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456"); Jedis jedis = jedisPool.getResource();
jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
jedis.set("key2".getBytes(), serialize(new Dog("tom")));
jedisPool.returnResourceObject(jedis); try {
jedis = jedisPool.getResource();
Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
System.out.println(person);
} catch (Exception e) {
// 发生了异常之后,未对该连接做任何处理
System.out.println(e.getMessage());
} finally {
if (jedis != null) {
jedisPool.returnResourceObject(jedis);
}
} try {
jedis = jedisPool.getResource();
Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
System.out.println(dog);
} catch (Exception e) {
System.out.println(e.getMessage());
} finally {
if (jedis != null) {
jedisPool.returnResourceObject(jedis);
}
}
}

连接超时时间设置2000ms,为了方便测试,可以在redis服务器上使用gdb命令断住redis进程(如果redis部署在Linux系统上的话,还可以使用iptable命令在防火墙禁止某个回包),比如在执行 jedis.get("key1".getBytes() 代码前,对redis进程使用gdb命令断住,那么就会导致读取超时,然后就会触发如下异常:

Person cannot be cast to Dog

既然已经知道了该问题原因并且本地复现了该问题,对应解决方案是,在发生异常时归还给连接池时关闭该连接即可(jedis.close内部已经做了判断),代码如下:

 try {
jedis = jedisPool.getResource();
// jedis业务读写操作
} catch (Exception e) {
// 异常处理
} finally {
if (jedis != null) {
// 归还给连接池
jedis.close();
}
}

至此,该问题解决。注意,因为使用了hessian序列化(其包含了类型信息,类似的有Java本身序列化机制),所有会报类转换异常;如果使用了json序列化(其只包含对象属性信息),反序列化时不会报异常,只不过因为不同类的属性不同,会导致反序列化后的对象属性为空或者属性值混乱,使用时会导致问题,并且这种问题因为没有报异常所以更不容易发现。

既然说到了Redis的连接,要知道的是,Redis基于RESP(Redis Serialization Protocol)协议来通信,并且通信方式是停等方式,也就说一次通信独占一个连接直到client读取到返回结果之后才能释放该连接让其他线程使用。小伙伴们可以思考一下,Redis通信能否像dubbo那样使用单连接+序列号(标识单次通信)通信方式呢?理论上是可以的,不过由于RESP协议中并没有一个"序列号"的字段,所以直接靠原生的通信方法来实现是不现实的。不过我们可以通过echo命令传递并返回"序列号"+正常的读写方式来实现,这里要保证二者执行的原子性,可以通过lua脚本或者事务来实现,事务方式如下:

MULTI
ECHO "唯一序列号"
GET key1
EXEC

然后客户端收到的结果是一个 [ "唯一序列号", "value1" ]的列表,你可以根据前一项识别出这是你发送的哪个请求。

为什么Redis通信方式并没有采用类似于dubbo这种通信方式呢,个人认为有以下几点:

  • 使用停等这种通信方式实现简单,并且协议字段尽可能紧凑;
  • Redis都是内存操作,处理性能较强,停等协议不会造成客户端等待时间较长;
  • 目前来看,通信方式这块不是Redis使用上的性能瓶颈,这一点很重要。

推荐阅读:

欢迎小伙伴扫描以下二维码阅读更多精彩好文。

 

一次线上Redis类转换异常排查引发的思考的更多相关文章

  1. 线上Redis偶发性链接失败排查记

    问题过程 输入法业务于12月12日上线了词库接受业务,对部分用户根据用户uuid判断进行回传,在12月17日早上8点多开始出现大量的php报错(Redis went away),报错导致了大量的链接积 ...

  2. 线上redis服务内存异常分析。

    项目中,新增了一个统计功能,用来统计不同手机型号的每天访问pv,看了下redis2.6有个setbit的功能,于是打算尝尝鲜把 redis从2.4更新到了2.6 因为是租了vps.服务器的内存只有4g ...

  3. 线上Kafka突发rebalance异常,如何快速解决?

    文章首发于[陈树义的博客],点击跳转到原文<线上Kafka突发rebalance异常,如何快速解决?> Kafka 是我们最常用的消息队列,它那几万.甚至几十万的处理速度让我们为之欣喜若狂 ...

  4. Linux(2)---记录一次线上服务 CPU 100%的排查过程

    Linux(2)---记录一次线上服务 CPU 100%的排查过程 当时产生CPU飙升接近100%的原因是因为项目中的websocket时时断开又重连导致CPU飙升接近100% .如何排查的呢 是通过 ...

  5. 一次线上CPU高的问题排查实践

    一次线上CPU高的问题排查实践 前言 近期某一天上班一开电脑,就收到了运维警报,有两台服务CPU负载很高,同时收到一线同事反馈 系统访问速度非常慢,几乎无响应. 一个美好的早晨,最怕什么就来什么.只好 ...

  6. 线上CPU飙升100%问题排查

    本文转载自线上CPU飙升100%问题排查 引子 对于互联网公司,线上CPU飙升的问题很常见(例如某个活动开始,流量突然飙升时),按照本文的步骤排查,基本1分钟即可搞定!特此整理排查方法一篇,供大家参考 ...

  7. 线上redis问题修复:JedisConnectionException: Unexpected end of stream.

    经过: 项目上线后经常报 Unexpected end of stream.; nested exception is redis.clients.jedis.exceptions.JedisConn ...

  8. 线上Redis高并发性能调优实践

    项目背景 最近,做一个按优先级和时间先后排队的需求.用 Redis 的 sorted set 做排队队列. 主要使用的 Redis 命令有, zadd, zcount, zscore, zrange ...

  9. 一次性搞清楚线上CPU100%,频繁FullGC排查套路

    “ 处理过线上问题的同学基本上都会遇到系统突然运行缓慢,CPU 100%,以及 Full GC 次数过多的问题. 当然,这些问题最终导致的直观现象就是系统运行缓慢,并且有大量的报警. 本文主要针对系统 ...

随机推荐

  1. 绑定 Binding Path=.,Binding.,Binding Source={StaticResource ResourceKey="Hello"} xmlns:sys="clr-namespace:System;assembly=mscorlib"

    xmlns:sys="clr-namespace:System;assembly=mscorlib" <Window.Resources> <Style Targ ...

  2. html 实体编码转换成原字符

    今天遇到件很恶心的事,某国外歌词网站提供的歌词在源文件里使用“&#数字;”格式的编码表示abcd....原来小菜我实在才疏学浅不知此为何物,于是特有的搜索引擎控,搜之.片刻得解,此乃html实 ...

  3. CTF必备技能丨Linux Pwn入门教程——栈溢出基础

    这是一套Linux Pwn入门教程系列,作者依据i春秋Pwn入门课程中的技术分类,并结合近几年赛事中出现的一些题目和文章整理出一份相对完整的Linux Pwn教程. 课程回顾>>Linux ...

  4. AccessCenter 模块结构

    AccessCenter 模块结构

  5. 强大的 strace 工具

    什么是 strace strace是Linux环境下的一款程序调试工具,用来监察一个应用程序所使用的系统调用. Strace是一个简单的跟踪系统调用执行的工具.在其最简单的形式中,它可以从开始到结束跟 ...

  6. 2-3 arrary数组的数值的计算

    In [2]: import numpy as np tang_array=np.array([[1,2,3],[4,5,6]]) tang_array Out[2]: array([[1, 2, 3 ...

  7. linux下网卡捆绑

    七种bond模式说明:mod=0:(balance-rr) Round-robin policy(平衡抡循环策略)mod=1:(active-backup) Active-backup policy( ...

  8. 网关地址和网关IP是什么,他们有什么关系?

    2019-12-19  新用户541...  转自 小糊涂大神 修改   通常情况下,一台终端上网必须设置IP地址.子网掩码.网关IP地址,终端IP地址与网关IP属于同一个网段,网关IP是终端访问外网 ...

  9. 第 33课 C++中的字符串(下)

    字符串与数字转换-标准库中提供了相关的类对字符串和数字进行转换-字符串流类(sstream)用于string的转换.<sstream>-相关头文件.istringstream-字符串输入流 ...

  10. 第 33课 C++中的字符串(上)

    历史的遗留问题在C语言中没有真正意义上的字符串,为了表达字符串这个概念利用字符数组来模拟字符串.C语言不支持真正意义上的字符串 (C++也不支持)C语言用字符数组和一组函数实现字符串操作 (C++中同 ...