Redis協議規範
redis的客戶端和redis服務器端的通信協議使用RESP(Redis Serialization Protocol,即Redis序列化協議)
RESP是以下幾點的一個折衷方案:
- 實現簡單
- 解析快速
- 易於理解
RESP不僅可以序列化像整數、字符串、數組這樣的類型,同時也可以針對錯誤類型。Redis客戶端發送一個字符串數組格式的命令,Redis服務器迴應命令相對應的數據類型
RESP是二進制安全的,同時不需要從一個進程到另一個進程的去處理數據塊,因爲傳輸的數據塊是使用預定義長度
注意:這裏所說的協議僅僅適用於客戶端和服務器端的通信。Redis集羣在兩個節點之間交換數據使用不同的二進制協議
網絡層
客戶端通過TCP連接服務器端
雖然RESP在技術上並不是只針對TCP,但是通過這個協議的上下文來講,這個協議只適用於TCP連接或者類似的面向流的連接(比如Unix sockets)
請求—響應模型
Redis接受由不同參數組成的命令,一旦一個命令被接受,這個命令就會被處理,並將處理結果返回給客戶端
這可能是一個非常簡單的模型,然而有兩種意外:
- Redis支持流水線技術,因此客戶端可能一次發送多個命令,然後等待回覆
- 當一個Redis客戶端訂閱了一個Pub/Sub頻道,這個協議改變了語義,成爲推送協議。此時客戶端不再需要發送指令,因爲當服務器端接收到客戶端所訂閱的那個頻道的新信息,服務器端將會自動的向客戶端發送消息
RESP協議描述
RESP協議在Redis1.2中被引入,在Redis2.0中成爲Redis服務器通訊的標準方式,因此你應該在你的Redis客戶端中實現該協議
RESP實際上是一個序列化協議,支持簡單字符串,大塊字符串,錯誤,整數和數組類型
RESP在redis中作爲請求響應協議使用:
- 客戶端發送給服務器端的命令是大塊字符串類型的數組
- 服務器端根據具體的命令回覆RESP類型中的一種
在RESP中,數據的類型取決於第一個字符:
- 簡單字符串的第一個字符是
+
- 錯誤的第一個字符是
-
- 整數的第一個字符是
:
- 大塊字符串的第一個字符是
$
- 數組的第一個字符是
*
在RESP中,協議的不同部分總是以\r\n
分割
RESP簡單字符串
簡單字符串是由一個+
和不包含\r
、\n
的字符串組成,以CRLF(\r\n
)結尾
簡單字符串是非二進制安全,在傳輸過程中開銷較小。比方說許多Redis命令僅僅需要回復“OK”表示命令執行成功,使用RESP簡單字符串編碼只需要5比特:
+OK\r\n
於此相反,RESP大塊字符串類型是二進制安全的
RESP錯誤類型
錯誤類型與簡單字符串類型類似,但是第一個字符是-
。錯誤類型是針對客戶端的異常操作,其中包含錯誤提示信息。
基本格式爲:
-Error message\r\n
只有在發送錯誤信息時纔會發送錯誤回覆,例如嘗試對錯誤的數據類型進行操作,命令不存在等等
-WRONGTYPE Operation against a key holding the wrong kind of value
-ERR unknown command 'foobar'
在-
後的第一個單詞一般表示爲錯誤類型,但這僅僅是Redis使用時的約定,並不是RESP錯誤格式的一部分
比如說,ERR
是一個一般性的錯誤,然而WRONGTYPE
是一個更加具體的錯誤,指明瞭客戶端嘗試對錯誤的類型進行操作。這個被叫做錯誤前綴,用於在不依賴於可能會隨着時間改變的確切錯誤消息的情況下,便於客戶端理解服務器端返回的錯誤。這樣的特性不應該認爲它是至關重要的,因爲它很少用的上
RESP整數
只是用CRLF結尾字符串來表示整型,用一個字節的:
作爲前綴。例如::0\r\n
,或者:1000\r\n
整數也可以用於返回真和假
RESP大塊字符串
大塊字符串用於表示單個二進制安全的字符串,長度最大爲512MB
大塊字符串通過以下方式編碼:
- 以
$
開頭,緊接着是字符串長度,以CRLF
結尾 - 實際的字符串
CRLF
終止
字符串“foobar”編碼如下
$6\r\nfoobar\r\n
空串表示爲
$0\r\n\r\n
RESP大塊字符串同樣也可以用來表示單個不存在的值,表現爲NULL
$-1\r\n
RESP數組
客戶端使用RESP數組發送命令給服務器端,同樣服務器端也有可能返回RESP數組格式的內容
RESP數組是由以下方式組成
- 以
*
開頭,緊接着是用十進制數表示的數組元素個數,以CRLF
結尾 - RESP類型的元素
空數組表示爲
*0\r\n
帶有兩個RESP大塊字符串"foo"和"bar"元素的數組編碼爲
*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n
在數組*<count>CRLF
前綴後,組成數組的其他數據類型只是一個接一個地連接起來
*3\r\n:1\r\n:2\r\n:3\r\n
數組並不要求元素都是相同的類型,比如說整數和大塊字符串組成的列表可以通過以下方式編碼
*5\r\n:1\r\n:2\r\n:3\r\n:4\r\n$6\r\nfoobar\r\n
對於NULL
值的數組(並非空數組),表示方法爲
*-1\r\n
RESP還有可能出現數組的數組,比如說兩個數組組成的數組
*2\r\n*3\r\n:1\r\n:2\r\n:3\r\n*2\r\n+Foo\r\n-Bar\r\n
在數組中的空元素
在數組中可能存在某一個元素爲NULL
*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n
第二個元素是一個NULL
值,客戶端可以理解爲
["foo",nil,"bar"]
這並不是一個異常,這只是協議中的一種特殊情況
發送命令給服務器端
相信大家已經熟悉了RESP序列化格式,寫一個Redis客戶端也變得簡單起來。
此時我使用linux中nc工具來進行實際的演示
客戶端發送set mykey myvalue
命令,服務器端返回簡單字符串+OK
客戶端發送get mykey
命令,服務器端返回大塊字符串$7\r\nmyvalue
批量命令以及流水線技術
一個客戶端可以使用相同的連接來處理多條命令。支持流水線技術,客戶端可以在一次寫入操作中發送多條命令,不需要在發送下一個命令之前等待服務器對上一個命令的回覆。所有的命令能夠在最後被讀到
內置命令
有些時候,你需要發送命令給Redis服務器,但手上只有telnet工具。雖然Redis協議易於實現,但在交互式會話中的使用並不理想,同時redis-cli
並不總是可用的。基於這些原因,Redis接受另一種人們可讀的命令,這種方式稱爲內嵌命令格式
基本上,在一個telnet會話裏,你可以簡單的書寫空格分割的命令,由於命令沒有像在統一的請求協議裏的那樣以*
開頭,所以Redis可以區分出這種情況,然後解析你的命令
Redis協議的高性能解析器
大塊字符串以及批量的大塊字符的處理可以使用對每一個字符進行單個操作,同時掃描CR
字符的程序
#include <stdio.h>
int main(void) {
unsigned char *p = "$123\r\n";
int len = 0;
p++;
while(*p != '\r') {
len = (len*10)+(*p - '0');
p++;
}
/* 現在p指針指向了 '\r', 同時len是大塊字符串的長度 */
printf("%d\n", len);
return 0;
}
在第一個CR
字符確定後,可以在不做任何處理的情況下跳過LF
字符,然後可以在不檢查有效負載的情況下,單個讀取操作就可以讀取大塊字符串,最終CRLF
結尾符也可以在不做處理的情況下丟棄
與二進制協議相比,Redis協議更加易於在大部分的高級語言中實現,減少了客戶端軟件的bug數量