目前業界有各種各樣的網路輸出傳輸時的序列化和反序列化方案,它們在技術上的實現的初衷和背景有較大的區別,因此在設計的架構也會有很大的區別,最終在落地後的:解析速度、對系統的影響、傳輸資料的大小、可維護性及可閱讀性等方面有著較大的區別,本文分享一些我在一些常見序列化技術的分析和理解:

文章分成3個部分:

1、列舉常見的序列化和反序列化方案(ObjectXXStream、XML、JSON)

2、MySQL JDBC結果集的處理方案

3、Google Protocol Buffer處理方案

【一、常見的在API及訊息通訊調的用中Serialize方案】:

方案1、基於Java原生的ObjectOutputStream。write()和ObjectInputStream。read()來進行物件序列化和反序列化。

方案2、基於JSON進行序列化和反序列化。

方案3、基於XML進行序列化和反序列化。

【方案1淺析,ObjectXXXStream】:

優點:

(1)、由Java自帶API序列化,簡單、方便、無第三方依賴。

(2)、不用擔心其中的資料解析會丟失精度、丟失欄位、Object的反序列化型別不確定等問題。

缺點:

(1)、雙方除錯麻煩,傳送方和接收方最好是同版本的物件描述,否則會有奇怪的問題,調試周期相對長,跨團隊合作升級問題很多。

(2)、傳遞的物件中包含了元資料資訊,佔用空間較大。

【方案2淺析,JSON序列化】:

優點:

(1)、簡單、方便,無需關注要序列化的物件格式。

(2)、開源界有較多的元件可以支援,例如FastJSON效能非常好。

(3)、在現在很多RPC的框架中,基本都支援這樣的方案。

缺點:

(1)、物件屬性中如果包含Object型別,在反序列化的時候如果業務也本身也不明確資料型別,處理起來會很麻煩。

(2)、由於文字型別,所以一定會佔用較大的資料空間,例如下圖。

(3)、比較比較依賴於JSON的解析包的相容性和效能,在JSON的一些細節處理上(例如一些非標的JSON),各自處理方式可能不一樣。

(4)、序列化無論任何資料型別先要轉換為String,轉成byte[],會增加記憶體複製的次數。

(5)、反序列化的時候,必須將整個JSON反序列化成物件後才能進行讀取,大家應該知道,Java物件尤其是層次巢狀較多的物件,佔用的記憶體空間將會遠遠大於資料本身的空間。

資料放大的極端案例1:

傳遞資料描述資訊為:

class PP {

long userId = 102333320132133L;

int passportNumber = 123456;

}

此時傳遞JSON的格式為:

{

“userId”:102333320132133,

“passportNumber”:123456

}

我們要傳遞的資料是1個long、1個int,也就是12個位元組的資料,這個JSON的字串長度將是實際的位元組數(不包含回車、空格,這裡只是為了可讀性,同時注意,這裡的long在JSON裡面是字串了),這個字串有:51個位元組,也就是資料放到了4。25倍左右。

資料放大極端案例2:

當你的物件內部有資料是byte[]型別,JSON是文字格式的資料,是無法儲存byte[]的,那麼要序列化這樣的資料,只有一個辦法就是把byte轉成字元,通常的做法有兩種:

(1)使用BASE64編碼,目前JSON中比較常用的做法。

(2)按照位元組進行16進位制字元編碼,例如字串:“FF”代表的是0xFF這個位元組。

不論上面兩種做法的那一種,1個位元組都會變成2個字元來傳遞,也就是byte[]資料會被放大2倍以上。為什麼不用ISO-8859-1的字元來編碼呢?因為這樣編碼後,在最終序列化成網路byte[]資料後,對應的byte[]雖然沒變大,但是在反序列化成文字的時候,接收方並不知道是ISO-8859-1,還會用例如GBK、UTF-8這樣比較常見的字符集解析成String,才能進一步解析JSON物件,這樣這個byte[]可能在編碼的過程中被改變了,要處理這個問題會非常麻煩。

【方案2淺析,XML序列化】:

優點:

(1)、使用簡單、方便,無需關注要序列化的物件格式

(2)、可讀性較好,XML在業界比較通用,大家也習慣性在配置檔案中看到XML的樣子

(3)、大量RPC框架都支援,透過XML可以直接形成文件進行傳閱

缺點:

(1)、在序列化和反序列化的效能上一直不是太好。

(2)、也有與JSON同樣的資料型別問題,和資料放大的問題,同時資料放大的問題更為嚴重,同時記憶體複製次數也和JSON型別,不可避免。

XML資料放大說明:

XML的資料放大通常比JSON更為嚴重,以上面的JSON案例來講,XML傳遞這份資料通常會這樣傳:

102333320132133

123456

這個訊息就有80+以上的位元組了,如果XML裡面再搞一些Property屬性,物件再巢狀巢狀,那麼這個放大的比例有可能會達到10倍都是有可能的,因此它的放大比JSON更為嚴重,這也是為什麼現在越來越多的API更加喜歡用JSON,而不是XML的原因。

【放大的問題是什麼】:

(1)、花費更多的時間去拼接字串和複製記憶體,佔用更多的Java記憶體,產生更多的碎片。

(2)、產生的JSON物件要轉為byte[]需要先轉成String文字再進行byte[]編碼,因為這本身是文字協議,那麼自然再多一次記憶體全量的複製。

(3)、傳輸過程由於資料被放大,佔用更大的網路流量。

(4)、由於網路的package變多了,所以TCP的ACK也會變多,自然系統也會更大,同等丟包率的情況下丟包數量會增加,整體傳輸時間會更長,如果這個資料傳送的網路延遲很大且丟包率很高,我們要儘量降低大小;壓縮是一條途徑,但是壓縮會帶來巨大的CPU負載提高,在壓縮前儘量降低資料的放大是我們所期望的,然後傳送資料時根據RT和資料大小再判定是否壓縮,有必要的時候,壓縮前如果資料過大還可以進行部分取樣資料壓縮測試壓縮率。

(5)、接收方要處理資料也會花費更多的時間來處理。

(6)、由於是文字協議,在處理過程中會增加開銷,例如數字轉字串,字串轉數字;byte[]轉字串,字串轉byte[]都會增加額外的記憶體和計算開銷。

不過由於在平時大量的應用程式中,這個開銷相對業務邏輯來講簡直微不足道,所以最佳化方面,這並不是我們關注的重點,但面臨一些特定的資料處理較多的場景,即核心業務在資料序列化和反序列化的時候,就要考慮這個問題了,那麼下面我繼續討論問題。

此時提出點問題:

(1)、網路傳遞是不是有更好的方案,如果有,為什麼現在沒有大面積採用?

(2)、相對底層的資料通訊,例如JDBC是如何做的,如果它像上面3種方案傳遞結果集,會怎麼樣?

【二、MySQL JDBC資料傳遞方案】:

在前文中提到資料在序列化過程被放大數倍的問題,我們是否想看看一些相對底層的通訊是否也是如此呢?那麼我們以MySQL JDBC為例子來看看它與JDBC之間進行通訊是否也是如此。

JDBC驅動程式根據資料庫不同有很多實現,每一種資料庫實現細節上都有巨大的區別,本文以MySQL JDBC的資料解析為例(MySQL 8。0以前),給大家說明它是如何傳遞資料的,而傳遞資料的過程中,相信大家最為關注的就是ResultSet的資料是如何傳遞的。

拋開結果集中的MetaData等基本資訊,單看資料本身:

(1)JDBC會讀取資料行的時候,首先會從緩衝區讀取一個row packege,row package就是從網路package中拿到的,根據協議中傳遞過來的package的頭部判定package大小,然後從網路緩衝中讀取對應大小的內容,下圖想表達網路傳遞的package和業務資料中的package之間可能並不是完全對應的。另外,網路中的package如果都到了本地緩衝區,邏輯上講它們是連續的(圖中故意分開是讓大家瞭解到網路中傳遞是分不同的package傳遞到本地的),JDBC從本地buffer讀取row package這個過程就是核心package到JVM的package複製過程,對於我們Java來講,我們主要關注row package(JDBC中可能存在一些特殊情況讀取過來的package並不是行級別的,這種特殊情況請有興趣的同學自行查閱原始碼)。

常見Serialize技術探秘(XML、JSON、JDBC byte編碼、Protobuf)

(2)、單行資料除頭部外,就是body了,body部分包含各種各樣不同的資料型別,此時在body上放資料型別顯然是佔空間的,所以資料型別是從metadata中提取的,body中資料列的順序將會和metdata中的列的順序保持一致。

(3)、MySQL詳細解析資料型別:

3。1、如果Metadata對應資料發現是int、longint、tinyint、year、float、double等資料型別會按照定長位元組數讀取,例如int自然按照4位元組讀取,long會按照8位元組讀取。

3。2、如果發現是其它的型別,例如varchar、binary、text等等會按照變長讀取。

3。3、變長字串首先讀取1個位元組標誌位。

3。4、如果這個標誌位的值小於等於250,則直接代表後續位元組的長度(注意字串在這裡是算轉換為位元組的長度),這樣確保大部分業務中存放的變長字串,在網路傳遞過程中只需要1個位元組的放大。

3。5、如果這個標誌位是:251,代表這個欄位為NULL

3。6、如果標誌位是:252,代表需要2個位元組代表欄位的長度,此時加上標誌位就是3個位元組,在65536以內的長度的資料(64KB),注意,這裡會在轉成long的時候高位補0,所以2個位元組可以填滿到65536,只需要放大3個位元組來表示這個資料。

3。7、如果標誌位是:253,代表需要4個自己大表欄位的長度,可以表示4GB(同上高位補0),這樣的資料幾乎不會出現在資料庫裡面,即使出現,只會出現5個位元組的放大。

3。8、如果標誌位是:254,8個位元組代表長度,此時沒有高位補0,最多可以讀取Long。MAX_VALUE的長度,但是這個空間目前不可能有記憶體放得下,所以無需擔心使用問題,此時9個位元組的放大,原始碼如下圖:

常見Serialize技術探秘(XML、JSON、JDBC byte編碼、Protobuf)

(4)、我們先按照這個理解,MySQL在傳遞資料的過程中,對資料的放大是很小很小的,是不是真的這樣呢?請下面第5點說明。

補充說明:

a、在MySQL JDBC中對於ResultSetRow資料的解析(除對MySQL 8。0以上JDBC版本)有2個實現類:BufferRow、ByteArrayRow,這兩種方式在讀取單行資料在解析這個階段是一樣的邏輯,只不過解析存放資料的方式有所不同,BufferRow一個會解析成資料行的byte[],ByteArrayRow會解析成byte[][]二維陣列,第二維就是每1個列的資訊,這都是客戶端行為,與網路傳遞資料的格式無關。(兩者在不同場景下使用,例如其中一種場景是:ByteArrayRow在遊標開啟UPDATE模式的時候會啟用,但這不是本文的重點,這裡提到主要告知大家,無論哪一種方式,讀取資料的方式是一致的)

b、在MySQL JDBC中的RowData是ResultSet裡面資料處理的入口,其實現類有3個:RowStatStatic、RowDataCursor、RowDataDynamic,這雖然有3個實現類,但是同樣不會影響資料的格式,它們只是從緩衝區讀取資料的方式有所不同:RowStatStatic、RowDataCursor會每次將緩衝區的資料全部讀取到JDBC當中形成陣列,RowDataCursor在處理上有一個區別在於資料庫每次返回的是FetchSize大小的資料內容(實現的細節在上一篇文章中有提到);RowDataDynamic是需要行的時候再從pakcege中去讀,package讀取完成後就嘗試讀取下一個package。這些都不會影響資料本身在網路上的傳遞格式,所以文字提到的解析是目前MySQL JDBC比較通用的解析,與它內部的執行路徑無關。

(5)、以BufferRow為例,當你發起getString(‘xxx’)、getDate(int)的時候,首先它需要在內部找到是第幾個列(傳數字省略該動作),然後其內部會有一個lastRequestedIndex、lastRequestedPos分別記錄最後讀取的第幾個欄位和所在位元組的位置,如果你傳入的index比這個index大,則從當前位置開始,向後掃描,掃描規則和上面的資料庫寬度一致,找到對應位置,複製出對應的byte[]陣列,轉換你要的物件型別。

PS:

lastRequestedIndex、lastRequestedPos這種其實就是JDBC認為你絕大部分情況是從前向後讀取的,因此這樣讀取對JDBC程式也是最友好的方案,否則指標向前移動,需要從0開始,理由很簡單(資料的長度不是在尾部,而是在頭部),因此指標來回來回移動的時候,這樣會產生很多開銷,同時會產生更多的記憶體複製出來的碎片。ByteArrayRow雖然可以解決這個問題,但是其本身會佔用相對較大的空間另外,其內部的二維陣列返回的byte[]位元組是可以被外部所修改的(因為沒有複製)。

另外,按照這種讀取資料的方式,如果單行資料過大(例如有大欄位100MB+),讀取到Java記憶體裡面來,即使使用CursorFetch和Stream讀取,讀取幾十條資料,就能把JVM記憶體幹掛掉。到目前為止,我還沒看到MySQL裡面可以“設定限制單行資料長度”的引數,後續估計官方支援這類特殊需求的可能性很小,大多也只能自己改原始碼來實現。

【回到話題本身:MySQL和JDBC之間的通訊似乎放大很小?】

其實不然,MySQL傳遞資料給JDBC預設是走文字協議的,而不是Binary協議,雖然說它的byte[]陣列不會像JSON那樣放大,並不算真正意義上的文字協議,但是它很多種資料型別預設情況下,都是文字傳輸,例如一個上面提到的賬號:102333320132133在資料庫中是8個自己,但是網路傳遞的時候如果有文字格式傳遞將會是:15個位元組,如果是DateTime資料在資料庫中可以用8個位元組存放,但是網路傳遞如果按照YYYY-MM-DD HH:MI:SS傳遞,可以達到19個位元組,而當他們用String在網路傳遞的時候,按照我們前面提到的,MySQL會將其當成變長字元,因此會在資料頭部加上最少1個自己的標誌位。另外,這裡增加不僅僅是幾個位元組,而是你要取到真正的資料,接收方還需要進一步計算處理才能得到,例如102333320132133用文字傳送後,接收方是需要將這個字串轉換為long型別才能可以得到long的,大家試想一下你處理500萬資料,每一行資料有20個列,有大量的類似的處理不是開銷增加了特別多呢?

JDBC和MySQL之間可以透過binary協議來進行通訊的,也就是按照實際數字佔用的空間大小來進行通訊,但是比較坑的時,MySQL目前開啟Binary協議的方案是:“開啟服務端prepareStatemet”,這個一旦開啟,會有一大堆的坑出來,尤其是在網際網路的程式設計中,我會在後續的文章中逐步闡述。

拋開“開啟binary協議的坑”,我們認為MySQL JDBC在通訊的過程中對資料的編碼還是很不錯的,非常緊湊的編碼(當然,如果你開啟了SSL訪問,那麼資料又會被放大,而且加密後的資料基本很難壓縮)。

對比傳統的資料序列化優劣勢彙總:

優勢:

(1)、資料全部按照byte[]編碼後,由於緊湊編碼,所以對資料本身的放大很小。

(2)、由於編碼和解碼都沒有解析的過程,都是向ByteBuffer的尾部順序地寫,也就是說不用找位置,讀取的時候根據設計也可以減少找位置,即使找位置也是移動偏移量,非常高效。

(3)、如果傳遞多行資料,反序列化的過程不用像XML或JSON那樣一次要將整個傳遞過來的資料全部解析後再處理,試想一下,如果5000行、20列的結果集,會產生多少Java物件,每一個Java物件對資料本身的放大又是多少,採用位元組傳遞後可以按需轉變為Java物件,使用完的Java物件可以釋放,這樣就不用同時佔用那樣大的JVM記憶體,而byte[]陣列也只是資料本身的大小,也可以按需釋放。

(4)、相對前面提到的3種方式,例如JSON,它不需要在序列化和反序列化的時候要經歷一次String的轉換,這樣會減少一次記憶體複製。

(5)、自己寫程式碼用類似的通訊方案,可以在網路最佳化上做到極致。

劣勢:

(1)、編碼是MySQL和MySQL JDBC之間自定義的,別人沒法用(我們可以參考別人的思路)

(2)、byte編碼和解碼過程程式設計師自己寫,對程式設計師水平和嚴謹性要求都很高,前期需要大量的測試,後期在網路問題上考慮稍有偏差就可能出現不可預期的Bug。(所以在公司內部需要把這些內容進行封裝,大部分程式設計師無需關注這個內容)。

(3)、從記憶體複製上來講,從rowBuffer到應用中的資料,這一層記憶體複製是無法避免的,如果你寫自定義程式,在必要的條件下,這個地方可以進一步減少記憶體複製,但無法杜絕;同上文中提到,這點開銷,對於整個應用程式的業務處理來講,簡直微不足道。

為什麼傳統通訊協議不選擇這樣做:

(1)、參考劣勢中的3點。

(2)、傳統API通訊,我們更講究快速、通用,也就是會經常和不同團隊乃至不同公司除錯程式碼,要設計binary協議,開發成本和除錯成本非常高。

(3)、可讀性,對於業務程式碼來講,byte[]的可讀性較差,尤其是物件巢狀的時候,byte[]表達的方式是很複雜的。

MySQL JDBC如果用binary協議後,資料的緊湊性是不是達到極致了呢?

按照一般的理解,就是達到極致了,所有資料都不會進一步放大,int就只用4個位元組傳遞,long就只用8個位元組傳遞,那麼還能繼續變小,難道壓縮來做?

非也、非也,在二進位制的世界裡,如果你探究細節,還有更多比較神奇的東西,下面我們來探討一下:

例如:

long id = 1L;

此時網路傳遞的時候會使用8個位元組來方,大家可以看下8個位元組的排布:

常見Serialize技術探秘(XML、JSON、JDBC byte編碼、Protobuf)

我們先不考慮按照bit有31個bit是0,先按照位元組來看有7個0,代表位元組沒有資料,只有1個位元組是有值的,大家可以去看一下自己的資料庫中大量的自動增長列,在id小於4194303之前,前面5個位元組是浪費掉的,在增長到4個位元組(2的32次方-1)之前前面4個自己都是0,浪費掉的。另外,即使8個位元組中的第一個位元組開始使用,也會有大量的資料,中間位元組是為:0的機率極高,就像十進位制中進入1億,那麼1億下面最多會有8個0,越高位的0約難補充上去。

如果真的想去嘗試,可以用這個辦法:用1個位元組來做標誌,但會佔用一定的計算開銷,所以是否為了這個空間去做這個事情,由你決定,本文僅僅是技術性探討:

方法1:表達目前有幾個低位被使用的位元組數,由於long只有8個位元組,所以用3個bit就夠了,另外5個bit是浪費掉的,也無所謂了,反序列化的時候按照高位數量補充0x00即可。

方法2:相對方法1,更徹底,但處理起來更復雜,用1這個位元組的8個bit的0、1分別代表long的8個位元組是被使用,序列化和反序列化過程根據標誌位和資料本身進行位元組補0x00操作,補充完整8個位元組就是long的值了,最壞情況是9個位元組代表long,最佳情況0是1個位元組,位元組中只佔用了2個位元組的時候,即使資料變得相當大,也會有大量的資料的位元組存在空位的情況,在這些情況下,就通常可以用少於8個位元組的情況來表達,要用滿7個位元組才能夠與原數字long的佔用空間一樣,此時的資料已經是比2的48次方-1更大的資料了。

【三、Google Protocol Buffer技術方案】:

這個對於很多人來講未必用過,也不知道它是用來幹什麼的,不過我不得不說,它是目前資料序列化和反序列化的一個神器,這個東西是在谷歌內部為了約定好自己內部的資料通訊設計出來的,大家都知道谷歌的全球網路非常牛逼,那麼自然在資料傳輸方面做得那是相當極致,在這裡我會講解下它的原理,就本身其使用請大家查閱其它人的部落格,本文篇幅所限沒法step by step進行講解。

看到這個名字,應該知道是協議Buffer,或者是協議編碼,其目的和上文中提到的用JSON、XML用來進行RPC呼叫類似,就是系統之間傳遞訊息或呼叫API。但是谷歌一方面為了達到類似於XML、JSON的可讀性和跨語言的通用性,另一方面又希望達到較高的序列化和反序列化效能,資料放大能夠進行控制,所以它又希望有一種比底層編碼更容易使用,而又可以使用底層編碼的方式,又具備文件的可讀效能力。

它首先需要定義一個格式檔案,如下:

syntax = “proto2”;

package com。xxx。proto。buffer。test;

message TestData2 {

optional int32 id = 2;

optional int64 longId = 1;

optional bool boolValue = 3;

optional string name = 4;

optional bytes bytesValue = 5;

optional int32 id2 = 6;

}

這個檔案不是Java檔案,也不是C檔案,和語言無關,通常把它的字尾命名為proto(檔案中1、2、3數字代表序列化的順序,反序列化也會按照這個順序來做),然後本地安裝了protobuf後(不同OS安裝方式不同,在官方有下載和說明),會產生一個protoc執行檔案,將其加入環境變數後,執行命令指定一個目標目錄:

protoc --java_out=~/temp/ TestData2.proto

此時會在指定的目錄下,產生package所描述的目錄,在其目錄內部有1個Java原始檔(其它語言的會產生其它語言),這部分程式碼是谷歌幫你生成的,你自己寫的話太費勁,所以谷歌就幫你幹了;本地的Java project裡面要引入protobuf包,maven引用(版本自行選擇):

<dependency>

<groupId>com.google.protobuf</groupId>

<artifactId>protobuf-java</artifactId>

<version>3.6.1</version>

</dependency>

此時生成的程式碼會呼叫這個谷歌包裡面提供的方法庫來做序列化動作,我們的程式碼只需要呼叫生成的這個類裡面的API就可以做序列化和反序列化操作了,將這些生成的檔案放在一個模組裡面釋出到maven倉庫別人就可以引用了,關於測試程式碼本身,大家可以參考目前有很多部落格有提供測試程式碼,還是很好用的。

谷歌編碼比較神奇的是,你可以按照物件的方式定義傳輸資料的格式,可讀性極高,甚至於相對XML和JSON更適合程式設計師閱讀,也可以作為交流文件,不同語言都通用,定義的物件還是可以巢狀的,但是它序列化出來的位元組比原始資料只大一點點,這尼瑪太厲害了吧。

經過測試不同的資料型別,故意製造資料巢狀的層數,進行二進位制陣列多層巢狀,發現其資料放大的比例非常非常小,幾乎可以等價於二進位制傳輸,於是我把序列化後的資料其二進位制進行了輸出,發現其編碼方式非常接近於上面的JDBC,雖然有一些細節上的區別,但是非常接近,除此之外,它在序列化的時候有幾大特徵:

(1)、如果欄位為空,它不會產生任何位元組,如果整合物件的屬性都為null,產生的位元組將是0

(2)、對int32、int64這些資料採用了變長編碼,其思路和我們上面描述有一些共通之處,就是一個int64值在比較小的時候用比較少的位元組就可以表達了,其內部有一套位元組的移位和異或演算法來處理這個事情。

(3)、它對字串、byte[]沒有做任何轉換,直接放入位元組陣列,和二進位制編碼是差不多的道理。

(4)、由於欄位為空它都可以不做任何位元組,它的做法是有資料的地方會有一個位置編碼資訊,大家可以嘗試透過調整數字順序看看生成出來的byte是否會發生改變;那麼此時它就有了很強相容性,也就是普通的加欄位是沒問題的,這個對於普通的二進位制編碼來講很難做到。

(5)、序列化過程沒有產生metadata資訊,也就是它不會把物件的結構寫在位元組裡面,而是反序列化的接收方有同一個物件,就可以反解析出來了。

這與我自己寫編碼有何區別?

(1)、自己寫編碼有很多不確定性,寫不好的話,資料可能放得更大,也容易出錯。

(2)、google的工程師把內部規範後,谷歌開源的產品也大量使用這樣的通訊協議,越來越多的業界中介軟體產品開始使用該方案,就連MySQL資料庫最新版本在資料傳輸方面也會開始相容protobuf。

(3)、谷歌相當於開始在定義一個業界的新的資料傳輸方案,即有效能又降低程式碼研發的難度,也有跨語言訪問的能力,所以才會有越來越多的人喜歡使用這個東西。

那麼它有什麼缺點呢?還真不多,它基本兼顧了很多序列化和反序列化中你需要考慮的所有的事情,達到了一種非常良好的平衡,但是硬要挑缺陷,我們就得找場景才行:

(1)、protobuf需要雙方明確資料型別,且定義的檔案中每一個物件要明確資料型別,對於Object型別的表達沒有方案,你自己必須提前預知這個Object到底是什麼型別。

(2)、使用repeated可以表達陣列,但是隻能表達相同型別的資料,例如上面提到的JDBC一行資料的多個列資料型別不同的時候,要用這個表達,會比較麻煩;另外,預設情況下陣列只能表達1維陣列,要表達二維陣列,需要使用物件巢狀來間接完成。

(3)、它提供的資料型別都是基本資料型別,如果不是普通型別,要自己想辦法轉換為普通型別進行傳輸,例如從MongoDB查處一個Docment物件,這個物件序列化是需要自己先透過別的方式轉換為byte[]或String放進去的,而相對XML、JSON普通是提供了遞迴的功能,但是如果protobuf要提供這個功能,必然會面臨資料放大的問題,通用和效能永遠是矛盾的。

(4)、相對於自定義byte的話,序列化和反序列化是一次性完成,不能逐步完成,這樣如果傳遞陣列巢狀,在反序列化的時候會產生大量的Java物件,另外自定義byte的話可以進一步減少記憶體複製,不過谷歌這個相對文字協議來講記憶體複製已經少很多了。

補充說明:

在第2點中提到repeated表達的陣列,每一個元素必須是同類型的,無法直接表達不同型別的元素,因為它沒有像Java那樣Object[]這樣的陣列,這樣它即使透過本地判定Object的型別傳遞了,反序列化會很麻煩,因為接收方也不知道資料是什麼型別,而protobuf網路傳遞資料是沒有metadata傳遞的,那麼判定唯一的地方就是在客戶端自己根據業務需要進行傳遞。

因此,如果真的有必要的話,可以用List表達一行資料的方案,也就是裡面的每1個byte[]元素透過其它地方獲得metadata,例如資料庫的Metadata得到,然後再自己去轉換,但是傳遞過程全是byte[];就JDBC來講,我個人更加推薦於一行資料使用一個byte[]來傳遞,而不是每一項資料用一個byte[],因為在序列化和反序列化過程中,每一個數組元素都會在protobuf會被包裝成物件,此時產生的Java物件數量是列的倍數,例如有40個列,會產生40倍的Java物件,很誇張吧。

總之,每一種序列化和反序列化方案目前都有應用場景,它們在設計之初決定了架構,也將決定了最終的效能、穩定性、系統開銷、網路傳輸大小等等。

阿里雲雙十一1折拼團活動:已滿6人,都是最低折扣了

【滿6人】1核2G雲伺服器99。5元一年298。5元三年 2核4G雲伺服器545元一年 1227元三年

【滿6人】1核1G MySQL資料庫 119。5元一年

【滿6人】3000條國內簡訊包 60元每6月

參團地址:

http://

click。aliyun。com/m/1000

020293/

本文作者:鍾隱

原文連結

更多技術乾貨敬請關注雲棲社群知乎機構號:阿里云云棲社群 - 知乎