Redis源码探究系列—Pipeline 在源码中是如何支持的?

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode

在上一篇《客户端请求处理完整链路源码分析》中,我们走完了一条命令从网络读取、协议解析、命令执行到响应写回的完整链路。同学们可能已经注意到:每次 redisCommand 都要等上一条命令的回复回来,才能发下一条,那么N条命令就是N次RTT。那能不能把多条命令打包一次发过去,回复也一次性收回来呢?这就是Pipeline做的事:将多条命令打包一次性发送给服务端,服务端依次处理后将所有响应一次性返回,从而将N次RTT压缩为1次。

普通模式:
  Client → SET a 1     → Server   (RTT 1)
  Client ← OK          ← Server
  Client → SET b 2     → Server   (RTT 2)
  Client ← OK          ← Server
  Client → GET a       → Server   (RTT 3)
  Client ← 1           ← Server

Pipeline模式:
  Client → SET a 1 \r\n SET b 2 \r\n GET a \r\n  → Server  (RTT 1)
  Client ← OK \r\n OK \r\n 1 \r\n               ← Server

Redis服务端没有一条专门的"pipeline"命令或协议字段。Pipeline能工作,完全依赖于 Redis 现有的请求处理机制——输入缓冲区批量读取 + 循环处理 + 输出缓冲区延迟刷写。下面我们从源码角度逐一拆解。如果你没有看过之前的《客户端请求处理完整链路源码分析》这篇文章,建议可以先看一下这一篇文章之后再回过头来看这篇文章,这样理解起来会更加顺畅。甚至于在前面的文章中,聪明的你就应该可以想到Redis的Pipline实现的机制。

一、服务端:输入侧—一次读取,循环处理

1.1 读取数据到querybuf

当客户端连接可读时,事件循环触发 readQueryFromClient

// src/networking.c:1492
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
    client *c = (client*)privdata;
    // ...
    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);   // 一次性读到querybuf
    // ...
    sdsIncrLen(c->querybuf, nread);
    // ...
    processInputBufferAndReplicate(c);  // 进入处理流程
}

核心点:read() 系统调用一次性把TCP接收缓冲区中的数据全部读到 c->querybuf 中。如果客户端通过Pipeline发送了100条命令,这100条命令的文本会被一次性读到querybuf,而不是分100次读取。

// src/server.h:717
typedef struct client {
    sds querybuf;           // 输入缓冲区,累积客户端发来的查询
    size_t qb_pos;          // 当前在querybuf中已处理到的位置
    size_t querybuf_peak;   // querybuf最近的峰值大小
    int multibulklen;       // 当前命令还剩多少个bulk参数未读取
    long bulklen;           // 当前bulk参数的长度
    // ...
} client;

1.2 循环解析和执行:processInputBuffer

processInputBuffer 是Pipeline的心脏——它在一个 while 循环中不断从 querybuf 中解析并执行命令,直到缓冲区被消费完毕:

// src/networking.c:1406
void processInputBuffer(client *c) {
    server.current_client = c;

    while(c->qb_pos < sdslen(c->querybuf)) {
        // 各种中断检查:客户端暂停、阻塞、关闭等

        if (!c->reqtype) {
            if (c->querybuf[c->qb_pos] == '*') {
                c->reqtype = PROTO_REQ_MULTIBULK;
            } else {
                c->reqtype = PROTO_REQ_INLINE;
            }
        }

        if (c->reqtype == PROTO_REQ_MULTIBULK) {
            if (processMultibulkBuffer(c) != C_OK) break; // 解析一条命令
        }

        if (c->argc == 0) {
            resetClient(c);
        } else {
            if (processCommand(c) == C_OK) {    // 执行一条命令
                // ...
                resetClient(c);
            }
        }
    }

    /* Trim to pos — 把已处理的部分裁掉 */
    if (c->qb_pos) {
        sdsrange(c->querybuf, c->qb_pos, -1);
        c->qb_pos = 0;
    }
    server.current_client = NULL;
}

Pipeline的工作原理就在这个 while 循环里:

  1. processMultibulkBuffer() 尝试从 querybuf + qb_pos 位置解析出一条完整的Redis 命令,将参数填入 c->argv
  2. 如果数据不足(命令不完整),返回 C_ERRbreak 退出循环,等待更多数据到达。
  3. 如果解析成功,processCommand() 执行该命令,将响应写入输出缓冲区(见下一节)。
  4. resetClient() 清理 argv 等字段,准备解析下一条命令。
  5. 循环回到 while,继续从 querybuf 中解析下一条命令。

这就是Pipeline无需特殊协议支持的根本原因:Redis本来就是从输入缓冲区循环取命令执行的,多条命令拼在一起发送,只是让这个循环多跑几轮而已。

1.3 协议解析:processMultibulkBuffer

// src/networking.c:1255
int processMultibulkBuffer(client *c) {
    // ...

    /* 读 *<count>\r\n */
    if (c->multibulklen == 0) {
        newline = strchr(c->querybuf + c->qb_pos, '\r');
        // ...
        ok = string2ll(c->querybuf+1+c->qb_pos, newline-(c->querybuf+1+c->qb_pos), &ll);
        c->multibulklen = ll;
        c->argv = zmalloc(sizeof(robj*)*c->multibulklen);
    }

    /* 循环读取每个 $<len>\r\n<data>\r\n */
    while(c->multibulklen) {
        if (c->bulklen == -1) {
            // 解析 $<len>\r\n
            newline = strchr(c->querybuf + c->qb_pos, '\r');
            // ...
            ok = string2ll(c->querybuf+c->qb_pos+1, newline-(c->querybuf+c->qb_pos+1), &ll);
            c->bulklen = ll;
            c->qb_pos = (newline-c->querybuf)+2;
        }

        // 读取bulk data
        // 优化:大参数直接复用sds,零拷贝
        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);  // 移除尾部的\r\n
            c->querybuf = sdsnewlen(SDS_NOINIT, c->bulklen+2);  // 预分配新缓冲区
            sdsclear(c->querybuf);  // 重置长度为0
        } else {
            c->argv[c->argc++] = createStringObject(c->querybuf+c->qb_pos, c->bulklen);
            c->qb_pos += c->bulklen+2;
        }
        c->bulklen = -1;
        c->multibulklen--;
    }

    if (c->multibulklen == 0) return C_OK;  // 一条完整命令解析完毕
    return C_ERR;  // 数据不完整,等待更多数据
}

注意 PROTO_MBULK_BIG_ARG 的优化(src/server.h:188,值为 32KB):当某个参数很大且独占整个 querybuf 时,直接把 querybuf 的sds作为对象,避免内存拷贝。

二、服务端:输出侧——缓冲聚合,延迟刷写

2.1 双层输出缓冲区

每个客户端的输出缓冲区由两层组成:

// src/server.h:735-773
typedef struct client {
    // 固定大小缓冲区(16KB),用于小响应
    int bufpos;
    char buf[PROTO_REPLY_CHUNK_BYTES];  // PROTO_REPLY_CHUNK_BYTES = 16*1024

    // 链表缓冲区,用于大响应或响应累积过多时
    list *reply;                         // reply链表节点
    unsigned long long reply_bytes;      // reply链表总字节数
    size_t sentlen;                      // 当前正在发送的块已发送字节数
} client;
// src/server.h:636
typedef struct clientReplyBlock {
    size_t size, used;
    char buf[];
} clientReplyBlock;

为什么是两层? 小响应直接写入固定数组 buf,避免链表节点的内存分配开销;大响应或 buf 满了之后,降级到链表 reply 中动态扩展。

2.2 写入响应:addReply 家族

// src/networking.c:297
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);
    }
}

优先写入固定缓冲区,失败则降级到链表:

// src/networking.c:238
int _addReplyToBuffer(client *c, const char *s, size_t len) {
    size_t available = sizeof(c->buf) - c->bufpos;

    if (c->flags & CLIENT_CLOSE_AFTER_REPLY) return C_OK;

    /* 如果reply链表已有数据,就不能再写buf了(保证顺序) */
    if (listLength(c->reply) > 0) return C_ERR;

    if (len > available) return C_ERR;

    memcpy(c->buf + c->bufpos, s, len);
    c->bufpos += len;
    return C_OK;
}
// src/networking.c:255
void _addReplyStringToList(client *c, const char *s, size_t len) {
    if (c->flags & CLIENT_CLOSE_AFTER_REPLY) return;

    listNode *ln = listLast(c->reply);
    clientReplyBlock *tail = ln ? listNodeValue(ln) : NULL;

    /* 尝试追加到尾部节点的剩余空间 */
    if (tail) {
        size_t avail = tail->size - tail->used;
        size_t copy = avail >= len ? len : avail;
        memcpy(tail->buf + tail->used, s, copy);
        tail->used += copy;
        s += copy;
        len -= copy;
    }
    if (len) {
        /* 新建节点,最少分配PROTO_REPLY_CHUNK_BYTES (16KB) */
        size_t size = len < PROTO_REPLY_CHUNK_BYTES ? PROTO_REPLY_CHUNK_BYTES : len;
        tail = zmalloc(size + sizeof(clientReplyBlock));
        tail->size = zmalloc_usable(tail) - sizeof(clientReplyBlock);
        tail->used = len;
        memcpy(tail->buf, s, len);
        listAddNodeTail(c->reply, tail);
        c->reply_bytes += tail->size;
    }
    asyncCloseClientOnOutputBufferLimitReached(c);
}

Pipeline关键点:在 processInputBufferwhile 循环中,每执行完一条命令,响应只是追加到输出缓冲区,并不会立刻发送。多条Pipeline命令的响应被聚合在一起,最终一次性发送。

2.3 准备写入:prepareClientToWrite

// src/networking.c:211
int prepareClientToWrite(client *c) {
    if (c->flags & (CLIENT_LUA|CLIENT_MODULE)) return C_OK;

    // CLIENT REPLY OFF / SKIP: 不发送回复
    if (c->flags & (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) return C_ERR;

    if ((c->flags & CLIENT_MASTER) &&
        !(c->flags & CLIENT_MASTER_FORCE_REPLY)) return C_ERR;

    if (c->fd <= 0) return C_ERR;

    // 如果客户端还没有待发送数据,注册到待写队列
    if (!clientHasPendingReplies(c)) clientInstallWriteHandler(c);

    return C_OK;
}

2.4 注册待写:clientInstallWriteHandler

// src/networking.c:170
void clientInstallWriteHandler(client *c) {
    if (!(c->flags & CLIENT_PENDING_WRITE) &&
        (c->replstate == REPL_STATE_NONE ||
         (c->replstate == SLAVE_STATE_ONLINE && !c->repl_put_online_on_ack)))
    {
        c->flags |= CLIENT_PENDING_WRITE;
        listAddNodeHead(server.clients_pending_write, c);
    }
}

这里没有立刻注册 AE_WRITABLE 事件,而是将客户端加入 server.clients_pending_write 队列,等到事件循环的 beforeSleep 阶段统一处理。这是一个重要的性能优化——避免了频繁的 aeCreateFileEvent 系统调用。

2.5 beforeSleep中刷写:handleClientsWithPendingWrites

// src/server.c:1358
void beforeSleep(struct aeEventLoop *eventLoop) {
    // ... 其他处理 ...

    handleClientsWithPendingWrites();   // 关键!
}
// src/networking.c:1064
int handleClientsWithPendingWrites(void) {
    listIter li;
    listNode *ln;
    int processed = listLength(server.clients_pending_write);

    listRewind(server.clients_pending_write, &li);
    while((ln = listNext(&li))) {
        client *c = listNodeValue(ln);
        c->flags &= ~CLIENT_PENDING_WRITE;
        listDelNode(server.clients_pending_write, ln);

        if (c->flags & CLIENT_PROTECTED) continue;

        /* 同步写入:尝试直接写到 socket */
        if (writeToClient(c->fd, c, 0) == C_ERR) continue;

        /* 如果同步写完了还有数据(socket 缓冲区满了),才注册可写事件 */
        if (clientHasPendingReplies(c)) {
            int ae_flags = AE_WRITABLE;
            if (server.aof_state == AOF_ON &&
                server.aof_fsync == AOF_FSYNC_ALWAYS)
            {
                ae_flags |= AE_BARRIER;
            }
            if (aeCreateFileEvent(server.el, c->fd, ae_flags,
                                  sendReplyToClient, c) == AE_ERR)
            {
                freeClientAsync(c);
            }
        }
    }
    return processed;
}

这个函数是Pipeline响应刷写的核心调度入口,在 beforeSleep 中被调用:

  1. 遍历待写队列:取出 server.clients_pending_write 链表中的所有客户端,清除 CLIENT_PENDING_WRITE 标记并从队列中移除。
  2. 优先同步写入:对每个客户端先调用 writeToClient(),尝试直接将缓冲区数据写入socket。在大多数情况下,socket 发送缓冲区未满,所有响应数据可以一次性写完——这意味着 Pipeline中多条命令的响应被一个 write() 系统调用批量发出,无需注册任何可写事件。
  3. 降级异步写入:如果同步写完后仍有数据未发出去(socket发送缓冲区已满),才注册 AE_WRITABLE 事件,由 sendReplyToClient 在后续事件循环中异步发送。
  4. AE_BARRIER保护:当AOF策略为 always 时,为可写事件附加 AE_BARRIER 标志,确保先处理读事件(AOF刷盘)再处理写事件(发送响应),避免客户端在数据落盘前就收到成功回复。

processInputBuffer 的while循环中,N条命令的响应只是不断追加到输出缓冲区;直到 beforeSleep 阶段,handleClientsWithPendingWrites 才统一将这N条响应一次性写回客户端。这正是Pipeline能将N次RTT压缩为1次的服务端保障——写入端也做了批量聚合。

2.6 实际发送:writeToClient

// src/networking.c:961
int writeToClient(int fd, client *c, int handler_installed) {
    ssize_t nwritten = 0, totwritten = 0;

    while(clientHasPendingReplies(c)) {
        if (c->bufpos > 0) {
            // 先发固定缓冲区
            nwritten = write(fd, c->buf + c->sentlen, c->bufpos - c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            if ((int)c->sentlen == c->bufpos) {
                c->bufpos = 0;
                c->sentlen = 0;
            }
        } else {
            // 再发 reply 链表
            clientReplyBlock *o = listNodeValue(listFirst(c->reply));
            size_t objlen = o->used;
            // ...
            nwritten = write(fd, o->buf + c->sentlen, objlen - c->sentlen);
            if (nwritten <= 0) break;
            c->sentlen += nwritten;
            totwritten += nwritten;

            if (c->sentlen == objlen) {
                c->reply_bytes -= o->size;
                listDelNode(c->reply, listFirst(c->reply));
                c->sentlen = 0;
            }
        }

        /* 单次写操作上限:64KB,防止单客户端独占 CPU */
        if (totwritten > NET_MAX_WRITES_PER_EVENT &&
            (server.maxmemory == 0 ||
             zmalloc_used_memory() < server.maxmemory) &&
            !(c->flags & CLIENT_SLAVE)) break;
    }
    // ...
}

NET_MAX_WRITES_PER_EVENT 定义为 64KBsrc/server.h:92),这是公平性保障:避免一个客户端发送海量Pipeline命令后,其响应写入独占整个事件循环。

三、客户端:hiredis如何支持Pipeline

hiredis是Redis官方提供的C语言客户端库,源码位于 deps/hiredis/ 目录下。它封装了与 Redis Server的网络通信和RESP协议解析等底层细节,提供了同步、异步以及Pipeline三种使用模式。Redis自带的 redis-cli 工具以及许多第三方Redis客户端都基于它构建。下面我们看它如何支持Pipeline。

3.1 普通模式 vs Pipeline模式

在hiredis中,普通模式使用 redisCommand,Pipeline模式使用 redisAppendCommand + redisGetReply

普通模式

// deps/hiredis/hiredis.c:1008
void *redisCommand(redisContext *c, const char *format, ...) {
    va_list ap;
    void *reply = NULL;
    va_start(ap, format);
    reply = redisvCommand(c, format, ap);
    va_end(ap);
    return reply;
}

void *redisvCommand(redisContext *c, const char *format, va_list ap) {
    if (redisvAppendCommand(c, format, ap) != REDIS_OK)  // 1. 追加到输出缓冲区
        return NULL;
    return __redisBlockForReply(c);                       // 2. 立刻发送并等待回复
}

static void *__redisBlockForReply(redisContext *c) {
    void *reply;
    if (c->flags & REDIS_BLOCK) {
        if (redisGetReply(c, &reply) != REDIS_OK)
            return NULL;
        return reply;
    }
    return NULL;
}

每次 redisCommand 都是:追加命令 → 发送 → 等待回复。N条命令 = N次RTT。

Pipeline模式

// 步骤 1:连续追加多条命令到输出缓冲区
redisAppendCommand(c, "SET a 1");
redisAppendCommand(c, "SET b 2");
redisAppendCommand(c, "GET a");

// 步骤 2:统一发送并逐条读取回复
void *reply;
redisGetReply(c, &reply);  // 第一条回复
freeReplyObject(reply);
redisGetReply(c, &reply);  // 第二条回复
freeReplyObject(reply);
redisGetReply(c, &reply);  // 第三条回复
freeReplyObject(reply);

3.2 redisAppendCommand:只追加,不发送

// deps/hiredis/hiredis.c:951
int redisAppendCommand(redisContext *c, const char *format, ...) {
    va_list ap;
    int ret;
    va_start(ap, format);
    ret = redisvAppendCommand(c, format, ap);
    va_end(ap);
    return ret;
}

int redisvAppendCommand(redisContext *c, const char *format, va_list ap) {
    char *cmd;
    int len;
    len = redisvFormatCommand(&cmd, format, ap);  // 格式化为 RESP 协议文本
    // ...
    if (__redisAppendCommand(c, cmd, len) != REDIS_OK) {
        free(cmd);
        return REDIS_ERR;
    }
    free(cmd);
    return REDIS_OK;
}

int __redisAppendCommand(redisContext *c, const char *cmd, size_t len) {
    sds newbuf;
    newbuf = sdscatlen(c->obuf, cmd, len);  // 追加到输出缓冲区 obuf
    if (newbuf == NULL) {
        __redisSetError(c, REDIS_ERR_OOM, "Out of memory");
        return REDIS_ERR;
    }
    c->obuf = newbuf;
    return REDIS_OK;
}

redisContext 的定义:

// deps/hiredis/hiredis.h:140
typedef struct redisContext {
    int err;
    char errstr[128];
    int fd;
    int flags;
    char *obuf;          // 输出缓冲区 — 所有待发送的命令文本
    redisReader *reader; // 协议读取器 — 解析服务端的回复
    // ...
} redisContext;

多次调用 redisAppendCommand,命令文本只是追加到 c->obuf,并不发送。

3.3 redisGetReply:发送 + 接收

// deps/hiredis/hiredis.c:870
int redisGetReply(redisContext *c, void **reply) {
    int wdone = 0;
    void *aux = NULL;

    /* 先尝试从 reader 中获取已读到的回复 */
    if (redisGetReplyFromReader(c, &aux) == REDIS_ERR)
        return REDIS_ERR;

    /* 如果 reader 中没有回复,且是阻塞模式,则发送并读取 */
    if (aux == NULL && c->flags & REDIS_BLOCK) {
        /* 发送 obuf 中的所有数据 */
        do {
            if (redisBufferWrite(c, &wdone) == REDIS_ERR)
                return REDIS_ERR;
        } while (!wdone);

        /* 读取服务端回复 */
        do {
            if (redisBufferRead(c) == REDIS_ERR)
                return REDIS_ERR;
            if (redisGetReplyFromReader(c, &aux) == REDIS_ERR)
                return REDIS_ERR;
        } while (aux == NULL);
    }

    if (reply != NULL) *reply = aux;
    return REDIS_OK;
}

redisBufferWriteobuf 中的数据通过 write() 发送出去:

// deps/hiredis/hiredis.c:831
int redisBufferWrite(redisContext *c, int *done) {
    int nwritten;
    if (c->err) return REDIS_ERR;

    if (sdslen(c->obuf) > 0) {
        nwritten = write(c->fd, c->obuf, sdslen(c->obuf));
        // ...
        if (nwritten > 0) {
            if (nwritten == (signed)sdslen(c->obuf)) {
                sdsfree(c->obuf);
                c->obuf = sdsempty();
            } else {
                sdsrange(c->obuf, nwritten, -1);  // 只写了部分,保留剩余
            }
        }
    }
    if (done != NULL) *done = (sdslen(c->obuf) == 0);
    return REDIS_OK;
}

redisBufferRead 从socket读取数据并喂给协议解析器:

// deps/hiredis/hiredis.c:794
int redisBufferRead(redisContext *c) {
    char buf[1024*16];
    int nread;
    if (c->err) return REDIS_ERR;

    nread = read(c->fd, buf, sizeof(buf));
    // ...
    if (nread > 0) {
        if (redisReaderFeed(c->reader, buf, nread) != REDIS_OK) {
            __redisSetError(c, c->reader->err, c->reader->errstr);
            return REDIS_ERR;
        }
    }
    return REDIS_OK;
}

Pipeline模式下第一次调用 redisGetReply 时,obuf 中累积了所有命令,redisBufferWrite 会一次性全部发送。后续 redisGetReply 调用时,obuf已空,只需从reader中逐条取出回复即可。

3.4 客户端Pipeline流程

redisAppendCommand(c, "SET a 1")   → obuf += "SET a 1\r\n"
redisAppendCommand(c, "SET b 2")   → obuf += "SET b 2\r\n"
redisAppendCommand(c, "GET a")     → obuf += "GET a\r\n"

第一次 redisGetReply():
  redisBufferWrite() → write(obuf) → 一次性发送所有命令  ← 只有 1 次 RTT
  redisBufferRead()  → read() → 读取服务端所有响应到 reader
  redisReaderGetReply() → 取出第一条回复 OK

第二次 redisGetReply():
  redisReaderGetReply() → 取出第二条回复 OK  ← 直接从 reader 取,无需网络 I/O

第三次 redisGetReply():
  redisReaderGetReply() → 取出第三条回复 "1"

四、redis-cli 的 --pipe 模式

redis-cli --pipe 是一种更极致的Pipeline方式,用于批量导入数据。它从stdin读取原始RESP协议文本,直接写到socket,同时异步读取服务端回复:

// src/redis-cli.c : pipeMode函数(简化逻辑)
void pipeMode(void) {
    // ...
    while(1) {
        // 向socket写入stdin读取的命令
        if (obuf_len != 0) {
            nwritten = write(context->fd, obuf + obuf_pos, obuf_len);
            // ...
        }

        // 从socket读取回复
        // 使用ECHO命令作为结束标记
        // stdin读完后,发送一个特殊的ECHO命令
        // 当读到该ECHO的回复时,说明所有命令都已执行完毕
    }
}

--pipe 模式的特殊之处:

  1. 不从stdin逐行解析命令,而是直接读取原始RESP文本,零解析开销
  2. 写和读并行,不等待每条命令的回复
  3. 使用一个随机ECHO命令作为结束标记,检测所有命令是否已执行完毕

五、CLIENT REPLY OFF/SKIP:Pipeline的响应优化

Redis 3.2 引入了 CLIENT REPLY OFFCLIENT REPLY SKIP,允许客户端在Pipeline中选择性地关闭响应,进一步减少网络传输:

// src/server.h:253
#define CLIENT_REPLY_OFF (1<<22)       // 关闭所有回复
#define CLIENT_REPLY_SKIP_NEXT (1<<23) // 下一条命令跳过回复
#define CLIENT_REPLY_SKIP (1<<24)      // 当前命令跳过回复

prepareClientToWrite 中的检查:

// src/networking.c:217
if (c->flags & (CLIENT_REPLY_OFF|CLIENT_REPLY_SKIP)) return C_ERR;

返回 C_ERR 后,addReply 系列函数直接跳过,响应根本不会写入输出缓冲区。对于只需要执行而不关心返回值的Pipeline场景(如批量SET),可以显著减少响应数据量。

六、事件循环的配合

6.1 aeMain循环

// src/ae.c (简化)
void aeMain(aeEventLoop *eventLoop) {
    eventLoop->stop = 0;
    while (!eventLoop->stop) {
        if (eventLoop->beforesleep != NULL)
            eventLoop->beforesleep(eventLoop);       // ← beforeSleep在这里

        aeProcessEvents(eventLoop, AE_ALL_EVENTS);   // ← 事件处理
    }
}

6.2 事件分发中的读写顺序

// src/ae.c:423
int invert = fe->mask & AE_BARRIER;

if (!invert && fe->mask & mask & AE_READABLE) {
    fe->rfileProc(eventLoop, fd, fe->clientData, mask);  // 先读
}
if (fe->mask & mask & AE_WRITABLE) {
    fe->wfileProc(eventLoop, fd, fe->clientData, mask);  // 后写
}
if (invert && fe->mask & mask & AE_READABLE) {
    fe->rfileProc(eventLoop, fd, fe->clientData, mask);  // AE_BARRIER时反转
}

默认先读后写——读到Pipeline命令后,响应先缓冲,在 beforeSleep 中统一写回。AE_BARRIER 标志可以反转顺序,用于AOF fsync=always 场景下保证数据先落盘再回复客户端。

通过从Redis服务端和客户端两个角度的分析,我们可以知道其实在Redis服务端并不需要知道客户端是否在使用Pipeline。Pipeline的本质是客户端行为——将多条命令打包发送,利用服务端已有的"输入缓冲区批量读取 + 循环处理 + 输出缓冲区延迟刷写"机制,自然实现了命令批量执行和响应批量返回。Redis的RESP协议天然支持这种流式处理,因为每条命令都有明确的边界标识(\r\n),服务端可以无歧义地从字节流中切分出独立的命令。

至此,我们从整体架构、启动流程、配置加载、事件循环模型,一路走到Reactor模式、文件/时间事件、epoll/kqueue封装、高并发连接处理、客户端请求完整链路,再到 Pipeline 的源码支持已全部完成。我们已经完整的学习了Redis是如何接收请求、处理命令、返回响应的,也就是搞清楚了Redis的"神经系统"。

但 Redis 之所以被称为数据结构服务器,核心不在于网络模型,而在于它在内存中精心实现的一系列高效数据结构。为什么String的SDS要和C字符串不一样?为什么 ZipList 能把小数据压缩到极致?为什么 Skiplist 被选作 Zset 的底层实现而非红黑树?这些问题的答案,决定了 Redis 的性能天花板。

下一篇开始,我们将进入Redis数据结构底层实现的学习,从SDSZipListQuickListDictIntSetSkipListZiplist 到Redis对象系统的多态编码,进一步深入的探究Redis数据结构的精妙设计。

欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代
不忘初心,戒骄戒躁,认真沉淀
Deepincode
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值