美文网首页
序列化的原理

序列化的原理

作者: WEIJAVA | 来源:发表于2019-06-05 10:08 被阅读0次

    Java 序列化

    1. Java 序列化只是针对对象的状态进行保存,至于对象中的方法,序列化不关心
    2. 当一个父类实现了序列化,那么子类会自动实现序列化,不需要显示实现序列化接口
    3. 当一个对象的实例变量引用了其他对象,序列化这个对象的时候会自动把引用的对象也进行序列化(实现深度克隆)
    4. 当某个字段被申明为 transient 后,默认的序列化机制会忽略这个字段
    5. 被申明为 transient 的字段,如果需要序列化,可以添加两个私有方法:writeObject 和readObject

    各种序列化技术

    XML 序列化

    XML 序列化的好处在于可读性好,方便阅读和调试。但是序列化以后的字节码文件比较大,而且效率不高,适用于对性能不高,而且 QPS 较低的企业级内部系统之间的数据交换的场景,同时 XML 又具有语言无关性,所以还可以用于异构系统之间的数据交换和协议。比如我们熟知的 webservice,就是采用 XML 格式对数据进行序列化的。XML 序列化/反序列化的实现方式有很多,熟知的方式有 XStream 和 Java 自带的 XML 序列化和反序列化两种

    JSON 序列化

    JSON(JavaScript Object Notation)是一种轻量级的数据交换格式,相对于 XML 来说,JSON的字节流更小,而且可读性也非常好。现在 JSON 数据格式在企业运用是最普遍的JSON 序列化常用的开源工具有很多

    1. Jackson (https://github.com/FasterXML/jackson
    2. 阿里开源的 FastJson (https://github.com/alibaba/fastjon
    3. Google 的 GSON (https://github.com/google/gson)

    这几种 json 序列化工具中,Jackson 与 fastjson 要比 GSON 的性能要好,但是 Jackson、GSON 的稳定性要比 Fastjson 好。而 fastjson 的优势在于提供的 api 非常容易使用

    Hessian 序列化

    Hessian 是一个支持跨语言传输的二进制序列化协议,相对于 Java 默认的序列化机制来说,Hessian 具有更好的性能和易用性,而且支持多种不同的语言
    实际上 Dubbo 采用的就是 Hessian 序列化来实现,只不过 Dubbo 对 Hessian 进行了重构,性能更高

    Avro 序列化

    Avro 是一个数据序列化系统,设计用于支持大批量数据交换的应用。它的主要特点有:支持二进制序列化方式,可以便捷,快速地处理大量数据;动态语言友好,Avro 提供的机制使动态语言可以方便地处理 Avro 数据

    kyro 序列化

    Kryo 是一种非常成熟的序列化实现,已经在 Hive、Storm)中使用得比较广泛,不过它不能跨语言. 目前 dubbo 已经在 2.6 版本支持 kyro 的序列化机制。它的性能要优于之前的hessian2

    Protobuf 序列化

    Protobuf 是 Google 的一种数据交换格式,它独立于语言、独立于平台。Google 提供了多种语言来实现,比如 Java、C、Go、Python,每一种实现都包含了相应语言的编译器和库文件,Protobuf 是一个纯粹的表示层协议,可以和各种传输层协议一起使用。
    Protobuf 使用比较广泛,主要是空间开销小和性能比较好,非常适合用于公司内部对性能要求高的 RPC 调用。 另外由于解析性能比较高,序列化以后数据量相对较少,所以也可以应用在对象的持久化场景中但是要使用 Protobuf 会相对来说麻烦些,因为他有自己的语法,有自己的编译器,如果需要用到的话必须要去投入成本在这个技术的学习中

    protobuf 有个缺点就是要传输的每一个类的结构都要生成对应的 proto 文件,如果某个类发生修改,还得重新生成该类对应的 proto 文件

    选型建议

    1. 对性能要求不高的场景,可以采用基于 XML 的 SOAP 协议
    2. 对性能和间接性有比较高要求的场景,那么 Hessian、Protobuf、Thrift、Avro 都可以。
    3. 基于前后端分离,或者独立的对外的 api 服务,选用 JSON 是比较好的,对于调试、可读性都很不错
    4. Avro 设计理念偏于动态类型语言,那么这类的场景使用 Avro 是可以的

    各个序列化技术的性能比较
    这个地址有针对不同序列化技术进行性能比较: https://github.com/eishay/jvm-serializers/wiki

    Protobuf 序列化的原理

    protobuf 的基本应用

    使用 protobuf 开发的一般步骤是

    1. 配置开发环境,安装 protocol compiler 代码编译器
    2. 编写.proto 文件,定义序列化对象的数据结构
    3. 基于编写的.proto 文件,使用 protocol compiler 编译器生成对应的序列化/反序列化工具类
    4. 基于自动生成的代码,编写自己的序列化应用

    Protobuf 案例演示

    编写 proto 文件

    syntax="proto2";
    package com.gupaoedu.serial;
    option java_package = 
    "com.wei.serial";
    option java_outer_classname="UserProtos";
    message User {
    required string name=1;
    required int32 age=2;
    }
    

    数据类型
    string / bytes / bool / int32(4 个字节)
    /int64/float/double
    enum 枚举类
    message 自定义类
    修饰符
    required 表示必填字段
    optional 表示可选字段
    repeated 可重复,表示集合
    1,2,3,4 需要在当前范围内是唯一的,表示顺序

    生成实体类

    .\protoc.exe --java_out=./user.proto
    

    实现序列化

       public static void main(String[] args) {
            UserProtos.User user = UserProtos.User.newBuilder().setAge(300).setName("Mic").build();
            byte[] bytes = user.toByteArray();
            for (byte bt : bytes) {
                System.out.print(bt + " ");
            }
        }
    

    输出结果:10 3 77 105 99 16 -84 2
    可以看到,序列化出来的数字基本看不懂,但是序列化以后的数据确实很小,那我们来了解一下底层的原理

    正常来说,要达到最小的序列化结果,一定会用到压缩的技术,而 protobuf 里面用到了两种
    压缩算法,一种是 varint,另一种是 zigzag

    -varint
    先来看 age=300 这个数字是如何被压缩的


    image.png

    这两个字节字节分别的结果是:-84 、2
    -84 怎么计算来的呢? 我们知道在二进制中表示负数的方法,高位设置为 1, 并且是对应数字的二进制取反以后再计算补码表示(补码是反码+1)
    所以如果要反过来计算

    1. 【补码】10101100 -1 得到 10101011
    2. 【反码】01010100 得到的结果为 84. 由于高位是 1,表示负数所以结果为-84

    字符如何转化为编码
    “Mic”这个字符,需要根据 ASCII 对照表转化为数字。
    M =77、i=105、c=99
    所以结果为 77 105 99
    这里的结果为什么直接就是 ASCII 编码的值呢?怎么没有做压缩呢?
    原因是,varint 是对字节码做压缩,但是如果这个数字的二进制只需要一个字节表示的时候,其实最终编码出来的结果是不会变化的
    还有两个数字,3 和 16 代表什么呢?那就要了解 protobuf 的存储格式了

    存储格式
    protobuf 采用 T-L-V 作为存储方式


    image.png

    tag 的计算方式是 field_number(当前字段的编号) << 3 | wire_type
    比如 Mic 的字段编号是 1 ,类型 wire_type 的值为 2 所以 : 1 <<3 | 2 =10
    age=300 的字段编号是 2,类型 wire_type 的值是 0, 所以 : 2<<3|0 =16

    第一个数字 10,代表的是 key,剩下的都是 value

    负数的存储
    在计算机中,负数会被表示为很大的整数,因为计算机定义负数符号位为数字的最高位,所以如果采用 varint 编码表示一个负数,那么一定需要 5 个比特位。所以在 protobuf 中通过sint32/sint64 类型来表示负数,负数的处理形式是先采用 zigzag 编码(把符号数转化为无符号数),再采用 varint 编码。

    sint32:(n << 1) ^ (n >> 31)
    sint64:(n << 1) ^ (n >> 63)
    比如存储一个(-300)的值
    -300
    原码:0001 0010 1100
    取反:1110 1101 0011
    加 1 :1110 1101 0100
    n<<1: 整体左移一位,右边补 0 -> 1101 1010 1000
    n>>31: 整体右移 31 位,左边补 1 -> 1111 1111 1111
    n<<1 ^ n >>31
    1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
    十进制: 0010 0101 0111 = 599
    varint 算法: 从右往做,选取 7 位,高位补 1/0(取决于字节数)
    得到两个字节
    1101 0111 和 0000 0100
    -41 和 4

    Protocol Buffer 的性能好,主要体现在 序列化后的数据体积小 & 序列化速度快,最终使得传输效率高,其原因如下:

    • 序列化速度快的原因:
      a. 编码 / 解码 方式简单(只需要简单的数学运算 = 位移等等)
      b. 采用 Protocol Buffer 自身的框架代码 和 编译器 共同完成

    • 序列化后的数据量体积小(即数据压缩效果好)的原因:
      a. 采用了独特的编码方式,如 Varint、Zigzag 编码方式等等
      b. 采用 T - L - V 的数据存储方式:减少了分隔符的使用 & 数据存储得紧凑

      ——学自咕泡学院

    相关文章

      网友评论

          本文标题:序列化的原理

          本文链接:https://www.haomeiwen.com/subject/xvbrxctx.html