Redis 命令执行
这篇看一条命令从客户端发到 Redis 后,源码里大概怎么走。例子还是:
BASH
SET name alice客户端一般发的是 RESP:
TEXT
*3\r\n$3\r\nSET\r\n$4\r\nname\r\n$5\r\nalice\r\n主线可以压成这一条:
TEXT
readQueryFromClient
-> processInputBuffer
-> processMultibulkBuffer / processInlineBuffer
-> preprocessCommand
-> processCommandAndResetClient
-> processCommand
-> call
-> setCommand
-> addReply
-> handleClientsWithPendingWrites / writeToClient主要源码:
| 环节 | 源码 |
|---|---|
| 读请求、协议解析、回写 | src/networking.c |
| 查命令、校验、调度 | src/server.c / src/server.h |
| 命令声明 | src/commands/*.json / src/commands.def |
SET 实现 | src/t_string.c |
| 写入数据库 | src/db.c |
client:一条连接的执行现场
先看 server.h 里的 client,删掉大量无关字段后是这样:
C
typedef struct client {
connection *conn; /* socket/连接抽象 */
redisDb *db; /* 当前 SELECT 到的 DB */
sds querybuf; /* socket 读进来的原始字节 */
size_t qb_pos; /* querybuf 解析到哪里 */
int argc; /* 当前命令参数个数 */
robj **argv; /* 当前命令参数对象 */
pendingCommandList pending_cmds; /* 已解析、待执行的命令 */
pendingCommand *current_pending_cmd;
struct redisCommand *cmd; /* 当前命令 */
struct redisCommand *lastcmd; /* 上一条命令,用于复用查表结果 */
struct redisCommand *lookedcmd; /* lookahead 阶段提前查到的命令 */
struct redisCommand *realcmd; /* 真正用于统计/错误的命令 */
int reqtype; /* inline 还是 multibulk */
int multibulklen; /* RESP 数组还剩几个 bulk */
long bulklen; /* 当前 bulk 的长度 */
list *reply; /* 大回复链表 */
size_t sentlen; /* 当前回复已经写出去多少 */
} client;所以 Redis 不是每来一条命令就创建一个执行对象,而是复用连接上的 client:
TEXT
querybuf 保存字节
pending_cmds 保存解析好的命令
argc/argv 指向当前正在执行的那条命令
cmd 指向命令表里的 redisCommand
reply 保存待写回的 RESP 回复读请求:connRead 追加到 querybuf
客户端可读时,事件循环会调用 networking.c:readQueryFromClient。核心逻辑删减后是:
C
void readQueryFromClient(connection *conn) {
client *c = connGetPrivateData(conn);
/* 1. 给 querybuf 预留空间 */
c->querybuf = sdsMakeRoomFor(c->querybuf, readlen);
/* 2. 从 socket 读字节,追加到 querybuf 尾部 */
qblen = sdslen(c->querybuf);
nread = connRead(c->conn, c->querybuf + qblen, readlen);
if (nread <= 0) {
freeClientAsync(c);
goto done;
}
sdsIncrLen(c->querybuf, nread);
/* 3. querybuf 里可能已经有完整命令,继续解析/执行 */
if (processInputBuffer(c) == C_ERR)
c = NULL;
done:
beforeNextClient(c);
}这里的关键是:connRead 只保证读到“一段字节”,不保证刚好是一条完整命令。
比如第一次只读到:
TEXT
*3\r\n$3\r\nSET\r\n$4\r\nna这时 processMultibulkBuffer 发现 bulk 还没收齐,会把状态留在 client 里,下次继续读到:
TEXT
me\r\n$5\r\nalice\r\n才能凑成完整的 argv。
解析请求:querybuf 变成 pendingCommand
新版本 Redis 不是解析完马上执行,而是先把命令放到 pending_cmds。processInputBuffer 的骨架是:
C
int processInputBuffer(client *c) {
const int lookahead = authRequired(c) ? 1 : server.lookahead;
while (querybuf 还有数据 || pending_cmds 里有 ready 命令) {
/* 1. 先解析,最多向前看 lookahead 条命令 */
while (c->pending_cmds.ready_len < lookahead &&
c->querybuf && c->qb_pos < sdslen(c->querybuf))
{
if (!c->reqtype) {
c->reqtype = c->querybuf[c->qb_pos] == '*'
? PROTO_REQ_MULTIBULK
: PROTO_REQ_INLINE;
}
pendingCommand *pcmd = acquirePendingCommand();
if (c->reqtype == PROTO_REQ_INLINE)
processInlineBuffer(c, pcmd);
else
processMultibulkBuffer(c, pcmd);
addPendingCommand(&c->pending_cmds, pcmd);
/* 这里会提前查命令、检查参数数量、算 key slot */
preprocessCommand(c, pcmd);
resetClientQbufState(c);
}
/* 2. 取出队首 ready command,塞回老的 c->argc/c->argv 字段 */
pendingCommand *curcmd = c->pending_cmds.head;
c->argc = curcmd->argc;
c->argv = curcmd->argv;
c->slot = curcmd->slot;
c->lookedcmd = curcmd->cmd;
c->current_pending_cmd = curcmd;
/* 3. IO 线程只负责读/解析,命令执行要回主线程 */
if (c->running_tid != IOTHREAD_MAIN_THREAD_ID) {
c->io_flags |= CLIENT_IO_PENDING_COMMAND;
enqueuePendingClientsToMainThread(c, 0);
break;
}
processCommandAndResetClient(c);
}
}对于 SET name alice,RESP 解析后得到的不是字符串数组,而是 Redis 对象数组:
C
c->argc = 3;
c->argv[0] = createStringObject("SET", 3);
c->argv[1] = createStringObject("name", 4);
c->argv[2] = createStringObject("alice", 5);这一步之后,命令实现函数就不用再关心 socket、\r\n、bulk 长度这些协议细节了。
preprocessCommand:提前查命令和 key
server.c:preprocessCommand 会在真正执行前做一轮轻量预处理:
C
void preprocessCommand(client *c, pendingCommand *pcmd) {
struct redisCommand *last_cmd =
pcmd->prev ? pcmd->prev->cmd : c->lastcmd;
/* pipeline 里连续多条同名命令,可以复用上一次查表结果 */
if (isCommandReusable(last_cmd, pcmd->argv[0]))
pcmd->cmd = last_cmd;
else
pcmd->cmd = lookupCommand(pcmd->argv, pcmd->argc);
if (!pcmd->cmd) {
pcmd->read_error = CLIENT_READ_COMMAND_NOT_FOUND;
return;
}
/* arity > 0 表示必须等于;arity < 0 表示至少 -arity 个参数 */
if ((pcmd->cmd->arity > 0 && pcmd->cmd->arity != pcmd->argc) ||
(pcmd->argc < -pcmd->cmd->arity))
{
pcmd->read_error = CLIENT_READ_BAD_ARITY;
return;
}
/* 找出 key 参数,并顺手算 cluster slot */
int num_keys = extractKeysAndSlot(pcmd->cmd, pcmd->argv, pcmd->argc,
&pcmd->keys_result, &pcmd->slot);
if (num_keys > 0 && pcmd->slot == CLUSTER_CROSSSLOT) {
pcmd->read_error = CLIENT_READ_CROSS_SLOT;
pcmd->slot = INVALID_CLUSTER_SLOT;
}
}所以很多错误并不是到了 setCommand 才发现的,比如命令不存在、参数数量不对、Cluster 多 key 跨 slot,前面就已经标记好了。
命令表:SET 怎么变成 setCommand
SET 的声明在 src/commands/set.json 里,删减后是:
JSON
{
"SET": {
"arity": -3,
"function": "setCommand",
"command_flags": ["WRITE", "DENYOOM"],
"key_specs": [{
"begin_search": { "index": { "pos": 1 } },
"find_keys": { "range": { "lastkey": 0, "step": 1 } }
}]
}
}生成到 commands.def 后,大概就是:
C
MAKE_CMD("set", ..., setCommand, -3,
CMD_WRITE | CMD_DENYOOM,
ACL_CATEGORY_STRING,
SET_Keyspecs, 1, setGetKeys, ...)Redis 启动时把这些声明灌进 server.commands 字典。查表函数很直接:
C
struct redisCommand *lookupCommandLogic(dict *commands,
robj **argv, int argc, int strict)
{
struct redisCommand *base_cmd =
dictFetchValue(commands, argv[0]->ptr);
if (argc == 1 || !base_cmd->subcommands_dict)
return base_cmd;
return lookupSubcommand(base_cmd, argv[1]->ptr);
}
struct redisCommand *lookupCommand(robj **argv, int argc) {
return lookupCommandLogic(server.commands, argv, argc, 0);
}也就是说:
TEXT
argv[0] = "SET"
dictFetchValue(server.commands, "SET")
-> redisCommand {
proc = setCommand,
arity = -3,
flags = CMD_WRITE | CMD_DENYOOM,
key_specs = 第 1 个参数是 key
}命令分发不是 if (cmd == "SET"),而是查字典拿函数指针。
processCommand:统一拦截层
processCommand 很长,但主干可以删成这样:
C
int processCommand(client *c) {
/* 1. 拿到命令。通常 preprocessCommand 已经查好了 */
struct redisCommand *cmd = c->lookedcmd;
if (!cmd)
cmd = lookupCommand(c->argv, c->argc);
c->cmd = c->lastcmd = c->realcmd = cmd;
/* 2. 最基础的错误:命令不存在、参数数量不对 */
if (!commandCheckExistence(c, &err)) {
rejectCommandSds(c, err);
return C_OK;
}
if (!commandCheckArity(c->cmd, c->argc, &err)) {
rejectCommandSds(c, err);
return C_OK;
}
/* 3. 根据命令 flags 得到读/写/OOM/loading 等属性 */
const uint64_t cmd_flags = getCommandFlags(c);
int is_write_command = cmd_flags & CMD_WRITE;
int is_denyoom_command = cmd_flags & CMD_DENYOOM;
/* 4. 通用校验:这些和 SET 自己的业务逻辑无关 */
if (authRequired(c)) rejectCommand(c, shared.noautherr);
if (ACLCheckAllPerm(c, &acl_errpos) != ACL_OK)
rejectCommandFormat(c, "-NOPERM ...");
if (server.cluster_enabled && cluster_redirection_needed)
clusterRedirectClient(c, n, c->slot, error_code);
if (server.maxmemory && performEvictions() == EVICT_FAIL &&
is_denyoom_command) rejectCommand(c, shared.oomerr);
if (writeCommandsDeniedByDiskError() && is_write_command)
rejectCommandSds(c, err);
if (server.masterhost && server.repl_slave_ro && is_write_command)
rejectCommand(c, shared.roslaveerr);
if (server.loading && is_denyloading_command)
rejectCommand(c, shared.loadingerr);
/* 5. MULTI 里先排队,否则真正执行 */
if ((c->flags & CLIENT_MULTI) && should_queue_for_exec) {
queueMultiCommand(c, cmd_flags);
addReply(c, shared.queued);
} else {
call(c, CMD_CALL_FULL);
}
return C_OK;
}所以 SET name alice 真正到 setCommand 之前,已经过了这些门:
TEXT
命令存在?
参数至少 3 个?
当前连接已认证?
ACL 允许执行 SET、访问 key=name?
Cluster slot 属于本节点?
写命令在当前内存、磁盘、主从状态下允许执行?
如果在 MULTI 中,是排队还是执行?这就是为什么具体命令函数通常很薄:大量通用规则都在 processCommand 里统一处理了。
call:真正调用函数指针
server.c:call 里最关键的一行就是 c->cmd->proc(c):
C
void call(client *c, int flags) {
long long dirty = server.dirty;
struct redisCommand *real_cmd = c->realcmd;
c->flags &= ~(CLIENT_FORCE_AOF |
CLIENT_FORCE_REPL |
CLIENT_PREVENT_PROP);
enterExecutionUnit(1, call_timer);
c->flags |= CLIENT_EXECUTING_COMMAND;
/* 真正的命令函数调用 */
c->cmd->proc(c);
exitExecutionUnit();
/* 命令是否改了数据集 */
dirty = server.dirty - dirty;
if (dirty < 0) dirty = 0;
slowlogPushCurrentCommand(c, real_cmd, c->duration);
replicationFeedMonitors(c, server.monitors, c->db->id, c->argv, c->argc);
real_cmd->calls++;
real_cmd->microseconds += c->duration;
/* 写命令真的改了数据,才传播到 AOF / replica */
if (flags & CMD_CALL_PROPAGATE) {
int propagate_flags = PROPAGATE_NONE;
if (dirty) propagate_flags |= PROPAGATE_AOF | PROPAGATE_REPL;
if (propagate_flags != PROPAGATE_NONE)
alsoPropagate(c->db->id, c->argv, c->argc, propagate_flags);
}
}call 负责的是“执行框架”:计时、慢日志、MONITOR、命令统计、AOF/复制传播。具体 SET 怎么写 key,不在这里。
SET:真正写数据的地方
SET 的入口在 t_string.c:setCommand:
C
void setCommand(client *c) {
extendedStringArgs args;
/* 解析 NX / XX / GET / EX / PX / KEEPTTL 等参数 */
if (parseExtendedStringArgumentsOrReply(c, 3, &args, COMMAND_SET) != C_OK)
return;
/* 尝试把 value 编码成 int/embstr/raw 等更合适的对象 */
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c, args.flags,
c->argv[1], /* key */
&(c->argv[2]), /* value */
args.expire,
args.unit,
args.match_value,
NULL, NULL);
}SET name alice 进入这里时:
TEXT
c->argv[0] = "SET"
c->argv[1] = "name"
c->argv[2] = "alice"
args.flags = 0
args.expire = NULL如果是:
BASH
SET name tom NX EX 60进入 setGenericCommand 时就会变成:
TEXT
key = "name"
value = "tom"
flags 包含 OBJ_SET_NX | OBJ_EX
expire = "60"
unit = UNIT_SECONDSsetGenericCommand:条件判断 + 写入 DB
setGenericCommand 才是 SET 的核心,删减后:
C
void setGenericCommand(client *c, int flags,
robj *key, robj **valref,
robj *expire, int unit,
robj *match_value,
robj *ok_reply, robj *abort_reply)
{
long long milliseconds = 0;
int relative_ttl = (flags & (OBJ_EX | OBJ_PX)) != 0;
/* 1. 过期时间先转成毫秒时间戳 */
if (expire &&
getExpireMillisecondsOrReply(c, expire, relative_ttl,
unit, &milliseconds) != C_OK)
return;
/* 2. 先查 key 是否存在,同时拿到 dictEntryLink,后面写入少查一次 */
dictEntryLink link = NULL;
int found = lookupKeyWriteWithLink(c->db, key, &link) != NULL;
/* 3. NX / XX 这种条件不满足,直接回复 null,不写 DB */
if ((flags & OBJ_SET_NX && found) ||
(flags & OBJ_SET_XX && !found))
{
addReply(c, abort_reply ? abort_reply : shared.null[c->resp]);
return;
}
/* 4. 真正写入 */
int setkey_flags = 0;
setkey_flags |= expire ? SETKEY_KEEPTTL : 0;
setkey_flags |= found ? SETKEY_ALREADY_EXIST : SETKEY_DOESNT_EXIST;
setKeyByLink(c, c->db, key, valref, setkey_flags, &link);
/* 5. 如果带 EX/PX,再维护 expires */
if (expire)
*valref = setExpireByLink(c, c->db, key->ptr, milliseconds, link);
incrRefCount(*valref);
server.dirty++;
notifyKeyspaceEvent(NOTIFY_STRING, "set", key, c->db->id);
addReply(c, ok_reply ? ok_reply : shared.ok);
}第一次执行:
BASH
SET name alice大致路径是:
TEXT
found = false
setkey_flags = SETKEY_DOESNT_EXIST
setKeyByLink -> dbAddByLink
server.dirty++
addReply("+OK\r\n")如果再执行:
BASH
SET name bob路径变成:
TEXT
found = true
setkey_flags = SETKEY_ALREADY_EXIST
setKeyByLink -> dbSetValue
server.dirty++
addReply("+OK\r\n")如果执行:
BASH
SET name tom NX因为 name 已存在:
TEXT
found = true
flags contains OBJ_SET_NX
直接 addReply(null)
不调用 setKeyByLink
server.dirty 不增加setKeyByLink:新增还是覆盖
最终写 DB 的逻辑在 db.c:setKeyByLink:
C
void setKeyByLink(client *c, redisDb *db,
robj *key, robj **valref,
int flags, dictEntryLink *plink)
{
int exists;
kvobj *oldval = NULL;
if (flags & SETKEY_ALREADY_EXIST) {
oldval = dictGetKV(**plink);
exists = 1;
} else if (flags & SETKEY_DOESNT_EXIST) {
exists = 0;
} else {
oldval = lookupKeyWriteWithLink(db, key, link);
exists = oldval != NULL;
}
if (exists) {
/* 覆盖旧 value */
dbSetValue(db, key, valref, *link, 1, 1, flags & SETKEY_KEEPTTL);
notifyKeyspaceEvent(NOTIFY_OVERWRITTEN, "overwritten", key, db->id);
} else {
/* 新增 key */
dbAddByLink(db, key, valref, link);
}
keyModified(c, db, key, *valref, !(flags & SETKEY_NO_SIGNAL));
}可以把 Redis DB 简化成两张核心表:
TEXT
db->keys:
"name" -> redisObject("alice")
db->expires:
"name" -> expire timestamp // 只有带过期时间的 key 才有SET name alice 默认只写 db->keys。SET name alice EX 60 会先写 db->keys,再通过 setExpireByLink 写 db->expires。
addReply:命令只生成回复,不直接 write
setGenericCommand 最后调用的是:
C
addReply(c, shared.ok);networking.c:addReply 的骨架:
C
void addReply(client *c, robj *obj) {
if (_prepareClientToWrite(c) != C_OK)
return;
if (sdsEncodedObject(obj)) {
_addReplyToBufferOrList(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);
_addReplyToBufferOrList(c, buf, len);
}
}再往下一层:
C
void _addReplyToBufferOrList(client *c, const char *s, size_t len) {
/* 小回复先进固定 buffer */
size_t reply_len = _addReplyPayloadToBuffer(c, s, len, PLAIN_REPLY);
/* 固定 buffer 放不下的部分,进 c->reply 链表 */
if (len > reply_len)
_addReplyPayloadToList(c, c->reply,
s + reply_len, len - reply_len,
PLAIN_REPLY);
}所以命令函数只是把 +OK\r\n 放进输出缓冲区,并不直接写 socket。
真正写回在事件循环后半段:
C
int handleClientsWithPendingWrites(void) {
while ((ln = listNext(&li))) {
client *c = listNodeValue(ln);
if (writeToClient(c, 0) == C_ERR)
continue;
if (clientHasPendingReplies(c))
installClientWriteHandler(c);
}
}writeToClient 会循环把 client.buf / client.reply 写到连接里:
C
int writeToClient(client *c, int handler_installed) {
while (_clientHasPendingRepliesNonSlave(c)) {
int ret = _writeToClientNonSlave(c, &nwritten);
if (ret == C_ERR) break;
/* 单次写太多就让出事件循环,避免大回复拖死其他客户端 */
if (totwritten > NET_MAX_WRITES_PER_EVENT &&
(server.maxmemory == 0 ||
zmalloc_used_memory() < server.maxmemory) &&
is_normal_client)
break;
}
if (!clientHasPendingReplies(c) && handler_installed)
connSetWriteHandler(c->conn, NULL);
}这就是 Redis 回复链路的分工:
TEXT
命令函数:addReply,生成 RESP 回复
网络层:writeToClient,把回复刷到 socket
事件循环:一次写不完就注册写事件,下次可写继续发Pipeline 怎么落到源码上
Pipeline 只是一次发多条命令:
TEXT
SET a 1
INCR a
GET a源码上对应的是:
C
while (c->pending_cmds.ready_len < lookahead &&
c->querybuf && c->qb_pos < sdslen(c->querybuf))
{
processMultibulkBuffer(c, pcmd);
addPendingCommand(&c->pending_cmds, pcmd);
preprocessCommand(c, pcmd);
}也就是一次 read 后,Redis 可以连续解析出多条 pendingCommand:
TEXT
pending_cmds:
[SET a 1] [INCR a] [GET a]但执行时还是队首一条一条来:
TEXT
curcmd = pending_cmds.head
c->argc/c->argv = curcmd->argc/curcmd->argv
processCommandAndResetClient(c)Pipeline 提升的是网络往返和系统调用效率,不是把三条命令并行执行。
I/O 线程到底做什么
processInputBuffer 里有一段很能说明问题:
C
if (c->running_tid != IOTHREAD_MAIN_THREAD_ID) {
c->io_flags |= CLIENT_IO_PENDING_COMMAND;
enqueuePendingClientsToMainThread(c, 0);
break;
}
processCommandAndResetClient(c);意思是:I/O 线程可以读 socket、解析协议、准备 pending command;但真正修改数据库的 processCommand -> call -> setCommand 还是回主线程做。
所以 Redis 的“单线程”更准确地说是:
TEXT
网络读写:可以由 I/O 线程分担
命令执行、数据结构修改、过期淘汰、传播:主线程串行处理总结
把 SET name alice 串起来就是:
TEXT
readQueryFromClient
connRead 读到 RESP 字节,追加到 c->querybuf
processInputBuffer
processMultibulkBuffer 把 RESP 拆成 pendingCommand(argc=3, argv=[SET,name,alice])
preprocessCommand 提前查到 redisCommand(setCommand),检查 arity,提取 key/slot
把 pendingCommand 填回 c->argc/c->argv/c->lookedcmd
processCommand
统一做认证、ACL、Cluster、OOM、磁盘错误、主从状态、loading、事务等检查
call
记录 dirty/耗时,调用 c->cmd->proc(c),执行后做慢日志、统计、AOF/复制传播
setCommand
解析 SET 参数,调用 setGenericCommand
setGenericCommand
lookupKeyWriteWithLink 判断 key 是否存在
根据 NX/XX/EX/PX 等条件决定是否写入
setKeyByLink 新增或覆盖 db->keys
必要时写 db->expires
server.dirty++
addReply(shared.ok)
handleClientsWithPendingWrites / writeToClient
把 client 输出缓冲区里的 +OK\r\n 写回 socket这条链路里,SET 自己只负责字符串命令的业务规则;协议解析、命令查表、通用校验、慢日志、统计、AOF/复制、网络回写,都在 Redis 的命令执行框架里统一完成。