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的结果

客户端类库在遇到值不存在的情况时不要返回空字符串应该返回空对象(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应该解析成空对象,例如:

BLPOP key 1

*-1

这种情况下客户端API应该返回一个空对象而不是空列表.这样就可以区分空列表和发生错误的状况.

多块响应的Nil elements in Multi-Bulk replies

多块响应中的元素可能会返回长度为-1的情况,这表示该元素没有找到以区别于空字符串.在使用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