redis源码分析八--从客户端发送set命令后看服务端的执行流程上

1、Redis服务端的执行流程

Redis程序分为客户端和服务端,之间通信是通过socket来传递指令信息,这里有一个问题就是我客户端发送一个set命令后服务器会做哪一些操作呢?具体操作语句见下图
在这里插入图片描述
这里我设置了一个字符串number值为12306传递给Redis服务器端来进行保存,服务器端是如何实时监听我们的发送指令,并且如何解析的呢?

首先我给出服务器端的调用流程图如下
在这里插入图片描述
首先服务器端在启动时会注册acceptTcpHandler这个文件事件来进行监听socket是否来进行连接,具体可以见我的另一片文档Redis服务自启动时的事件机制

如果有客户端成功建立连接,那么就是调用createClient函数来创建一个readQueryFromClient的文件事件函数用来监听是否客户端来进行set这个操作,如果有则调用接下来的流程来进行set操作

2、源码分析

服务器端如何建立监听流程

void acceptTcpHandler(aeEventLoop *el, int fd, void *privdata, int mask) {
    int cport, cfd;
    char cip[128];
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);
    REDIS_NOTUSED(privdata);
	/*调用了unix的等待连接函数等待客户端进行连接*/
    cfd = anetTcpAccept(server.neterr, fd, cip, &cport);
    if (cfd == AE_ERR) {
        redisLog(REDIS_WARNING,"Accepting client connection: %s", server.neterr);
        return;
    }
    redisLog(REDIS_VERBOSE,"Accepted %s:%d", cip, cport);
    /*走到这一步说明已经成功连接了,如何证明已经连接是排除了下面两种情况
     1、如果还没有客户端来进行连接,那么程序就会堵塞在anetTcpAccept函数上
     2、如果有客户端来进行连接但是没有连接成功,这样在上面cfd == AE_ERR判断处就直接反悔了,也走不到这一步
    具体acceptCommonHandler函数见下面分析*/
    acceptCommonHandler(cfd,0);
}
static void acceptCommonHandler(int fd, int flags) {
    redisClient *c;
    /*可以发现这里调用了createClient这个函数,并且这个创建的Client是用连接描述符fd表示的,说明可以唯一
      每一个clinet,实际上就是我客户端程序启动来连接服务端,连接上后发送命令给服务器端,然后服务器程序中
      自己建立一个clinet对象来和自己进行交互,然后将得到结果的数据利用socket返回给真正的客户端。
      createClient的函数就下面会详细分析*/
    if ((c = createClient(fd)) == NULL) {
        redisLog(REDIS_WARNING,
            "Error registering fd event for the new client: %s (fd=%d)",
            strerror(errno),fd);
        close(fd); /* May be already closed, just ignore errors */
        return;
    }
    /* 如果设置了server.maxclients并且客户端的连接数超过了这个最大值,那么就不再进行连接 */
    if (listLength(server.clients) > server.maxclients) {
        char *err = "-ERR max number of clients reached\r\n";

        /* That's a best effort error message, don't check write errors */
        if (write(c->fd,err,strlen(err)) == -1) {
            /* Nothing to do, Just to avoid the warning... */
        }
        server.stat_rejected_conn++;
        freeClient(c);
        return;
    }
    server.stat_numconnections++;
    c->flags |= flags;
}
redisClient *createClient(int fd) {
	/*创建一个redisclient的对象来和服务器进行交互,所以说在服务器端也会自己创建一个redisClient对象出来*/
    redisClient *c = zmalloc(sizeof(redisClient));

    /*传递-1作为fd可以创建一个非连接的客户端。这很有用,因为所有Redis命令都需要在客户端的上下文中执行。
     当命令在其他上下文中执行时(例如Lua脚本),我们需要一个未连接的客户端。
     所以这里需要注意的是fd是-1这种情况也是存在的,当然我们这里已经成功建立了连接说明fd必定不为0 */
    if (fd != -1) {
        anetNonBlock(NULL,fd);
        anetEnableTcpNoDelay(NULL,fd);
        /*设置客户端和服务器端的连接为长连接*/
        if (server.tcpkeepalive)
            anetKeepAlive(NULL,fd,server.tcpkeepalive);
        /*这里创建一个文件事件,这个readQueryFromClient函数就是用来读取客户端发送过来的命令的*/
        if (aeCreateFileEvent(server.el,fd,AE_READABLE,
            readQueryFromClient, c) == AE_ERR)
        {
            close(fd);
            zfree(c);
            return NULL;
        }
    }
	/*下面就是一些属性的赋值,不在详细分析*/
    selectDb(c,0);
    c->fd = fd;
    c->name = NULL;
    c->bufpos = 0;
    c->querybuf = sdsempty();
    c->querybuf_peak = 0;
    c->reqtype = 0;
    c->argc = 0;
    c->argv = NULL;
    c->cmd = c->lastcmd = NULL;
    c->multibulklen = 0;
    c->bulklen = -1;
    c->sentlen = 0;
    c->flags = 0;
    c->ctime = c->lastinteraction = server.unixtime;
    c->lastrequest = 0;
    c->authenticated = 0;
    c->replstate = REDIS_REPL_NONE;
    c->slave_listening_port = 0;
    c->reply = listCreate();
    c->reply_bytes = 0;
    c->obuf_soft_limit_reached_time = 0;
    listSetFreeMethod(c->reply,decrRefCount);
    listSetDupMethod(c->reply,dupClientReplyValue);
    c->bpop.keys = dictCreate(&setDictType,NULL);
    c->bpop.timeout = 0;
    c->bpop.target = NULL;
    c->io_keys = listCreate();
    c->watched_keys = listCreate();
    listSetFreeMethod(c->io_keys,decrRefCount);
    c->pubsub_channels = dictCreate(&setDictType,NULL);
    c->pubsub_patterns = listCreate();
    listSetFreeMethod(c->pubsub_patterns,decrRefCount);
    listSetMatchMethod(c->pubsub_patterns,listMatchObjects);
    if (fd != -1) listAddNodeTail(server.clients,c);
    initClientMultiState(c);
    return c;
}

就是说在服务器端创建了一个redisClient对象后注册了一个文件事件readQueryFromClient,用来读取客户端发送过来的指令,这里就是客户端和服务器端已经建立连接,连接描述符是fd,然后在客户端发送命令后,通过select发现这个readQueryFromClient文件事件上可以操作,然后出发调用。

服务器端如何处理客户端发送过来的指令

在上面我们可以知道在客户端发送一个命令后会调用到readQueryFromClient这个函数来接受客户端发送过来的命令,接下来我们来看下服务器端是如何处理这些命令的。

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    redisClient *c = (redisClient*) privdata;
    int nread, readlen;
    size_t qblen;
    REDIS_NOTUSED(el);
    REDIS_NOTUSED(mask);

    /* 节流时不要处理新请求 */
    if (handleRequestThrottling(c) != REDIS_OK) return;

    server.current_client = c;
    readlen = REDIS_IOBUF_LEN;
    /* 如果这是一个多批量请求,并且我们正在处理足够大的批量答复,这时候我们就需要将缓冲区最大限度的使用
    起来,即使我们这样做可能会不能放下一个完整的sds命令,这样的话就需要读取两遍,
    这样,函数processMultiBulkBuffer()可以避免复制缓冲区以创建表示参数的Redis对象。 */
    if (c->reqtype == REDIS_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1
        && c->bulklen >= REDIS_MBULK_BIG_ARG)
    {
        int remaining = (unsigned)(c->bulklen+2)-sdslen(c->querybuf);

        if (remaining < readlen) readlen = remaining;
    }
	/*这个获取的是在未获取到新的命令行之前querybuf中的数据长度,然后后面的命令行数据可以在此基础上继续添加*/
    qblen = sdslen(c->querybuf);
    /*用来记录最近一段时间内客户端发送过来的请求的最长长度*/
    if (c->querybuf_peak < qblen) c->querybuf_peak = qblen;
    /* 这个readlen可以保证querybuf拥有足够大的空间可以存放下新的命令行 */
    c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
    /* 读取新的命令行数据 */
    nread = read(fd, c->querybuf+qblen, readlen);
    if (nread == -1) {
        if (errno == EAGAIN) {
            nread = 0;
        } else {
            redisLog(REDIS_VERBOSE, "Reading from client: %s",strerror(errno));
            freeClient(c);
            return;
        }
    } else if (nread == 0) {
    	/*可以读取但是没有读取到参数说明连接结束*/
        redisLog(REDIS_VERBOSE, "Client closed connection");
        freeClient(c);
        return;
    }
    if (nread) {
    	/*更新c->querybuf的长度*/
        sdsIncrLen(c->querybuf,nread);
        /*更新最新的时间点*/
        c->lastinteraction = server.unixtime;
        c->lastrequest = server.unixtime;
    } else {
        server.current_client = NULL;
        return;
    }
    /*如果请求超过最大的限度则报错*/
    if (sdslen(c->querybuf) > server.client_max_querybuf_len) {
        sds ci = getClientInfoString(c), bytes = sdsempty();

        bytes = sdscatrepr(bytes,c->querybuf,64);
        redisLog(REDIS_WARNING,"Closing client that reached max query buffer length: %s (qbuf initial bytes: %s)", ci, bytes);
        sdsfree(ci);
        sdsfree(bytes);
        freeClient(c);
        return;
    }
    processInputBuffer(c);
    server.current_client = NULL;
}

上面主要是获取到了最新的请求命令,并更新了redisClient对象中最新的关于请求命令相关的数据,具体处理在这个processInputBuffer函数中,下面来具体分析一下这个函数

void processInputBuffer(redisClient *c) {
    /* 判断是否有请求命令进入,如果没有请求命令,则不进入逻辑 */
    while(sdslen(c->querybuf)) {
        /* 如果客户端处于中断状态,需要立即中止操作 */
        if (c->flags & REDIS_BLOCKED) return;

        /* 将回复写入客户端后,REDIS_CLOSE_AFTER_REPLY将关闭连接。 设置此标志后,请确保不要让回复增加(即不要处理更多命令) */
        if (c->flags & REDIS_CLOSE_AFTER_REPLY) return;

        /* 确定request的类型 */
        if (!c->reqtype) {
        	/*一般我们使用redis的命令都是这种类型的,用*开头表示参数一共有几个
        	   简单来说,多条查询(REDIS_REQ_MULTIBULK)是一般客户端发送来的,
        	   而内联查询(REDIS_REQ_INLINE)则是 TELNET 发送来的 */
            if (c->querybuf[0] == '*') {
                c->reqtype = REDIS_REQ_MULTIBULK;
            } else {
                c->reqtype = REDIS_REQ_INLINE;
            }
        }

        if (c->reqtype == REDIS_REQ_INLINE) {
            if (processInlineBuffer(c) != REDIS_OK) break;
        } else if (c->reqtype == REDIS_REQ_MULTIBULK) {
        	/*内联查询平时用到的不多,所以我们主要看下多条查询的机制,详见下面的函数解析
        	  其主要是为了获取命令请求中的数据放入到c->argv中
        	 */
            if (processMultibulkBuffer(c) != REDIS_OK) break;
        } else {
            redisPanic("Unknown request type");
        }

        /* Multibulk processing could see a <= 0 length. */
        if (c->argc == 0) {
            resetClient(c);
        } else {
            /* 在上面processMultibulkBuffer获取到参数后,实际上调用processCommand来执行命令 */
            if (processCommand(c) == REDIS_OK)
                resetClient(c);
        }
    }
}

我们要清楚,加入我们的客户端发送为set number 12345命令,这就是设置了一个sds,内容为12345,但是这个发送到服务器端后为了标识清楚,数据会变成3$3set$6number$512345,其中3表示后面有3个参数,$3表示后的数据的长度为3个字符的长度

//这个函数有点长,需要一点耐心
//这个函数的主要功能是提取命令请求的各个参数
//假设我们在c->querybuf上放入的查询语句是*3$3set$6number$512345
//后面我会根据一步一步查询语句的变化来分析这个函数是如何提取出set number 12345这三个数据
int processMultibulkBuffer(redisClient *c) {
    char *newline = NULL;
    int pos = 0, ok;
    long long ll;

    if (c->multibulklen == 0) {
        /* The client should have been reset */
        redisAssertWithInfo(c,NULL,c->argc == 0);

        /* 实际上*3$3set$6number$512345的内部实际为*3\r\n$3\r\nset\r\n$6\r\nnumber\r\n$5\r\n12345\r\n
            这样newline的数据为\r\n$3\r\nset\r\n$6\r\nnumber\r\n$5\r\n12345\r\n
        */
        newline = strchr(c->querybuf,'\r');
        if (newline == NULL) {
        	/* 判断是否超过限制值*/
            if (sdslen(c->querybuf) > REDIS_INLINE_MAX_SIZE) {
                addReplyError(c,"Protocol error: too big mbulk count string");
                setProtocolError(c,0);
            }
            return REDIS_ERR;
        }

        /* 当这个条件符合时,说明newline的长度已经小于2个字节,这样就不能容纳下\r\n,但是我们最后必须为\r\n */
        if (newline-(c->querybuf) > ((signed)sdslen(c->querybuf)-2))
            return REDIS_ERR;

        /* We know for sure there is a whole line since newline != NULL,
         * so go ahead and find out the multi bulk length. */
        redisAssertWithInfo(c,NULL,c->querybuf[0] == '*');
        /* 获取第一个*3的数据,也就是获取到3这个值,其中3的值保存在ll中,ok返回的是是否获取成功,如果ok为1,表示获取成功
        因为newline-(c->querybuf+1)的值就是为指针的差值,这边的newline-(c->querybuf+1)值就是为1*/
        ok = string2ll(c->querybuf+1,newline-(c->querybuf+1),&ll);
        if (!ok || ll > 1024*1024) {
            addReplyError(c,"Protocol error: invalid multibulk length");
            setProtocolError(c,pos);
            return REDIS_ERR;
        }
		/*获取到3后还需要获取之后的值,但是首先需要跳过\r\n,所以这边还需要加上2 */
        pos = (newline-c->querybuf)+2;
        if (ll <= 0) {
        	/*如果ll为负值,则后面的值只有一个参数,这边将获取到后面所有的值 */
            c->querybuf = sdsrange(c->querybuf,pos,-1);
            return REDIS_OK;
        }
		/* 更新这个参数 */
        c->multibulklen = ll;

        /* Setup argv array on client structure */
        if (c->argv) zfree(c->argv);
        /* 后面的set number 12345这些数据都将存储在这个参数中 */
        c->argv = zmalloc(sizeof(robj*)*c->multibulklen);
    }

    redisAssertWithInfo(c,NULL,c->multibulklen > 0);
    /* 开始一个一个的获取 */
    while(c->multibulklen) {
        /* Read bulk length if unknown */
        if (c->bulklen == -1) {
        	/* 这时newline的值为\r\nset\r\n$6\r\nnumber\r\n$5\r\n12345\r\n */
            newline = strchr(c->querybuf+pos,'\r');
            if (newline == NULL) {
                if (sdslen(c->querybuf) > REDIS_INLINE_MAX_SIZE) {
                    addReplyError(c,"Protocol error: too big bulk count string");
                    setProtocolError(c,0);
                }
                break;
            }

            /* 再次确认这个参数最后是否含有\r\n */
            if (newline-(c->querybuf) > ((signed)sdslen(c->querybuf)-2))
                break;
			
			/*判断是否为$,如果不是则不符合条件,直接报错 */
            if (c->querybuf[pos] != '$') {
                addReplyErrorFormat(c,
                    "Protocol error: expected '$', got '%c'",
                    c->querybuf[pos]);
                setProtocolError(c,pos);
                return REDIS_ERR;
            }
			/*获取$后面的数据,然后跟据$后面的数据获取参数,依次进行,最终成功获取到所有的参数数据*/
            ok = string2ll(c->querybuf+pos+1,newline-(c->querybuf+pos+1),&ll);
            if (!ok || ll < 0 || ll > 512*1024*1024) {
                addReplyError(c,"Protocol error: invalid bulk length");
                setProtocolError(c,pos);
                return REDIS_ERR;
            }

            pos += newline-(c->querybuf+pos)+2;
            if (ll >= REDIS_MBULK_BIG_ARG) {
                /* If we are going to read a large object from network
                 * try to make it likely that it will start at c->querybuf
                 * boundary so that we can optimized object creation
                 * avoiding a large copy of data. */
                c->querybuf = sdsrange(c->querybuf,pos,-1);
                pos = 0;
                /* Hint the sds library about the amount of bytes this string is
                 * going to contain. */
                c->querybuf = sdsMakeRoomFor(c->querybuf,ll+2);
            }
            c->bulklen = ll;
        }

        /* Read bulk argument */
        if (sdslen(c->querybuf)-pos < (unsigned)(c->bulklen+2)) {
            /* Not enough data (+2 == trailing \r\n) */
            break;
        } else {
            /* Optimization: if the buffer contains JUST our bulk element
             * instead of creating a new object by *copying* the sds we
             * just use the current sds string. */
            if (pos == 0 &&
                c->bulklen >= REDIS_MBULK_BIG_ARG &&
                (signed) sdslen(c->querybuf) == c->bulklen+2)
            {
                c->argv[c->argc++] = createObject(REDIS_STRING,c->querybuf);
                sdsIncrLen(c->querybuf,-2); /* remove CRLF */
                c->querybuf = sdsempty();
                /* Assume that if we saw a fat argument we'll see another one
                 * likely... */
                c->querybuf = sdsMakeRoomFor(c->querybuf,c->bulklen+2);
                pos = 0;
            } else {
                c->argv[c->argc++] =
                    createStringObject(c->querybuf+pos,c->bulklen);
                pos += c->bulklen+2;
            }
            c->bulklen = -1;
            c->multibulklen--;
        }
    }

    /* 去除querybuf的参数 */
    if (pos) c->querybuf = sdsrange(c->querybuf,pos,-1);

    /* 只有最后multibulklen为0才是正常的,返回成功 */
    if (c->multibulklen == 0) return REDIS_OK;

    /* Still not read to process the command */
    return REDIS_ERR;
}

经过这一个函数后命令行的数据都将放到c->argv中,然后最后调用到processCommand来执行相对应的命令
因为篇幅原因,具体processCommand是如何执行的我将放到下一篇来具体讲解。

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值