Java 序列化
- Java 序列化只是針對對象的狀態(tài)進(jìn)行保存,至于對象中的方法,序列化不關(guān)心
- 當(dāng)一個父類實現(xiàn)了序列化,那么子類會自動實現(xiàn)序列化,不需要顯示實現(xiàn)序列化接口
- 當(dāng)一個對象的實例變量引用了其他對象,序列化這個對象的時候會自動把引用的對象也進(jìn)行序列化(實現(xiàn)深度克?。?/li>
- 當(dāng)某個字段被申明為 transient 后,默認(rèn)的序列化機(jī)制會忽略這個字段
- 被申明為 transient 的字段,如果需要序列化,可以添加兩個私有方法:writeObject 和readObject
各種序列化技術(shù)
XML 序列化
XML 序列化的好處在于可讀性好,方便閱讀和調(diào)試。但是序列化以后的字節(jié)碼文件比較大,而且效率不高,適用于對性能不高,而且 QPS 較低的企業(yè)級內(nèi)部系統(tǒng)之間的數(shù)據(jù)交換的場景,同時 XML 又具有語言無關(guān)性,所以還可以用于異構(gòu)系統(tǒng)之間的數(shù)據(jù)交換和協(xié)議。比如我們熟知的 webservice,就是采用 XML 格式對數(shù)據(jù)進(jìn)行序列化的。XML 序列化/反序列化的實現(xiàn)方式有很多,熟知的方式有 XStream 和 Java 自帶的 XML 序列化和反序列化兩種
JSON 序列化
JSON(JavaScript Object Notation)是一種輕量級的數(shù)據(jù)交換格式,相對于 XML 來說,JSON的字節(jié)流更小,而且可讀性也非常好?,F(xiàn)在 JSON 數(shù)據(jù)格式在企業(yè)運用是最普遍的JSON 序列化常用的開源工具有很多
- Jackson (https://github.com/FasterXML/jackson)
- 阿里開源的 FastJson (https://github.com/alibaba/fastjon)
- Google 的 GSON (https://github.com/google/gson)
這幾種 json 序列化工具中,Jackson 與 fastjson 要比 GSON 的性能要好,但是 Jackson、GSON 的穩(wěn)定性要比 Fastjson 好。而 fastjson 的優(yōu)勢在于提供的 api 非常容易使用
Hessian 序列化
Hessian 是一個支持跨語言傳輸?shù)亩M(jìn)制序列化協(xié)議,相對于 Java 默認(rèn)的序列化機(jī)制來說,Hessian 具有更好的性能和易用性,而且支持多種不同的語言
實際上 Dubbo 采用的就是 Hessian 序列化來實現(xiàn),只不過 Dubbo 對 Hessian 進(jìn)行了重構(gòu),性能更高
Avro 序列化
Avro 是一個數(shù)據(jù)序列化系統(tǒng),設(shè)計用于支持大批量數(shù)據(jù)交換的應(yīng)用。它的主要特點有:支持二進(jìn)制序列化方式,可以便捷,快速地處理大量數(shù)據(jù);動態(tài)語言友好,Avro 提供的機(jī)制使動態(tài)語言可以方便地處理 Avro 數(shù)據(jù)
kyro 序列化
Kryo 是一種非常成熟的序列化實現(xiàn),已經(jīng)在 Hive、Storm)中使用得比較廣泛,不過它不能跨語言. 目前 dubbo 已經(jīng)在 2.6 版本支持 kyro 的序列化機(jī)制。它的性能要優(yōu)于之前的hessian2
Protobuf 序列化
Protobuf 是 Google 的一種數(shù)據(jù)交換格式,它獨立于語言、獨立于平臺。Google 提供了多種語言來實現(xiàn),比如 Java、C、Go、Python,每一種實現(xiàn)都包含了相應(yīng)語言的編譯器和庫文件,Protobuf 是一個純粹的表示層協(xié)議,可以和各種傳輸層協(xié)議一起使用。
Protobuf 使用比較廣泛,主要是空間開銷小和性能比較好,非常適合用于公司內(nèi)部對性能要求高的 RPC 調(diào)用。 另外由于解析性能比較高,序列化以后數(shù)據(jù)量相對較少,所以也可以應(yīng)用在對象的持久化場景中但是要使用 Protobuf 會相對來說麻煩些,因為他有自己的語法,有自己的編譯器,如果需要用到的話必須要去投入成本在這個技術(shù)的學(xué)習(xí)中
protobuf 有個缺點就是要傳輸?shù)拿恳粋€類的結(jié)構(gòu)都要生成對應(yīng)的 proto 文件,如果某個類發(fā)生修改,還得重新生成該類對應(yīng)的 proto 文件
選型建議
- 對性能要求不高的場景,可以采用基于 XML 的 SOAP 協(xié)議
- 對性能和間接性有比較高要求的場景,那么 Hessian、Protobuf、Thrift、Avro 都可以。
- 基于前后端分離,或者獨立的對外的 api 服務(wù),選用 JSON 是比較好的,對于調(diào)試、可讀性都很不錯
- Avro 設(shè)計理念偏于動態(tài)類型語言,那么這類的場景使用 Avro 是可以的
各個序列化技術(shù)的性能比較
這個地址有針對不同序列化技術(shù)進(jìn)行性能比較: https://github.com/eishay/jvm-serializers/wiki
Protobuf 序列化的原理
protobuf 的基本應(yīng)用
使用 protobuf 開發(fā)的一般步驟是
- 配置開發(fā)環(huán)境,安裝 protocol compiler 代碼編譯器
- 編寫.proto 文件,定義序列化對象的數(shù)據(jù)結(jié)構(gòu)
- 基于編寫的.proto 文件,使用 protocol compiler 編譯器生成對應(yīng)的序列化/反序列化工具類
- 基于自動生成的代碼,編寫自己的序列化應(yīng)用
Protobuf 案例演示
編寫 proto 文件
syntax="proto2";
package com.gupaoedu.serial;
option java_package =
"com.wei.serial";
option java_outer_classname="UserProtos";
message User {
required string name=1;
required int32 age=2;
}
數(shù)據(jù)類型
string / bytes / bool / int32(4 個字節(jié))
/int64/float/double
enum 枚舉類
message 自定義類
修飾符
required 表示必填字段
optional 表示可選字段
repeated 可重復(fù),表示集合
1,2,3,4 需要在當(dāng)前范圍內(nèi)是唯一的,表示順序
生成實體類
.\protoc.exe --java_out=./user.proto
實現(xiàn)序列化
public static void main(String[] args) {
UserProtos.User user = UserProtos.User.newBuilder().setAge(300).setName("Mic").build();
byte[] bytes = user.toByteArray();
for (byte bt : bytes) {
System.out.print(bt + " ");
}
}
輸出結(jié)果:10 3 77 105 99 16 -84 2
可以看到,序列化出來的數(shù)字基本看不懂,但是序列化以后的數(shù)據(jù)確實很小,那我們來了解一下底層的原理
正常來說,要達(dá)到最小的序列化結(jié)果,一定會用到壓縮的技術(shù),而 protobuf 里面用到了兩種
壓縮算法,一種是 varint,另一種是 zigzag
-varint
先來看 age=300 這個數(shù)字是如何被壓縮的
這兩個字節(jié)字節(jié)分別的結(jié)果是:-84 、2
-84 怎么計算來的呢? 我們知道在二進(jìn)制中表示負(fù)數(shù)的方法,高位設(shè)置為 1, 并且是對應(yīng)數(shù)字的二進(jìn)制取反以后再計算補(bǔ)碼表示(補(bǔ)碼是反碼+1)
所以如果要反過來計算
- 【補(bǔ)碼】10101100 -1 得到 10101011
- 【反碼】01010100 得到的結(jié)果為 84. 由于高位是 1,表示負(fù)數(shù)所以結(jié)果為-84
字符如何轉(zhuǎn)化為編碼
“Mic”這個字符,需要根據(jù) ASCII 對照表轉(zhuǎn)化為數(shù)字。
M =77、i=105、c=99
所以結(jié)果為 77 105 99
這里的結(jié)果為什么直接就是 ASCII 編碼的值呢?怎么沒有做壓縮呢?
原因是,varint 是對字節(jié)碼做壓縮,但是如果這個數(shù)字的二進(jìn)制只需要一個字節(jié)表示的時候,其實最終編碼出來的結(jié)果是不會變化的
還有兩個數(shù)字,3 和 16 代表什么呢?那就要了解 protobuf 的存儲格式了
存儲格式
protobuf 采用 T-L-V 作為存儲方式
tag 的計算方式是 field_number(當(dāng)前字段的編號) << 3 | wire_type
比如 Mic 的字段編號是 1 ,類型 wire_type 的值為 2 所以 : 1 <<3 | 2 =10
age=300 的字段編號是 2,類型 wire_type 的值是 0, 所以 : 2<<3|0 =16
第一個數(shù)字 10,代表的是 key,剩下的都是 value
負(fù)數(shù)的存儲
在計算機(jī)中,負(fù)數(shù)會被表示為很大的整數(shù),因為計算機(jī)定義負(fù)數(shù)符號位為數(shù)字的最高位,所以如果采用 varint 編碼表示一個負(fù)數(shù),那么一定需要 5 個比特位。所以在 protobuf 中通過sint32/sint64 類型來表示負(fù)數(shù),負(fù)數(shù)的處理形式是先采用 zigzag 編碼(把符號數(shù)轉(zhuǎn)化為無符號數(shù)),再采用 varint 編碼。
sint32:(n << 1) ^ (n >> 31)
sint64:(n << 1) ^ (n >> 63)
比如存儲一個(-300)的值
-300
原碼:0001 0010 1100
取反:1110 1101 0011
加 1 :1110 1101 0100
n<<1: 整體左移一位,右邊補(bǔ) 0 -> 1101 1010 1000
n>>31: 整體右移 31 位,左邊補(bǔ) 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十進(jìn)制: 0010 0101 0111 = 599
varint 算法: 從右往做,選取 7 位,高位補(bǔ) 1/0(取決于字節(jié)數(shù))
得到兩個字節(jié)
1101 0111 和 0000 0100
-41 和 4
Protocol Buffer 的性能好,主要體現(xiàn)在 序列化后的數(shù)據(jù)體積小 & 序列化速度快,最終使得傳輸效率高,其原因如下:
序列化速度快的原因:
a. 編碼 / 解碼 方式簡單(只需要簡單的數(shù)學(xué)運算 = 位移等等)
b. 采用 Protocol Buffer 自身的框架代碼 和 編譯器 共同完成-
序列化后的數(shù)據(jù)量體積小(即數(shù)據(jù)壓縮效果好)的原因:
a. 采用了獨特的編碼方式,如 Varint、Zigzag 編碼方式等等
b. 采用 T - L - V 的數(shù)據(jù)存儲方式:減少了分隔符的使用 & 數(shù)據(jù)存儲得緊湊——學(xué)自咕泡學(xué)院