Dubbo

您所在的位置:网站首页 dubbo报错 Dubbo

Dubbo

2024-07-12 20:40| 来源: 网络整理| 查看: 265

摘要

RPC 框架需要通过网络通信实现跨 JVM 的调用。既然需要网络通信,那就必然会使用到序列化与反序列化的相关技术,Dubbo 也不例外。下面我们从 Java 序列化的基础内容开始,介绍一下常见的序列化算法,最后再分析一下 Dubbo 是如何支持这些序列化算法的。

序列化的设计原则 解析效率:互联网业务具有高并发的特点,解析效率决定了使用协议的CPU成本(序列化和反序列);编码长度:信息编码出来的长度,编码长度决定了使用协议的网络带宽及存储成本;易于实现:互联网业务需要一个轻量级的协议,而不是大而全的(三种序列化协议的对比);可读性:编码后的数据的可读性决定了使用协议的调试及维护成本(不同的序列化协议是有不同的应用的场景);兼容性:互联网的需求具有灵活多变的特点,协议会经常升级,使用协议的双方是否可以独立升级协议、增减协议中的字段是非常重要的;跨平台跨语言:互联网的的业务涉及到不同的平台和语言,比如Windows用C++,Android用Java,Web用Js,IOS用object-c。准确、有序、完整(UDP/TCP的选择)边界 序列化基础方式 为什么需要序列化呢? 一般情况下Java对象的声明周期都比Java虚拟机的要短,实际应用中我们希望在JVM停止运行之后能够持久化指定的对象,这时候就需要把对象进行序列化之后保存。需要把Java对象通过网络进行传输的时候。因为数据只能够以二进制的形式在网络中进行传输,因此当把对象通过网络发送出去之前需要先序列化成二进制数据,在接收端读到二进制数据之后反序列化成Java对象。如果要对对象进行保存,就需要进行序列化计算机计算时只能使用二进制,因为只有高电平和低电平,所以用二进制,用十进制表示只是为了方便些。计算机的电子元件使用二进制能比较快速进行逻辑判断和计算,所以网络传输需要进行序列化,转为二进制处理。 为什么实现序列化呢? 实现 Serializable 接口 public class Student implements Serializable { private static final long serialVersionUID = 1L; private String name; private int age; private transient StudentUtil studentUtil; }

在这个示例中我们可以看到transient 关键字,它的作用就是:在对象序列化过程中忽略被其修饰的成员属性变量。一般情况下,它可以用来修饰一些非数据型的字段以及一些可以通过其他字段计算得到的值。通过合理地使用 transient 关键字,可以降低序列化后的数据量,提高网络传输效率。

生成一个序列号 serialVersionUID

这个序列号不是必需的,但还是建议你生成。serialVersionUID 的字面含义是序列化的版本号,只有序列化和反序列化的 serialVersionUID 都相同的情况下,才能够成功地反序列化。如果类中没有定义 serialVersionUID,那么 JDK 也会随机生成一个 serialVersionUID。如果在某些场景中,你希望不同版本的类序列化和反序列化相互兼容,那就需要定义相同的 serialVersionUID。

重写 writeObject()/readObject() 方法

根据需求决定是否要重写 writeObject()/readObject() 方法,实现自定义序列化

调用 java.io.ObjectOutputStream()方法

最后一步,调用 java.io.ObjectOutputStream 的 writeObject()/readObject() 进行序列化与反序列化。

既然 Java 本身的序列化操作如此简单,那为什么市面上还依旧出现了各种各样的序列化框架呢?因为这些第三方序列化框架的速度更快、序列化的效率更高,而且支持跨语言操作。

序列化的实现原理

为了更好地理解Java序列化与反序列化,举一个简单的示例如下:

public class SerialDemo { public static void main(String[] args) throws IOException, ClassNotFoundException { //序列化 FileOutputStream fos = new FileOutputStream("object.out"); ObjectOutputStream oos = new ObjectOutputStream(fos); User user1 = new User("xuliugen", "123456", "male"); oos.writeObject(user1); oos.flush(); oos.close(); //反序列化 FileInputStream fis = new FileInputStream("object.out"); ObjectInputStream ois = new ObjectInputStream(fis); User user2 = (User) ois.readObject(); System.out.println(user2.getUserName()+ " " + user2.getPassword() + " " + user2.getSex()); //反序列化的输出结果为:xuliugen 123456 male } } public class User implements Serializable { private String userName; private String password; private String sex; //全参构造方法、get和set方法省略 }

object.out文件如下:

 注:上图中0000000h-000000c0h表示行号;0-f表示列;行后面的文字表示对这行16进制的解释;类似于我们Java代码编译之后的.class文件,每一个字符都代表一定的含义。序列化和反序列化的过程就是生成和解析上述字符的过程!

序列化相关注意事项 序列化时,只对对象的状态进行保存,而不管对象的方法;当一个父类实现序列化,子类自动实现序列化,不需要显式实现Serializable接口;当一个对象的实例变量引用其他对象,序列化该对象时也把引用对象进行序列化;并非所有的对象都可以序列化,至于为什么不可以,有很多原因了,比如: 安全方面的原因,比如一个对象拥有private,public等field,对于一个要传输的对象,比如写到文件,或者进行RMI传输等等,在序列化进行传输的过程中,这个对象的private等域是不受保护的; 资源分配方面的原因,比如socket,thread类,如果可以序列化,进行传输或者保存,也无法对他们进行重新的资源分配,而且,也是没有必要这样实现;声明为static和transient类型的成员数据不能被序列化。因为static代表类的状态,transient代表对象的临时数据。序列化运行时使用一个称为 serialVersionUID 的版本号与每个可序列化类相关联,该序列号在反序列化过程中用于验证序列化对象的发送者和接收者是否为该对象加载了与序列化兼容的类。为它赋予明确的值。显式地定义serialVersionUID有两种用途: 在某些场合,希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有相同的serialVersionUID; 在某些场合,不希望类的不同版本对序列化兼容,因此需要确保类的不同版本具有不同的serialVersionUID。Java有很多基础类已经实现了serializable接口,比如String,Vector等。但是也有一些没有实现serializable接口的;如果一个对象的成员变量是一个对象,那么这个对象的数据成员也会被保存!这是能用序列化解决深拷贝的重要原因; 序列化常见算法

Apache Avro序列化格式

Avro 依赖于用户自定义的 Schema,在进行序列化数据的时候,无须多余的开销,就可以快速完成序列化,并且生成的序列化数据也较小。当进行反序列化的时候,需要获取到写入数据时用到的 Schema。在 Kafka、Hadoop 以及 Dubbo 中都可以使用 Avro 作为序列化方案。

FastJsonJSON解析库

它支持将 Java 对象序列化为 JSON 字符串,反过来从 JSON 字符串也可以反序列化为 Java 对象。FastJson 是 Java 程序员常用到的类库之一,正如其名,“快”是其主要卖点。从官方的测试结果来看,FastJson 确实是最快的,比 Jackson 快 20% 左右,但是近几年 FastJson 的安全漏洞比较多,所以你在选择版本的时候,还是需要谨慎一些。

fast-serialization对象序列化

100% 兼容 JDK 原生环境,序列化速度大概是JDK 原生序列化的 4~10 倍,序列化后的数据大小是 JDK 原生序列化大小的 1/3 左右。目前,Fst 已经更新到 3.x 版本,支持 JDK 14。

Kryo序列化/反序列化库

目前 Twitter、Yahoo、Apache 等都在使用该序列化技术,特别是 Spark、Hive 等大数据领域用得较多。Kryo 提供了一套快速、高效和易用的序列化 API。无论是数据库存储,还是网络传输,都可以使用 Kryo 完成 Java 对象的序列化。Kryo 还可以执行自动深拷贝和浅拷贝,支持环形引用。Kryo 的特点是 API 代码简单,序列化速度快,并且序列化之后得到的数据比较小。另外,Kryo 还提供了 NIO 的网络通信库——KryoNet,你若感兴趣的话可以自行查询和了解一下。

Hessian2序列化协议

Java 对象序列化的二进制流可以被其他语言使用。Hessian2 序列化之后的数据可以进行自描述,不会像 Avro 那样依赖外部的 Schema 描述文件或者接口定义。Hessian2 可以用一个字节表示常用的基础类型,这极大缩短了序列化之后的二进制流。需要注意的是,在 Dubbo 中使用的 Hessian2 序列化并不是原生的 Hessian2 序列化,而是阿里修改过的 Hessian Lite,它是 Dubbo 默认使用的序列化方式。其序列化之后的二进制流大小大约是 Java 序列化的 50%,序列化耗时大约是 Java 序列化的 30%,反序列化耗时大约是 Java 序列化的 20%。

Protobuf序列化协议。

但相比于常用的 JSON 格式,Protobuf 有更高的转化效率,时间效率和空间效率都是 JSON 的 5 倍左右。Protobuf 可用于通信协议、数据存储等领域,它本身是语言无关、平台无关、可扩展的序列化结构数据格式。目前 Protobuf提供了 C++、Java、Python、Go 等多种语言的 API,gRPC 底层就是使用 Protobuf 实现的序列化。

序列化和反序列化的总结 protobufjacksonxstreamSerializablehessian2hessian2压缩hessian1序列化(单位ns)11545421 92406 101892679410076629027反序列化(单位ns)13348743 117329 640273787118843237596bytes97311 664 824374283495 protobuf 不管是处理时间上,还是空间占用上都优于现有的其他序列化方式。内存暂用是java 序列化的1/9,时间也是差了一个数量级,一次操作在1us左右。缺点:就是对象结构体有限制,只适合于内部系统使用。

json格式在空间占用还是有一些优势,是java序列化的1/2.6。序列化和反序列化处理时间上差不多,也就在5us。当然这次使用的jackson,如果使用普通的jsonlib可能没有这样好的性能,jsonlib估计跟java序列化差不多。

xml相比于java序列化来说,空间占用上有点优势,但不明显。处理时间上比java序列化多了一个数量级,在100us左右。

hessian测试有点意外,具体序列化数据上还步入json。性能上不如jackjson。

hessian使用压缩,虽然在字节上有20%以上的空间提升,但性能上差了4,5倍,典型的以时间换空间。总的来说还是google protobuf比较给力

dubbo-serialization

dubbo-serialization-api 模块中定义了 Dubbo 序列化层的核心接口,其中最核心的是 Serialization 这个接口,它是一个扩展接口,被 @SPI 接口修饰,默认扩展实现是 Hessian2Serialization。Serialization 接口的具体实现如下:

@SPI("hessian2")// 被@SPI注解修饰,默认是使用hessian2序列化算法 public interface Serialization { /** * 每一种序列化算法都对应一个ContentType,该方法用于获取ContentType */ byte getContentTypeId(); /** * 获取ContentType的ID值,是一个byte类型的值,唯一确定一个算法 */ String getContentType(); /** * 创建一个ObjectOutput对象,ObjectOutput负责实现序列化的功能,即将Java * 对象转化为字节序列 */ @Adaptive ObjectOutput serialize(URL url, OutputStream output) throws IOException; /** *创建一个ObjectInput对象,ObjectInput负责实现反序列化的功能,即将 *字节序列转换成Java对象 */ @Adaptive ObjectInput deserialize(URL url, InputStream input) throws IOException; }

Dubbo 提供了多个 Serialization 接口实现,用于接入各种各样的序列化算法,如下图所示: 

这里我们以默认的 hessian2 序列化方式为例,介绍 Serialization 接口的实现以及其他相关实现。 Hessian2Serialization 实现如下所示:

public class Hessian2Serialization implements Serialization { // hessian2的ContentType ID @Override public byte getContentTypeId() { return HESSIAN2_SERIALIZATION_ID; } // hessian2的ContentType @Override public String getContentType() { return "x-application/hessian2"; } // 创建ObjectOutput对象 @Override public ObjectOutput serialize(URL url, OutputStream out) throws IOException { return new Hessian2ObjectOutput(out); } // 创建ObjectInput对象 @Override public ObjectInput deserialize(URL url, InputStream is) throws IOException { return new Hessian2ObjectInput(is); } }

Hessian2Serialization 中的 serialize() 方法创建的 ObjectOutput 接口实现为Hessian2ObjectOutput,继承关系如下图所示:

在 DataOutput 接口中定义了序列化 Java 中各种数据类型的相应方法,如下图所示,其中有序列化 boolean、short、int、long 等基础类型的方法,也有序列化 String、byte[] 的方法。

ObjectOutput 接口继承了 DataOutput 接口,并在其基础之上,添加了序列化对象的功能,具体定义如下图所示,其中的 writeThrowable()、writeEvent() 和 writeAttachments() 方法都是调用 writeObject() 方法实现的。

Hessian2ObjectOutput 中会封装一个 Hessian2Output 对象,需要注意,这个对象是 ThreadLocal 的,与线程绑定。在 DataOutput 接口以及 ObjectOutput 接口中,序列化各类型数据的方法都会委托给 Hessian2Output 对象的相应方法完成,实现如下:

/** * Hessian2 object output implementation */ public class Hessian2ObjectOutput implements ObjectOutput, Cleanable { private static ThreadLocal OUTPUT_TL = ThreadLocal.withInitial(() -> { // 初始化Hessian2Output对象 Hessian2Output h2o = new Hessian2Output(null); h2o.setSerializerFactory(Hessian2FactoryInitializer.getInstance().getSerializerFactory()); h2o.setCloseStreamOnClose(true); return h2o; }); private final Hessian2Output mH2o; public Hessian2ObjectOutput(OutputStream os) { // 触发OUTPUT_TL的初始化 mH2o = OUTPUT_TL.get(); mH2o.init(os); } ******省略代码 }

Hessian2ObjectInput 具体的实现与 Hessian2ObjectOutput 类似:在 DataInput 接口中实现了反序列化各种类型的方法,在 ObjectInput 接口中提供了反序列化 Java 对象的功能,在 Hessian2ObjectInput 中会将所有反序列化的实现委托为 Hessian2Input。

博文参考

16 Dubbo Serialize 层:多种序列化算法,总有一款适合你.md



【本文地址】


今日新闻


推荐新闻


CopyRight 2018-2019 办公设备维修网 版权所有 豫ICP备15022753号-3