|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 | |
在上一篇《客户端请求处理完整链路源码分析》中,我们走完了一条命令从网络读取、协议解析、命令执行到响应写回的完整链路。同学们可能已经注意到:每次 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 循环里:
processMultibulkBuffer()尝试从querybuf + qb_pos位置解析出一条完整的Redis 命令,将参数填入c->argv。- 如果数据不足(命令不完整),返回
C_ERR,break退出循环,等待更多数据到达。 - 如果解析成功,
processCommand()执行该命令,将响应写入输出缓冲区(见下一节)。 resetClient()清理argv等字段,准备解析下一条命令。- 循环回到
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关键点:在 processInputBuffer 的 while 循环中,每执行完一条命令,响应只是追加到输出缓冲区,并不会立刻发送。多条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 中被调用:
- 遍历待写队列:取出
server.clients_pending_write链表中的所有客户端,清除CLIENT_PENDING_WRITE标记并从队列中移除。 - 优先同步写入:对每个客户端先调用
writeToClient(),尝试直接将缓冲区数据写入socket。在大多数情况下,socket 发送缓冲区未满,所有响应数据可以一次性写完——这意味着 Pipeline中多条命令的响应被一个write()系统调用批量发出,无需注册任何可写事件。 - 降级异步写入:如果同步写完后仍有数据未发出去(socket发送缓冲区已满),才注册
AE_WRITABLE事件,由sendReplyToClient在后续事件循环中异步发送。 - 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 定义为 64KB(src/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;
}
redisBufferWrite 将 obuf 中的数据通过 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 模式的特殊之处:
- 不从stdin逐行解析命令,而是直接读取原始RESP文本,零解析开销
- 写和读并行,不等待每条命令的回复
- 使用一个随机ECHO命令作为结束标记,检测所有命令是否已执行完毕
五、CLIENT REPLY OFF/SKIP:Pipeline的响应优化
Redis 3.2 引入了 CLIENT REPLY OFF 和 CLIENT 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数据结构底层实现的学习,从SDS、ZipList、QuickList、Dict、IntSet、SkipList、Ziplist 到Redis对象系统的多态编码,进一步深入的探究Redis数据结构的精妙设计。
|
欢迎各位同学关注我哦~
在这个 AI 喧嚣的时代 不忘初心,戒骄戒躁,认真沉淀 | |

5万+

被折叠的 条评论
为什么被折叠?



