之前同事反馈说线上遇到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. AngleSharp 实战(03)之遍历内部子元素

    文档地址:https://anglesharp.github.io/docs/Examples.html 直接贴代码了: using System; using System.Linq; using ...

  2. 使用三层架构+EF添加单元测试

    在运行测试的时候抛异常了: “System.InvalidOperationException”类型的异常在 mscorlib.dll 中发生,但未在用户代码中进行处理 The Entity Fram ...

  3. 2019年ASP.NET Core学习路线

    - [先决条件] + C# + Entity Framework + ASP.NET Core + SQL 基础知识 - [通用开发技能] + 学习 GIT, 在 GitHub 中创建开源项目 + 掌 ...

  4. CA of sharepoint2016

    总体来说,CA界面变动不大

  5. 尝试在iOS上使用gRPC

    简介 gRPC,google的远程过程调用框架,传输协议使用 HTTP2, 序列化协议使用 protobuf.gRPC 使用 HTTP2 传输协议传输 protobuf 序列化的二进制数据,有极高的效 ...

  6. system execl

    1. system(); int system(const char *command); 在Windows下,用来调用常用的Dos命令 在Linux下,system()会调用fork()产生子进程, ...

  7. 个人项目(WC.exe)(java)(基于图形界面)

    一.Github项目地址:https://github.com/Leungdc/ENhomework 二.PSP: PSP2.1 Personal Software Process Stages 预估 ...

  8. leetcode - 链表两两元素交换 + 判断链表有无环

    链表两两元素交换 给定一个链表,两两交换其中相邻的节点,并返回交换后的链表. 你不能只是单纯的改变节点内部的值,而是需要实际的进行节点交换. 示例: 给定 1->2->3->4, 你 ...

  9. Delphi-基础(运算符)

    一.运算符 1.变量 2.运算符** 3.表达式 1.变量 变量解释:编程中最小的存储单元(空间),它的空间大小由它在声明时的数据类型决定. 1.1.声明 : 定义一个变量,告诉Delphi一个名字的 ...

  10. SQL之单表与多表查询

    DML语句使用 source  路径  :把SQL脚本导入到数据库中 查询语句类型:[简单查询|多表查询|子查询] 投影:select    字段名,字段名   from   表名   where   ...