Redis協議spec(翻譯)

Redis客戶端和Redis服務器通過一個叫做RESP(REdis Serialization Protocol,Redis序列化協議)的協議進行通訊。雖然這個協議是爲Redis設計的,但是它也能被用在其它的客戶端-服務器軟件項目。

RESP是以下幾個方面妥協的結果:

  • 易於實現
  • 快速解析
  • 可讀性好

RESP可以序列化不同的數據類型,比如整型,字符串,數組。另外還有特定的類型表示錯誤。請求由客戶端以字符串數組(要執行的命令的參數)的形式發往Redis服務器。Redis回覆一個和命令對應的數據類型。

RESP二進制安全的,並且不需要處理進程間傳輸的批量數據,因爲它使用prefixed-length來傳輸批量數據。

Note:這裏討論的協議僅僅用於客戶端-服務器通信。Redis集羣使用一個不同的二進制協議來交換不同節點之間的消息。

網絡層

一個客戶端連接到一個Redis服務器,就是創建一個TCP連接。儘管RESP是和TCP獨立的,到那時在Redis的上下文下面,我們僅僅和TCP(或者等價的像Unix套接字這樣的面向流的連接)一起使用。

請求相應模型

Redis接收由不同參數組成的命令。一旦一個命令接收到,它被處理並且一個reply會被髮送回客戶端。這是可能的最簡單的模型,然而有兩個例外:

  • Redis支持管道(會在這個本章的後面提到)。也就是說,一個客戶端可以一次性發送多個命令,然後等待replies。
  • 當一個Redis客戶端訂閱一個Pub/Sub channel的時候,這個協議改變語義,變成push協議。也就是說,客戶端不再需要發送命令,因爲服務器只要接受到信息就會自動給客戶端發送消息(對於客戶端訂閱的channels)。

除了上述兩個例外,Redis協議就是一個請求相應協議。

RESP協議描述

RESP協議在Redis1.2的時候開始引入,在Redis2.0的時候成爲和Redis服務器通信的標準。這是一個你需要在你的Redis客戶端中實現的協議。

RESP實際上是一個支持一下數據類型的序列化協議:Simple Strings,Errors,Integers,Bulk strings 和Arrays。

RESP在Redis下面被應用爲請求相應協議的方式如下:

  • 客戶端給Redis服務器發送的commands:一個RESP Bulk string數組。
  • 根據command的實現,服務器會回覆一個RESP數據類型。

在RESP,數據的類型由第一個字節決定:

  • Simple string:“+”。
  • Errors:“-”。
  • Integer:“:”。 Bulk strings:“$”。 Arrays:“*”。 另外,RESP可以通過一個Bulk string或者Array的變體表示Null。在RESP,協議的不同部分都會用“\n\r”(CRLF)來表示結尾。

RESP simple strings

Simple strings 通過以下方式編碼:一個加號,string(不能包含CR和LF),由CRLF終止。

Simple strings被用來以最小的overhead傳輸非二進制安全的字符串。比如,很多Redis命令會回覆“OK”當執行成功的時候。這個時候就是RESP Simple strings:

“+OK\r\n”

如果要發送二進制安全的字符串就要使用RESP bulk strings。

當Redis用一個simple string回覆的時候,一個客戶端應該返回給調用者一個由“+”開始的字符串,但是省略CRLF。

RESP Errors

RESP有一個專門針對Errors的數據類型。事實上errors真的很像RESP simple string,但是第一個字符是一個“-”。errors和simple string真正的差異是對於客戶端來說的,Errors被當作一種異常,其中的string被當作錯誤消息。基本的形式如下:

"-Error message\r\n"

Error只有當有錯誤出現的時候纔會由Redis發送,比如你在一個錯誤的數據類型上進行一個操作,或者command不存在等等。客戶端應當在收到Error回覆的時候拋出異常。以下是Errors的例子:

-ERR unknown command 'foobar'
-WRONGTYPE Operation against a key holding the wrong kind of value

“-”後面第一個word表示錯誤的類型,這只是Redis的一個convention不是RESP error format。

比如,ERR是一個general的錯誤,WRONGTYPE是一個更加specific的錯誤(意味着客戶端正在嘗試對一個錯誤的數據類型進行某種操作)。這被叫做Error Prefix,可以幫助客戶端理解錯誤。

一個客戶端實現可能針對不同的錯誤實現不同的異常,也有可能僅僅是將Redis返回的信息作爲一個字符串返回給調用者。however,這樣的特性可能並不是很重要,因爲沒什麼用,有些簡單的客戶端可能僅僅返回false給客戶端。

RESP integers

這個類型就是一個CRLF終止的字符串,以“:”爲前綴。比如“:0\r\n”,“:1000\r\n”就是整型回覆。

很多Redis命令返回RESP整型,比如INCR,LLEN和LASTSAVE。

返回的整型數沒有什麼特殊的意義,對於INCR來說就是一個增量後的數據,對於LASTSAVE就是一個UNIX時間等等。但是返回的整型數據保證是一個64位的有符號整數。整數也被大量用於返回true和false。比如EXISTS或者SISMEMEBER會返回1表示true,返回0表示false。

其它像SADD,SREM,SETNX這些命令當成功的時候會返回1,失敗返回0。

以下命令會產生一個整數回覆:SETNX,DEL,EXISTS,INCR,INCRBY,DECR,DECRBY,DBSIZE,LASTSAVE,RENAMENX,MOVE,LLEN,SADD,SREM,SISMEMBER,SCARD。

RESP Bulk strings

bulk string是用來表示單個二進制安全的字符串(最大512MB)。Bulk string通過以下方式編碼:

  • 一個“$”開始,然後跟着字符串長度(也就是prefixed length),然後用CRLF終止。
  • 實際字符串數據
  • CRLF

所以字符串“foobar”編碼如下:

"$6\r\nfoobar\r\n"

空字符串:

"$0\r\n\r\n"

RESP bulk string 還可以通過是用一種特殊的形式表示一個不存在的值,也就是Null。這種特殊的形式有長度-1,沒有數據,所以Null可以表示爲:

"$-1\r\n"

這個被叫做Null Bulk string。

當server回覆一個Null Bulk string數據類型的時候,客戶端API應該返回一個nil對象,而不是一個空字符串。比如一個Ruby客戶端應該返回'nil',而一個C庫應該返回NULL(或者在回覆對象中設置一個特殊的標誌)等等。

RESP數組(譯者注:類似C中的結構體)

客戶端通過RESP Arrays給Redis服務器發送命令。相似的,一些Redis命令會使用Arrays的形式從服務器得到返回值。比如,LARANGE命令。

RESP數組通過以下形式發送:

  • 一個“*”作爲第一個字節,緊跟一個十進制數用於表示數組中元素的數量,以CRLF結尾。
  • 另外的RESP類型數據來表示數組中的元素。

所以一個空的數組可以表示爲:

"*0\r\n"

而一個由2個bulk string,“foo”和“bar”組成的數組可以表示爲:

"*2\r\n$3\r\nfoo\r\n$3\r\nbar\r\n"

正如你所看到的,除了前綴之外後面就是由其它數據類型一個一個連接而成的。比如一個由3個整數組成的數組編碼爲:

"*3\r\n:1\r\n:2\r\n:3\r\n"

數組不需要每個數組元素都是同一類型的,可以包換混合類型的元素。比如,4個整數和一個bulk string可以編碼如下:

*5\r\n
:1\r\n
:2\r\n
:3\r\n
:4\r\n
$6\r\n
foobar\r\n

(爲了清晰一點,回覆被切分成了多行)

第一行,服務器發送*5\r\n來表示有5個元素會發送。然後發送這5個元素。

Null數組的概念也存在,並且是另一種方式來表示Null值。(Usually會用Null bulk string,但是由於歷史原因我們保留兩種形式的Null)。比如當BLPOP timeout的時候,它返回一個Null數組,它帶有-1計數:

"*-1\r\n"

一個客戶端得到一個Null數組的回覆的時候,需要返回一個null對象,而不是一個空數組。當用於區分一個空字符串和其它事件(比如BLPOP的timeout)的時候這就變得很有必要。

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

上述RESP數據編碼了一個由2個數組組成的數組,一個數組由整數1,2,3組成,另一個數組由一個simple string和一個errror組成。

字符串中的Null元素

數組中的某個元素可能是Null。這可以用來表示有一個元素缺失,但不是空元素。這個可以出現,比如SORT命令結合GET命令一起使用的時候。包含一個Null元素的數組的例子:

*3\r\n
$3\r\n
foo\r\n
$-1\r\n
$3\r\n
bar\r\n

第二個元素是一個Null,客戶端應該這樣返回:

["foo",nil,"bar"]

注意這不是前述的例外,只是一個用於進一步明確協議的例子。

給Redis服務器發送命令

現在你對RESP序列化的形式比較熟悉了,寫一個Redis客戶端應該是很容易的。我們可以進一步描述一下客戶端和服務器之間的交互:

  • 客戶端給Redis服務器發送一個僅僅由bulk string組成的RESP數組。
  • 一個服務器給客戶端回覆任意有效的RESP數據類型。

所以一個典型的交互是這樣的:

客戶端發送一個命令LLEN mylist,來獲取存在key mylist的list的長度。服務器回覆一個整數回覆(C:客戶端,S:服務器):

C: *2\r\n
C: $4\r\n
C: LLEN\r\n
C: $6\r\n
C: mylist\r\n

S: :48293\r\n

還是一樣,我們爲了簡單起見,將交互數據寫成了一行一行,但是實際上的交互數據,客戶端是發送*2\r\n$4\r\nLLEN\r\n$6\r\nmylist\r\n這樣一個整體。

多個命令和管道

一個客戶端可以使用相同的連接來發送多個命令。Redis支持管道,從而支持客戶端通過一個寫操作來發送多個命令,而不需要讀取服務器對於上一個命令的回覆再發送下一個命令。所有的回覆都可以在最後讀取。更堵的信息可以查閱page about Pipelining。

Inline命令

有時候,你手上只有Telnet,但是你想向Redis server發送命令。儘管Redis協議實現起來很簡單,但是在交互式會話下面還是不理想。同事,redis-cli又不可用。出於這個原因,Redis也通過一個特殊的方式接收命令。這種方式是爲人設計的,叫做inline 命令形式。

以下就是一個使用inline命令來進行server/client對話的例子。

C: PING
S: +PONG

以下是另一個例子:使用inline命令返回一個整數

C: EXISTS someky
S: :0

基本上,在telnet會話下面,你只是寫了以空格分隔的參數。因爲沒有命令以*開始,所以Redis使用unified request protocol,因此redis有能力檢測這個情況,並且解析你的命令。

Redis協議的高性能解釋器

Redis協議是一個可讀性好,而且可以很容易實現爲像二進制協議那樣快速的高性能協議。

RESP使用前綴長度來傳輸bulk數據,所以不需要像JSON那樣掃描特殊字符,或者quote來確定payload。

Bulk和多Bulk長度可以被程序使用,比如,在C語言下:

#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++
    }
    /* Now p points at '\r', and the len is in bulk_len. */
    printf("%d\n", len);
    return 0;
}

在CR識別之後,後面可以在收到LF之後退出。然後bulk數據就可以只需要讀一次就可以了,不需要再次檢查payload的長度。最後的CR和LF將被直接discard。

在用用二進制協議一樣的性能的前提下,Redis協議可以在大部分高級語言下面很方便的實現,因此減少客戶端軟件中的bug。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章