一次线上Redis类转换异常排查引起的思考

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

XxxClass1 cannot be cast to XxxClass2

已知信息以下:json

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

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

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis业务读写操做
 4 } catch (Exception e) {
 5     // 异常处理
 6 } finally {
 7     if (jedis != null) {
 8         // 归还给链接池
 9         jedisPool.returnResourceObject(jedis);
10     }
11 }

初步认定缘由为:发生了读写超时的链接,直接归还给链接池,下次使用该链接时读取到了上一次Redis返回的数据。所以本地验证下,示例代码以下:服务器

 1 @Data
 2 @NoArgsConstructor
 3 @AllArgsConstructor
 4 static class Person implements Serializable {
 5     private String name;
 6     private int age;
 7 }
 8 @Data
 9 @NoArgsConstructor
10 @AllArgsConstructor
11 static class Dog implements Serializable {
12     private String name;
13 }
14 
15 public static void main(String[] args) throws Exception {
16     JedisPoolConfig config = new JedisPoolConfig();
17     config.setMaxTotal(1);
18     JedisPool jedisPool = new JedisPool(config, "192.168.193.133", 6379, 2000, "123456");
19 
20     Jedis jedis = jedisPool.getResource();
21     jedis.set("key1".getBytes(), serialize(new Person("luoxn28", 26)));
22     jedis.set("key2".getBytes(), serialize(new Dog("tom")));
23     jedisPool.returnResourceObject(jedis);
24 
25     try {
26         jedis = jedisPool.getResource();
27         Person person = deserialize(jedis.get("key1".getBytes()), Person.class);
28         System.out.println(person);
29     } catch (Exception e) {
30         // 发生了异常以后,未对该链接作任何处理
31         System.out.println(e.getMessage());
32     } finally {
33         if (jedis != null) {
34             jedisPool.returnResourceObject(jedis);
35         }
36     }
37 
38     try {
39         jedis = jedisPool.getResource();
40         Dog dog = deserialize(jedis.get("key2".getBytes()), Dog.class);
41         System.out.println(dog);
42     } catch (Exception e) {
43         System.out.println(e.getMessage());
44     } finally {
45         if (jedis != null) {
46             jedisPool.returnResourceObject(jedis);
47         }
48     }
49 }

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

Person cannot be cast to Dog

既然已经知道了该问题缘由而且本地复现了该问题,对应解决方案是,在发生异常时归还给链接池时关闭该链接便可(jedis.close内部已经作了判断),代码以下:分布式

 1 try {
 2     jedis = jedisPool.getResource();
 3     // jedis业务读写操做
 4 } catch (Exception e) {
 5     // 异常处理
 6 } finally {
 7     if (jedis != null) {
 8         // 归还给链接池
 9         jedis.close();
10     }
11 }

至此,该问题解决。注意,由于使用了hessian序列化(其包含了类型信息,相似的有Java自己序列化机制),全部会报类转换异常;若是使用了json序列化(其只包含对象属性信息),反序列化时不会报异常,只不过由于不一样类的属性不一样,会致使反序列化后的对象属性为空或者属性值混乱,使用时会致使问题,而且这种问题由于没有报异常因此更不容易发现。性能

 

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

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

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

为何Redis通讯方式并无采用相似于dubbo这种通讯方式呢,我的认为有如下几点:spa

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

 

推荐阅读:

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

相关文章
相关标签/搜索