简介: 序列化与反序列化是咱们平常数据持久化和网络传输中常用的技术,可是目前各类序列化框架让人眼花缭乱,不清楚什么场景到底采用哪一种序列化框架。本文会将业界开源的序列化框架进行对比测试,分别从通用性、易用性、可扩展性、性能和数据类型与Java语法支持五方面给出对比测试。java
做者 | 云烨
来源 | 阿里技术公众号数组
序列化与反序列化是咱们平常数据持久化和网络传输中常用的技术,可是目前各类序列化框架让人眼花缭乱,不清楚什么场景到底采用哪一种序列化框架。本文会将业界开源的序列化框架进行对比测试,分别从通用性、易用性、可扩展性、性能和数据类型与Java语法支持五方面给出对比测试。安全
1 JDK Serializable
JDK Serializable是Java自带的序列化框架,咱们只须要实现java.io.Serializable或java.io.Externalizable接口,就可使用Java自带的序列化机制。实现序列化接口只是表示该类可以被序列化/反序列化,咱们还须要借助I/O操做的ObjectInputStream和ObjectOutputStream对对象进行序列化和反序列化。网络
下面是使用JDK 序列化框架进行编解码的Demo:数据结构
通用性框架
因为是Java内置序列化框架,因此自己是不支持跨语言序列化与反序列化。maven
易用性函数
做为Java内置序列化框架,无序引用任何外部依赖便可完成序列化任务。可是JDK Serializable在使用上相比开源框架难用许多,能够看到上面的编解码使用很是生硬,须要借助ByteArrayOutputStream和ByteArrayInputStream才能够完整字节的转换。oop
可扩展性性能
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在数据兼容性上也很是友好,int3二、unit3二、int6四、unit6四、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语法结构也可以很好的支持。
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数据类型支持的对比:
![上传中...]()
注:集合类型测试基本覆盖了全部对应的实现类。
注1:static内部类须要实现序列化接口。
注2:外部类须要实现序列化接口。
注3:须要在Lambda表达式前添加(IXxx & Serializable)。
因为Protobuf、Thrift是IDL定义类文件,而后使用各自的编译器生成Java代码。IDL没有提供定义staic内部类、非static内部类等语法,因此这些功能没法测试。
原文连接本文为阿里云原创内容,未经容许不得转载。