Redis源码剖析和注释(二十一)--- 单机服务器实现

Redis 单机服务器实现

1. Redis 服务器

Redis服务器负责与客户端建立网络连接,处理发送的命令请求,在数据库中保存客户端执行命令所产生的数据,并且通过一系列资源管理措施来维持服务器自身的正常运转。本次主要剖析server.c文件,本文主要介绍Redis服务器的一下几个实现:

  • 命令的执行过程
  • Redis服务器的周期性任务
  • maxmemory的策略
  • Redis服务器的main函数

其他的注释请上github查看:Redis 单机服务器实现源码注释

2. 命令的执行过程

Redis一个命令的完整执行过程如下:

  1. 客户端发送命令请求
  2. 服务器接收命令请求
  3. 服务器执行命令请求
  4. 将回复发送给客户端

关于命令接收与命令回复,在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"
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值