Analysis of the life cycle of redis command execution

Time:2020-8-14

baiyan

introduce

First of all, let’s take a look at the redis command execution diagram that we are very familiar with
Analysis of the life cycle of redis command execution
Consider this question. After connecting to the redis server, enter and execute a redis command, such as set key1 value1. How is this command sent to the redis server? How does the redis server parse, process and return the successful execution?

Client to server command transmission (request)

Redis encapsulates its own set of protocol specifications based on TCP protocol, which is convenient for the server and client to receive and analyze data, draw the boundary between command parameters, and facilitate the final processing of data transmitted by TCP byte stream. Next, we use tcpdump to capture the packets when redis cli sends commands:

tcpdump port 6379 -i lo -X

At this point, we enter the set key1 value1 command in the client. The packets captured in tcpdump are as follows:
Analysis of the life cycle of redis command execution
The first is the packet when the client sends the command to the redis server, and the second is the packet that the redis server responds to the client. Let’s first look at the first packet, which is sent from port 43856 of the client to port 6379 of the redis server. First, the first 20 bytes are the IP header, and the last 32 bytes are the TCP header (because there are options after the TCP header).
We mainly focus on the data information starting from “2a33”. From here on, we will see the specific data format of redis. From an ASCII code translation of the data on the right, you can also see the words set, key1 and value1, and there are some characters in the middle represented by. So here, we will analyze the protocol format of redis data transmission according to the packet capture results.

  • 2a33: 0x2a is the ASCII value of the character “*”, and 0x33 is the ASCII value of “3” (decimal value is 51)
  • 0d0a: 0d is the ASCII value of “R”, 0A is the ASCII value of “n”
  • 7365: is the ASCII value of “s” and “e”
  • 740d: is the ASCII value of “t” and “R”
  • 0a24: is the ASCII value of “n” and “$”
  • 340D: is the ASCII value of “4” and “R”
  • 0a6b: is the ASCII value of “n” and “K”
  • 6579: is the ASCII value of “e” and “Y”
  • 310D: is the ASCII value of “1” and “R”
  • 0a24: is the ASCII value of “n” and “$”
  • 360D: is the ASCII value of “6” and “R”
  • 0a76: is the ASCII value of “n” and “V”
  • 616c: is the ASCII value of “a” and “L”
  • 7565: is the ASCII value of “U” and “e”
  • 310D: is the ASCII value of “1” and “R”
  • 0A: is the ASCII value of “n”

If we see here, can we find the following rules:

  • Redis is marked with “*” to indicate the beginning of the command. The number immediately following * represents the number of parameters (set key1 value1 has three parameters, so it is 3)
  • Redis takes “$” as the beginning of the command parameter, and the following number represents the length of the parameter (for example, the length of key1 is 4, so it is $4)
  • Redis uses “RN” as the separator between parameters, which is convenient to locate the boundary position when parsing TCP byte stream data

In summary, the format of redis packets sent by the client to the server is as follows:

*3 \r\n set \r\n $4 \r\n key1 \r\n $6 \r\n value1 \r\n

Compared with fastcgi protocol, redis only uses a few separators and special characters to standardize the command transmission syntax and data format. At the same time, the server can easily and efficiently parse and read the correct data from the byte stream data through the separator defined in it. This communication protocol is simple and efficient, and can meet the requirements of redis for high performance.

Server processing of commands

Since the command has been safely sent to the server through redis data transmission protocol, the server will begin to process the byte stream data transferred. Since we have clearly defined the boundary of each parameter in the protocol, it is very easy for the redis server to parse.

Step 1: the use of callback function

Redis is a typical event driver. In order to improve the performance of single process redis, redis uses IO multiplexing technology to process the client’s command request. When creating a client instance, redis specifies the event handling function to be executed when the server receives the event requested by the client command:

client *createClient(int fd) {
    
    client *c = zmalloc(sizeof(client));

    if (fd != -1) {
        Anetnonblock (null, FD); // set non blocking
        Anetenabletcpnodelay (null, FD); // the Nagle algorithm is not used to avoid half packets and sticky packets
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd, server.tcpkeepalive ); // set keep alive
        //Notice that a file event is created here. When the client read event is ready, call back the readqueryfromclient() function
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,readQueryFromClient, c) == AE_ERR) {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
    ... 
}

In order to hold the byte stream data requested by the client to the server, redis encapsulates a receive buffer to cache the data read from the socket. The subsequent command processing process reads the command data from the buffer and processes it. The advantage of a buffer is that you don’t have to maintain a read-write socket all the time. In the subsequent process, we only need to read the data from the buffer, rather than still reading from the socket. In this way, the socket can be released ahead of time to save resources. The establishment and use of buffer is completed in the client callback function readqueryfromclient(), which was mentioned earlier

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
   ...
    Qblen = sdslen (c > querybuf); // get buffer length
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen; 
    C - > querybuf = sdsmakeroomfor (c > querybuf, readlen); // create an SDS structure as a buffer
    Nread = read (FD, C - > querybuf + qblen, readlen); // read data from socket to buffer for temporary storage
    ...
    //Actually handle commands
    processInputBufferAndReplicate(c);
}

Step 2: use of distributor

This code creates and writes byte stream data to the buffer, then calls processInputBufferAndReplicate () to really process the command. The = = processinputbuffer() function is simply called in the processinputbufferandreplicate() function. Since our previous buffer already contains byte stream data sent by the client to the server, we need to conduct preliminary data filtering and processing in this layer

void processInputBuffer(client *c) {
    //If the buffer is not finished, continue with the loop
    while(c->qb_pos < sdslen(c->querybuf)) {
         ...
        //Customized distribution of byte stream data
        if (c->reqtype == PROTO_ REQ_ Inline) {// if it is an inline request
            if (processInlineBuffer(c) != C_ OK) break; // call processinlinebuffer to parse the buffer data
        } else if (c->reqtype == PROTO_ REQ_ Muitbulk) {// if it is a request of type multi
            if (processMultibulkBuffer(c) != C_ OK) break; // call processmultibulkbuffer to parse the buffer data
        } else { 
            serverPanic("Unknown request type");
        }

       //Start processing specific commands
        If (C - > argc = = 0) {// command parameters are 0, illegal
            resetClient(c);
        }Else {// command parameter is not 0, legal
            //Call processcommand() to actually process the command
            if (processCommand(c) == C_OK) { //
                ...
            }
        }
    }
}

Readers may be puzzled by this. What is inline and what is multiple? In redis, there are two types of request commands:

  • Inline type: simple string format, such as ping command
  • Multiple type: string array format. Most commands, such as set, get, and so on, are of this type

This function is actually a distributor. Since the underlying byte stream data is irregular, we need to distinguish which request byte stream data belongs to according to the reqtype field of the client, and then distribute it to the corresponding function for processing. Since the commands we often execute are of type multiblock, we also take the type of multiblock as an example. For the multiple request types such as set and get, they will be distributed to the processmultibulkbuffer() function for processing.

Step 3: check the data integrity of the receive buffer

When the Nagle algorithm of TCP is enabled, TCP will merge or split the packets requested by multiple redis commands. This will result in incomplete commands in one packet or multiple commands in one packet. To solve this problem, the processmultibulkbuffer() function ensures that only when a complete request is included in the buffer, the function will successfully parse the command parameters in the byte stream and return the success status code. Otherwise, it will break the external while loop, wait for the next event loop, read the remaining data from the socket, and then parse the command. This ensures the integrity of the data in redis protocol and the integrity of the actual command parameters.

int processMultibulkBuffer(client *c) {
    while(c->multibulklen) {
        ...
        /*Read command parameter byte stream*/
        if (sdslen(c->querybuf)-c->qb_ pos < (size_ t) (c > bulklen + 2)) {// if the number representing the parameter length after $does not match the actual command length (+ 2 is at the position of ﹤ 2), the data is incomplete. You can jump out of the loop directly and wait for the next time to read the remaining data
            break;
        }Else {// the command is complete. Do some initialization before executing the command
            if (c->qb_pos == 0 && c->bulklen >= PROTO_MBULK_BIG_ARG && sdslen(c->querybuf) == (size_t)(c->bulklen+2)) {
                c->argv[c->argc++] = createObject(OBJ_STRING,c->querybuf); 
                sdsIncrLen(c->querybuf,-2); 
                c->querybuf = sdsnewlen(SDS_NOINIT,c->bulklen+2);
                sdsclear(c->querybuf);
            } else {
                c->argv[c->argc++] =
                    createStringObject(c->querybuf+c->qb_pos,c->bulklen);
                c->qb_pos += c->bulklen+2;
            }
            c->bulklen = -1;
            C - > multibulklen --; // process the next command parameter
        }
    }
}

Step 4: actually process commands

We go back to the outer layer. When we successfully execute the processmultibulkbuffer() function, it shows that the current command is complete and can be processed. Let’s think about it. What should we do to call different processing functions according to different commands to complete different functions? After thinking about it, we can simply write the following code:

if (command == "get") {
    Dogetcommand(); // get command processing function
} else if (command == "set") {
    Dosetcommand(); // set command processing function
} else {
    Printf ("illegal command")
}

The above code is very simple, just according to the different command requests we get, they are distributed to different command processing functions for customized processing. In fact, the same is true for redis. How does redis do it

int processCommand(client *c) {
    //If it is a direct exit command, the
    if (!strcasecmp(c->argv[0]->ptr,"quit")) { 
        addReply(c,shared.ok);
        c->flags |= CLIENT_CLOSE_AFTER_REPLY;
        return C_ERR;
    }
    //Go to the dictionary to find the command, and assign the command processing function to the CMD field in the C structure
    c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
    //Return value verification
    If (! C - > CMD) {// the command is not found
        flagTransaction(c);
        sds args = sdsempty();
        int i;
        for (i=1; i < c->argc && sdslen(args) < 128; i++)
            args = sdscatprintf(args, "`%.*s`, ", 128-(int)sdslen(args), (char*)c->argv[i]->ptr);
        addReplyErrorFormat(c,"unknown command `%s`, with args beginning with: %s",
            (char*)c->argv[0]->ptr, args);
        sdsfree(args);
        return C_OK;
    }Else if ((c > CMD > arity > 0 & & C > CMD > arity! = C > argc) | // the command parameters do not match
               (c->argc < -c->cmd->arity)) {
        flagTransaction(c);
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",
            c->cmd->name);
        return C_OK;
    }
   //Real execution of orders
    if (c->flags & CLIENT_MULTI &&
        c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
        c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
    {
        queueMultiCommand(c);
        addReply(c,shared.queued);
    }Else {// actually execute the command
        call(c,CMD_ CALL_ Full); // core function
        c->woff = server.master_repl_offset;
        if (listLength(server.ready_keys))
            handleClientsBlockedOnKeys();
    }
    return C_OK;
}

In this function, the most important is the call of lookupcommand() function and call() function. In redis, all commands are stored in a dictionary, which is as long as:

struct redisCommand redisCommandTable[] = {
    {"module",moduleCommand,-2,"as",0,NULL,0,0,0,0,0},
    {"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
    {"setnx",setnxCommand,3,"wmF",0,NULL,1,1,1,0,0},
    {"setex",setexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"psetex",psetexCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"append",appendCommand,3,"wm",0,NULL,1,1,1,0,0},
    {"strlen",strlenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"del",delCommand,-2,"w",0,NULL,1,-1,1,0,0},
    {"unlink",unlinkCommand,-2,"wF",0,NULL,1,-1,1,0,0},
    {"exists",existsCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"setbit",setbitCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getbit",getbitCommand,3,"rF",0,NULL,1,1,1,0,0},
    {"bitfield",bitfieldCommand,-2,"wm",0,NULL,1,1,1,0,0},
    {"setrange",setrangeCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"getrange",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"substr",getrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"incr",incrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"decr",decrCommand,2,"wmF",0,NULL,1,1,1,0,0},
    {"mget",mgetCommand,-2,"rF",0,NULL,1,-1,1,0,0},
    {"rpush",rpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"lpush",lpushCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"rpushx",rpushxCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"lpushx",lpushxCommand,-3,"wmF",0,NULL,1,1,1,0,0},
    {"linsert",linsertCommand,5,"wm",0,NULL,1,1,1,0,0},
    {"rpop",rpopCommand,2,"wF",0,NULL,1,1,1,0,0},
    {"lpop",lpopCommand,2,"wF",0,NULL,1,1,1,0,0},
    {"brpop",brpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
    {"brpoplpush",brpoplpushCommand,4,"wms",0,NULL,1,2,1,0,0},
    {"blpop",blpopCommand,-3,"ws",0,NULL,1,-2,1,0,0},
    {"llen",llenCommand,2,"rF",0,NULL,1,1,1,0,0},
    {"lindex",lindexCommand,3,"r",0,NULL,1,1,1,0,0},
    {"lset",lsetCommand,4,"wm",0,NULL,1,1,1,0,0},
    {"lrange",lrangeCommand,4,"r",0,NULL,1,1,1,0,0},
    {"ltrim",ltrimCommand,4,"w",0,NULL,1,1,1,0,0},
    {"lrem",lremCommand,4,"w",0,NULL,1,1,1,0,0},
    ...
};

As we can see, this dictionary is a collection of all commands. We call lookupcommand from here to get the command and command related information. It is an array of structures, including all command names, command processing functions, the number of parameters, and various tags. In fact, this is equivalent to the maintenance of configuration information and the mapping relationship of command channel processing function names, which solves the problem that it is difficult to maintain and has poor scalability when we use if else to distribute command processing functions at the beginning.
After we successfully find a command processing function in the dictionary, we just need to call the corresponding command processing function. In the last call() function above, the corresponding command processing function is called, and the call result is returned to the client. For example, setcommand() is the actual processing function of the set command

void setCommand(client *c) {
    int j;
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_SET_NO_FLAGS;

    for (j = 3; j < c->argc; j++) {
        char *a = c->argv[j]->ptr;
        robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];

        if ((a[0] == 'n' || a[0] == 'N') &&
            (a[1] == 'x' || a[1] == 'X') && a[2] == '
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
' && !(flags & OBJ_SET_XX)) { flags |= OBJ_SET_NX; } else if ((a[0] == 'x' || a[0] == 'X') && (a[1] == 'x' || a[1] == 'X') && a[2] == '
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
' && !(flags & OBJ_SET_NX)) { flags |= OBJ_SET_XX; } else if ((a[0] == 'e' || a[0] == 'E') && (a[1] == 'x' || a[1] == 'X') && a[2] == '
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
' && !(flags & OBJ_SET_PX) && next) { flags |= OBJ_SET_EX; unit = UNIT_SECONDS; expire = next; j++; } else if ((a[0] == 'p' || a[0] == 'P') && (a[1] == 'x' || a[1] == 'X') && a[2] == '
void setCommand(client *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = OBJ_SET_NO_FLAGS;
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_XX))
{
flags |= OBJ_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_NX))
{
flags |= OBJ_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_PX) && next)
{
flags |= OBJ_SET_EX;
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' &&
!(flags & OBJ_SET_EX) && next)
{
flags |= OBJ_SET_PX;
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
' && !(flags & OBJ_SET_EX) && next) { flags |= OBJ_SET_PX; unit = UNIT_MILLISECONDS; expire = next; j++; } else { addReply(c,shared.syntaxerr); return; } } c->argv[2] = tryObjectEncoding(c->argv[2]); setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL); }

This function first judges and processes the NX and ex parameters, and finally calls setgenericcommand() to execute the general logic part of the set command

void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
    long long milliseconds = 0; /* initialized to avoid any harmness warning */

    if (expire) {
        if (getLongLongFromObjectOrReply(c, expire, &milliseconds, NULL) != C_OK)
            return;
        if (milliseconds <= 0) {
            addReplyErrorFormat(c,"invalid expire time in %s",c->cmd->name);
            return;
        }
        if (unit == UNIT_SECONDS) milliseconds *= 1000;
    }

    if ((flags & OBJ_SET_NX && lookupKeyWrite(c->db,key) != NULL) ||
        (flags & OBJ_SET_XX && lookupKeyWrite(c->db,key) == NULL))
    {
        addReply(c, abort_reply ? abort_reply : shared.nullbulk);
        return;
    }
    setKey(c->db,key,val);
    server.dirty++;
    if (expire) setExpire(c,c->db,key,mstime()+milliseconds);
    notifyKeyspaceEvent(NOTIFY_STRING,"set",key,c->db->id);
    if (expire) notifyKeyspaceEvent(NOTIFY_GENERIC,
        "expire",key,c->db->id);
    addReply(c, ok_reply ? ok_reply : shared.ok);
}

Finally, addreply() general return function will be called, which should return the execution result to the client. Let’s look at what is done in this function:

void addReply(client *c, robj *obj) {
    if (prepareClientToWrite(c) != C_OK) return;

    if (sdsEncodedObject(obj)) {
        if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
            _addReplyStringToList(c,obj->ptr,sdslen(obj->ptr));
    } else if (obj->encoding == OBJ_ENCODING_INT) {
        char buf[32];
        size_t len = ll2string(buf,sizeof(buf),(long)obj->ptr);
        if (_addReplyToBuffer(c,buf,len) != C_OK)
            _addReplyStringToList(c,buf,len);
    } else {
        serverPanic("Wrong obj->encoding in addReply()");
    }
}

We read this code carefully, and we can’t find out when the execution result is returned to the client. In this function, just add the returned result to the output buffer, and a command is executed. So when did you return? Do you remember when introducing the opening of an event loop, you mentioned that the function beforesleep() will be executed before each event loop blocks waiting for file events. It mainly performs some operations that are not time-consuming, such as deleting expiration keys, returning command replies to the client, etc. In this way, we can reduce the network communication overhead when returning the execution results, return multiple commands on the same client for multiple times, cache multiple commands, and finally return uniformly at one time, which reduces the number of returns and improves the performance.

Command transmission from client to server (response)

After executing the set key1 value1 command, we get an “OK” return, indicating that the command was successfully executed. In fact, we carefully observe the second packet returned above. In fact, the bottom layer is a return value of “+ OK”. So why have a + sign? Because in addition to the set command, get command, lpush command and so on, their return values are different. Get returns the data set, lpush returns an integer representing the length of the list, and so on. The representation of a string is far from enough. Therefore, in the redis communication protocol, a total of five return value structures are defined. The client uses the first character of each return structure to determine the type of return value

  • Status reply: the first character is “+”; for example, after the set command is executed, it will return “+ OK to the client”.
  • Error reply: the first character is “- for example,” err unknown command ‘testcmd’ will be returned to the client when the client request command does not exist.
  • Integer reply: the first character is’: ‘; for example, after the incr command is executed, it returns to the client “: 100.
  • Batch reply: the first character is “$”; for example, the get command lookup key returns the result “$5 / R / nhello / R / N” to the client, where $5 represents the length of the returned string.
  • Multiple batch replies: the first character is’ ‘; for example, the lrange command may return multiple values in the format of “3-r-n $6-r-nvalue1-r-n $6rnvalue2rn $6-r-nvalue3-r-n”, which is the same as the command request protocol format, “\ * 3” represents the number of returned values, “$6” represents the length of the current return value string, and multiple return values are separated by “\ \ * n”.

We execute the set command, which is the first type, state recovery. Through the + sign, the client can know that this is a state reply, and thus know how to read the following byte stream content.

summary

At this point, we have completed the whole life cycle of a redis command, and we also understand the format and specification of redis communication protocol. Next, I will go deep into the implementation of each command. Come on.

reference material

  • Redis source code analysis redis command processing life cycle