关于序列化:几种Java常用序列化框架的选型与对比

4次阅读

共计 12261 个字符,预计需要花费 31 分钟才能阅读完成。

简介:序列化与反序列化是咱们日常数据长久化和网络传输中常常应用的技术,然而目前各种序列化框架让人目迷五色,不分明什么场景到底采纳哪种序列化框架。本文会将业界开源的序列化框架进行比照测试,别离从通用性、易用性、可扩展性、性能和数据类型与 Java 语法反对五方面给出比照测试。

作者 | 云烨
起源 | 阿里技术公众号

一 背景介绍

序列化与反序列化是咱们日常数据长久化和网络传输中常常应用的技术,然而目前各种序列化框架让人目迷五色,不分明什么场景到底采纳哪种序列化框架。本文会将业界开源的序列化框架进行比照测试,别离从通用性、易用性、可扩展性、性能和数据类型与 Java 语法反对五方面给出比照测试。

  • 通用性:通用性是指序列化框架是否反对跨语言、跨平台。
  • 易用性:易用性是指序列化框架是否便于应用、调试,会影响开发效率。
  • 可扩展性:随着业务的倒退,传输实体可能会发生变化,然而旧实体有可能还会被应用。这时候就须要思考所抉择的序列化框架是否具备良好的扩展性。
  • 性能:序列化性能次要包含工夫开销和空间开销。序列化的数据通常用于长久化或网络传输,所以其大小是一个重要的指标。而编解码工夫同样是影响序列化协定抉择的重要指标,因为现在的零碎都在谋求高性能。
  • Java 数据类型和语法反对:不同序列化框架所可能反对的数据类型以及语法结构是不同的。这里咱们要对 Java 的数据类型和语法个性进行测试,来看看不同序列化框架对 Java 数据类型和语法结构的反对度。
    上面别离对 JDK Serializable、FST、Kryo、Protobuf、Thrift、Hession 和 Avro 进行比照测试。

二 序列化框架

1 JDK Serializable
JDK Serializable 是 Java 自带的序列化框架,咱们只须要实现 java.io.Serializable 或 java.io.Externalizable 接口,就能够应用 Java 自带的序列化机制。实现序列化接口只是示意该类可能被序列化 / 反序列化,咱们还须要借助 I / O 操作的 ObjectInputStream 和 ObjectOutputStream 对对象进行序列化和反序列化。

上面是应用 JDK 序列化框架进行编解码的 Demo:

通用性

因为是 Java 内置序列化框架,所以自身是不反对跨语言序列化与反序列化。

易用性

作为 Java 内置序列化框架,无序援用任何内部依赖即可实现序列化工作。然而 JDK Serializable 在应用上相比开源框架难用许多,能够看到下面的编解码应用十分僵硬,须要借助 ByteArrayOutputStream 和 ByteArrayInputStream 才能够残缺字节的转换。

可扩展性

JDK Serializable 中通过 serialVersionUID 管制序列化类的版本,如果序列化与反序列化版本不统一,则会抛出 java.io.InvalidClassException 异样信息,提醒序列化与反序列化 SUID 不统一。

java.io.InvalidClassException: com.yjz.serialization.java.UserInfo; local class incompatible: stream classdesc serialVersionUID = -5548195544707231683, local class serialVersionUID = -5194320341014913710

下面这种状况,是因为咱们没有定义 serialVersionUID,而是由 JDK 主动 hash 生成的,所以序列化与反序列化前后后果不统一。

然而咱们能够通过自定义 serialVersionUID 形式来躲避掉这种状况(序列化前后都是应用定义的 serialVersionUID),这样 JDK Serializable 就能够反对字段扩大了。

private static final long serialVersionUID = 1L;

性能

JDK Serializable 是 Java 自带的序列化框架,然而在性能上其实一点不像亲生的。上面测试用例是咱们贯通全文的一个测试实体。

public class MessageInfo implements Serializable {

    private String username;
    private String password;
    private int age;
    private HashMap<String,Object> params;
    ...
    public static MessageInfo buildMessage() {MessageInfo messageInfo = new MessageInfo();
        messageInfo.setUsername("abcdefg");
        messageInfo.setPassword("123456789");
        messageInfo.setAge(27);
        Map<String,Object> map = new HashMap<>();
        for(int i = 0; i< 20; i++) {map.put(String.valueOf(i),"a");
        }
        return messageInfo;
    }
}

应用 JDK 序列化后字节大小为:432。光看这组数字兴许不会感觉到什么,之后咱们会拿这个数据和其它序列化框架进行比照。

咱们对该测试用例进行 1000 万次序列化,而后计算工夫总和:

同样咱们之后会同其它序列化框架进行比照。

数据类型和语法结构支持性

因为 JDK Serializable 是 Java 语法原生序列化框架,所以根本都可能反对 Java 数据类型和语法。

WeakHashMap 没有实现 Serializable 接口。

注 1:但咱们要序列化上面代码:

Runnable runnable = () -> System.out.println("Hello");

间接序列化会失去以下异样:

com.yjz.serialization.SerializerFunctionTest$$Lambda$1/189568618

起因就是咱们 Runnable 的 Lambda 并没有实现 Serializable 接口。咱们能够做如下批改,即可反对 Lambda 表达式序列化。

Runnable runnable = (Runnable & Serializable) () -> System.out.println("Hello");

2 FST 序列化框架

FST(fast-serialization)是齐全兼容 JDK 序列化协定的 Java 序列化框架,它在序列化速度上能达到 JDK 的 10 倍,序列化后果只有 JDK 的 1 /3。目前 FST 的版本为 2.56,在 2.17 版本之后提供了对 Android 的反对。

上面是应用 FST 序列化的 Demo,FSTConfiguration 是线程平安的,然而为了避免频繁调用时其成为性能瓶颈,个别会应用 TreadLocal 为每个线程调配一个 FSTConfiguration。

private final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> {FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
      return conf;
  });

public byte[] encoder(Object object) {return conf.get().asByteArray(object);
}

public <T> T decoder(byte[] bytes) {Object ob = conf.get().asObject(bytes);
    return (T)ob;
}

通用性

FST 同样是针对 Java 而开发的序列化框架,所以也不存在跨语言个性。

易用性

在易用性上,FST 能够说可能甩 JDK Serializable 几条街,语法极其简洁,FSTConfiguration 封装了大部分办法。

可扩展性

FST 通过 @Version 注解可能反对新增字段与旧的数据流兼容。对于新增的字段都须要通过 @Version 注解标识,没有版本正文意味着版本为 0。

private String origiField;
@Version(1)
private String addField;

留神:

删除字段将毁坏向后兼容性,然而如果咱们在原始字段状况下删除字段是可能向后兼容的(没有新增任何字段)。然而如果新增字段后,再删除字段的话就会毁坏其兼容性。

Version 注解性能不能利用于本人实现的 readObject/writeObject 状况。
如果本人实现了 Serializer,须要本人管制 Version。

综合来看,FST 在扩展性下面尽管反对,然而用起来还是比拟繁琐的。

性能

应用 FST 序列化下面的测试用例,序列化后大小为:172,相比 JDK 序列化的 432,将近缩小了 1 /3。上面咱们再看序列化与反序列化的工夫开销。

image.png

咱们能够优化一下 FST,将循环援用判断敞开,并且对序列化类进行余注册。

private static final ThreadLocal<FSTConfiguration> conf = ThreadLocal.withInitial(() -> {

  FSTConfiguration conf = FSTConfiguration.createDefaultConfiguration();
  conf.registerClass(UserInfo.class);
  conf.setShareReferences(false);
  return conf;

});
通过下面的优化配置,失去的工夫开销如下:

能够看到序列化工夫将近晋升了 2 倍,然而通过优化后的序列化数据大小增长到了 191。

数据类型和语法结构支持性

FST 是基于 JDK 序列化框架而进行开发的,所以在数据类型和语法上和 Java 支持性统一。

3 Kryo 序列化框架
Kryo 一个疾速无效的 Java 二进制序列化框架,它依赖底层 ASM 库用于字节码生成,因而有比拟好的运行速度。Kryo 的指标就是提供一个序列化速度快、后果体积小、API 简略易用的序列化框架。Kryo 反对主动深 / 浅拷贝,它是间接通过对象 -> 对象的深度拷贝,而不是对象 -> 字节 -> 对象的过程。

上面是应用 Kryo 进行序列化的 Demo:

须要留神的是应用 Output.writeXxx 时候肯定要用对应的 Input.readxxx,比方 Output.writeClassAndObject()要与 Input.readClassAndObject()。
通用性

首先 Kryo 官网说本人是一款 Java 二进制序列化框架,其次在网上搜了一遍没有看到 Kryo 的跨语言应用,只是一些文章提及了跨语言应用非常复杂,然而没有找到其它语言的相干实现。

易用性

在应用形式上 Kryo 提供的 API 也是十分简洁易用,Input 和 Output 封装了你简直可能想到的所有流操作。Kryo 提供了丰盛的灵便配置,比方自定义序列化器、设置默认序列化器等等,这些配置应用起来还是比拟吃力的。

可扩展性

Kryo 默认序列化器 FiledSerializer 是不反对字段扩大的,如果想要应用扩大序列化器则须要配置其它默认序列化器。

比方:

private static final ThreadLocal<Kryo> kryoLocal = ThreadLocal.withInitial(() -> {Kryo kryo = new Kryo();
        kryo.setRegistrationRequired(false);
        kryo.setDefaultSerializer(TaggedFieldSerializer.class);
        return kryo;
    });

性能

应用 Kryo 测试下面的测试用例,Kryo 序列化后的字节大小为 172,和 FST 未经优化的大小统一。工夫开销如下:

咱们同样敞开循环援用配置和预注册序列化类,序列化后的字节大小为 120,因为这时候类序列化的标识是应用的数字,而不是类全名。应用的是工夫开销如下:

数据类型和语法结构支持性

Kryo 对于序列化类的根本要求就是须要含有无参构造函数,因为反序列化过程中须要应用无参构造函数创建对象。

4 Protocol buffer
Protocol buffer 是一种语言中立、平台无关、可扩大的序列化框架。Protocol buffer 相较于后面几种序列化框架而言,它是须要事后定义 Schema 的。

上面是应用 Protobuf 的 Demo:

(1)编写 proto 形容文件:

syntax = "proto3";

option java_package = "com.yjz.serialization.protobuf3";

message MessageInfo
{
    string username = 1;
    string password = 2;
    int32 age = 3;
    map<string,string> params = 4;
}

(2)生成 Java 代码:

protoc --java_out=./src/main/java message.proto

(3)生成的 Java 代码,曾经自带了编解码办法:

通用性

protobuf 设计之初的指标就是可能设计一款与语言无关的序列化框架,它目前反对了 Java、Python、C++、Go、C# 等,并且很多其它语言都提供了第三方包。所以在通用性上,protobuf 是十分给力的。

易用性

protobuf 须要应用 IDL 来定义 Schema 形容文件,定义完形容文件后,咱们能够间接应用 protoc 来间接生成序列化与反序列化代码。所以,在应用上只须要简略编写形容文件,就能够应用 protobuf 了。

可扩展性

可扩展性同样是 protobuf 设计之初的指标之一,咱们能够十分轻松的在.proto 文件进行批改。

新增字段:对于新增字段,咱们肯定要保障新增字段要有对应的默认值,这样才可能与旧代码交互。相应的新协定生成的音讯,能够被旧协定解析。

删除字段:删除字段须要留神的是,对应的字段、标签不可能在后续更新中应用。为了防止谬误,咱们能够通过 reserved 躲避带哦。

protobuf 在数据兼容性上也十分敌对,int32、unit32、int64、unit64、bool 是齐全兼容的,所以咱们能够依据须要批改其类型。

通过下面来看,protobuf 在扩展性上做了很多,可能很敌对的反对协定扩大。

性能

咱们同样应用下面的实例来进行性能测试,应用 protobuf 序列化后的字节大小为 192,上面是对应的工夫开销。

能够看出 protobuf 的反序列化性能要比 FST、Kryo 差一些。

数据类型和语法结构反对

Protobuf 应用 IDL 定义 Schema 所以不反对定义 Java 办法,上面序列化变量的测试:

注:List、Set、Queue 通过 protobuf repeated 定义测试的。只有实现 Iterable 接口的类都能够应用 repeated 列表。

5 Thrift 序列化框架

Thrift 是由 Facebook 实现的一种高效的、反对多种语言的近程服务调用框架,即 RPC(Remote Procedure Call)。起初 Facebook 将 Thrift 开源到 Apache。能够看到 Thrift 是一个 RPC 框架,然而因为 Thrift 提供了多语言之间的 RPC 服务,所以很多时候被用于序列化中。

应用 Thrift 实现序列化次要分为三步,创立 thrift IDL 文件、编译生成 Java 代码、应用 TSerializer 和 TDeserializer 进行序列化和反序列化。

(1)应用 Thrift IDL 定义 thrift 文件:

namespace java com.yjz.serialization.thrift

struct MessageInfo{
    1: string username;
    2: string password;
    3: i32 age;
    4: map<string,string> params;
}

(2)应用 thrift 编译器生成 Java 代码:

thrift --gen java message.thrift
(3)应用 TSerializer 和 TDeserializer 进行编解码:

public static byte[] encoder(MessageInfo messageInfo) throws Exception{TSerializer serializer = new TSerializer();
        return serializer.serialize(messageInfo);
    }
    public static MessageInfo decoder(byte[] bytes) throws Exception{TDeserializer deserializer = new TDeserializer();
        MessageInfo messageInfo = new MessageInfo();
        deserializer.deserialize(messageInfo,bytes);
        return messageInfo;
    }

通用性

Thrift 和 protobuf 相似,都须要应用 IDL 定义形容文件,这是目前实现跨语言序列化 /RPC 的一种无效形式。Thrift 目前反对 C++、Java、Python、PHP、Ruby、Erlang、Perl、Haskell、C#、Cocoa、JavaScript、Node.js、Smalltalk、OCaml、Delphi 等语言,所以能够看到 Thrift 具备很强的通用性。

易用性

Thrift 在易用性上和 protobuf 相似,都须要通过三步:应用 IDL 编写 thrift 文件、编译生成 Java 代码和调用序列化与反序列化办法。protobuf 在生成类中曾经内置了序列化与反序列化办法,而 Thrift 须要独自调用内置序列化器来进行编解码。

可扩展性

Thrift 反对字段扩大,在扩大字段过程中须要留神以下问题:

批改字段名称:批改字段名称不影响序列化与反序列化,反序列化数据赋值到更新过的字段上。因为编解码过程利用的是编号对应。

批改字段类型:批改字段类型,如果批改的字段为 optional 类型字段,则返回数据为 null 或 0(数据类型默认值)。如果批改是 required 类型字段,则会间接抛出异样,提醒字段没有找到。

新增字段:如果新增字段是 required 类型,则须要为其设置默认值,负责在反序列化过程抛出异样。如果为 optional 类型字段,反序列化过程不会存在该字段(因为 optional 字段没有赋值的状况,不会参加序列化与反序列化)。如果为缺省类型,则反序列化值为 null 或 0(和数据类型无关)。

删除字段:无论 required 类型字段还是 optional 类型字段,都能够删除,不会影响反序列化。

删除后的字段整数标签不要复用,负责会影响反序列化。

性能

下面的测试用例,应用 Thrift 序列化后的字节大小为:257,上面是对应的序列化工夫与反序列化工夫开销:

Thrift 在序列化和反序列化的工夫开销总和上和 protobuf 差不多,protobuf 在序列化工夫上更占优势,而 Thrift 在反序列化上有本人的劣势。

数据类型和语法结构反对

数据类型反对:因为 Thrift 应用 IDL 来定义序列化类,所以可能反对的数据类型就是 Thrift 数据类型。Thrift 所可能反对的 Java 数据类型:

8 中根底数据类型,没有 short、char,只能应用 double 和 String 代替。
汇合类型,反对 List、Set、Map,不反对 Queue。
自定义类类型(struct 类型)。

枚举类型。

字节数组。

Thrift 同样不反对定义 Java 办法。

6 Hessian 序列化框架

Hessian 是 caucho 公司开发的轻量级 RPC(Remote Procedure Call)框架,它应用 HTTP 协定传输,应用 Hessian 二进制序列化。

Hessian 因为其反对跨语言、高效的二进制序列化协定,被常常用于序列化框架应用。Hessian 序列化协定分为 Hessian1.0 和 Hessian2.0,Hessian2.0 协定对序列化过程进行了优化(优化内容待看),在性能上相较 Hessian1.0 有显著晋升。

应用 Hessian 序列化非常简单,只须要通过 HessianInput 和 HessianOutput 即可实现对象的序列化,上面是 Hessian 序列化的 Demo:

public static <T> byte[] encoder2(T obj) throws Exception{ByteArrayOutputStream bos = new ByteArrayOutputStream();
        Hessian2Output hessian2Output = new Hessian2Output(bos);
        hessian2Output.writeObject(obj);
        return bos.toByteArray();}

    public static <T> T decoder2(byte[] bytes) throws Exception {ByteArrayInputStream bis = new ByteArrayInputStream(bytes);
        Hessian2Input hessian2Input = new Hessian2Input(bis);
        Object obj = hessian2Input.readObject();
        return (T) obj;
    }

通用性

Hessian 与 Protobuf、Thrift 一样,反对跨语言 RPC 通信。Hessian 相比其它跨语言 PRC 框架的一个次要劣势在于,它不是采纳 IDL 来定义数据和服务,而是通过自描述来实现服务的定义。目前 Hessian 曾经实现了语言包含:Java、Flash/Flex、Python、C++、.Net/C#、D、Erlang、PHP、Ruby、Object-C。

易用性

相较于 Protobuf 和 Thrift,因为 Hessian 不须要通过 IDL 来定义数据和服务,对于序列化的数据只须要实现 Serializable 接口即可,所以应用上相比 Protobuf 和 Thrift 更加容易。

可扩展性

Hession 序列化类尽管须要实现 Serializable 接口,然而它并不受 serialVersionUID 影响,可能轻松反对字段扩大。

批改字段名称:反序列化后新字段名称为 null 或 0(受类型影响)。
新增字段:反序列化后新增字段为 null 或 0(受类型影响)。
删除字段:可能失常反序列化。
批改字段类型:如果字段类型兼容可能失常反序列化,如果不兼容则间接抛出异样。
性能

应用 Hessian1.0 协定序列化下面的测试用例,序列化后果大小为 277。应用 Hessian2.0 序列化协定,序列化后果大小为 178。

序列化化与反序列化的工夫开销如下:

能够看到 Hessian1.0 的无论在序列化后体积大小,还是在序列化、反序列化工夫上都比 Hessian2.0 相差很远。

数据类型和语法结构反对

因为 Hession 应用 Java 自描述序列化类,所以 Java 原生数据类型、汇合类、自定义类、枚举等根本都可能反对(SynchronousQueue 不反对),Java 语法结构也可能很好的反对。

7 Avro 序列化框架

Avro 是一个数据序列化框架。它是 Apache Hadoop 下的一个子项目,由 Doug Cutting 主导 Hadoop 过程中开发的数据序列化框架。Avro 在设计之初就用于反对数据密集型利用,很适宜近程或本地大规模数据交换和存储。

应用 Avro 序列化分为三步:

(1)定义 avsc 文件:

{
    "namespace": "com.yjz.serialization.avro",
    "type": "record",
    "name": "MessageInfo",
    "fields": [{"name": "username","type": "string"},
        {"name": "password","type": "string"},
        {"name": "age","type": "int"},
        {"name": "params","type": {"type": "map","values": "string"}
        }
    ]
}

(2)应用 avro-tools.jar 编译生成 Java 代码(或 maven 编译生成):

java -jar avro-tools-1.8.2.jar compile schema src/main/resources/avro/Message.avsc ./src/main/java

(3)借助 BinaryEncoder 和 BinaryDecoder 进行编解码:

public static  byte[] encoder(MessageInfo obj) throws Exception{DatumWriter<MessageInfo> datumWriter = new SpecificDatumWriter<>(MessageInfo.class);
        ByteArrayOutputStream outputStream = new ByteArrayOutputStream();
        BinaryEncoder binaryEncoder = EncoderFactory.get().directBinaryEncoder(outputStream,null);
        datumWriter.write(obj,binaryEncoder);
        return outputStream.toByteArray();}

    public static MessageInfo decoder(byte[] bytes) throws Exception{DatumReader<MessageInfo> datumReader = new SpecificDatumReader<>(MessageInfo.class);
        BinaryDecoder binaryDecoder = DecoderFactory.get().directBinaryDecoder(new ByteArrayInputStream(bytes),null);
        return datumReader.read(new MessageInfo(),binaryDecoder);
    }

通用性

Avro 通过 Schema 定义数据结构,目前反对 Java、C、C++、C#、Python、PHP 和 Ruby 语言,所以在这些语言之间 Avro 具备很好的通用性。

易用性

Avro 对于动静语言无需生成代码,但对于 Java 这类动态语言,还是须要应用 avro-tools.jar 来编译生成 Java 代码。在 Schema 编写上,个人感觉相比 Thrift、Protobuf 更加简单。

可扩展性

给所有 field 定义 default 值。如果某 field 没有 default 值,当前将不能删除该 field。
如果要新增 field,必须定义 default 值。
不能批改 field type。
不能批改 field name,不过能够通过减少 alias 解决。
性能

应用 Avro 生成代码序列化之后的后果为:111。上面是应用 Avro 序列化的工夫开销:

数据类型和语法结构反对

Avro 须要应用 Avro 所反对的数据类型来编写 Schema 信息,所以可能反对的 Java 数据类型即为 Avro 所反对的数据类型。Avro 反对数据类型有:根底类型(null、boolean、int、long、float、double、bytes、string),简单数据类型(Record、Enum、Array、Map、Union、Fixed)。

Avro 主动生成代码,或者间接应用 Schema,不能反对在序列化类中定义 java 办法。

三 总结

1 通用性
上面是从通用性上比照各个序列化框架,能够看出 Protobuf 在通用上是最佳的,可能反对多种支流变成语言。

2 易用性
上面是从 API 应用的易用性下面来比照各个序列化框架,能够说除了 JDK Serializer 外的序列化框架都提供了不错 API 应用形式。

3 可扩展性
上面是各个序列化框架的可扩展性比照,能够看到 Protobuf 的可扩展性是最不便、天然的。其它序列化框架都须要一些配置、注解等操作。

4 性能
序列化大小比照

比照各个序列化框架序列化后的数据大小如下,能够看出 kryo preregister(事后注册序列化类)和 Avro 序列化后果都很不错。所以,如果在序列化大小上有需要,能够抉择 Kryo 或 Avro。

序列化工夫开销比照

上面是序列化与反序列化的工夫开销,kryo preregister 和 fst preregister 都能提供优异的性能,其中 fst pre 序列化工夫就最佳,而 kryo pre 在序列化和反序列化工夫开销上基本一致。所以,如果序列化工夫是次要的思考指标,能够抉择 Kryo 或 FST,都能提供不错的性能体验。

image.png

5 数据类型和语法结构反对
各序列化框架对 Java 数据类型反对的比照:

![上传中 …]()

注:汇合类型测试根本笼罩了所有对应的实现类。

  • List 测试内容:ArrayList、LinkedList、Stack、CopyOnWriteArrayList、Vector。
  • Set 测试内容:HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet。
  • Map 测试内容:HashMap、LinkedHashMap、TreeMap、WeakHashMap、ConcurrentHashMap、Hashtable。
  • Queue 测试内容:PriorityQueue、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、SynchronousQueue、ArrayDeque、LinkedBlockingDeque 和 ConcurrentLinkedDeque。
    上面依据测试总结了以上序列化框架所能反对的数据类型、语法。

注 1:static 外部类须要实现序列化接口。
注 2:外部类须要实现序列化接口。
注 3:须要在 Lambda 表达式前增加(IXxx & Serializable)。

因为 Protobuf、Thrift 是 IDL 定义类文件,而后应用各自的编译器生成 Java 代码。IDL 没有提供定义 staic 外部类、非 static 外部类等语法,所以这些性能无奈测试。
原文链接
本文为阿里云原创内容,未经容许不得转载。

正文完
 0