RPC实战与核心原理——序列化(笔记)
序列化的作用
序列化就是将对象转换成二进制数据的过程,而反序列就是反过来将二进制转换为对象的过程。因为网络传输的数据必须是二进制数据,所以在 RPC 调用中,对入参对象与返回值对象进行序列化与反序列化是一个必须的过程。
设计核心思想
任何一种序列化框架,核心思想就是设计一种序列化协议,将对象的类型、属性类型、属性值一一按照固定的格式写到二进制字节流中来完成序列化,再按照固定的格式一一读出对象的类型、属性类型、属性值,通过这些信息重新创建出一个新的对象,来完成反序列化。
常见的序列化方式
JDK原生序列化
JDK 自带的序列化机制对使用者而言是非常简单的。序列化具体的实现是
由 ObjectOutputStream
完成的,而反序列化的具体实现是ObjectInputStream
完成的。
序列化过程就是在读取对象数据的时候,不断加入一些特殊分隔符,这些特殊分隔符用于在反序列化过程中截断用。
- 头部数据用来声明序列化协议、序列化版本,用于高低版本向后兼容
- 对象数据主要包括类名、签名、属性名、属性类型及属性值,当然还有开头结尾等数据,除了属性值属于真正的对象值,其他都是为了反序列化用的元数据
- 存在对象引用、继承的情况下,就是递归遍历“写对象”逻辑
JSON
JSON 是典型的 Key-Value 方式,没有数据类型,是一种文本型序列化框架
缺点:
- JSON 进行序列化的额外空间开销比较大,对于大数据量服务这意味着需要巨大的内存和磁盘开销;
- JSON 没有类型,但像 Java 这种强类型语言,需要通过反射统一解决,所以性能不会太好。
Hessian
Hessian 是动态类型、二进制、紧凑的,并且可跨语言移植的一种序列化框架。Hessian 协议要比 JDK、JSON 更加紧凑,性能上要比 JDK、JSON 序列化高效很多,而且生成的字节数也更小。有非常好的兼容性和稳定性
缺点:
官方版本对 Java 里面一些常见对象的类型不支持:
- Linked 系列,LinkedHashMap、LinkedHashSet 等,但是可以通过扩展CollectionDeserializer 类修复;
- Locale 类,可以通过扩展 ContextSerializerFactory 类修复;
- Byte/Short 反序列化的时候变成 Integer。
Protobuf
Protobuf 是 Google 内部的混合语言数据标准,是一种轻便、高效的结构化数据存储格式,可以用于结构化数据序列化,支持 Java、Python、C++、Go 等语言。Protobuf 使用的时候需要 定义 IDL(Interface description language),然后使用不同语言的 IDL编译器,生成序列化工具类,它的优点是:
- 序列化后体积相比 JSON、Hessian 小很多;
- IDL 能清晰地描述语义,所以足以帮助并保证应用程序之间的类型不会丢失,无需类似XML 解析器;
- 序列化反序列化速度很快,不需要通过反射获取类型;
- 消息格式升级和兼容性不错,可以做到向后兼容。
缺点: - 对于具有反射和动态能力的语言来说,这样用起来很费劲,这一点就不如 Hessian
- 不支持 null;
- ProtoStuff 不支持单纯的 Map、List 集合对象,需要包在对象里面。
选择序列化的标准
- 效率和性能
- 空间开销:序列化之后的二进制数据的体积大小;
- 通用性和兼容性:考虑的优先级高于前两者。会直接关系到服务调用的稳定性和可用率的;
- 序列的安全性
建议使用类型
首选是 Hessian 与 Protobuf,因为他们在性能、时间开销、空间开销、通用性、兼容性和安全性上,都满足了我们的要求。其中 Hessian 在使用上更加方便,在对象的兼容性上更好;Protobuf 则更加高效,通用性上更有优势。
使用过程中需要避免的问题
- 对象构造得过于复杂:属性很多,并且存在多层的嵌套。出现会导致浪费性能、消耗CPU,严重的会影响RPC框架的整体性能,在反序列过程中出错的概率增加;
- 对象过于庞大:会导致RPC请求经常超时
- 使用序列化框架不支持的类作为入参类:在大多数情况下最好不要使用第三方集合类;
- 对象有复杂的继承关系:大多数序列化框架在序列化对象时都会将对象的属性一一进行序列化,当有继承关系时,会不停地寻找父类,遍历属性。