hive序列生成_常见的序列化框架及Protobuf原理
轉載請聲明出處!
上次我們詳細的學習了Java中的序列化機制,但是我們日常開發過程中,因為java的序列化機制的壓縮效率問題,以及序列化大小帶來的傳輸的效率問題,一般很少會使用原生的序列化機制,而是使用常見的序列化開源框架來實現序列化操作,接下來我們學習一下開發常用的序列化機制及原理分析
常見的序列化框架
xml序列化在java發展早期開始,為了統一接口,xml協議橫空出世,良好的可讀性,自由度極高的擴展性,成了很長一段時間的序列化標準規范。實現xml序列化/反序列化的方案有很多,最常見的是XStream 和 Java 自帶的 XML 序列化和反序列化兩種 ,并且還有基于xml協議的soap協議實現的webservice接口等。可以說xml序列化是開發中最常見也是發展時間最久的協議,并且支持跨進程和跨語言交互,但是缺陷也很明顯,即xml規范下的每一個屬性和值都是固定的標簽形式,導致序列化后的字節流文件很大,遠超于java自身的序列化方案,而且效率很低,一般建議使用在內部系統或者性能要求不高,但是對于接口的復雜度和準確性要求比較高的接口交互,或者適合多語言多進程之間交互的統一規范,不適合QPS過高的工程使用
JSON序列化在xml序列化發展了多年后,也浮現了一些問題,比如開發并不簡便,解析xml復雜度較高,還有xml的標準規范比較多,自由度過高,導致很難有效的指定格式校驗等,于是一種新的輕量級的序列化交互的方案--JSON(JavaScript Object Notation)出現了,相對于xml來說,json格式語法簡單,自由度較高,有很高的可讀性,并且在JSON序列化后的字節流小于xml序列化的結果,解析起來更方便,于是基于JSON的接口成了新的標準規范之一,到現在也出現了很多JSON的序列化/反序列化的開源框架,比如開發中最常見到的Jackson、阿里巴巴開源的FastJson、谷歌的GSON等,而這三種框架各有優劣,通過測試一萬個對象的序列化和反序列化的效率,對比如下:
序列化:
反序列化:
可以看出來序列化的時候,Gson的速度明顯稍微慢了一些,Jackson反而最快,而在反序列化的時候,三個表現都很穩定,時間都差不多,但是當數據比較大的時候,測試結果又有所不同,測試結果和數據來自https://blog.csdn.net/Sword52888/article/details/81062575 提供的代碼和腳本,可以得出對應結論:
- 1、 當數據小于 100K 的時候,建議使用 Gson
- 2、 當數據100K 與 1M 的之間時候,建議使用各個JSON引擎性能差不多
- 3、 當數據大與 1M 的時候,建議使用 JackSon 與 FastJson
而在穩定性上面,默認情況下Gson在各種情況下的表現最好,Jackson配合對應的配置化也能達到很好的穩定性,而FastJson表現的不穩定,所以對于這幾種json庫的使用,建議環境較復雜場景下使用JackSon,加上自定義的配置化可以更靈活的處理更多的場景,但是在復雜度一般,僅僅在乎性能的場景下,建議使用FastJson,因為FastJson的api更易用,依賴少,簡單場景下使用簡單
Hessian序列化Hessian是一個支持跨語言傳輸的二進制文本序列化協議,對比Java默認的序列化,Hessian的使用較簡單,并且性能較高,現在的主流遠程通訊框架幾乎都支持Hessian,比如Dubbo,默認使用的就是Hessian,不過是Hessian的重構版
Avro序列化Avro序列化設計初衷是為了支持大批量數據交換的應用,支持二進制序列化方式,并且自身提供了動態語言支持,可以更加便捷、快速處理大批量的Avro數據
Kyro序列化Kyro序列化是主流的比較成熟的序列化方案之一,目前廣泛使用在大數據組件中,比如Hive、Storm等,性能比起Hessian還要優越,但是缺陷較明顯,不支持跨語言交互,在dubbo2.6.x版本開始已經加入了Kyro序列化的支持
Protobuf序列化Protobuf是谷歌提出的序列化方案,不同的是此方案獨立于語言、平臺,谷歌提供了多個語言如java、c、go、python等語言的實現,也提供了多平臺的庫文件支持,使用比較廣泛,優點在于性能開銷很小,壓縮率很高,但是缺陷也很明顯,可讀性很差,并且protobuf需要使用特定語言的庫進行翻譯轉換,使用起來較為麻煩
Protobuf序列化的使用
首先現在使用Protobuf,有手動編譯和maven依賴jar兩種方案,實際開發中我們一般使用maven坐標引入jar,坐標如下:
<dependency><groupId>com.dyuproject.protostuff</groupId><artifactId>protostuff-core</artifactId><version>1.0.8</version></dependency><dependency><groupId>com.dyuproject.protostuff</groupId><artifactId>protostuff-runtime</artifactId><version>1.0.8</version></dependency>編寫一個便捷的序列化轉換工具類:
package com.demo.utils;import com.dyuproject.protostuff.LinkedBuffer; import com.dyuproject.protostuff.ProtobufIOUtil; import com.dyuproject.protostuff.runtime.RuntimeSchema;public class SerializeUtils{/****序列化方法*/public static <T> byte[] serialize(T t,Class<T> clazz) {return ProtobufIOUtil.toByteArray(t, RuntimeSchema.createFrom(clazz),LinkedBuffer.allocate(LinkedBuffer.DEFAULT_BUFFER_SIZE));}/****反序列化方法*/public static <T> T deSerialize(byte[] data,Class<T> clazz) {RuntimeSchema<T> runtimeSchema = RuntimeSchema.createFrom(clazz);T t = runtimeSchema.newMessage();ProtobufIOUtil.mergeFrom(data, t, runtimeSchema);return t;}使用的時候直接使用工具類進行自動的轉換傳輸即可
注:使用的時候注意jdk版本和jar版本的兼容問題,并且需要序列化的實體并不需要實現Serializable 接口當然,我們接下來手動編譯protobuf使用,了解下protobuf的語法以及原理
手動編譯Protobuf
手動編譯protobuf我們需要一個Protobuf編譯器的支持,這里推薦直接點擊地址,在github上下載:
https://github.com/google/protobuf/releases
或者直接百度云:http://pan.baidu.com/s/1gefsM9X 下載,這里博主選擇直接百度云集成的環境下載
1:解壓protoc-3.0.0-beta-2-win32會得到一個protoc.exe的文件.
2:解壓protobuf-3.0.0-beta-2.(3.0.0-beta是版本號,可能會有所不同)
3.將protoc.exe文件放到2步驟解壓后文件夾java/src/這個目錄里面(src里面,不是跟src并級)
4.WINDOS+R 輸入cmd命令并切換至3步驟的src目錄的上級目錄,就是java目錄下會發現這個目錄有個POM文件,使用maven編譯命令編譯(mvn install),然后會在java目錄下生成target以及一個jar。OK到目前位置,安裝算完成了
接下來是編譯環節,將上面生成的那個jar和一開始的那個exe文件放到需要編譯文件的同一目錄下 ,使用編譯指令(cmd):
protoc --java_out=xxx/xxx.proto如果出現:Missing input file錯誤,那么就使用 以下指令:
protoc xxx/xxx.proto --java_out=./接下來,我們開始編寫一個protobuf的簡單demo,后綴為proto,代碼如下:
syntax="proto2"; package com.demo.serial; option java_package = "com.demo.serial"; option java_outer_classname="UserProtos"; message User { required string name=1; required int32 age=2; }首先我們先看看上面編寫的內容分別代表什么意思:
syntax="proto2";這里指定了protobuf編譯的版本,目前主流為proto2,當然也有不少選擇最新的proto3版本,而每個大版本之間的差異還是很大的,具體區別參見官方說明:https://developers.google.com/protocol-buffers/docs/proto3
接著是:
option java_package = "com.demo.serial"這里指定的是上一行我們設置的package對應java文件里面的package名稱
option java_outer_classname="UserProtos"這里指定了如果編譯完畢生成的java類的名稱
message User這里的message代表給User類指定對應屬性類型
required string name=1這里出現了一個特殊的修飾符類型required,在protobuf中,有如下幾種修飾符:
- required: 格式良好的 message 必須包含該字段一次。
- optional: 格式良好的 message 可以包含該字段零次或一次(不超過一次)。
- repeated: 該字段可以在格式良好的消息中重復任意多次(包括零)。其中重復值的順序會被保留。
完成這些以后,我們使用指令:
protoc --java_out=xxx/xxx.proto生成protobuf轉換后的實體類,然后我們在pom中引入:
<dependency> <groupId>com.google.protobuf</groupId><artifactId>protobuf.java</artifactId><version>3.7.0</version> </dependency>然后進行序列化:
UserProtos.User user=UserProtos.User.newBuilder().setAge(300).setName("Mic").build(); byte[] bytes=user.toByteArray(); for(byte bt:bytes){ System.out.print(bt+" "); }我們將這個結果打印出來的字節如下:
10 3 77 105 99 16 -84 2可以看出來序列化的數值看不明白,但是的確字節數很小,說明protobuf進行了算法壓縮,那么我們就要了解下protobuf壓縮算法相關的詳細操作,首先我們要知道protobuf的type對應的各個語言的類型:
.proto TypeNotesC++ TypeJava TypePython Type[2]Go Typedoubledoubledoublefloat*float64floatfloatfloatfloat*float32int32使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint32int32intint*int32int64使用可變長度編碼。編碼負數的效率低 - 如果你的字段可能有負值,請改用 sint64int64longint/long[3]*int64uint32使用可變長度編碼uint32int[1]int/long[3]*uint32uint64使用可變長度編碼uint64long[1]int/long[3]*uint64sint32使用可變長度編碼。有符號的 int 值。這些比常規 int32 對負數能更有效地編碼int32intint*int32sint64使用可變長度編碼。有符號的 int 值。這些比常規 int64 對負數能更有效地編碼int64longint/long[3]*int64fixed32總是四個字節。如果值通常大于 228,則比 uint32 更有效。uint32int[1]int/long[3]*uint32fixed64總是八個字節。如果值通常大于 256,則比 uint64 更有效。uint64long[1]int/long[3]*uint64sfixed32總是四個字節int32intint*int32sfixed64總是八個字節int64longint/long[3]*int64boolboolbooleanbool*boolstring字符串必須始終包含 UTF-8 編碼或 7 位 ASCII 文本stringStringstr/unicode[4]*stringbytes可以包含任意字節序列stringByteStringstr[]byte
Protobuf序列化的原理分析
了解了Protobuf的type轉換的格式以后,我們再來看,Protobuf的存儲格式,Protobuf采用了T-L-V的存儲格式存儲數據,其中的T代表tag,即key,L則是length,代表當前存儲的類型的數據長度,當是數值類型的時候L被忽略,V代表value,即存入的值,protobuf會將每一個key根據不同的類型對應的序列化算法進行序列化,然后按照keyvaluekeyvalue的格式存儲,其中key的type類型與對應的壓縮算法關系如下:
write_type編碼方式type存儲方式0Varint(負數使用Zigzag輔助)int32、int64、uint32、uint64、sint32、sint64、bool、enumT-V164-bitfixed、sfixed64、doubleT-V2Length-delimistring、bytes、embedded、messages、packed repeated fieldsT-L-V3(棄用)Start groupGroups(deprecated)棄用4(棄用)End groupGroups(deprecated)棄用532-bitfixed32、sfixed32、floatT-V
需要注意的是protobuf的key計算按照(field_number << 3) | wire_type 方式計算,而這里的field_number是指定義的時候該字段的域號,如:required string name=1;這里的name字段的域號為1,在protobuf中規定:
- 如果域號在[1,15]范圍內,會使用一個字節表示Key;
- 如果域號大于等于16,會使用兩個字節表示Key;
key編碼完成后,該字節的第一個比特位表示后一個字節是否與當前字節有關系,即:
- 如果第一個比特位為1,表示有關,即連續兩個字節都是Key的編碼;
- 如果第一個比特位為0,表示Key的編碼只有當前一個字節,后面的字節是Length或者Value;
了解了以上的那些,我們看看,上述我們編寫的案例,算法是如何實現的呢?
varint編碼
上述我們的案例中,出現了int32類型,對應的壓縮算法為varint,我們看下age=300,這個值是如何序列化的
可以看出來,我們首先將300轉為二進制,結果為100101100,由于當前是int32,所以不足32位,高位全部補0,即為00000000000000000000000100101100,接著第二步,從低位到高位取7位,8位是一個字節,當前的最高位為標志位,如果下一個字節內還有非0得數值(即有意義存在),則最高位補1,如果沒有最高位補0,當最高位為0后,壓縮存儲結束,從age=300,我們可以看出來,取7位則是0101100,由于后一個字節中還存在值,所以最高位補1,則為10101100,而下一個字節則從第8位(低位到高位)開始,繼續獲取7個字節,則為0000010,由于后續的一個字節中,不存在有意義的值,則最高位補0,代表后續不存在有意義的值了,不需要繼續壓縮,則為00000010,也就是說原本32個比特位的數值,現在只有16個比特位,4個字節壓縮到了2個字節,而我們都知道計算機中,高位為1代表負數,計算機中對負數的計算為先將結果取反后,再去補碼操作,而負數的補碼則是在反碼的基礎上+1,那么我們現在將結果反過來,先去-1,得到反碼,則為10101011,再去取反,得到原碼,則為01010100,現在我們將這個值轉換為十進制,則可以知道結果為84,由于高位為1,則代表是負數,最終結果為-84,而00000010由于高位是0,代表本身為正數,正數的原碼反碼補碼都是自身,所以直接轉換為十進制結果為2,現在我們把這兩個結果和上述打印的結果比較一下,是不是發現是一樣的?當然,我們也從這個過程中發現了一些問題,比如小于128的值,我們甚至只需要1個字節就能存儲完畢,但是如果我們需要存儲的值很大,超過了268435455以后的數值,甚至需要五個字節來存儲(超過28個有效比特位),但是絕大多數情況下,我們都不會使用這么大的數值,所以一般情況下,我們都能比之前使用更小的字節存儲,達到壓縮的目的
字符串壓縮
在Protobuf中存儲字符串格式,使用的T-L-V存儲方式,標識符Tag采用Varint編碼,字節長度Length采用Varint編碼,string類型字段值采用UTF-8編碼方式存儲,所以tag得值為1 <<3 | 2 =10,L的值存儲為00000011,即為3,而V的存儲,把每一個字符按照UTF-8的編碼后的字節流數組,分別為77 105 99,而在Protobuf編碼后的字節流則是按照如圖的順序,所以打印出來的結果如上的10 3 77 105 99 16 -84 2
負數存儲-write_type為0
在計算機中,負數會被表示為很大的整數,因為計算機定義負數符號位為數字的最高位,所
以如果采用 varint 編碼表示一個負數,那么一定需要 5 個比特位。所以在 protobuf 中通過
sint32/sint64 類型來表示負數,負數的處理形式是先采用 zigzag 編碼(把符號數轉化為無符
號數),在采用 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: 整體左移一位,右邊補 0 -> 1101 1010 1000
n>>31: 整體右移 31 位,左邊補 1 -> 1111 1111 1111
n<<1 ^ n >>31
1101 1010 1000 ^ 1111 1111 1111 = 0010 0101 0111
十進制: 0010 0101 0111 = 599
然后再使用varint 算法得到兩個字節
1101 0111(-41),0000 0100(4)
總結
基于Protobuf序列化原理分析,為了有效降低序列化后數據量的大小,可以采用以下措施:
2.將結果賦值給a,則a = 3
public static void main(String[] args){int a = 8;a ^= 11;//a = 8^11 = 3System.out.println(a);//3}既然來了,點個關注再走唄~總結
以上是生活随笔為你收集整理的hive序列生成_常见的序列化框架及Protobuf原理的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: CentOS7 源码编译安装NodeJS
- 下一篇: CentOS7 shell脚本安装jdk