redis处理命令过程

816 阅读9分钟

上一章我们介绍了redis处理用户连接请求的过程,如下图所示,redis为一个连接建立了一个client,当用户发送命令时,监听用户命令的fd有事件响应,调用fd绑定的aeFileEvent->rfileProc,就是readQueryFromClient()

本文先忽略一些分布式机制,后面单独开篇讲解(其实是懒得看)

另外,我没全程以set命令举例!

image.png

readQueryFromClient()

我们直接看这个函数实现的关键部分

void readQueryFromClient(connection *conn) {
    // 多线程模式,下一篇文章讲
    if (postponeClientRead(c)) return;

    qblen = sdslen(c->querybuf);
    //***** 读取数据
    int nread = connRead(c->conn, c->querybuf+qblen, readlen);
    // ***** 增加当前数据长度
    sdsIncrLen(c->querybuf,nread);
    // **** 正式处理读到的数据(执行命令)
    processInputBuffer(c);
}

这个函数总共分为三部分:

  1. 读取数据
  2. 增加当前数据长度
  3. 处理数据(执行命令)

读取数据

读取数据通过调用connRead()函数

static inline int connRead(connection *conn, void *buf, size_t buf_len) {
    return conn->type->read(conn, buf, buf_len);
}

conn->type->read是connSocketRead()函数

static int connSocketRead(connection *conn, void *buf, size_t buf_len) {
    int ret = read(conn->fd, buf, buf_len);
    conn->state = xxx  //一些状态设置
    return ret;
}

调用链最后就是这句最关键

read(conn->fd,  c->querybuf+qblen, readlen)

可以看到,其实就是调用网络编程里的read()函数,从 fd 对应的socket的接收缓冲区中读取数据到 client 中的 querybuf ,再设置一些conn的状态

clent.

image.png

这里可以看到,client->querybuf用来缓存用户发送的数据,这是一个sds结构,其实就是redis自己实现的string,后面文章会详细介绍

增加数据长度

sds是redis的字符串类型,buf是为一个sds分配的空间指针,alloc表示分配空间大小,len表示已用空间大小,如下图: image.png

sdsIncrLen(c->querybuf,nread);

querybufs是一个sds结构,上面这句就是把querybuf.len加上nread,因为上一步调用read()函数向querybuf里添加了数据,已用的空间变大了

处理数据(执行命令)

执行命令的过程在processInputBuffer()中实现,processInputBuffer()函数外层是个while循环,循环的判断是client的读缓冲区(querybuf)还有未处理的数据,那么就进入循环对数据进行处理

void processInputBuffer(client *c) {
    while(c->qb_pos < sdslen(c->querybuf)) {
        ***处理数据的过程***
    }
}

下面看看while循环里都做了什么

/***前面是一堆条件判断,满足就break退出while循环,省略***/

/* 判断命令请求类型 telnet发送的命令和redis-cli发送的命令请求格式不同,见后面补充知识 */
if (c->querybuf[0] == '*') { 
    c->reqtype = PROTO_REQ_MULTIBULK; 
} else { 
    c->reqtype = PROTO_REQ_INLINE; 
}

/***** 解析命令,根据请求类型调用下面函数中的一个 ******/
processInlineBuffer(c)
processMultibulkBuffer(c)

processCommandAndResetClient(c)
    /****** *执行命令 *******/
    processCommand(c)
    /******* 命令后处理*******/
    commandProcessed(c)

可以看到上面的伪代码主要分为3步

  1. 解析命令
  2. 执行命令
  3. 命令后处理

补充知识:请求协议的类型。因为Redis服务器支持Telnet的连接,因此Telnet命令请求协议类型是PROTO_REQ_INLINE,而redis-cli命令请求的协议类型是PROTO_REQ_MULTIBULK。输入命令 telnet 127.0.0.1 6379 就用telnet登陆本地redis了

解析命令

解析命令由函数processMultibulkBuffer()或者processInlineBuffer()实现 当前命令存在读缓冲区,也就是client.querybuf中,是一串连续字符,解析命令就是解析这一串字符,填写client->agrv、client->argc 例如:当我们输入的命令是set mykey myvalue,此时缓冲区的字符串是 :

"*3\r\n3\nSET˚\n˚3\r\nSET\r\n5\r\nmykey\r\n$7\r\nmyvalue\r\n"

分割成字符数组保存在redisCLient数组中:

c->argv[0]="set"
c->argv[1]="mykey"
c->argv[2]="myvalue"

c->argc=3

执行命令

processCommand(c)开始执行命令了,这里面有很多条件判断,详情我们先省略,只看常规的执行

如果命令是quit,就返回退出

从命令表中查找命令
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);

异常情况的检测,包括是否超过内存限制、是否合法命令等

如果是事务执行命令, 除 EXEC 、 DISCARD 、 MULTI 和 WATCH 命令之外其他命令都会被函数 queueMultiCommand() 入队到事务队列中处理
queueMultiCommand(c);

如果是常规命令,调用 call() 函数执行命令
call(c,CMD_CALL_FULL);

对于普通命令,processCommand(c)主要做了两件事:

  1. 首先通过lookupCommand从server.commands命令表中查找命令,设置client->cmd
  2. 最后就是调用一个call()函数执行命令

我们这次只看call()中这一句,这是执行命令

c->cmd->proc(c);

set命令对应的proc()是setCommand()

struct redisCommand redisCommandTable[] = {
    ...
    {"set",setCommand,-3,
     "write use-memory @string",
     0,NULL,1,1,1,0,0,0},
    ...

setCommand()的代码如下:

void setCommand(client *c) {
    robj *expire = NULL;
    int unit = UNIT_SECONDS;
    int flags = OBJ_NO_FLAGS;
    parseExtendedStringArgumentsOrReply(c,&flags,&unit,
        &expire,COMMAND_SET);

    c->argv[2] = tryObjectEncoding(c->argv[2]);
    setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}

这里expire是传入的超时时间,setCommand最后调用setGenericCommand()函数

void setGenericCommand(client *c, int flags, robj *key, 
     robj *val, robj *expire, int unit, 
     robj *ok_reply, robj *abort_reply) {
    前面各种解析flag和超时事件
    
    // 写入数据库
    genericSetKey(c,c->db,key, val,flags & OBJ_KEEPTTL,1);
    // 设置超时
    setExpire(c,c->db,key,when);
    // 填写返回给客户端的信息
    addReply(c, ok_reply ? ok_reply : shared.ok);

setGenericCommand()函数通过genericSetKey()把用户输入的kv对写入数据库,setExpire()把超时时间写入数据库,这两个函数这里就不展开说了。

最后通过addReply()函数将把服务端对客户端的响应写入到缓冲区,发送给客户端

addReply 方法做了两件事情:

  • prepareClientToWrite 判断是否需要返回数据,并且将当前 client 添加到等待写返回数据队列中。
  • 调用 _addReplyToBuffer 和 _addReplyObjectToList 方法将返回值写入到输出缓冲区中,等待写入 socekt。

这里简单介绍,后面一节单独讲一下返回客户端的过程

命令后处理

命令后处理就是执行完命令的一些收尾工作,主要在resetClient()函数里,就是把client的各种标志位初始化,准备接收用户的下一条命令

服务端返回

写入输出缓冲区

上一节说过,redis服务端最后通过addReply()函数将把服务端对客户端的响应写入到缓冲区,发送给客户端,我们这一节主要看看addReply()和返回的过程

如果命令执行成功,都会执行这一句

addReply(c,shared.ok);

shared是一个obj类型,redis把所有数据都包装成一个obj,obj指向实际的数据,并且记录了编码方式。shared是一个全局的obj对象

struct sharedObjectsStruct shared;

shared对象成员的填写在createSharedObjects()函数中

void createSharedObjects(void) {
    int j;
    shared.crlf = createObject(OBJ_STRING,sdsnew("\r\n"));
    shared.ok = createObject(OBJ_STRING,sdsnew("+OK\r\n"));
    ...
}

可以看到shared.ok也在这个函数里被初始化,就是一个"+OK"字符串

我们看看addReply()做了什么

void addReply(client *c, robj *obj) {
    prepareClientToWrite(c);

    if (sdsEncodedObject(obj)) {
        if (_addReplyToBuffer(c,obj->ptr,sdslen(obj->ptr)) != C_OK)
            _addReplyProtoToList(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)
            _addReplyProtoToList(c,buf,len);
    } else {
        serverPanic("Wrong obj->encoding in addReply()");
    }
}

其实就做了两件事

  1. prepareClientToWrite 判断是否需要返回数据,并且将当前 client 添加到等待写返回数据队列中(server.clients_pending_write)。最后实现的是下面这句:

listAddNodeHead(server.clients_pending_write,c);

  1. 调用 _addReplyToBuffer 和 _addReplyObjectToList 方法将返回值写入到输出缓冲区中,等待写入 socekt。

image.png

client的输出缓冲区有两个,一个固定大小的 buffer 和一个响应内容数据的链表。在链表为空并且 buffer 有足够空间时,则将响应添加到 buffer 中。如果 buffer 满了则创建一个节点追加到链表上。_addReplyToBuffer 和 _addReplyObjectToList 就是分别向这两个空间写数据的方法。

到此,要返回给用户的数据就到了输出缓冲区了,接下来要做的就是写入socket

将命令返回值从输出缓冲区写入 socket

上一篇文章说过,在redis的事件循环中,一直循环调用aeProcessEvents()

int aeProcessEvents(aeEventLoop *eventLoop, int flags) {
    eventLoop->beforesleep(eventLoop);
    numevents = aeApiPoll(eventLoop, tvp);
    eventLoop->aftersleep(eventLoop);
    for (j = 0; j < numevents; j++) {
        处理事件
    }
}

可以看到每次事件循环等待事件触发前后都会执行例行函数beforesleep()和aftersleep()

beforeSleep 函数会调用 handleClientsWithPendingWritesUsingThreads()函数,如果redis未开启多线程模式,就会调用handleClientsWithPendingWrites()函数来来处理 clients_pending_write 列表,写socket的操作就在这里面。

关于单线程和多线程,下一篇文章解释,本文只看单线程

我们看看handleClientsWithPendingWrites()函数

int handleClientsWithPendingWrites(void) {
    // li指向client链表头
    listRewind(server.clients_pending_write,&li);
    // 遍历链表开始处理
    while((ln = listNext(&li))) {
        // 从链表中删除然后处理
        listDelNode(server.clients_pending_write,ln);
        
        // 写数据
        if (writeToClient(c,0) == C_ERR) continue;
        
        // 没写完,注册写事件
        if (clientHasPendingReplies(c)) {
            connSetWriteHandlerWithBarrier(c->conn, 
                  sendReplyToClient, ae_barrier)
        }
    }
}

handleClientsWithPendingWrites()函数遍历待处理client链表server.clients_pending_write,先通过writeToClient()函数把数据从输出缓冲区写入socket

int writeToClient(client *c, int handler_installed) {
     while(clientHasPendingReplies(c)) { // client缓冲区有东西
         if (c->bufpos > 0) {
              // buf缓冲区有数据,就把buf里的数据写入socket
              connWrite(c->conn,c->buf+c->sentlen,c->bufpos-c->sentlen);
         } else {
             //如果buf没有数据,就从reply链表中取一个写
             o = listNodeValue(listFirst(c->reply));
             nwritten = connWrite(c->conn, o->buf + c->sentlen, objlen - c->sentlen); 
         }
         
         如果写的太多了,就break等下次再写,注意这里会造成这次没写完的情况
         if (totwritten > NET_MAX_WRITES_PER_EVENT &&。。。){ break; }
         如果client上没有数据了,就释放client
         
     }
}

connWrite()函数最后调用的是connSocketWrite()函数,最后就是调用write()

static int connSocketWrite(connection *conn, const void *data, size_t data_len) {
    int ret = write(conn->fd, data, data_len);
    if (ret < 0 && errno != EAGAIN) {
        conn->last_errno = errno;
        if (conn->state == CONN_STATE_CONNECTED)
            conn->state = CONN_STATE_ERROR;
    }

    return ret;
}

到此,数据终于写回了socket!

没写完的情况

如果一次没写完,会设置写事件,给connection的write_handler绑定为sendReplyToClient()

connSetWriteHandlerWithBarrier(c->conn, sendReplyToClient, ae_barrier)

static inline int connSetWriteHandlerWithBarrier(connection *conn, ConnectionCallbackFunc func, int barrier) {
    return conn->type->set_write_handler(conn, func, barrier);
}

调用的connSocketSetWriteHandler

static int connSocketSetWriteHandler(connection *conn, ConnectionCallbackFunc func, int barrier) {
    if (func == conn->write_handler) return C_OK;

    conn->write_handler = func;
    if (barrier)
        conn->flags |= CONN_FLAG_WRITE_BARRIER;
    else
        conn->flags &= ~CONN_FLAG_WRITE_BARRIER;
    if (!conn->write_handler)
        aeDeleteFileEvent(server.el,conn->fd,AE_WRITABLE);
    else
        if (aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE,
                    conn->type->ae_handler,conn) == AE_ERR) return C_ERR;
    return C_OK;
}

这一套操作下来,又aeCreateFileEvent()往el.events里注册了一写个事件,等同于下面这句:

conn->write_handler = connSocketSetWriteHandler
aeCreateFileEvent(server.el,conn->fd,AE_WRITABLE, 
    connSocketEventHandler,conn)

注册完以后,因为刚向socket里写了数据,beforesleep()之后的epoll_wait()会触发fd绑定的写事件,connSocketEventHandler()函数,这个函数就看connection有无绑定读写函数,进行调用

tatic void connSocketEventHandler(struct aeEventLoop *el, int fd, void *clientData, int mask) {
    ...
    callHandler(conn, conn->write_handler);
    ...
}

这里调用的就是sendReplyToClient(),函数里面调用的也是writeToClient(c,1);就是尽可能多的写,如果写完了,会删除注册的write_handler,这样下次事件循环就不会再调用了!下次循环发现client没有数据,就不会write(),再下次循环就不会触发写事件了。

到此,数据已经全部写进了socket!后续就是计算机网络自己的底层工作,我们就不用care了