参考文献:
- Redis 是如何处理命令的(客户端)
- 我是如何通过添加一条命令学习redis源码的
- 从零开始写redis客户端(deerlet-redis-client)之路——第一个纠结很久的问题,restore引发的血案
- redis命令执行流程分析
- 通信协议(protocol)
- Redis主从复制原理
- Redis配置文件详解
当用户在redis客户端键入一个命令的时候,客户端会将这个命令发送到服务端。服务端会完成一系列的操作。一个redis命令在服务端大体经历了以下的几个阶段:
- 读取命令请求
- 查找命令的实现
- 执行预备操作
- 调用命令实现函数
- 执行后续工作
读取命令的请求
从redis客户端发送过来的命令,都会在readQueryFromClient函数中被读取。当客户端和服务器的连接套接字变的可读的时候,就会触发redis的文件事件。在aeMain函数中,将调用readQueryFromClient函数。在readQueryFromClient函数中,需要完成了2件事情:
- 将命令的内容读取到redis客户端数据结构中的查询缓冲区。
- 调用processInputBuffer函数,根据协议格式,得出命令的参数等信息。
例如命令 set key value 在query_buffer中将会以如下的格式存在:
void readQueryFromClient(aeEventLoop *el, int fd, void *privdata, int mask) {
redisClient *c = (redisClient*) privdata;
int nread, readlen;
size_t qblen;
REDIS_NOTUSED(el);
REDIS_NOTUSED(mask);
// 设置服务器的当前客户端
server.current_client = c;
// 读入长度(默认为 16 MB)
readlen = REDIS_IOBUF_LEN;
........
........
// 读入内容到查询缓存
nread = read(fd, c->querybuf+qblen, readlen);
........
........
processInputBuffer(c);
}
命令参数的解析
在上一节中,我们看到在readQueryFromClient函数中会将套接字中的数据读取到redisClient的queryBuf中。而对于命令的处理,实际是在processInputBuffer函数中进行的。
在函数中主要做了以下的2个工作:
- 判断请求的类型,例如是内联查询还是多条查询。具体的区别可以在通信协议(protocol)里面看到。本文就不详细叙述了。
- 根据请求的类型,调用不同的处理函数:
2.1 processInlineBuffer
2.2 processMultibulkBuffer
// 处理客户端输入的命令内容
void processInputBuffer(redisClient *c) {
while(sdslen(c->querybuf)) {
.......
.......
/* Determine request type when unknown. */
// 判断请求的类型
// 两种类型的区别可以在 Redis 的通讯协议上查到:
// http://redis.readthedocs.org/en/latest/topic/protocol.html
// 简单来说,多条查询是一般客户端发送来的,
// 而内联查询则是 TELNET 发送来的
if (!c->reqtype) {
if (c->querybuf[0] == ‘*‘) {
// 多条查询
c->reqtype = REDIS_REQ_MULTIBULK;
} else {
// 内联查询
c->reqtype = REDIS_REQ_INLINE;
}
}
// 将缓冲区中的内容转换成命令,以及命令参数
if (c->reqtype == REDIS_REQ_INLINE) {
if (processInlineBuffer(c) != REDIS_OK) break;
} else if (c->reqtype == REDIS_REQ_MULTIBULK) {
if (processMultibulkBuffer(c) != REDIS_OK) break;
} else {
redisPanic("Unknown request type");
}
/* Multibulk processing could see a <= 0 length. */
if (c->argc == 0) {
resetClient(c);
} else {
/* Only reset the client when the command was executed. */
// 执行命令,并重置客户端
if (processCommand(c) == REDIS_OK)
resetClient(c);
}
}
}
processMultibulkBuffer 和 processInlineBuffer
processMultibulkBuffer主要完成的工作是将 c->querybuf 中的协议内容转换成 c->argv 中的参数对象。 比如 *3\r\n$3\r\nSET\r\n$3\r\nMSG\r\n$5\r\nHELLO\r\n将被转换为:
argv[0] = SET
argv[1] = MSG
argv[2] = HELLO
具体的过程就不贴代码了。同样processInlineBuffer也会完成将c->querybuf 中的协议内容转换成 c->argv 中的参数的工作。
查找命令的实现
到了这一步,准备工作都做完了。redis服务器已将查询缓冲中的命令转换为参数对象了。接下来将调用processCommand函数进行命令的处理。processCommand函数比较长,接下来我们分段进行解析。
查找命令
服务器端首先开始查找命令。主要就是使用lookupCommand函数,根据命令对应的名字,去找到对应的执行函数以及相关的属性信息。
// 特别处理 quit 命令
if (!strcasecmp(c->argv[0]->ptr,"quit")) {
addReply(c,shared.ok);
c->flags |= REDIS_CLOSE_AFTER_REPLY;
return REDIS_ERR;
}
/* Now lookup the command and check ASAP about trivial error conditions
* such as wrong arity, bad command name and so forth. */
// 查找命令,并进行命令合法性检查,以及命令参数个数检查
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
if (!c->cmd) {
// 没找到指定的命令
flagTransaction(c);
addReplyErrorFormat(c,"unknown command ‘%s‘",
(char*)c->argv[0]->ptr);
return REDIS_OK;
} else if ((c->cmd->arity > 0 && c->cmd->arity != c->argc) ||
(c->argc < -c->cmd->arity)) {
// 参数个数错误
flagTransaction(c);
addReplyErrorFormat(c,"wrong number of arguments for ‘%s‘ command",
c->cmd->name);
return REDIS_OK;
}
那么命令的定义在哪里呢?答案在redis.c文件中,定义了一个如下的实现:
struct redisCommand redisCommandTable[]= {
.....
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
.....
}
Redis将所有它能支持的命令以及对应的“命令处理函数”之间对应关系存放在数组redisCommandTable[]中,该数组中保存元素的类型为结构体redisCommand,此中包括命令的名字以及对应处理函数的地址,在Redis服务初始化的时候,这个结构体会在初始化函数中被转换成struct redisServer结构体中的一个dict,这个dict被赋值到commands域中。结构体详细的实现如下:
/*
* Redis 命令
*/
struct redisCommand {
// 命令名字
char *name;
// 实现函数
redisCommandProc *proc;
// 参数个数
int arity;
// 字符串表示的 FLAG
char *sflags; /* Flags as string representation, one char per flag. */
// 实际 FLAG
int flags; /* The actual flags, obtained from the ‘sflags‘ field. */
/* Use a function to determine keys arguments in a command line.
┆* Used for Redis Cluster redirect. */
// 从命令中判断命令的键参数。在 Redis 集群转向时使用。
redisGetKeysProc *getkeys_proc;
/* What keys should be loaded in background when calling this command? */
// 指定哪些参数是 key
int firstkey; /* The first argument that‘s a key (0 = no keys) */
int lastkey; /* The last argument that‘s a key */
int keystep; /* The step between first and last key */
// 统计信息
// microseconds 记录了命令执行耗费的总毫微秒数
// calls 是命令被执行的总次数
long long microseconds, calls;
}
根据这个结构体,我们可以看到set执行的信息如下:
- 命令名称是set
- 执行函数是setCommand
- 参数个数是3
执行命令前的准备工作
在上节,我们看到了Redis是如何查找命令,以及一个命令最终的定义和实现是在哪里的。接下来我们来看下 processCommand后面部分的实现。这部分主要的工作是在执行命令之前做一点的检查工作 :
- 检查认证信息,如果redis服务器配置有密码,在此处会做一次验证
- 集群模式下的处理,此处不多做展开。
- 检查是否到了Redis配置文件中,限制的最大内存数。如果达到了限制,需要根据配置的内存释放策略做一定的释放操作。
- 检查是否主服务,并且这个服务器之前是否执行 BGSAVE 时发生了错误,如果发生了错误则不执行。
- 如果Redis服务器打开了min-slaves-to-write配置,则没有足够多的slave可写的时候,拒绝执行写操作。
- 如果当前的Redis服务器是个只读的slave的话,拒绝执行写操作。
- 当redis处于发布和订阅上下文的时候,只能执行订阅和退订相关的命令。
- 如果slave-serve-stale-data 配置为no的时候,只允许INFO 和 SLAVEOF 命令。( Redis配置文件详解)
- 如果服务器正在载入数据到数据库,那么只执行带有 REDIS_CMD_LOADING 标识的命令,否则将出错。
- 如果Lua 脚本超时,只允许执行限定的操作,比如 SHUTDOWN 和 SCRIPT KILL。
到此Redis执行一个命令前的检查工作基本算完成了。接下来将调用call函数执行命令。
调用命令实现函数
在call函数里面,在真正的执行一个命令的实现函数。
// 执行实现函数
c->cmd->proc(c);
那么这个c是指什么呢?我们来看下call函数的定义:
void call(redisClient *c, int flags)
可见call函数传入的是redisClient这个结构体的指针。那么这个结构体在哪里创建的呢?是在"读取命令的请求"的阶段就已经创建好了。在redisClient中,定义了一个struct redisCommand *cmd 属性,在查找命令的阶段便被赋予了对应命令的执行函数。因此在此处,将会调用对应的函数完成命令的执行。
typedef struct redisClient {
// 记录被客户端执行的命令
struct redisCommand *cmd, *lastcmd;
}
执行后续工作
在执行完命令的实现函数之后,Redis还有做一些后续工作包括:
- 计算命令的执行时间
- 计算命令执行之后的 dirty 值
- 是否需要将命令记录到SLOWLOG中
- 命令复制到 AOF 和 slave 节点
原文地址:https://www.cnblogs.com/bush2582/p/9326745.html