在分布式應用或者微服務架構中,各個服務之間通常使用json
或者xml
結構數據進行通信,通常情況下,是沒什么問題的,但是在高性能和大數據通信的系統當中,如果有辦法可以壓縮數據量,提高傳輸效率,顯然會給用戶帶來更快更流暢的體驗。
google公司就通過使用一種新的數據交換格式辦到了這點,新的數據交換的格式叫做protobuf
。
protobuf
有多屌呢,可以看一下下面的官方測試報告:
可以看到,一條消息數據,用protobuf
序列化后的大小是json
的10分之一,是xml
格式的20分之一,但是性能卻是它們的5~100倍,我覺得用戶一定會尖叫的:oh my god!
。
把數據變小一點
下面我們以json
數據為基礎出發,通過一步一步的對它進行優化,來理解protobuf
的實現原理。
對于一條信息,json
的表示方式為:
{ "age": 30, "name": "zhangsan", "height": 175.33, "weight": 140 }
顯然,中間有很多冗余的字符,比如{
,"
等,為了把數據變小一點,我們可以暴力一點,直接表示為:
30 | zhangsan | 175.33 | 140 |
---|
通過直接將value
拼在了一起,舍去了不必要的冗余字符,我們大幅度的壓縮了空間,但是會有一些問題,就是當我們將這段數據發送給接收端,接收端怎么知道每個value
對應哪個key
呢?比如zhangsan
這個值,對應的是age
還是name
呢?
比較好的方式是事先跟接收端約定好有哪些字段,順序是啥樣子的,然后接收端按照順序對應起來:
字段1:age | 字段2:name | 字段3: height | 字段4:weight |
---|---|---|---|
↓ | ↓ | ↓ | ↓ |
30 | zhangsan | 175.33 | 140 |
很完美,這樣我們確實壓縮了不少數據,棒棒的。
能不能更小一點
假設height
這個字段為null
,我們其實是不必要傳遞這個字段的,這個時候我們需要傳遞的數據就為:
30 | zhangsan | 140 |
---|
但是在接收端,解析數據并按照順序進行字段匹配的時候就會出問題:
字段1:age | 字段2:name | 字段3: height | 字段4:weight |
---|---|---|---|
↓ | ↓ | ↓ | ↓ |
30 | zhangsan | 140 |
顯然已經亂套了,為了保證能夠正確的配對,我們可以使用tag
技術:
tag|30 | tag|zhangsan | tag|175.33 | tag|140 |
---|
也就是說,每個字段我們都用tag|value
的方式來存儲的,在tag
當中記錄兩種信息,一個是value
對應的字段的編號,另一個是value
的數據類型(比如是整形還是字符串等),因為tag
中有字段編號信息,所以即使沒有傳遞height
字段的value
值,根據編號也能正確的配對。
Tag
的開銷
有的同學會問,使用tag
的話,會增加額外的空間,這跟json
的key/value
有什么區別嗎?
這個問題問的好,json
中的key
是字符串,每個字符就會占據一個字節,所以像name
這個key
就會占據4個字節,但在protobuf
中,tag
使用二進制進行存儲,一般只會占據一個字節,它的代碼為:
static int makeTag(final int fieldNumber, final int wireType) {
return (fieldNumber << 3) | wireType;
}
fieldNumber
表示后面的value
所對應的字段的編號是多少,比如fieldNumber
為1,就表示age
,如果為2,就表示name
等;wireType
表示value
的數據類型,以此來計算value
占用字節的大小。
在protobuf
當中,wireType
可以支持的字段類型如下:
因為tag
一般占用一個字節,開銷還算是比較小的,所以protobuf
整體的存儲空間占用還是相對小了很多的。
能不能更小點
在實際的傳輸過程中,會傳遞整數,我們知道整數在計算機當中占據4個字節,但是絕大部分的整數,比如價格,庫存等,都是比較小的整數,實際用不了4個字節,像127這種數,在計算機中的二進制是:
00000000 00000000 00000000 01111111
(4字節32位)
完全可以用最后1個字節來進行存儲,protobuf
當中定義了Varint
這種數據類型,可以以不同的長度來存儲整數,將數據進一步的進行了壓縮。
但是這里面也有一個問題,在計算機當中的負數是用補碼表示的,對于-1,它的二進制表示方式為:
11111111 11111111 11111111 11111111
(4字節32位)
顯然無法用1個字節來表示了,但-1確實是一個比較簡單的數,這個時候就可以使用zigzag算法來對負數進行進一步的壓縮,最終我們可以使用2個字節來表示-1。
要快
雖然數據現在很小了,但是解析速度還是有很大的提升空間的,因為每個字段都是用tag|value
來表示的,在tag
中含有value
的數據類型的信息,而不同的數據類型有不同的大小,比如如果value
是bool
型,我們就知道肯定占了一個字節,程序從tag
后面直接讀一個字節就可以解析出value
,非常快,而json
則需要進行字符串解析才可以辦到。
能不能更快一點
如果value
是字符串類型的,具體value
有多長,我們無法從tag
當中了解到,但是如果不知道value
的長度,我們就不得不做字符串匹配操作,要知道字符串匹配是非常耗時的。
為了能夠快速解析字符串類型的數據,protobuf在存儲的時候,做了特殊的處理,分成了三部分:tag|leg|value
,其中的leg記錄了字符串的長度,同樣使用了varint
來存儲,一般一個字節就能搞定,然后程序從leg
后截取leg
個字節的數據作為value
,解析速度非常快。
protobuf
能幫助我們干什么?
了解額protobuf
的牛逼之處,對我們來說有什么好處呢?
首先,對于我們系統當中的一些大數據傳輸,顯然用protobuf
是可以獲得很大的改善的,如果你這么干了,領導一定會想給你漲工資的。
第二,給我們優化數據傳輸提供了一種思路,通過提供更多的數據元數據(數據類型,長度等),我們可以大幅度提高解析數據,比如在nodejs
當中就有一個框架叫fastify
,通過給json
設計了schema
來提供更快的解析速度,具體的實現原理大家可以點擊 這里 看。
第三:未知中...