Redis6通信協議升級至RESP3,一口氣看完13種新數據類型

原創:微信公眾號 碼農參上,歡迎分享,轉載請保留出處。

在前面的文章 Redis:我是如何與客戶端進行通信的 中,我們介紹過RESP V2版本協議的規範,RESP的全程是Redis Serialization Protocol,基於這個實現簡單且解析性能優秀的通信協議,Redis的服務端與客戶端可以通過底層命令的方式進行數據的通信。

隨着Redis版本的不斷更新以及功能迭代,RESP V2協議開始漸漸無法滿足新的需求,為了適配在Redis6.0中出現的一些新功能,在它的基礎上發展出了全新的下一代RESP3協議。

下面我們先來回顧一下繼承自RESP V2的5種數據返回類型,在了解這些類型的局限性後,再來看看RESP3中新的數據返回類型都在什麼地方做出了改進。

繼承RESP v2的類型

首先,協議中數據的請求格式與RESP V2完全相同,請求的格式如下:

*<參數數量> CRLF
$<參數1的位元組長度> CRLF
<參數1的數據> CRLF
$<參數2的位元組長度> CRLF
<參數2的數據> CRLF
...
$<參數N的位元組長度> CRLF
<參數N的數據> CRLF

每行末尾的CRLF轉換成程序語言是\r\n,也就是回車加換行。以set name hydra這條命令為例,轉換過程及轉換後的結果如下:

在了解了發送的協議後,下面對不同類型的回復進行測試。這一過程如何進行模擬呢?在前面的文章中,我們是在java代碼中通過Socket連接redis服務,發送數據並收到返回結果來模擬這一協議。

不過我們今天採用一種更為簡單的方式,直接在命令行下使用telnet進行連接就可以了,以我本機啟動的redis為例,直接輸入telnet 127.0.0.1 6379就可以連接到redis服務了。之後再將包含換行的指令一次性拷貝到命令行,然後回車,就能夠收到來自Redis服務的回復了:

下面先來看看繼承自RESP V2的5種返回格式,為了統一命名規範,介紹中均採用RESP3官方文檔中的新的名稱來替代RESP V2中的舊命名,例如不再使用舊的批量回復多條批量回復等類型名稱。

Simple string

表示簡單字符串回復,它只有一行回復,回復的內容以+作為開頭,不允許換行,並以\r\n結束。有很多指令在執行成功後只會回復一個OK,使用的就是這種格式,能夠有效地將傳輸、解析的開銷降到最低。

還是以上面的set指令為例,發送請求:

*3
$3
set
$4
name
$5
hydra

收到回復:

+OK\r\n

Simple error

錯誤回復,它可以看做簡單字符串回復的變種形式,它們之間的格式也非常類似,區別只有第一個字符是以-作為開頭,錯誤回復的內容通常是錯誤類型及對錯誤描述的字符串。錯誤回復出現在一些異常的場景,例如當發送了錯誤的指令、操作數的數量不對時,都會進行錯誤回復。

發送一條錯誤的指令:

*1
$8
Dr.Hydra

收到回復,提示錯誤信息:

-ERR unknown command `Dr.Hydra`, with args beginning with:\r\n

Number

整數回復,它的應用也非常廣泛,它以:作為開頭,以\r\n結束,用於返回一個整數。例如當執行incr後返回自增後的值,執行llen返回數組的長度,或者使用exists命令返回的0或1作為判斷一個key是否存在的依據,這些都使用了整數回復。

發送一條查看數組長度的指令:

*2
$4
llen
$7
myarray

收到回復:

:4\r\n

Blob string

多行字符串的回復,也被叫做批量回復,在RESP V2中將它稱為Bulk String。以$作為開頭,後面是發送的位元組長度,然後是\r\n,然後發送實際的數據,最終以\r\n結束。如果要回復的數據不存在,那麼回復長度為-1。

發送一條get命令請求:

*2
$3
get
$4
name

收到回復:

$5\r\n
hydra\r\n

Array

可以理解為RESP V2中的多條批量回復,當服務端要返回多個值時,例如返回一些元素的集合時,就會使用Array。它以*作為開頭,後面是返回元素的個數,之後再跟隨多個上面的Blob String

*4
$6
lrange
$7
myarray
$1
0
$2
-1

收到回復,包含了集合中的4個元素:

*4
$1
1
$1
2
$1
2
$2
32

這5種繼承自RESP V2協議的返回數據類型的簡單回顧到此結束,下面我們來開啟RESP3協議新特性的探索之旅。

RESP3中新的類型

目前在Redis6.0.X版本中,仍然是默認使用的RESP V2協議,並且在兼容RESP V2的基礎上,也同時也支持開啟RESP3。估計在未來的某個版本,Redis可能會全面切換到RESP3,不過這麼做的話對目前的Redis客戶端連接工具會有不小的衝擊,都需要根據協議內容進行底層通信的改造。

在使用telnet連接到redis服務後,先輸入下面的命令來切換到RESP3版本的協議,至於hello命令的具體返回數據以及數據表示的意義,這裡暫且略過,後面會具體來看。

hello 3

下面我們就來詳細看看在RESP3中,除了保留上面的5種舊回復類型外,新增的13種通信返回數據類型,部分數據類型會配合示例進行演示。為了看起來更加簡潔,下面的演示例子發送命令均使用原始命令,不再轉化為協議格式,並且省略數據返回結果中每行結束的\r\n

1、Null

新協議中使用下劃線字符後跟CRLF字符來表示空值,也就是用_\r\n來替代原先的單個空值的返回$-1。例如在使用get命令查找一個不存在的key時:

get hydra

RESP V2返回:

$-1

RESP3返回:

_

2、Double

浮點數返回時以逗號開頭,格式為 ,<floating-point-number>\r\n,使用zset score key member獲取分數的命令來進行測試:

zscore fruit apple

RESP V2返回時使用的是Bulk String的格式:

$18
5.6600000000000001

RESP3返回格式:

,5.6600000000000001

3、Boolean

布爾類型的數據返回值,其中true被表示為#t\r\n,而false被表示為#f\r\n。不過Hydra暫時沒有找到返回布爾類型結果的例子,即使是用lua腳本直接返回布爾類型也無法實現。

eval "return true" 0 
eval "return false" 0

上面的lua腳本在返回true時結果為:1\r\n,返回false時結果為_\r\n,這是因為lua中布爾類型的true會轉換為redis中的整數回復1,而false類型會轉換成Nil Bulk。至於有哪些指令能夠返回布爾類型的數據,有了解的小夥伴可以給我留言補充。

4、Blob error

與字符串類型比較相似,它的格式為!<length>\r\n<bytes>\r\n,但是與簡單錯誤類型一樣,開頭使用!表示返回的是一段錯誤信息描述。例如錯誤SYNTAX invalid syntax會按照下面的格式返回:

!21
SYNTAX invalid syntax

5、Verbatim string

Verbatim string也表示一個字符串格式,與Blob String非常相似,但是使用=開頭替換了$,另外之後的三個位元組提供了有關字符串格式的信息,例如txt表示純文本,mkd表示markdown格式,第四個位元組則固定為 :。這種格式適用於在沒有任何轉義或過濾的情況下顯示給用戶。

使用延時事件統計與分析指令進行測試,發送:

latency doctor

RESP2返回的數據還是Blob String格式:

$196
Dave, no latency spike was observed during the lifetime of this Redis instance, not in the slightest bit. I honestly think you ought to sit down calmly, take a stress pill, and think things over.

RESP V3返回的數據採用了新的格式:

=200
txt:Dave, no latency spike was observed during the lifetime of this Redis instance, not in the slightest bit. I honestly think you ought to sit down calmly, take a stress pill, and think things over.

6、Big number

Big number類型用於返回非常大的整數數字,可以表示在有符號64位數字範圍內的整數,包括正數或負數,但是需要注意不能含有小數部分。數據格式為(<big number>\r\n,以左括號開頭,示例如下:

(3492890328409238509324850943850943825024385

注意,當Big number不可用時,客戶端會返回一個字符串格式的數據。

7、Aggregate data types

與前面我們介紹的給定數據類型的單個值不同,Aggregate data types可以理解為聚合數據類型。這也是RESP3的一個核心思想,要能夠從協議和類型的角度,來描述不同語義的聚合數據類型。

聚合數據類型的格式如下,通常由聚合類型、元素個數以及具體的單一元素構成:

<aggregate-type-char><numelements><CR><LF>
... numelements other types ...

例如一個包含三個數字的數組[1,2,3]可以表示為:

*3
:1
:2
:3

當然聚合數據類型中的元素可以是其他聚合數據類型,例如在數組中也可以嵌套包含其他數組(下面的內容包含了縮進方便理解):

*2
    *3
        :1
        $5
        hello
        :2
    #f

上面的聚合數據類型所表示的數據為[[1,"hello",2],false]

8、Map

Map數據類型與數組比較類似,但是以%作為起始,後面是Map中鍵值對的數量,而不再是單個數據項的數量。它的數據內容是一個有序的鍵值對的數組,之後分行顯示鍵值對的keyvalue,因此後面的數據行一定是偶數行。先看一下官方文檔給出的例子,以下面的Json字符串為例:

{
    "first":1,
    "second":2
}

轉換為Map類型後格式為下面的形式:

%2
+first
:1
+second
:2

但是通過實驗,Hydra發現了點有意思的東西,當我們發送一條hgetall的命令來請求哈希類型的數據時:

hgetall user

RESP V2返回的數據仍然使用老的Array格式,符合我們的預期:

*4
$4
name
$5
Hydra
$3
age
$2
18

但是下面RESP3的數據返回卻出乎我們的意料,可以看到雖然前面的%2表示使用了Map格式,但是後面並沒有遵循官方文檔給出的規範,除了開頭的%2以外,其餘部分與Array完全相同()。

%2
$4
name
$5
Hydra
$3
age
$2
18

關於實際傳輸數據與文檔中給出示例的出入,Hydra有一點自己的猜測,放在最後總結部分。

9、Set

SetArray類型非常相似,但是它的第一個位元組使用~替代了*,它是一個無序的數據集合。還是先看一下官方文檔中給出的示例,下面是一個包含了5個元素的集合類型數據,並且其中具體的數據類型可以不同:

~5<CR><LF>
+orange<CR><LF>
+apple<CR><LF>
#t<CR><LF>
:100<CR><LF>
:999<CR><LF>

下面使用SMEMBERS命令獲取集合中的所有元素進行測試:

SMEMBERS  myset

RESP V2返回時仍然使用Array格式:

*3
$1
a
$1
c
$1
b

RESP3的數據返回情況和Map比較類似,使用~開頭,但是沒有完全遵從協議中的格式:

~3
$1
a
$1
c
$1
b

10、Attribute

Attribute類型與Map類型非常相似,但是頭一個位元組使用|來代替了%Attribute描述的數據內容比較像Map中的字典映射。客戶端不應該將這個字典內容看做數據回復的一部分,而是當做增強回復內容的輔助數據。

在文檔中提到,在未來某個版本的Redis中可能會出現這樣一個功能,每次執行指令時都會打印訪問的key的請求頻率,這個值可能使用一個浮點數表示,那麼在執行MGET a b時就可能會收到回復:

|1
    +key-popularity
    %2
        $1
        a
        ,0.1923
        $1
        b
        ,0.0012
*2
    :2039123
    :9543892

在上面的數據回復中,實際中回復的數據應該是[2039123,9543892],但是在前面附加了它們請求的屬性,當讀到這個Attribute類型數據後,應當繼續讀取後面的實際數據。

11、Push

Push數據類型是一種服務器向客戶端發送的異步數據,它的格式與Array類型比較類似,但是以>開頭,接下來的數組中的第一個數據為字符串類型,表示服務器發送給客戶端的推送數據是何種類型。數組中其他的數據也都包含自己的類型,需要按照協議中類型規範進行解析。

簡單看一下文檔中給出的示例,在執行get key命令後,可能會得到兩個有效回復:

>4
+pubsub
+message
+somechannel
+this is the message
$9
Get-Reply

在上面的這段回復中需要注意,收到的兩個回復中第一個是推送數據的類型,第二個才是真正回復的數據內容。

注意!這裡在文檔中有一句提示:雖然下面的演示使用的是Simple string格式,但是在實際數據傳輸中使用的是Blob string格式。所以盲猜一波,上面的MapSet也是同樣的情況?

這裡先簡單鋪墊一下Push回復類型在redis6中非常重要的一個使用場景客戶端緩存client-side caching,它允許將數據存儲在本地應用中,當訪問時不再需要訪問redis服務端,但是其他客戶端修改數據時需要通知當前客戶端作廢掉本地應用的客戶端緩存,這時候就會用到Push類型的消息。

我們先在客戶端A中執行下面的命令:

client tracking on
get key1

在客戶端B中執行:

set key1 newValue

這時就會在客戶端A中收到Push類型的消息,通知客戶端緩存失效。在下面收到的消息中就包含了兩部分,第一部分表示收到的消息類型為invalidate,第二部分則是需要作廢的緩存key1

>2
$10
invalidate
*1
$4
key1

12、Stream

在前面介紹的類型中,返回的數據字符串一般都具有指定的長度,例如下面這樣:

$1234<CR><LF>
.... 1234 bytes of data here ...<CR><LF>

但是有時候需要將一段不知道長度的字符串數據從客戶端傳給服務器(或者反向傳輸)時,很明顯這種格式無法使用,因此需要一種新的格式用來傳送不確定長度的數據。

文檔中提到,過去在服務端有一個私有擴展的數據格式,規範如下:

$EOF:<40 bytes marker><CR><LF>
... any number of bytes of data here not containing the marker ...
<40 bytes marker>

它以$EOF:作為起始位元組,然後是40位元組的marker標識符,在\r\n後跟隨的是真正的數據,結束後也是40位元組的標識符。標識符以偽隨機的方式生成,基本上不會與正常的數據發生衝突。

但是這種格式存在一定的局限性,主要問題就在於生成標識符以及解析標識符上,由於一些原因使得上面這種格式在實際使用中非常脆弱。因此最終在規範中提出了一種分塊編碼格式,舉一個簡單的例子,當需要發送事先不知道長度的字符串Hello world時:

$?
;4
Hell
;5
o wor
;2
ld
;0

這種格式以$?開頭,表示是一個不知道長度的分塊編碼格式,後面傳輸的數據數量沒有限制,在最後以零長度的;0作為結束傳輸的標識。文檔中提到,目前還沒有命令會以這個格式來進行數據回復,但是會在後面的功能模塊中實裝這個協議。

13、HELLO

在介紹RESP3的最開始,我們就在telnet中通過hello 3的命令來切換協議到V3版本。這個特殊的命令完成了兩件事:

  • 它允許服務器與RESP V2版本向後兼容,也方便以後更加輕鬆的切換到RESP3
  • hello命令可以返回有關服務器和協議的信息,以供客戶端使用

hello命令的格式如下,可以看到除了協議版本號外,還可以指定用戶名和密碼:

HELLO <protocol-version> [AUTH <username> <password>]

hello命令的返回結果是前面介紹過的Map類型,僅僅在客戶端和服務器建立連接的時候發送。

%7
$6
server
$5
redis
$7
version
$6
6.0.16
$5
proto
:3
$2
id
:18
$4
mode
$10
standalone
$4
role
$6
master
$7
modules
*0

轉換為我們可讀的Map格式後,可以看到它返回的Redis服務端的一些信息:

{
    "server":"redis",
    "version":"6.0.16",
    "proto":3,
    "id":18,
    "mode":"standalone",
    "role":"master",
    "modules":[]
}

總結

在RESP V2中,通信協議還是比較簡單,通信內容大多也都還是通過數組形式進行編碼和發送,這種情況帶來了很多不便,有很多情況需要根據操作命令的類型來判斷返回的數據具體是什麼類型,這無疑增加了客戶端解析數據的難度與複雜度。

而在RESP3中,通過引入新的多種數據類型,通過起始位元組的字符進行類型的區分編碼,使客戶端可以直接判斷返回數據的類型,在相當大的程度上,減輕了解析的複雜度,提升了效率。

本文中對於新的返回數據類型,一部分給出了通信數據的示例,但還是有一些類型暫時沒有找到合適的命令進行測試,有了解的小夥伴們可以給我補充。

另外對於MapSet,實際傳輸的數據與官方文檔給出的仍有一定出入,個人認為情況和Push相同,可能是官方文檔中更多隻偏向於演示,使用Simple string來代替了Blob string

最後再啰嗦一句,說說協議的命名,新一代的協議名稱就叫RESP3,而沒有繼承第二代的命名規範叫RESP V3,也不是RESP version3什麼亂七八糟的,所以就不要糾結文中為啥一會是RESP V2,一會是RESP3這種不對稱的命名了。

那麼,這次的分享就到這裡,我是Hydra,下篇文章再見。

參考文檔:

//github.com/redis/redis-doc/blob/master/docs/reference/protocol-spec.md

//github.com/antirez/RESP3/blob/master/spec.md

//redis.io/docs/reference/protocol-spec/#high-performance-parser-for-the-redis-protocol

作者簡介,碼農參上,一個熱愛分享的公眾號,有趣、深入、直接,與你聊聊技術。個人微信DrHydra9,歡迎添加好友,進一步交流。