一年前,笔者刚刚接触 RPC 框架,从单体式应用向分布式应用的变革无疑是让人兴奋的,同时也对 RPC 背后到底做了哪些工作产生了兴趣,但其底层的设计对新手而言并不是很友好,其涉及的一些常用技术点都有一定的门槛。如传输层常常使用的 netty,之前完全没听过,想要学习它,需要掌握前置知识点 nio;协议层,包括了很多自定义的协议,而每个 RPC 框架的实现都有差异;代理层的动态代理技术,如 jdk 动态代理,虽然实战经验不多,但至少还算会用,而 cglib 则又有一个盲区;序列化层倒还算是众多层次中相对简单的一环,但 RPC 为了追求可扩展性,性能等诸多因素,通常会支持多种序列化方式以供使用者插拔使用,一些常用的序列化方案 hessian,kryo,Protobuf 又得熟知…

这个系列打算就 RPC 框架涉及到的一些知识点进行探讨,本篇先从序列化层的一种选择 –kryo 开始进行介绍。

序列化概述

大白话介绍下 RPC 中序列化的概念,可以简单理解为对象 –> 字节的过程,同理,反序列化则是相反的过程。为什么需要序列化?因为网络传输只认字节。所以互信的过程依赖于序列化。有人会问,FastJson 转换成字符串算不算序列化?对象持久化到数据库算不算序列化?没必要较真,广义上理解即可。

JDK 序列化

可能你没用过 kryo,没用过 hessian,但你一定用过 jdk 序列化。我最早接触 jdk 序列化,是在大二的 JAVA 大作业中,《XX 管理系统》需要把对象保存到文件中(那时还没学数据库),jdk 原生支持的序列化方式用起来也很方便。

class Student implements Serializable{  private String name;
}
class Main{public static void main(String[] args) throws Exception{  // create a StudentStudent st = new Student("kirito");  // serialize the st to student.db file  ObjectOutputStream oos = new ObjectOutputStream(new FileOutputStream("student.db"));  oos.writeObject(st);  oos.close();  // deserialize the object from student.dbObjectInputStream ois = new ObjectInputStream(new FileInputStream("student.db"));  Student kirito = (Student) ois.readObject();  ois.close();  // assertassert "kirito".equals(kirito.getName());  }
}

Student 实体类需要实现 Serializable 接口,以告知其可被序列化。

序列化协议的选择通常有下列一些常用的指标:

  1. 通用性。是否只能用于 java 间序列化 / 反序列化,是否跨语言,跨平台。
  2. 性能。分为空间开销和时间开销。序列化后的数据一般用于存储或网络传输,其大小是很重要的一个参数;解析的时间也影响了序列化协议的选择,如今的系统都在追求极致的性能。
  3. 可扩展性。系统升级不可避免,某一实体的属性变更,会不会导致反序列化异常,也应该纳入序列化协议的考量范围。
  4. 易用性。API 使用是否复杂,会影响开发效率。

容易用的模型通常性能不好,性能好的模型通常用起来都比较麻烦。显然,JDK 序列化属于前者。我们不过多介绍它,直接引入今天的主角 kryo 作为它的替代品。

Kryo 入门

引入依赖

<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo</artifactId><version>4.0.1</version>
</dependency>

由于其底层依赖于 ASM 技术,与 Spring 等框架可能会发生 ASM 依赖的版本冲突(文档中表示这个冲突还挺容易出现)所以提供了另外一个依赖以供解决此问题

<dependency><groupId>com.esotericsoftware</groupId><artifactId>kryo-shaded</artifactId><version>4.0.1</version>
</dependency>

快速入门

class Student implements Serializable{  private String name;
}
public class Main {public static void main(String[] args) throws Exception{Kryo kryo = new Kryo();Output output = new Output(new FileOutputStream("student.db"));Student kirito = new Student("kirito");kryo.writeObject(output, kirito);output.close();Input input = new Input(new FileInputStream("student.db"));Student kiritoBak = kryo.readObject(input, Student.class);input.close();assert "kirito".equals(kiritoBak.getName());}
}

不需要注释也能理解它的执行流程,和 jdk 序列化差距并不是很大。

三种读写方式

Kryo 共支持三种读写方式

  1. 如果知道 class 字节码,并且对象不为空
kryo.writeObject(output, someObject);
// ...
SomeClass someObject = kryo.readObject(input, SomeClass.class);

快速入门中的序列化 / 反序列化的方式便是这一种。而 Kryo 考虑到 someObject 可能为 null,也会导致返回的结果为 null,所以提供了第二套读写方式。

  1. 如果知道 class 字节码,并且对象可能为空
kryo.writeObjectOrNull(output, someObject);
// ...
SomeClass someObject = kryo.readObjectOrNull(input, SomeClass.class);

但这两种方法似乎都不能满足我们的需求,在 RPC 调用中,序列化和反序列化分布在不同的端点,对象的类型确定,我们不想依赖于手动指定参数,最好是…emmmmm… 将字节码的信息直接存放到序列化结果中,在反序列化时自行读取字节码信息。Kryo 考虑到了这一点,于是提供了第三种方式。

  1. 如果实现类的字节码未知,并且对象可能为 null
kryo.writeClassAndObject(output, object);
// ...
Object object = kryo.readClassAndObject(input);
if (object instanceof SomeClass) {// ...
}

我们牺牲了一些空间一些性能去存放字节码信息,但这种方式是我们在 RPC 中应当使用的方式。

我们关心的问题

继续介绍 Kryo 特性之前,不妨让我们先思考一下,一个序列化工具或者一个序列化协议,应当需要考虑哪些问题。比如,支持哪些类型的序列化?循环引用会不会出现问题?在某个类增删字段之后反序列化会报错吗?等等等等….

带着我们考虑到的这些疑惑,以及我们暂时没考虑到的,但 Kryo 帮我们考虑到的,来看看 Kryo 到底支持哪些特性。

支持的序列化类型

boolean Boolean byte Byte char
Character short Short int Integer
long Long float Float double
Double byte[] String BigInteger BigDecimal
Collection Date Collections.emptyList Collections.singleton Map
StringBuilder TreeMap Collections.emptyMap Collections.emptySet KryoSerializable
StringBuffer Class Collections.singletonList Collections.singletonMap Currency
Calendar TimeZone Enum EnumSet

表格中支持的类型一览无余,这都是其默认支持的。

Kryo kryo = new Kryo();
kryo.addDefaultSerializer(SomeClass.class, SomeSerializer.class);

这样的方式,也可以为一个 Kryo 实例扩展序列化器。

总体而言,Kryo 支持以下的类型:

  • 枚举
  • 集合、数组
  • 子类 / 多态
  • 循环引用
  • 内部类
  • 泛型

但需要注意的是,Kryo 不支持 Bean 中增删字段 。如果使用 Kryo 序列化了一个类,存入了 Redis,对类进行了修改,会导致反序列化的异常。

另外需要注意的一点是使用反射创建的一些类序列化的支持。如使用 Arrays.asList(); 创建的 List 对象,会引起序列化异常。

Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): java.util.Arrays$ArrayList

但 new ArrayList() 创建的 List 对象则不会,使用时需要注意,可以使用第三方库对 Kryo 进行序列化类型的扩展。如 https://github.com/magro/kryo-serializers 所提供的。

不支持包含无参构造器类的反序列化 ,尝试反序列化一个不包含无参构造器的类将会得到以下的异常:

Exception in thread "main" com.esotericsoftware.kryo.KryoException: Class cannot be created (missing no-arg constructor): moe.cnkirito.Xxx

保证每个类具有无参构造器是应当遵守的编程规范,但实际开发中一些第三库的相关类不包含无参构造,的确是有点麻烦。

线程安全

Kryo 是线程不安全的,意味着每当需要序列化和反序列化时都需要实例化一次,或者借助 ThreadLocal 来维护以保证其线程安全

    private static final ThreadLocal<Kryo> KRYO_LOCAL = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();kryo.setReferences(false);kryo.setRegistrationRequired(false);return kryo;});// Somewhere else, use Kryo
Kryo k = KRYO_LOCAL.get();

Kryo 相关配置参数详解

每个 Kryo 实例都可以拥有两个配置参数,这值得被拉出来单独聊一聊。

kryo.setRegistrationRequired(false);// 关闭注册行为
kryo.setReferences(true);// 支持循环引用

Kryo 支持对注册行为,如 kryo.register(SomeClazz.class);, 这会赋予该 Class 一个从 0 开始的编号,但 Kryo 使用注册行为最大的问题在于,其不保证同一个 Class 每一次注册的号码想用,这与注册的顺序有关,也就意味着在不同的机器、同一个机器重启前后都有可能拥有不同的编号,这会导致序列化产生问题,所以在分布式项目中,一般关闭注册行为。

第二个注意点在于循环引用,Kryo 为了追求高性能,可以关闭循环引用的支持。不过我并不认为关闭它是一件好的选择,大多数情况下,请保持 kryo.setReferences(true)

常用 Kryo 工具类

public class KryoSerializer {public static byte[] serialize(Object obj) {try {Kryo kryo = KRYO_LOCAL.get();ByteArrayOutputStream byteArrayOutputStream = new ByteArrayOutputStream();Output output = new Output(byteArrayOutputStream);kryo.writeClassAndObject(output, obj);output.close();return byteArrayOutputStream.toByteArray();} catch (Throwable t) {return null;} finally {// 防止内存泄露KRYO_LOCAL.remove();}}public static <T> T deserialize(byte[] bytes) {try {Kryo kryo = KRYO_LOCAL.get();ByteArrayInputStream byteArrayInputStream = new ByteArrayInputStream(bytes);Input input = new Input(byteArrayInputStream);input.close();return (T) kryo.readClassAndObject(input);} catch (Throwable t) {return null;} finally {// 防止内存泄露KRYO_LOCAL.remove();}}private static final ThreadLocal<Kryo> KRYO_LOCAL = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();kryo.setReferences(false);kryo.setRegistrationRequired(false);return kryo;});}
  1. Kryo 的 Input 和 Output 接收一个 InputStream 和 OutputStream,Kryo 通常完成字节数组和对象的转换,所以常用的输入输出流实现为 ByteArrayInputStream/ByteArrayOutputStream。
  2. writeClassAndObject 和 readClassAndObject 配对使用在分布式场景下是最常见的,序列化时将字节码存入序列化结果中,便可以在反序列化时不必要传入字节码信息。
  3. 使用 ThreadLocal 维护 Kryo 实例,这样减少了每次使用都实例化一次 Kryo 的开销又可以保证其线程安全。

参考文章

https://github.com/EsotericSoftware/kryo

Kryo 使用指南

序列化与反序列化

转载至: https://www.cnkirito.moe/rpc-serialize-1/

深入理解 RPC 之序列化篇 --Kryo相关推荐

  1. 深入理解 RPC 之集群篇

    上一篇文章分析了服务的注册与发现,这一篇文章着重分析下 RPC 框架都会用到的集群的相关知识. 集群(Cluster)本身并不具备太多知识点,在分布式系统中,集群一般涵盖了负载均衡(LoadBalan ...

  2. 深入理解 RPC : 基于 Python 自建分布式高并发 RPC 服务

    RPC(Remote Procedure Call)服务,也即远程过程调用,在互联网企业技术架构中占据了举足轻重的地位,尤其在当下微服务化逐步成为大中型分布式系统架构的主流背景下,RPC 更扮演了重要 ...

  3. matlab温度数据怎么滤波_卡尔曼滤波算法思想理解 Kalman filter 第一篇

    卡尔曼滤波算法思想理解 Kalman filter 第一篇 最近在初步的理解目标跟踪的领域, 其中一个非常经典的算法卡尔曼滤波Kalman filter是需要有很好的理解才行, 由于已经脱离了学校,懂 ...

  4. 理解Java对象序列化

    理解Java对象序列化 关于Java序列化的文章早已是汗牛充栋了,本文是对我个人过往学习,理解及应用Java序列化的一个总结.此文内容涉及Java序列化的基本原理,以及多种方法对序列化形式进行定制.在 ...

  5. krait和kryo_java原生序列化和Kryo序列化性能实例对比分析

    简介 最近几年,各种新的高效序列化方式层出不穷,不断刷新序列化性能的上限,最典型的包括: 专门针对java语言的:Kryo,FST等等 跨语言的:Protostuff,ProtoBuf,Thrift, ...

  6. 关于IC卡密钥理解和修改(简易篇)

    关于IC卡密钥理解和修改(简易篇) 常用的IC卡由于结构简单,使用方便,价格低,越来越受到普通应用的欢迎.本文主要描述普通IC卡的密钥相关的知识. 关于IC卡的读写,必须使用IC卡读写器, 我们推荐使 ...

  7. 带你快速看完9.8分神作《Effective Java》—— 序列化篇(所有RPC框架的基石)

  8. 深入浅出 RPC - 浅出篇+深入篇

    摘自: http://blog.csdn.net/mindfloating/article/details/39473807 近几年的项目中,服务化和微服务化渐渐成为中大型分布式系统架构的主流方式,而 ...

  9. python rpc调用_从0到1:全面理解 RPC 远程调用

    上一篇关于 WSGI 的硬核长文,不知道有多少同学,能够从头看到尾的,不管你们有没有看得很过瘾,反正我是写得很爽,总有一种将一样知识吃透了的错觉. 今天我又给自己挖坑了,打算将 rpc 远程调用的知识 ...

  10. 【RPC】序列化与反序列化

    文章目录 1. 基本概念? 2. 文本格式的序列化方案 2.1 XML格式 2.2 JSON格式 3. 二进制格式的序列化方法 4. 序列化框架选型 1. 基本概念? 序列化和反序列化是一种数据转化的 ...

最新文章

  1. 信息批量提取工具bulk-extractor
  2. 面试必考的网络协议相关题目应该如何回答
  3. 2013年1月12日学习内容
  4. 计算机 360云盘删除,xp系统下如何删除360云盘显示图标
  5. UIView Animation效果
  6. Android自定义类似ProgressDialog效果的Dialog
  7. 2016年物联网技术将从概念走向落地
  8. java方法前面加上x_@Autowired 写在构造方法上
  9. 网易裁员事件引发的 5 点重要思考
  10. Python 正则表达式的$美元符号
  11. Archlinux的灵魂──PKGBUILD、AUR 和 ABS
  12. Spring Boot学习8——Redis
  13. 8种bootstrap团队会员头像样式代码
  14. java语言中如何表示素数,使用Java语言求素数的几个方法
  15. Oracle查询数据表数据很少却很慢(查询空表很很耗时)
  16. XML解析之DOM、SAX、JAXP、DOM4J
  17. 谁将成为人工智能行业的“领头羊”?
  18. java软件工程师自我评价_Java开发工程师岗位自我评价范文
  19. js 判断当前的手机系统类型
  20. Spring容器 SpringMVC容器 web容器的关系

热门文章

  1. 飚王硬盘盒怎么样_四款USB 3.0硬盘盒完全拆解_DIY攒机-中关村在线
  2. 八戒,别以为你站在路灯下就是夜明猪了!
  3. REST API Concerns
  4. 无法打开https网页终极解决方法
  5. SumatraPDF 高级设置
  6. QT6程序全屏和隐藏鼠标指针笔记
  7. OpenGL 栅格化
  8. Java之网络编程(一)
  9. 暑假2019培训:Day3Day4提高组测试赛
  10. 域名被hold了怎么办?