Hiredis realizes Redis pipeline

Keywords: Redis github network Python

Pipelining (pipeline) allows Redis clients to send multiple commands to Redis at one time. After receiving these commands, Redis processes them sequentially, and then returns the processing result of the request to the client at one time. Pipelining can reduce the number of network communications between the client and Redis to improve the performance of Redis client when sending multiple commands, which can be regarded as a sharp tool to improve the performance of the client.
The familiar Python version of redis-py provides StrictPipeline objects for pipelining, which is easy to use and can be referred to in the article. Redis Business Learning Notes . As the Edits client of C/C++ version, hiredis implementation pipeline is slightly complicated, but using hiredis to realize pipeline can help us understand the internal implementation principle of pipeline more deeply.

Hiredis provides redisCommand() functions to send commands to the Redis server. The prototype of redisCommand() functions is as follows:

void *redisCommand(redisContext *c, const char *format, ...);

After redisCommand() is executed, a redisReply * pointer is returned to the redisReply structure, which contains the returned result information.
The redisCommand() function is blocked (using the blocked redisContext object, as we assume below), and each call waits for the Redis server to return, and then continues to execute the logic below the program.
An example of using the redisCommand() function is shown below. For complete code and compilation, you can refer to the article. "The Use of hiredis on Redis C Language Client".

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);

If we need to send multiple commands to the Redis server, and if we use redisCommand() function to send them, then we have to wait for the result to be returned before we can continue to send them next time. This performance is obviously not acceptable to us. Hiredis provides redisAppendCommand() functions to implement a pipelined command delivery scheme.

int redisAppendCommand(redisContext *c, const char *format, ...);

The redisAppendCommand() function returns REDIS_OK when it executes successfully and REDIS_ERR when it fails.

#define REDIS_ERR -1
#define REDIS_OK 0

Like redisCommand() function, redisAppendCommand() function has other variants in hiredis. For the sake of simplicity of description, only redisCommand() function is used as an example.
After the redisAppendCommand() function is executed, the command is not immediately sent to Redis for execution, but cached into the redisContext object first. So when will the cached commands in the redisContext object be sent out? Hiredis provides the redisGetReply() function to send cached commands. The redisGetReply() function is processed as follows:

  1. Check to see if the result buffer still has results that have not been taken out, if so, the results will be taken out and returned directly; if not, step 2 will be executed.
  2. Send all commands in the command buffer to Redis processing and wait until a Redis processing result returns

The redisCommand() function mentioned above can directly get Redis's return result after execution because it calls redisAppendCommand() function internally and then redisGetReply() function.

At this point, the pipeline implementation process of hiredis is very clear. Whether redisCommand() or redisAppendCommand() functions, commands are cached first and then sent to Redis for execution. The difference is that the redisCommand() function sends the command immediately and gets the return result, while the redisAppendCommand() function calls the redisGetReply() function to send all the commands at once and get the return result of the first command.

Below is an example of a pipeline solution implemented using the redisAppendCommand() function.

redisReply *reply;
redisAppendCommand(context,"SET foo bar");
redisAppendCommand(context,"GET foo");
redisGetReply(context,&reply); // Return of SET command
freeReplyObject(reply);
redisGetReply(context,&reply); // Return of GET command
freeReplyObject(reply);

It is worth noting that the number of calls to redisAppendCommand() function needs to be consistent with the number of calls to redisGetReply(), otherwise the results of EDIS processing obtained will be inconsistent with expectations.

// Test the inconsistency between redisGetReply and redisAppendCommand calls
redisAppendCommand(conn, "get t");
// I wanted to get t he return of set a ddd, but I got the return of get
reply = redisCommand(conn, "set a ddd");
printf("set a res: %s\n", reply->str);

The output will be the return of the get command, not the return of the set a ddd command.

Reference material

  1. https://github.com/redis/hiredis
  2. https://gist.github.com/dspezia/1893378
  3. http://www.leoox.com/?p=316
  4. http://www.redis.cn/topics/pipelining.html

Attachment: Sample program testhiredis.c

Compile:

gcc -o testhiredis testhiredis.c -L/usr/local/lib -lhiredis

Implementation:

./testhiredis

Output:

bar
res: OK
res: b
watch res: OK
res: OK, num: 0, type: 5
res: QUEUED, num: 0, type: 5
res: QUEUED, num: 0, type: 5
res: QUEUED, num: 0, type: 5
res: (null), num: 3, type: 2
set a res: tt

Source program:

#include <stdio.h>
#include <hiredis/hiredis.h>

int main() {
    // Blocking redisContext
    redisContext *conn = redisConnect("127.0.0.1", 6379);
    if (conn != NULL && conn->err) {
        printf("connection error: %s\n", conn->errstr);
        return 0;
    }

    // Use redisCommand to send commands and get returns
    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);

    // Pipelining with redisAppendCommand
    redisAppendCommand(conn, "set a b");
    redisAppendCommand(conn,"get a");
    int r = redisGetReply(conn, (void **)&reply);
    if (r == REDIS_ERR) {
        printf("ERROR\n");
    }
    printf("res: %s\n", reply->str);
    freeReplyObject(reply);

    r = redisGetReply(conn, (void **)&reply);
    if (r == REDIS_ERR) {
        printf("ERROR\n");
    }
    printf("res: %s\n", reply->str);
    freeReplyObject(reply);

    // Use watch command to monitor key a
    reply = redisCommand(conn, "watch a");
    printf("watch res: %s\n", reply->str);
    freeReplyObject(reply);

    // Transaction pipeline, 5 commands in total
    redisAppendCommand(conn, "multi");
    redisAppendCommand(conn, "get foo");
    redisAppendCommand(conn, "set t tt");
    redisAppendCommand(conn, "set a aa");
    redisAppendCommand(conn, "exec");

    for (int i = 0; i < 5; ++i) {
        r = redisGetReply(conn, (void **)&reply);
        if (r == REDIS_ERR) {
            printf("ERROR\n");
        }
        printf("res: %s, num: %zu, type: %d\n", reply->str, reply->elements, reply->type);
        freeReplyObject(reply);
    }

    // Test the inconsistency between redisGetReply and redisAppendCommand calls
    redisAppendCommand(conn, "get t");
    // I wanted to get t he return of set a ddd, but I got the return of get
    reply = redisCommand(conn, "set a ddd");
    printf("set a res: %s\n", reply->str);

    redisFree(conn);

    return 0;
}

Posted by reneeshtk on Sun, 23 Jun 2019 12:09:44 -0700