简介: 序列化与反序列化是咱们日常数据长久化和网络传输中常常应用的技术,然而目前各种序列化框架让人目迷五色,不分明什么场景到底采纳哪种序列化框架。本文会将业界开源的序列化框架进行比照测试,别离从通用性、易用性、可扩展性、性能和数据类型与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.thriftstruct 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外部类等语法,所以这些性能无奈测试。
原文链接
本文为阿里云原创内容,未经容许不得转载。