Redis 单机服务器实现
1. Redis 服务器
Redis服务器负责与客户端建立网络连接,处理发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并且通过一系列资源管理措施来维持服务器自身的正常运转。本次主要剖析server.c
文件,本文主要介绍Redis服务器的一下几个实现:
- 命令的执行过程
- Redis服务器的周期性任务
- maxmemory的策略
- Redis服务器的main函数
其他的注释请上github查看:Redis 单机服务器实现源码注释
2. 命令的执行过程
Redis一个命令的完整执行过程如下:
- 客户端发送命令请求
- 服务器接收命令请求
- 服务器执行命令请求
- 将回复发送给客户端
关于命令接收与命令回复,在Redis 网络连接库剖析一文已经详细剖析过,本篇主要针对第三步,也就是服务器执行命令的过程进行剖析。
服务器在接收到命令后,会将命令以对象的形式保存在服务器client
的参数列表robj **argv
中,因此服务器执行命令请求时,服务器已经读入了一套命令参数保存在参数列表中。执行命令的过程对应的函数是processCommand()
,源码如下:
// 如果client没有被关闭则返回C_OK,调用者可以继续执行其他的操作,否则返回C_ERR,表示client被销毁
int processCommand(client *c) {
// 如果是 quit 命令,则单独处理
if (!strcasecmp(c->argv[0]->ptr,"quit")) {
addReply(c,shared.ok);
c->flags |= CLIENT_CLOSE_AFTER_REPLY; //设置client的状态为回复后立即关闭,返回C_ERR
return C_ERR;
}
// 从数据库的字典中查找该命令
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 C_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 C_OK;
}
/* Check if the user is authenticated */
// 如果服务器设置了密码,但是没有认证成功
if (server.requirepass && !c->authenticated && c->cmd->proc != authCommand)
{
flagTransaction(c); //如果是事务状态的命令,则设置事务为失败
addReply(c,shared.noautherr);
return C_OK;
}
// 如果开启了集群模式,则执行集群的重定向操作,下面的两种情况例外:
/*
1. 命令的发送是主节点服务器
2. 命令没有key
*/
if (server.cluster_enabled &&
!(c->flags & CLIENT_MASTER) &&
!(c->flags & CLIENT_LUA &&
server.lua_caller->flags & CLIENT_MASTER) &&
!(c->cmd->getkeys_proc == NULL && c->cmd->firstkey == 0 &&
c->cmd->proc != execCommand))
{
int hashslot;
int error_code;
// 从集群中返回一个能够执行命令的节点
clusterNode *n = getNodeByQuery(c,c->cmd,c->argv,c->argc,
&hashslot,&error_code);
// 返回的节点不合格
if (n == NULL || n != server.cluster->myself) {
// 如果是执行事务的命令,则取消事务
if (c->cmd->proc == execCommand) {
discardTransaction(c);
} else {
// 将事务状态设置为失败
flagTransaction(c);
}
// 执行client的重定向操作
clusterRedirectClient(c,n,hashslot,error_code);
return C_OK;
}
}
// 如果服务器有最大内存的限制
if (server.maxmemory) {
// 按需释放一部分内存
int retval = freeMemoryIfNeeded();
// freeMemoryIfNeeded()函数之后需要冲洗从节点的输出缓冲区,这可能导致被释放的从节点是一个活跃的client
// 如果当前的client被释放,返回C_ERR
if (server.current_client == NULL) return C_ERR;
// 如果命令会耗费大量的内存但是释放内存失败
if ((c->cmd->flags & CMD_DENYOOM) && retval == C_ERR) {
// 将事务状态设置为失败
flagTransaction(c);
addReply(c, shared.oomerr);
return C_OK;
}
}
// 如果 BGSAVE 命令执行错误而且服务器是一个主节点,那么不接受写命令
if (((server.stop_writes_on_bgsave_err &&
server.saveparamslen > 0 &&
server.lastbgsave_status == C_ERR) ||
server.aof_last_write_status == C_ERR) &&
server.masterhost == NULL &&
(c->cmd->flags & CMD_WRITE ||
c->cmd->proc == pingCommand))
{
// 将事务状态设置为失败
flagTransaction(c);
// 如果上一次执行AOF成功回复BGSAVE错误回复
if (server.aof_last_write_status == C_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 C_OK;
}
// 如果没有足够的良好的从节点而且用户配置了 min-slaves-to-write,那么不接受写命令
if (server.masterhost == NULL &&
server.repl_min_slaves_to_write &&
server.repl_min_slaves_max_lag &&
c->cmd->flags & CMD_WRITE &&
server.repl_good_slaves_count < server.repl_min_slaves_to_write)
{
// 将事务状态设置为失败
flagTransaction(c);
addReply(c, shared.noreplicaserr);
return C_OK;
}
// 如果这是一个只读的从节点服务器,则不接受写命令
if (server.masterhost && server.repl_slave_ro &&
!(c->flags & CLIENT_MASTER) &&
c->cmd->flags & CMD_WRITE)
{
addReply(c, shared.roslaveerr);
return C_OK;
}
// 如果处于发布订阅模式,但是执行的不是发布订阅命令,返回
if (c->flags & CLIENT_PUBSUB &&
c->cmd->proc != pingCommand &&
c->cmd->proc != subscribeCommand &&
c->cmd->proc != unsubscribeCommand &&
c->cmd->proc != psubscribeCommand &&
c->cmd->proc != punsubscribeCommand) {
addReplyError(c,"only (P)SUBSCRIBE / (P)UNSUBSCRIBE / PING / QUIT allowed in this context");
return C_OK;
}
// 如果是从节点且和主节点断开了连接,不允许从服务器带有过期数据,返回
if (server.masterhost && server.repl_state != REPL_STATE_CONNECTED &&
server.repl_serve_stale_data == 0 &&
!(c->cmd->flags & CMD_STALE))
{
flagTransaction(c);
addReply(c, shared.masterdownerr);
return C_OK;
}
// 如果服务器处于载入状态,如果命令不是CMD_LOADING标识,则不执行,返回
if (server.loading && !(c->cmd->flags & CMD_LOADING)) {
addReply(c, shared.loadingerr);
return C_OK;
}
// 如果lua脚本超时,限制执行一部分命令,如shutdown、scriptCommand
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 &&
c->argc == 2 &&
tolower(((char*)c->argv[1]->ptr)[0]) == 'k'))
{
flagTransaction(c);
addReply(c, shared.slowscripterr);
return C_OK;
}
// 执行命令
// client处于事务环境中,但是执行命令不是exec、discard、multi和watch
if (c->flags & CLIENT_MULTI &&
c->cmd->proc != execCommand && c->cmd->proc != discardCommand &&
c->cmd->proc != multiCommand && c->cmd->proc != watchCommand)
{
// 除了上述的四个命令,其他的命令添加到事务队列中
queueMultiCommand(c);
addReply(c,shared.queued);
// 执行普通的命令
} else {
call(c,CMD_CALL_FULL);
// 保存写全局的复制偏移量
c->woff = server.master_repl_offset;
// 如果因为BLPOP而阻塞的命令已经准备好,则处理client的阻塞状态
if (listLength(server.ready_keys))
handleClientsBlockedOnLists();
}
return C_OK;
}
我们总结出执行命令的大致过程:
- 查找命令。对应的代码是:
c->cmd = c->lastcmd = lookupCommand(c->argv[0]->ptr);
- 执行命令前的准备。对应这些判断语句。
- 执行命令。对应代码是:
call(c,CMD_CALL_FULL);
我们就大致就这三个过程详细解释。
2.1 查找命令
lookupCommand()
函数是对dictFetchValue(server.commands, name);
的封装。而这个函数的意思是:从server.commands
字典中查找name
命令。这个保存命令表的字典,键是命令的名称,值是命令表的地址。因此我们介绍服务器初始化时的一个操作,就是创建一张命令表。命令表代码简化表示如下:
struct redisCommand redisCommandTable[] = {
{
"get",getCommand,2,"rF",0,NULL,1,1,1,0,0},
{
"set",setCommand,-3,"wm",0,NULL,1,1,1,0,0},
......
};
我们只展示了命令表的两条,可以通过COMMAND COUNT
命令查看命令的个数。虽然只有两条,但是可以说明问题。
首先命令表是就是一个数组,数组的每个成员都是一个struct redisCommand
结构体,对每个数组成员都进行了初始化。我们一次对每个值进行分析:以GET
命令为例子。
- char *name:命令的名字。对应
"get"
。 - redisCommandProc *proc:命令实现的函数。对应
getCommand
。 - int arity:参数个数,-N表示大于等于N。对应
2
。 - char *sflags:命令的属性,用以下字符作为标识。对应
"rF"