从 setCommand->setGenericCommand->setKey->dictXXX这样一路追下来, 可以看到最终的操作, 是对于某个数据库实例中, 键空间的操作. 逻辑比较清晰. 其它的命令也基本类似, 鉴于Redis中的命令众多, 这里没有篇幅去一个一个的介绍, 也没有必要.
至此, 用户的命令经由服务端接收, 解析, 以及执行在了某个数据库实例上. 我们在一路跟代码的过程中, 随处可见的错误处理中都有addReplyXXX系列的函数调用, 在SET命令的最终执行中, 在setGenericCommand中, setKey操作成功后, 也会调用addReply. 显然, 这个函数是用于将服务端的回应数据写入到某个地方去的.
从逻辑上来讲, 服务端应当将每个命令的回应写入到一个缓冲区中, 然后在适宜的时刻, 序列化为回应体(符合协议规约的二进制数据), 经由网络IO回发给客户端. 接下来, 我们就从addReply入手, 看一看回包的流程.
7.4 写回包流程addReply系列函数众多, 有十几个, 其它函数诸如addReplyBulk, addReplyError, addReplyErrorFormat等, 只是一些简单的变体. 这一系列函数完成的功能都是类似的: 总结起来就是两点:
将回包信息写入回包缓冲区
addReply如下:
void addReply(client * c, robj * obj) { // 检查客户端是否可以写入回包数据, 这个函数在每个 addReplyXXX 函数中都会被首先调用 // 它的职责有: // 0. 多数情况下, 对于正常的客户端, 它会返回 C_OK. 并且如果该客户端的可写事件没有在事件处理器中注册回调的话, 它会将这个客户端先挂在 server.clients_pending_write 这个链表上 // 0. 在特殊情况下, 对于不能接收回包的客户端(比如这是一个假客户端, 没有数据连接sockfd, 这种客户端在AOF特性中有有到), 返回 C_ERR if (prepareClientToWrite(c) != C_OK) return; if (sdsEncodedObject(obj)) { // 如果回应数据, 是RAW或EMBSTR编码的string对象 if (_addReplyToBuffer(c, obj->ptr, sdslen(obj->ptr)) != C_OK) { // 先尝试调用 _addReplyToBuffer, 将回应数据写进 client->buf 缓冲区中 // 如果失败了, 则把回应数据挂到 client->reply 这个链表上去 _addReplyObjectToList(c, obj); } } else if (obj->encoding == OBJ_ENCODING_INT) // 如果回应数据是INT编码的string对象, 这里有优化操作 if (listLength(c->reply) == 0 && (sizeof(c->buf) - c->bufpos) >= 32) { char buf[32]; int len; // 由于INT编码的string对象内部是使用 redisObject->ptr 指针直接存储的数值 // 所以直接读这个指针的值, 将其转换为字符表示, 尝试存进 client->buf 缓冲区中 len = ll2string(buf, sizeof(buf), (long)obj->ptr); if (_addReplyToBuffer(c, buf, len) == C_OK) return; } // 如果存进 client->buf 失败, 就把对应的数值转换为 EMBSTR 或RAW 编码的string对象 // 挂在 client->reply 链表上 obj = getDecodedObject(obj); if (_addReplyToBuffer(c, obj->ptr, sdslen(obj->ptr)) != C_OK) _addReplyObjectToList(c, obj); decrRefCount(obj); } else { serverPanic("Wrong obj->encoding in addReply()") } }这里会看到, 回应缓冲区有两个:
client->buf是一个二进制的回应缓冲区, 但它的长度是有限的. 默认长度也是16KB, 受宏PROTO_REPLY_CHUNK_BYTES的值限定
client->reply是一个对象缓冲区, 它是一个链表, 无容量限制, 该链表中挂的应该都是以 RAW 或 EMBSTR 编码的 string 对象.
我们在addReply中会看到, 总是试图先把回应数据先通过_addReplyToBuffer添加到client->buf中去, 如果不成功的话, 再把数据通过_addReplyObjectToList挂在client->reply这个链表中去.
这个也很好理解, 当client->buf写满的时候, 再写不下下一个addReply要添加的数据时, 就会退而求其次, 将这个回应数据先挂在client->reply中去. client->buf就是每次调用网络IO时, 一次性写入的数据量. 如果超过这个量了, 会分多次写回应.