幾種Java經常使用序列化框架的選型與對比

簡介: 序列化與反序列化是咱們平常數據持久化和網絡傳輸中常用的技術,可是目前各類序列化框架讓人眼花繚亂,不清楚什麼場景到底採用哪一種序列化框架。本文會將業界開源的序列化框架進行對比測試,分別從通用性、易用性、可擴展性、性能和數據類型與Java語法支持五方面給出對比測試。
image.pngjava

做者 | 雲燁
來源 | 阿里技術公衆號數組

一 背景介紹

序列化與反序列化是咱們平常數據持久化和網絡傳輸中常用的技術,可是目前各類序列化框架讓人眼花繚亂,不清楚什麼場景到底採用哪一種序列化框架。本文會將業界開源的序列化框架進行對比測試,分別從通用性、易用性、可擴展性、性能和數據類型與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:數據結構

image.png

通用性框架

因爲是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萬次序列化,而後計算時間總和:
image.png

一樣咱們以後會同其它序列化框架進行對比。

數據類型和語法結構支持性

因爲JDK Serializable是Java語法原生序列化框架,因此基本都可以支持Java數據類型和語法。

image.png

WeakHashMap沒有實現Serializable接口。

image.png

注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;

});
經過上面的優化配置,獲得的時間開銷以下:

image.png

能夠看到序列化時間將近提高了2倍,可是經過優化後的序列化數據大小增加到了191 。

數據類型和語法結構支持性

FST是基於JDK序列化框架而進行開發的,因此在數據類型和語法上和Java支持性一致。

image.png

3 Kryo序列化框架
Kryo一個快速有效的Java二進制序列化框架,它依賴底層ASM庫用於字節碼生成,所以有比較好的運行速度。Kryo的目標就是提供一個序列化速度快、結果體積小、API簡單易用的序列化框架。Kryo支持自動深/淺拷貝,它是直接經過對象->對象的深度拷貝,而不是對象->字節->對象的過程。

下面是使用Kryo進行序列化的Demo:

image.png

須要注意的是使用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未經優化的大小一致。時間開銷以下:

image.png

咱們一樣關閉循環引用配置和預註冊序列化類,序列化後的字節大小爲120,由於這時候類序列化的標識是使用的數字,而不是類全名。使用的是時間開銷以下:

image.png

數據類型和語法結構支持性

Kryo對於序列化類的基本要求就是須要含有無參構造函數,由於反序列化過程當中須要使用無參構造函數建立對象。

image.png

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代碼,已經自帶了編解碼方法:

image.png

通用性

protobuf設計之初的目標就是可以設計一款與語言無關的序列化框架,它目前支持了Java、Python、C++、Go、C#等,而且不少其它語言都提供了第三方包。因此在通用性上,protobuf是很是給力的。

易用性

protobuf須要使用IDL來定義Schema描述文件,定義完描述文件後,咱們能夠直接使用protoc來直接生成序列化與反序列化代碼。因此,在使用上只須要簡單編寫描述文件,就可使用protobuf了。

可擴展性

可擴展性一樣是protobuf設計之初的目標之一,咱們能夠很是輕鬆的在.proto文件進行修改。

新增字段:對於新增字段,咱們必定要保證新增字段要有對應的默認值,這樣纔可以與舊代碼交互。相應的新協議生成的消息,能夠被舊協議解析。

刪除字段:刪除字段須要注意的是,對應的字段、標籤不可以在後續更新中使用。爲了不錯誤,咱們能夠經過reserved規避帶哦。

image.png

protobuf在數據兼容性上也很是友好,int3二、unit3二、int6四、unit6四、bool是徹底兼容的,因此咱們能夠根據須要修改其類型。

經過上面來看,protobuf在擴展性上作了不少,可以很友好的支持協議擴展。

性能

咱們一樣使用上面的實例來進行性能測試,使用protobuf序列化後的字節大小爲 192,下面是對應的時間開銷。

image.png

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

數據類型和語法結構支持

Protobuf使用IDL定義Schema因此不支持定義Java方法,下面序列化變量的測試:

image.png

注: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,下面是對應的序列化時間與反序列化時間開銷:

image.png

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。

序列化化與反序列化的時間開銷以下:

image.png

能夠看到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序列化的時間開銷:

image.png

數據類型和語法結構支持

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在通用上是最佳的,可以支持多種主流變成語言。

image.png

2 易用性
下面是從API使用的易用性上面來對比各個序列化框架,能夠說除了JDK Serializer外的序列化框架都提供了不錯API使用方式。

image.png

3 可擴展性
下面是各個序列化框架的可擴展性對比,能夠看到Protobuf的可擴展性是最方便、天然的。其它序列化框架都須要一些配置、註解等操做。

image.png

4 性能
序列化大小對比

對比各個序列化框架序列化後的數據大小以下,能夠看出kryo preregister(預先註冊序列化類)和Avro序列化結果都很不錯。因此,若是在序列化大小上有需求,能夠選擇Kryo或Avro。

image.png

序列化時間開銷對比

下面是序列化與反序列化的時間開銷,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。
    下面根據測試總結了以上序列化框架所能支持的數據類型、語法。

image.png

注1:static內部類須要實現序列化接口。
注2:外部類須要實現序列化接口。
注3:須要在Lambda表達式前添加(IXxx & Serializable)。

因爲Protobuf、Thrift是IDL定義類文件,而後使用各自的編譯器生成Java代碼。IDL沒有提供定義staic內部類、非static內部類等語法,因此這些功能沒法測試。
原文連接本文爲阿里雲原創內容,未經容許不得轉載。

相關文章
相關標籤/搜索