之前同事反馈说线上遇到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. 【转载】百度百科:FusionCube超融合

    [转载]百度百科:FusionCube超融合 华为FusionCube融合基础设施一体机(Huawei FusionCube Converged Infrastructure)是华为公司IT产品线云计 ...

  2. MySQL5.6升级到5.7详细教程

    前言:最近看了下系统的数据库是5.6的,想着升级到5.7,特此记录 一.官网下载MySQL5.7rpm包(4个) 进入MySQL community download页面,默认是MySQL最新版8.0 ...

  3. Prism——Window 必须是树的根目录。不能将 Window 添加为 Visual 的子目录。

    这个错误就是作为Region的view添加时选成了界面,正确的应在添加时选择用户控件. 解决方法: 这俩处的Window改为UserControl即可.

  4. [转]awsome c++

    原文链接 Awesome C++ A curated list of awesome C++ (or C) frameworks, libraries, resources, and shiny th ...

  5. maven Could not resolve dependencies

    错误语句 Could not resolve dependencies for project weiyinfu:poemqa:jar:1.0: The following artifacts cou ...

  6. git使用读书笔记

    Normal 0 7.8 磅 0 2 false false false EN-US ZH-CN X-NONE /* Style Definitions */ table.MsoNormalTable ...

  7. Android 拖动条/滑动条控件、星级评分控件

    ProgressBar有2个子控件: SeekBar   拖动条控件 RatingBar   星级评分控件 1.拖动条控件 <SeekBar android:layout_width=" ...

  8. ucoreOS_lab2 实验报告

    所有的实验报告将会在 Github 同步更新,更多内容请移步至Github:https://github.com/AngelKitty/review_the_national_post-graduat ...

  9. MyCat教程六:全局序列号-全局主键的自增长

      前面我们介绍了MyCat的分库分表操作,那么同一张表中的数据会被保存在不同的数据库中,那么这就涉及到了主键维护的问题,此时肯定不能使用单个数据库中id自增的方式来处理了,这时我们就可以通过MyCa ...

  10. composer基本操作详解

    原文转自微信公众号:qq1005349393 Composer介绍 Composer 是 PHP 的一个包依赖管理工具.我们可以在项目中声明所依赖的外部工具库,Composer 会帮你安装这些依赖的库 ...