簡介: 序列化與反序列化是我們日常數據持久化和網絡傳輸中經常使用的技術,但是目前各種序列化框架讓人眼花繚亂,不清楚什么場景到底采用哪種序列化框架。本文會將業界開源的序列化框架進行對比測試,分別從通用性、易用性、可擴展性、性能和數據類型與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萬次序列化,然后計算時間總和:
| 1000萬序列化耗時(ms) | 1000萬反序列化耗時(ms) |
| 38952 | 96508 |
同樣我們之后會同其它序列化框架進行對比。
數據類型和語法結構支持性
由於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。下面我們再看序列化與反序列化的時間開銷。

我們可以優化一下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,都能提供不錯的性能體驗。

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內部類等語法,所以這些功能無法測試。
本文為阿里雲原創內容,未經允許不得轉載。
