服务器
上一张了解了客户端以一个对象的方式来表明自己的身份以及当前所处的阶段,这章对应的会进行一个服务器的介绍.
Redis 服务器负责与多个客户端建立网络链接,处理客户端发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并通过资源管理来位置服务器自身的运转
14.1 命令请求的执行过程
以客户端向服务器发送命令为例,来进行客户端与服务器交互的过程说明.
当客户端发出
SET KEY VAL
到服务器回复
ok
中间经历的过程如下:
1): 客户端向服务器发送命令请求 SET KEY VALUE
2): 服务器接收并处理客户端发来的命令请求 SET KEY VALUE,服务器做完处理后产生回复字符 ok
3): 服务器将回复字符返回给客户端.
4): 客户端接收服务器返回的字符,并打印给用户
后续内容将对这些过程进行详细的说明
14.1.1 发送命令请求
当客户端发送 SET KEY VAl 时,客户端会将字符串转换成协议格式,再发送给服务器,
SET KEY VAl
*3\r\n$3\r\nSET\r\n$3\r\nKEY\r\n$5\r\nVALUE\r\n
从转换后的格式可以看出请求的协议格式非常简单粗暴,先标记命令的长度,再标记每个命令单词的长度,再附上对应的内容,用 \r\n 作分割.
14.1.2 读取命令请求
当客户端发送出命令之后,服务器上的与连接客户端绑定的套接字会被置为可读状态,此时,服务器将调用命令请求处理器进行相应的处理.
前几章也进行对应的说明.
当接收到可读事件时, 处理器将会将命令先放置在 querybuf 中,然后再进行进一步的解析,存储到 argv和argc 之中,最后根据 argv[0] 的字符从字符表中找到对应的命令处理器,修改 redisClient 中的 cmd 指针指向,然后调用命令执行期,执行客户端指定的命令, cmd + argv + argc .
14.13 命令执行器(1): 查找命令的实现
查找命令简单来说就是去字典中查找对应的键值对.
键为 字符串,值为 redisCommand 结构.
具体机构如下
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;
};
通过代码中的下面片段,我们也能找到 sflags 的对应说明
char *f = c->sflags;
int retval1, retval2;
// 根据字符串 FLAG 生成实际 FLAG
while(*f != '\0') {
switch(*f) {
case 'w': c->flags |= REDIS_CMD_WRITE; break;
case 'r': c->flags |= REDIS_CMD_READONLY; break;
case 'm': c->flags |= REDIS_CMD_DENYOOM; break;
case 'a': c->flags |= REDIS_CMD_ADMIN; break;
case 'p': c->flags |= REDIS_CMD_PUBSUB; break;
case 's': c->flags |= REDIS_CMD_NOSCRIPT; break;
case 'R': c->flags |= REDIS_CMD_RANDOM; break;
case 'S': c->flags |= REDIS_CMD_SORT_FOR_SCRIPT; break;
case 'l': c->flags |= REDIS_CMD_LOADING; break;
case 't': c->flags |= REDIS_CMD_STALE; break;
case 'M': c->flags |= REDIS_CMD_SKIP_MONITOR; break;
case 'k': c->flags |= REDIS_CMD_ASKING; break;
default: redisPanic("Unsupported command flag"); break;
}
f++;
}
#define REDIS_CMD_WRITE 1 /* "w" flag */
#define REDIS_CMD_READONLY 2 /* "r" flag */
#define REDIS_CMD_DENYOOM 4 /* "m" flag */
#define REDIS_CMD_NOT_USED_1 8 /* no longer used flag */
#define REDIS_CMD_ADMIN 16 /* "a" flag */
#define REDIS_CMD_PUBSUB 32 /* "p" flag */
#define REDIS_CMD_NOSCRIPT 64 /* "s" flag */
#define REDIS_CMD_RANDOM 128 /* "R" flag */
#define REDIS_CMD_SORT_FOR_SCRIPT 256 /* "S" flag */
#define REDIS_CMD_LOADING 512 /* "l" flag */
#define REDIS_CMD_STALE 1024 /* "t" flag */
#define REDIS_CMD_SKIP_MONITOR 2048 /* "M" flag */
#define REDIS_CMD_ASKING 4096 /* "k" flag */
标识 | 意义 | 带有该标识的命令 |
---|---|---|
w | 写入命令,可能会修改数据库 | SET,RPUSH,DEL等 |
r | 只读命令,不会修改数据库 | GET,STRLEN,EXISTS等 |
m | 会占用大量内存,执行前要检查内存使用情况 | SET,RPUSH,APPEND,LPUSH,SADD等 |
a | 管理命令 | SAVE,BGSAVE,BGWRITEAOF等 |
p | 发布订阅相关命令 | PUBLISH,SUBSCRIBE,PUBSUB等 |
s | 不能在lua中使用 | BRPOP,BLPOP,BRPOPLPUSH,LPOP等 |
R | 随机命令,对于相同的数据集,可能会有不同的返回结果 | SPOP,SRANDMEMBER,SSCAN,RANDOMKEY等 |
S | 当在Lua脚本中使用,会在返回结果后进行一个排序,使得结果是有序的 | SINTER,SUNION,SDIFF,SMEMBERS,KEYS等 |
l | 可以在服务器载入过程中使用 | INFO,SHUTDONW,PUBLISH等 |
t | 允许从服务器器带有过期数据时使用的命令 | SALVEOF,PING,INFO等 |
M | 在监视器模式中不会被自动传播 | EXEC |
以 set 命令执行函数为例
{"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
“wm” 从上表可知,该命令是写入命令,可能会修改数据库,会占用大量内存,执行前要检查内存使用情况.
/* SET key value [NX] [XX] [EX <seconds>] [PX <milliseconds>] */
void setCommand(redisClient *c) {
int j;
robj *expire = NULL;
int unit = UNIT_SECONDS;
int flags = REDIS_SET_NO_FLAGS;
// 设置选项参数
for (j = 3; j < c->argc; j++) {
char *a = c->argv[j]->ptr;
robj *next = (j == c->argc-1) ? NULL : c->argv[j+1];
if ((a[0] == 'n' || a[0] == 'N') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0') {
flags |= REDIS_SET_NX;
} else if ((a[0] == 'x' || a[0] == 'X') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0') {
flags |= REDIS_SET_XX;
} else if ((a[0] == 'e' || a[0] == 'E') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' && next) {
unit = UNIT_SECONDS;
expire = next;
j++;
} else if ((a[0] == 'p' || a[0] == 'P') &&
(a[1] == 'x' || a[1] == 'X') && a[2] == '\0' && next) {
unit = UNIT_MILLISECONDS;
expire = next;
j++;
} else {
addReply(c,shared.syntaxerr);
return;
}
}
// 尝试对值对象进行编码
c->argv[2] = tryObjectEncoding(c->argv[2]);
setGenericCommand(c,flags,c->argv[1],c->argv[2],expire,unit,NULL,NULL);
}
然后再看命令执行器的实现.
从第三个数起就是一个选项参数配置,后面两行会进行实际的存储过程的进行,暂不深入
需要再说的是, argv[0] 是无关大小写的, set,SET SEt,sEt,无论是哪种形式,都不会有区别
unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len) {
unsigned int hash = (unsigned int)dict_hash_function_seed;
while (len--)
hash = ((hash << 5) + hash) + (tolower(*buf++)); /* hash * 33 + c */
return hash;
}
hahs值的计算过程有一个tolower函数,进行大小写的统一
14.1.4 命令执行器(2):执行的预备操作
找到对应命令执行器后,将进行预检查:
简要的总结一下会执行的一些检查
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;
}
1): 检查 cmd 是否找到对应命令执行器.
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;
}
2): 检查参数是否正确
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand)
{
flagTransaction(c);
addReply(c,shared.noautherr);
return REDIS_OK;
}
3): 身份验证信息检查
if (server.maxmemory) {
// 如果内存已超过限制,那么尝试通过删除过期键来释放内存
int retval = freeMemoryIfNeeded();
// 如果即将要执行的命令可能占用大量内存(REDIS_CMD_DENYOOM)
// 并且前面的内存释放失败的话
// 那么向客户端返回内存错误
if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
flagTransaction(c);
addReply(c, shared.oomerr);
return REDIS_OK;
}
}
4): 进行内存检测
if (((server.stop_writes_on_bgsave_err &&
server.saveparamslen > 0 &&
server.lastbgsave_status == REDIS_ERR) ||
server.aof_last_write_status == REDIS_ERR) &&
server.masterhost == NULL &&
(c->cmd->flags & REDIS_CMD_WRITE ||
c->cmd->proc == pingCommand))
{
flagTransaction(c);
if (server.aof_last_write_status == REDIS_OK)
addReply(c, shared.bgsaveerr);
else
addReplySds(c,
sdscatprintf(sdsempty(),
"-MISCONF Errors writing to the AOF file: %s\r\n",
strerror(server.aof_last_write_errno)));
return REDIS_OK;
}
5): 如果 BGSAVE 命令错误,并且打开了 stop_writes_on_bgsave_err 功能,服务器又执行了一个写入命令 REDIS_CMD_WRITE, 那么将返回一个错误
/* Only allow SUBSCRIBE and UNSUBSCRIBE in the context of Pub/Sub */
// 在订阅于发布模式的上下文中,只能执行订阅和退订相关的命令
if ((dictSize(c->pubsub_channels) > 0 || listLength(c->pubsub_patterns) > 0)
&&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / QUIT allowed in this context");
return REDIS_OK;
}
6): 检查是否处于发布模式中,如果是将拒绝其他命令
if (server.loading && !(c->cmd->flags & REDIS_CMD_LOADING)) {
addReply(c, shared.loadingerr);
return REDIS_OK;
}
7): 当服务器正在进行数据载入,那么只会进行 REDIS_CMD_LOADING 标记的命令调用
if (server.lua_timedout &&
c->cmd->proc != authCommand &&
c->cmd->proc != replconfCommand &&
!(c->cmd->proc == shutdownCommand &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == 'n') &&
!(c->cmd->proc == scriptCommand &&