本文主要介绍使用Hiredis接口(Synchronous API)编写Redis流水线(pipelining)客户端的方法。
1 流水线(pipelining)介绍
流水线(pipelining)允许Redis客户端一次性向Redis服务器发送多个命令。Redis服务器在接收到这些命令后,按顺序处理这些命令,然后将(这些命令的)处理结果一次性返回给Redis客户端。
通过使用流水线,可以减少Redis客户端与Redis服务器之间的网络通信次数,以此提升Redis客户端在发送多个命令时的性能。
为了解释Hiredis如何在阻塞连接中支持流水线,我们通过分析redisCommand函数的执行步骤,了解Hiredis流水线的内部原理。
当redisCommand函数(或其同族函数)被调用时,Hiredis首先根据Redis协议,将需要执行的命令进行格式化。然后,将格式化后的命令放入到Redis连接的output buffer中(这个output buffer是动态的,所以它可以容纳任何数量的命令)。当命令被放入output buffer后,此时redisGetReply被调用了,这个函数会进行下面两种操作:
- 如果input buffer不为空,则从input buffer中解析一条来自Redis服务器的相应消息,并返回该消息;
- 如果input buffer为空,则将output buffer中的全部内容写入socket中,然后等待socket中Redis服务器返回的响应消息,读取并解析该消息。
函数redisGetReply作为Hiredis API,可以在socket中有(Redis服务器的)响应消息时使用。而对于流水线命令来说,只需要把想要执行的命令放入到output buffer中即可,通常我们使用如下函数(或其同族函数)来实现此目的:
void redisAppendCommand(redisContext *c, const char *format, ...);
上面的redisAppendCommand函数与redisCommand函数的区别在于:redisAppendCommand函数不返回Redis服务器的响应消息(实际上它只将命令放入到output buffer中),而redisCommand函数实际上包括了“redisAppendCommand函数”和“redisGetReply函数”两个步骤,所以redisCommand函数是阻塞的(使用了阻塞的redisContext对象),每次调用redisCommand函数时,都要等待Redis服务端的返回结果,然后才能继续执行程序后面的逻辑。
redisCommand函数的使用示例如下:
redisReply *reply;
reply = redisCommand(conn, "SET %s %s", "foo", "bar");
freeReplyObject(reply);
reply = redisCommand(conn, "GET %s", "foo");
printf("%s\n", reply->str);
freeReplyObject(reply);
如果我们需要向Redis服务端发送多条命令,如果使用redisCommand函数来发送,那么每次发送后都需要等待返回结果后才能继续下一次发送,这很显然会影响Redis客户端的处理性能。
因此,Hiredis提供了redisAppendCommand函数,来实现流水线命令发送方案:当我们需要向Redis服务端发送多条命令时,可以先调用若干次redisAppendCommand函数。之后,再调用redisGetReply函数来接收(并解析)redis服务器返回的响应消息。
redisAppendCommand函数实现流水线命令方案的示例如下:
redisReply *reply;
redisAppendCommand(context,"SET foo bar");
redisAppendCommand(context,"GET foo");
redisGetReply(context,&reply); // SET命令的返回
freeReplyObject(reply);
redisGetReply(context,&reply); // GET命令的返回
freeReplyObject(reply);
注意:redisAppendCommand函数的调用次数必须与redisGetReply函数的调用次数一致,否则会出现获取到的Redis服务端返回的处理结果跟预期不一致的情况。示例如下:
// 测试redisGetReply与redisAppendCommand 调用次数不一致的情况
redisAppendCommand(conn, "get foo");
reply = redisCommand(conn, "set fooo barr");
// 此处本想获取set fooo barr的返回信息,却获取了get foo的返回信息
printf("set info: %s\n", reply->str);
上述代码的printf函数打印出来的返回值是“get foo”命令的返回值,因为调用redisAppendCommand函数后,没有与之对应的redisGetReply函数函数调用,后面调用“redisCommand(conn, "set fooo barr");”时,该函数的子步骤redisGetReply函数会获取input buffer中第一个返回值,即“redisAppendCommand(conn, "get foo");”的返回值。
2 流水线客户端示例
2.1 示例代码
Redis流水线客户端的示例代码如下:
#include <iostream>
#include "hiredis/hiredis.h"
using namespace std;
int main()
{
// 建立redis连接
redisContext *c = redisConnect("192.168.213.128", 6379);
if ((c == NULL) || (c->err))
{
if (c)
{
cout << "Error: " << c->errstr << endl;
// 释放redis连接
redisFree(c);
return -1;
}
else
{
cout << "Can't allocate redis context." << endl;
return -1;
}
}
else
{
cout << "Connected to Redis." << endl;
}
redisReply *reply;
// 发送添加数据命令、查询数据命令
redisAppendCommand(c, "SET foo bar");
redisAppendCommand(c, "GET foo");
// 获取添加数据命令的返回结果
redisGetReply(c, (void**)&reply);
cout << "SET reply is: " << reply->str << endl;
freeReplyObject(reply);
// 获取查询数据命令的返回结果
redisGetReply(c, (void**)&reply);
cout << "GET reply is: " << reply->str << endl;
freeReplyObject(reply);
// 释放redis连接
redisFree(c);
return 0;
}
2.2 编译Redis流水线客户端
执行下面的命令编译上述代码,生成Redis客户端:
g++ -o hiredis_syncAPI_pipelining hiredis_syncAPI_pipelining.cpp -lhiredis
2.3 测试Redis流水线客户端
2.3.1 启动Redis服务器
在主机(IP地址为“192.168.213.133”)上打开Redis服务器。该Redis服务器监听对于“192.168.213.133”的连接,如下:
[root@node1 /opt/liitdar/hiredis]# redis-server /etc/redis.conf
查看redis-server是否在监听“192.168.213.133”:
[root@node1 /opt/liitdar/hiredis_for_demo]# netstat -anpot |grep 192.168.213.128
tcp 0 0 192.168.213.128:6379 0.0.0.0:* LISTEN 11606/redis-server off (0.00/0/0)
上面的结果显示redis-server已经在监听“192.168.213.133”地址了。
2.3.2 启动Redis流水线客户端
在另外一台主机(IP地址为“192.168.213.131”)上运行前面编译生成的Redis流水线客户端“hiredis_syncAPI_pipelining”,如下:
./hiredis_syncAPI_pipelining
2.3.3 观察测试结果
正常情况下,我们编写的Redis流水线客户端能够连接到Redis服务器,并执行指定的Redis流水线命令,如下:
如果运行Redis流水线客户端的终端中出现上述信息,说明我们的编写的Redis流水线客户端正常运行了。