本文主要介绍使用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被调用了,这个函数会进行下面两种操作:
1. 如果input buffer不为空:则从input buffer中解析一条来自redis服务器的相应消息,并返回该消息;
2. 如果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流水线客户端正常运行了。