天天看點

一個高性能、小而美的序列化工具!

一個高性能、小而美的序列化工具!

記錄類型資訊

這算是kryo的一個特點,可以把對象資訊直接寫到序列化資料裡,反序列化的時候可以精确地找到原始類資訊,不會出錯,這意味着在寫readxxx方法時,無需傳入Class或Type類資訊。

相應的,kryo提供兩種讀寫方式。記錄類型資訊的writeClassAndObject/readClassAndObject方法,以及傳統的writeObject/readObject方法。

線程安全

kryo的對象本身不是線程安全的,是以我們有兩種選擇來保障線程安全。

使用Threadlocal來保障線程安全:

一個高性能、小而美的序列化工具!

執行個體化器

在上面注意到kryo.setInstantiatorStrategy(new Kryo.DefaultInstantiatorStrategy(new StdInstantiatorStrategy())); 這句話顯示指定了執行個體化器。

在一些依賴了kryo的開源軟體中,可能由于執行個體化器指定的問題而抛出空指針異常。例如hive的某些版本中,預設指定了StdInstantiatorStrategy。

public static ThreadLocal<Kryo> runtimeSerializationKryo = new ThreadLocal<Kryo>() {
    @Override
    protected synchronized Kryo initialValue() {
        Kryo kryo = new Kryo();
        kryo.setClassLoader(Thread.currentThread().getContextClassLoader());
        kryo.register(java.sql.Date.class, new SqlDateSerializer());
        kryo.register(java.sql.Timestamp.class, new TimestampSerializer());
        kryo.register(Path.class, new PathSerializer());
        kryo.setInstantiatorStrategy(new StdInstantiatorStrategy());
        ......
            return kryo;
    };
};      

而StdInstantiatorStrategy在是依據JVM version資訊及JVM vendor資訊建立對象的,可以不調用對象的任何構造方法建立對象。

那麼例如碰到ArrayList這樣的對象時候,就會出問題。觀察一下ArrayList的源碼:

public ArrayList() {
    this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
}      

既然沒有調用構造器,那麼這裡elementData會是NULL,那麼在調用類似ensureCapacity方法時,就會抛出一個異常。

public void ensureCapacity(int minCapacity) {
     if (minCapacity > elementData.length
         && !(elementData == DEFAULTCAPACITY_EMPTY_ELEMENTDATA
              && minCapacity <= DEFAULT_CAPACITY)) {
         modCount++;
         grow(minCapacity);
     }
 }      

解決方案很簡單,就如架構中代碼寫的一樣,顯示指定執行個體化器,首先使用預設無參構造政策DefaultInstantiatorStrategy,若建立對象失敗再采用StdInstantiatorStrategy。

類注冊

當kryo寫一個對象的執行個體的時候,預設需要将類的完全限定名稱寫入。将類名一同寫入序列化資料中是比較低效的,是以kryo支援通過類注冊進行優化。

一個高性能、小而美的序列化工具!

注冊會給每一個class一個int類型的Id相關聯,這顯然比類名稱高效,但同時要求反序列化的時候的Id必須與序列化過程中一緻。這意味着注冊的順序非常重要。

但是由于現實原因,同樣的代碼,同樣的Class在不同的機器上注冊編号任然不能保證一緻,是以多機器部署時候反序列化可能會出現問題。

是以kryo預設會禁止類注冊,當然如果想要打開這個屬性,可以通過kryo.setRegistrationRequired(true);打開。

循環引用

這是對循環引用的支援,可以有效防止棧記憶體溢出,kryo預設會打開這個屬性。當你确定不會有循環引用發生的時候,可以通過kryo.setReferences(false);關閉循環引用檢測,進而提高一些性能。

可變長存儲

kryo對int和long類型都采用了可變長存儲的機制,以int為例,一般需要4個位元組去存儲,而對kryo來說,可以通過1-5個變長位元組去存儲,進而避免高位都是0的浪費。

最多需要5個位元組存儲是因為,在變長存儲int過程中,一個位元組的8位用來存儲有效數字的隻有7位,最高位用于标記是否還需讀取下一個位元組,1表示需要,0表示不需要。

在對string的存儲中也有變長存儲的應用,string序列化的整體結構為length+内容,那麼length也會使用變長int寫入字元的長度。

配合緩存使用的場景

在實際開發中,class增删字段是很常見的事情,但對于kryo來說,确是不支援的,而如果恰好需要使用緩存,那麼這個問題會被放得更大。

例如一個對象使用kryo序列化後,資料放入了緩存中,而這時候如果這個對象增删了一個屬性,那麼緩存中反序列化的時候就會報錯。是以頻繁使用緩存的場景,可以盡量避免kryo。

不過現在的Kryo提供了相容性的支援,使用CompatibleFieldSerializer.class,在kryo.writeClassAndObject時候寫入的資訊如下:

class name|field length|field1 name|field2 name|field1 value| filed2 value

而在讀入kryo.readClassAndObject時,會先讀入field names,然後比對目前反序列化類的field和順序再構造結果。

當然如果在做好緩存隔離的情況下,這一切都不用在意。