作者|雲燁
一 背景介紹
序列化與反序列化是我們日常資料持久化和網路傳輸中經常使用的技術,但是目前各種序列化框架讓人眼花繚亂,不清楚什麼場景到底採用哪種序列化框架。本文會將業界開源的序列化框架進行對比測試,分別從通用性、易用性、可擴充套件性、效能和資料型別與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 HashMapString
,
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 = ; i20
; 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 conf = ThreadLocal。withInitial(() -> {
FSTConfiguration conf = FSTConfiguration。createDefaultConfiguration();
return
conf;
});
public
byte
[]
encoder
(
Object
object
)
{
return
conf。
get
()。asByteArray(
object
);
}
public
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 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 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資料型別:
1。8中基礎資料型別,沒有short、char,只能使用double和String代替。
2。集合型別,支援List、Set、Map,不支援Queue。
3。自定義類型別(struct型別)。
4。列舉型別。
5。位元組陣列。
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
byte
[] encoder2(T obj)
throws
Exception{
ByteArrayOutputStream bos =
new
ByteArrayOutputStream();
Hessian2Output hessian2Output =
new
Hessian2Output(bos);
hessian2Output。writeObject(obj);
return
bos。toByteArray();
}
public
static
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影響,能夠輕鬆支援欄位擴充套件。
1。修改欄位名稱:反序列化後新欄位名稱為null或0(受型別影響)。
2。新增欄位:反序列化後新增欄位為null或0(受型別影響)。
3。刪除欄位:能夠正常反序列化。
4。修改欄位型別:如果欄位型別相容能夠正常反序列化,如果不相容則直接丟擲異常。
效能
使用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 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 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更加複雜。
可擴充套件性
1。給所有field定義default值。如果某field沒有default值,以後將不能刪除該field。
2。如果要新增field,必須定義default值。
3。不能修改field type。
4。不能修改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資料型別支援的對比:
注:集合型別測試基本覆蓋了所有對應的實現類。
1。List測試內容:ArrayList、LinkedList、Stack、CopyOnWriteArrayList、Vector。
2。Set測試內容:HashSet、LinkedHashSet、TreeSet、CopyOnWriteArraySet。
3。Map測試內容:HashMap、LinkedHashMap、TreeMap、WeakHashMap、ConcurrentHashMap、Hashtable。
4。Queue測試內容:PriorityQueue、ArrayBlockingQueue、LinkedBlockingQueue、ConcurrentLinkedQueue、SynchronousQueue、ArrayDeque、LinkedBlockingDeque和ConcurrentLinkedDeque。
下面根據測試總結了以上序列化框架所能支援的資料型別、語法。
注1:static內部類需要實現序列化介面。
注2:外部類需要實現序列化介面。
注3:需要在Lambda表示式前新增(IXxx & Serializable)。
由於Protobuf、Thrift是IDL定義類檔案,然後使用各自的編譯器生成Java程式碼。IDL沒有提供定義staic內部類、非static內部類等語法,所以這些功能無法測試。
投資避險工具看這裡,低風險理財、7%+收益、靈活申贖>>
開啟App看更多精彩內容