Redis中单机数据库的实现 (7)

我们先从宏观上看一下, 服务端是如何解析请求, 并进行逻辑处理的. 先从服务端数据连接的处理回调readQueryFromClient看起:

void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) { client *c = (client*) privdata; int nread, readlen; size_t qblen; UNUSED(el); UNUSED(mask); readlen = PROTO_IOBUF_LEN; // 对于大参数的优化 if (c->reqtype == PROTO_REQ_MULTIBULK && c->multibulklen && c->bulklen != -1 && c->bulklen >= PROTO_MBULK_BIG_ARG) { // 如果读取到了一个超大参数, 那么先把这个超大参数读进来 // 保证缓冲区中超大参数之后没有其它数据 ssize_t remaining = (size_t)(c->bulklen+2)-sdslen(c->querybuf); if (remaining < readlen) readlen = remaining; } // 接收数据 qblen = sdslen(c->querybuf); if (c->querybuf_peak < qblen) c->querybuf_peak = qblen; c->querybuf = sdsMakeRoomFor(c->querybuf, readlen); nread = read(fd, c->querybuf+qblen, readlen); // ... sdsIncrLen(c->querybuf,nread); c->lastinteraction = server.unixtime; if (c->flags & CLIENT_MASTER) c->read_reploff += nread; server.stat_net_input_bytes += nread; if (sdslen(c->querybuf) > server.client_max_querybuf_len) { // client的缓冲区中数据如果超长了, 最有可能的原因就是服务端处理能力不够, 数据堆积了 // 或者是受恶意攻击了 // 这种情况下, 干掉这个客户端 // ... return; } // 处理缓冲区的二进制数据 if (!(c->flags & CLIENT_MASTER)) { processInputBuffer(c); } else { // cluster特性相关的操作, 暂时忽略 // ... } }

这里有一个优化写法, 是针对特大参数的读取的. 在介绍这个优化方法之前, 先大致的看一下作为单机数据库, 服务端请求客户端请求的概览流程(你可能需要在阅读完以下几节之后再回头来看这里):

一般情况下, 请求体的长度不会超过16kb(即使包括多个命令), 那么处理流程(假设请求是协议数据, 而非内联命令)是这样的:

handle_request_1

而正常情况下, 一个请求体的长度不应该超过16kb, 而如果请求体的长度超过16kb的话, 有下面两种可能:

请求中的命令数量太多, 比如平均一个命令及其参数, 占用100字节, 而这个请求体中包含了200个命令

请求中命令数量不多, 但是有一个命令携带了超大参数, 比如, 只是一个SET单命令请求, 但其值参数是一个长度为40kb的二进制数据

附带多个命令, 导致请求体超过16kb时

在第一种情况下, 如果满足以下条件的话, 这个请求会被服务端当作两个请求去处理:

初次数据连接的可读回调接收了16kb的数据, 这16kb的数据恰好是多个请求. 没有请求被截断! 即是一个20kb的请求体, 被天然的拆分成了两个请求, 第一个请求恰好16kb, 一字节不多, 一字节不少, 第二个请求是剩余的4kb. 那么服务端看来, 这就是两个多命令请求

这种情况极其罕见, Redis中没有对这种情况做处理.

一般情况下, 如果发送一个20kb的多命令请求, 且请求中没有超大参数的话(意味着命令特别多, 上百个), 那么总会有一个命令被截断, 如果被阶段, 第一次的处理流程就会在处理最后一个被截断的半截命令时, 在如图红线处流程终止, 而仅在第二次处理流程, 即处理剩余的4kb数据时, 才会走到蓝线处. 即两次收包, 一次写回包:

handle_request_2

附带超大参数, 导致请求体超过16kb

在第二种情况下, 由于某几个参数特别巨大(大于PROTO_MBULK_BIG_ARG宏的值, 即32kb), 导致请求体一次不能接收完毕. 势必也要进行多次接收. 本来, 朴素的思想上, 只需要在遇到超大参数的时候, 调整16kb这个阈值即可. 比如一个参数为40kb的二进制数据, 那么在接收到这个参数时, 破例调用read时一次性把这个参数读取完整即可. 除此外不需要什么特殊处理.

即是, 处理流程应当如下:

内容版权声明:除非注明,否则皆为本站原创文章。

转载注明出处:https://www.heiqu.com/wpsydf.html