Redis從1.2版本開始採用新的統一協議,從2.0版本開始成為與Redis Server互動的標準方式.Redis協議是一個折中方案,它平衡了下面的需求:
概覽
用戶端通過TCP6379連接埠串連Redis伺服器.用戶端伺服器端之間傳送的每一個Redis命令或者資料都是\r\n(CRLF)結束.Redis接受命令和參數,伺服器接受命令之後處理後發回用戶端.
協議的完整內容請查看:http://redis.io/topics/protocol 下面是協議的概覽圖:
請求Requests
新統一請求協議所有發送到Redis伺服器的資料都是二進位安全的(binary safe).什麼是二進位安全?參見維基百科 http://en.wikipedia.org/wiki/Binary-safe 簡單講二進位安全的函數把所有的輸入當成原始的資料流沒有特定格式,換句話說不會按照特定格式去解析資料,一個位元組(8位)資料所有可能表達的256種取值都能夠正常解讀.
下面是Redis Request的格式說明:
*<number of arguments> CR LF %參數個數$<number of bytes of argument 1> CR LF %參數1的位元組數<argument data> CR LF %參數1的資料...$<number of bytes of argument N> CR LF %參數N的位元組數<argument data> CR LF %參數N的資料
下面是符合上述規範的一個例子:
"*3\r\n$3\r\nSET\r\n$5\r\nmykey\r\n$7\r\nmyvalue\r\n"
Redis的應答使用同樣的結構. 像$6\r\nmydata\r\n這樣一條應答被稱為
Bulk Reply.如果Redis返回是資料項目列表,被稱為Multi-bulk reply.這種情況下就會在一組Bulk Reply之前添加一個*<arg_count>\r\n資料頭.
響應 Replies
響應訊息的第一個位元組表示了訊息的類型:
- 單行訊息 "+"
- 錯誤訊息 "-"
- 返回一個整型值 ":"
- 返回bulk reply "$"
- 返回 multi-bulk reply "
*
"
狀態響應
狀態響應(單行響應)是一個單行字串以+開始\r\n結束,比如 +OK
用戶端類庫應該返回+字元後面的所有內容,上面例子中就是OK
The client library should return everything after the "+", that is, the string "OK" in this example.
錯誤響應
錯誤響應和狀態響應類似,唯一的區別就是第一個字元是"-";只有異常出現的時候才會發送錯誤響應,比如你在錯誤的資料類型上進行一個操作,命令不存在等等.當接收到錯誤響應的時候用戶端類庫應該拋出異常.
整形響應
這種類型的響應的返回就是":"開頭,資料體是一個整形值的字串並以CRLF結尾,範例: ":0\r\n" ":1000\r\n"
像INCR,LASTSAVE這樣的命令使用整型值響應,這種數值並沒有特殊的含義,INCR僅僅是自增數值,LASTSAVE是UNIX時間.EXISTS命令傳回值就是特殊含義的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
塊響應Bulk replies
Bulk replies 用來返回單條二進位安全的字串,比如:
GET mykey %用戶端請求
$6\r\nfoobar\r\n %伺服器端響應
伺服器的響應以$開頭後面跟一個數字代表響應的位元組數然後是CRLF,後面緊跟實際的資料,再往後就是CRLF兩個位元組表示結束.
如果沒有請求的值並不存在就會bulk reply就會使用特殊值-1來表示資料長度,例如:
GET nonexistingkey %用戶端請求一個不存在的key
$-1 %伺服器返回一個資料長度為-1的結果
用戶端類庫在遇到值不存在的情況時不要返回Null 字元串應該返回Null 物件(Nil object).例如Ruby類庫返回nil,C類庫返回NULL,等等
多塊響應Multi-bulk replies
像LRANGE這樣的命令會返回多個值(列表的每一個元素都一個值,LRANGE需要返回不止一個元素).傳回值結構以*開頭,然後是塊資料的數量.
如果給定的key不存在就認為這個key對應一個空列表,塊資料的數量值為0.例如:
LRANGE nokey 0 1 % 用戶端請求
*0 %伺服器端響應
BLPOP命令逾時,就會返回一個空多塊響應(nil multi bulk reply).這時使用的數量值是-1應該解析成Null 物件,例如:
BLPOP key 1
*-1
這種情況下用戶端API應該返回一個Null 物件而不是空列表.這樣就可以區分空列表和發生錯誤的狀況.
多塊響應的Nil elements in Multi-Bulk replies
多塊響應中的元素可能會返回長度為-1的情況,這表示該元素沒有找到以區別於Null 字元串.在使用GET配合SORT命令時會出現這種指定key值找不到的情況.例如:"*3\r\n$3\r\nfoo\r\n$-1\r\n$3\r\nbar\r\n"這裡第二個元素就是空值,用戶端應該返回類似這樣的值:["foo",nil,"bar"]
多條命令和管道
一個用戶端可以使用同一個串連發送多條命令.管道支援可以讓用戶端一次寫操作就可以發送多條命令.沒有必要等待服務響應之後再發送下一條命令.可以最後讀取所有的響應結果.通常Redis伺服器和用戶端一個快速的連結,用戶端是否實現這一特性並不太重要,如果一個應用程式短時間內需要發送大量的的命令使用管道要快得多.
理論與實踐的分隔線
.Net Client中的協議實現
Redis .net版本的開源用戶端有很多,這裡我們選取的項目是booksleeve,項目地址: http://code.google.com/p/booksleeve/
著名的技術類問答站Stack Exchange就是使用了這個項目,具體可以參考這裡: http://www.biaodianfu.com/stack-exchanges-architecture.html,我們選取其中一段Redis協議的實現代碼:
//source: https://github.com/migueldeicaza/redis-sharp/blob/master/redis-sharp.cs
public void Set(IDictionary<string, byte[]> dict)
{
if (dict == null)
throw new ArgumentNullException("dict");
var nl = Encoding.UTF8.GetBytes("\r\n");
var ms = new MemoryStream();
foreach (var key in dict.Keys)
{
var val = dict[key];
var kLength = Encoding.UTF8.GetBytes("$" + key.Length + "\r\n");
var k = Encoding.UTF8.GetBytes(key + "\r\n");
var vLength = Encoding.UTF8.GetBytes("$" + val.Length + "\r\n");
ms.Write(kLength, 0, kLength.Length);
ms.Write(k, 0, k.Length);
ms.Write(vLength, 0, vLength.Length);
ms.Write(val, 0, val.Length);
ms.Write(nl, 0, nl.Length);
}
SendDataCommand(ms.ToArray(), "*" + (dict.Count * 2 + 1) + "\r\n$4\r\nMSET\r\n");
ExpectSuccess();
}
Erlang Client中的協議實現
Redis Erlang版本的用戶端我們可以看一下立濤寫的erl-redis,項目地址:https://github.com/litaocheng/erl-redis
下面是一段代碼是redis_proto.erl的摘取的片段,可以明顯感受到Erlang對於位元據的表達能力更強一些,與上面.net的版本相比語言的文法噪音少了很多,Redis相關協議的實現更直觀一些;
%% @doc generate the mbulk command
mbulk(Type) ->
[<<"*1">>, ?CRLF, mbulk0(Type)].
mbulk(Type, Arg) ->
[<<"*2">>, ?CRLF, mbulk0(Type), mbulk0(Arg)].
mbulk(Type, Arg1, Arg2) ->
[<<"*3">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2)].
mbulk(Type, Arg1, Arg2, Arg3) ->
[<<"*4">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2), mbulk0(Arg3)].
mbulk(Type, Arg1, Arg2, Arg3, Arg4) ->
[<<"*5">>, ?CRLF, mbulk0(Type), mbulk0(Arg1), mbulk0(Arg2), mbulk0(Arg3), mbulk0(Arg4)].
mbulk_list(L) ->
N = length(L),
Lines = [mbulk0(E) || E <- L],
[<<"*">>, ?N2S(N), ?CRLF, Lines].
%% @doc parse the reply
parse_reply(<<"+", Rest/binary>>) ->
parse_status_reply(Rest);
parse_reply(<<"-", Rest/binary>>) ->
parse_error_reply(Rest);
parse_reply(<<":", Rest/binary>>) ->
b2n(Rest);
parse_reply(<<"$-1\r\n">>) ->
null;
parse_reply(<<"$0\r\n">>) ->
{bulk_more, 0};
parse_reply(<<"$", Rest/binary>>) ->
N = b2n(Rest),
{bulk_more, N};
parse_reply(<<"*-1\r\n">>) ->
null;
parse_reply(<<"*0\r\n">>) ->
null;
parse_reply(<<"*", Rest/binary>>) ->
N = b2n(Rest),
{mbulk_more, N}.
OK,今天就到這裡,上面兩個版本的實現代碼都非常棒,大家可以下載看一下.更多Redis用戶端的選擇請參考這裡:http://redis.io/clients