2023-03-09  阅读(1)
原文作者:men_wen 原文地址:https://blog.csdn.net/men_wen/article/details/70145752

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"

    • w:写入命令,会修改数据库。
    • r:读取命令,不会修改数据库。
    • m:一旦执行会增加内存使用,如果内存短缺则不被允许执行。
    • a:管理员命令,例如:SAVE or SHUTDOWN。
    • p:发布订阅有关的命令。
    • f:强制进行复制的命令,无视服务器的脏键。
    • s:不能在脚本中执行的命令。
    • R:随机命令。相同的键有相同的参数,在相同的数据库中,可能会有不同的结果。
    • S:如果在脚本中调用,那么会对这个命令的输出进行一次排序。
    • l:当载入数据库时,允许执行该命令。
    • t:从节点服务器持有过期数据时,允许执行的命令。
    • M:不能在 MONITOR 下自动传播的命令。
    • k:为该命令执行一个隐式的 ASKING,所以在集群模式下,如果槽被标记为’importing’,那这个命令会被接收。
    • F:快速执行的命令。时间复杂度为O(1) or O(log(N))的命令只要内核调度为Redis分配时间片,那么就不应该在执行时被延迟。
  • int flags :sflags的二进制标识形式,可以通过位运算进行组合。对应0

  • redisGetKeysProc *getkeys_proc :从命令中获取键的参数,是一个可选的功能,一般用于三个字段不够执行键的参数的情况。对应NULL

  • int firstkey :第一个参数是 key。对应1

  • int lastkey :最后一个参数是 key。对应1

  • int keystep :从第一个 key 到最后一个 key 的步长。MSET 的步长是 2 因为:key,val,key,val,…。对应1

  • long long microseconds :记录执行命令的耗费总时长。对应0

  • long long calls :记录命令被执行的总次数。对应0

当从命令表中找到命令后,会将找到的命令的地址,返回给struct redisCommand *cmd, *lastcmd;这两个指针保存起来。到此查找命令的操作就完成。

2.2 执行命令前的准备

此时,命令已经在命令表中查找到,并且保存在了对应的指针中。但是真正执行前,还进行了许多的情况的判断。我们简单列举几种。

  • 首先就是判断命令的参数是否匹配。
  • 检查服务器的认证是否通过。
  • 集群模式下的判断。
  • 服务器最大内存限制是否通过。
  • 某些情况下,不接受写命令。
  • 发布订阅模式。
  • 是否是lua脚本中的命令。
  • 等等……

所以,命令执行的过程还是很复杂的,简单总结一句: 命令不易,何况人生。

2.3 执行命令

执行命令调用了call(c,CMD_CALL_FULL)函数,该函数是执行命令的核心。但是不用想,这个函数一定是对回调函数c->cmd->proc(c)的封装,因为proc指向命令的实现函数。我们贴出该函数的代码:

    void call(client *c, int flags) {
        long long dirty, start, duration;
        int client_old_flags = c->flags;    //备份client的flags
    
        // 将命令发送给 MONITOR
        if (listLength(server.monitors) &&
            !server.loading &&
            !(c->cmd->flags & (CMD_SKIP_MONITOR|CMD_ADMIN)))
        {
            replicationFeedMonitors(c,server.monitors,c->db->id,c->argv,c->argc);
        }
    
        // 清除一些需要按照命令需求设置的标志,以防干扰
        c->flags &= ~(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
        // 初始化Redis操作数组,用来追加命令的传播
        redisOpArrayInit(&server.also_propagate);
    
        /* Call the command. */
        // 备份脏键数
        dirty = server.dirty;
        // 获取执行命令的开始时间
        start = ustime();
        // 执行命令
        c->cmd->proc(c);
        // 命令的执行时间
        duration = ustime()-start;
        // 命令修改的键的个数
        dirty = server.dirty-dirty;
        if (dirty < 0) dirty = 0;
    
        // 当执行 EVAL 命令时正在加载AOF,而且不希望Lua调用的命令进入slowlog或填充统计信息
        if (server.loading && c->flags & CLIENT_LUA)
            flags &= ~(CMD_CALL_SLOWLOG | CMD_CALL_STATS);  //取消慢查询和记录统计信息的标志
    
        // 如果函数调用者是Lua脚本,且命令的flags或客户端的flags指定了强制传播,我们要强制EVAL调用者传播脚本
        if (c->flags & CLIENT_LUA && server.lua_caller) {
            // 如果指定了强制将命令传播到从节点
            if (c->flags & CLIENT_FORCE_REPL)
                server.lua_caller->flags |= CLIENT_FORCE_REPL;  //强制执行lua脚本的client要传播命令到从节点
            // 如果指定了强制将节点传播到AOF中
            if (c->flags & CLIENT_FORCE_AOF)
                server.lua_caller->flags |= CLIENT_FORCE_AOF;   //强制执行lua脚本的client要传播命令到AOF文件
        }
    
        // 命令的flags指定了慢查询标志,要将总的统计信息推入慢查询日志中
        if (flags & CMD_CALL_SLOWLOG && c->cmd->proc != execCommand) {
            char *latency_event = (c->cmd->flags & CMD_FAST) ?
                                  "fast-command" : "command";
            // 记录将延迟事件和延迟时间关联到延迟诊断的字典中
            latencyAddSampleIfNeeded(latency_event,duration/1000);
            // 将总的统计信息推入慢查询日志中
            slowlogPushEntryIfNeeded(c->argv,c->argc,duration);
        }
        // 命令的flags指定了CMD_CALL_STATS,更新命令的统计信息
        if (flags & CMD_CALL_STATS) {
            c->lastcmd->microseconds += duration;
            c->lastcmd->calls++;
        }
    
        // 如果client设置了强制传播的标志或修改了数据集,则将命令发送给从节点服务器或追加到AOF中
        if (flags & CMD_CALL_PROPAGATE &&
            (c->flags & CLIENT_PREVENT_PROP) != CLIENT_PREVENT_PROP)
        {
            // 保存传播的标志,初始化为空
            int propagate_flags = PROPAGATE_NONE;
    
            // 如果命令修改了数据库中的键,则要传播到AOF和从节点中
            if (dirty) propagate_flags |= (PROPAGATE_AOF|PROPAGATE_REPL);
    
            // 如果client设置了强制AOF和复制的标志,则设置传播的标志
            if (c->flags & CLIENT_FORCE_REPL) propagate_flags |= PROPAGATE_REPL;
            if (c->flags & CLIENT_FORCE_AOF) propagate_flags |= PROPAGATE_AOF;
    
            // 如果client的flags设置了CLIENT_PREVENT_REPL/AOF_PROP,表示阻止命令的传播到从节点或AOF,则取消传播对应标志
            if (c->flags & CLIENT_PREVENT_REPL_PROP ||
                !(flags & CMD_CALL_PROPAGATE_REPL))
                    propagate_flags &= ~PROPAGATE_REPL;
            if (c->flags & CLIENT_PREVENT_AOF_PROP ||
                !(flags & CMD_CALL_PROPAGATE_AOF))
                    propagate_flags &= ~PROPAGATE_AOF;
    
            // 如果至少设置了一种传播,则执行相应传播命令操作
            if (propagate_flags != PROPAGATE_NONE)
                propagate(c->cmd,c->db->id,c->argv,c->argc,propagate_flags);
        }
    
        // 清除一些需要按照命令需求设置的标志,以防干扰
        c->flags &= ~(CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
        // 恢复client原始的flags
        c->flags |= client_old_flags &
            (CLIENT_FORCE_AOF|CLIENT_FORCE_REPL|CLIENT_PREVENT_PROP);
    
        // 传播追加在Redis操作数组中的命令
        if (server.also_propagate.numops) {
            int j;
            redisOp *rop;
            // 如果命令的flags设置传播的标志
            if (flags & CMD_CALL_PROPAGATE) {
                // 遍历所有的命令
                for (j = 0; j < server.also_propagate.numops; j++) {
                    rop = &server.also_propagate.ops[j];
                    int target = rop->target;
                    /* Whatever the command wish is, we honor the call() flags. */
                    // 执行相应传播命令操作
                    if (!(flags&CMD_CALL_PROPAGATE_AOF)) target &= ~PROPAGATE_AOF;
                    if (!(flags&CMD_CALL_PROPAGATE_REPL)) target &= ~PROPAGATE_REPL;
                    if (target)
                        propagate(rop->cmd,rop->dbid,rop->argv,rop->argc,target);
                }
            }
            // 释放Redis操作数组
            redisOpArrayFree(&server.also_propagate);
        }
        // 命令执行的次数加1
        server.stat_numcommands++;
    }

执行命令时,可以指定一个flags。这个flags是用于 执行完命令之后的一些后续工作 。我们说明这些flags的含义:

    CMD_CALL_NONE:没有指定flags
    CMD_CALL_SLOWLOG:检查命令的执行速度,如果需要记录在慢查询日志中
    CMD_CALL_STATS:记录命令的统计信息
    CMD_CALL_PROPAGATE_AOF:如果client设置了强制传播的标志或修改了数据集,则将命令追加到AOF文件中
    CMD_CALL_PROPAGATE_REPL:如果client设置了强制传播的标志或修改了数据集,则将命令发送给从节点服务器中
    CMD_CALL_PROPAGATE:如果client设置了强制传播的标志或修改了数据集,则将命令发送给从节点服务器或追加到AOF中
    CMD_CALL_FULL:包含以上所有的含义

执行命令c->cmd->proc(c)就相当于执行了命令实现的函数,然后会在执行完成后,由这些函数产生相应的命令回复,根据回复的大小,会将回复保存在 输出缓冲区 buf回复链表repl 中。然后服务器会调用writeToClient()函数来将回复写到fd中。详细请看:Redis 网络连接库剖析

至此,一条命令的执行过程就很清楚明了了。

3. Redis服务器的周期性任务

我们曾经在Redis 事件处理实现一文中说到,Redis的事件分为文件事件(file event)时间事件(time event)。时间事件虽然是晚于文件事件执行,但是会每隔100ms都会执行一次。话不多说直接上代码:Redis 单机服务器实现源码注释

    // 使用一个宏定义:run_with_period(milliseconds) { .... },实现一部分代码有次数限制的被执行
    int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
        int j;
        UNUSED(eventLoop);
        UNUSED(id);
        UNUSED(clientData);
    
        // 如果设置了看门狗,则在过期时间内,递达一个 SIGALRM 信号
        if (server.watchdog_period) watchdogScheduleSignal(server.watchdog_period);
    
        // 设置服务器的时间缓存
        updateCachedTime();
    
        // 更新服务器的一些统计值
        run_with_period(100) {
            // 命令执行的次数
            trackInstantaneousMetric(STATS_METRIC_COMMAND,server.stat_numcommands);
            // 从网络读到的字节数
            trackInstantaneousMetric(STATS_METRIC_NET_INPUT,
                    server.stat_net_input_bytes);
            // 已经写到网络的字节数
            trackInstantaneousMetric(STATS_METRIC_NET_OUTPUT,
                    server.stat_net_output_bytes);
        }
    
        // 服务器的LRU时间表示位数为24位,因此最长表示2^24秒,大约1.5年,只要在1.5年内,该对象被访问,那么就不会出现对象的LRU时间比服务器的时钟还要年轻的现象
        // LRU_CLOCK_RESOLUTION 可以改变LRU时间的精度
    
        // 获取服务器的LRU时钟
        server.lruclock = getLRUClock();
    
        // 更新服务器的最大内存使用量峰值
        if (zmalloc_used_memory() > server.stat_peak_memory)
            server.stat_peak_memory = zmalloc_used_memory();
    
        // 更新常驻内存的大小
        server.resident_set_size = zmalloc_get_rss();
    
        // 安全的关闭服务器
        if (server.shutdown_asap) {
            // 关闭服务器前的准备动作,成功则关闭服务器
            if (prepareForShutdown(SHUTDOWN_NOFLAGS) == C_OK) exit(0);
            // 失败则打印日志
            serverLog(LL_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
            // 撤销关闭服务器标志
            server.shutdown_asap = 0;
        }
    
        // 打印数据库的信息到日志中
        run_with_period(5000) {
            // 遍历数据库
            for (j = 0; j < server.dbnum; j++) {
                long long size, used, vkeys;
    
                // 获取当前数据库的键值对字典的槽位数,键值对字典已使用的数量,过期键字典已使用的数量
                size = dictSlots(server.db[j].dict);
                used = dictSize(server.db[j].dict);
                vkeys = dictSize(server.db[j].expires);
                // 打印到日志中
                if (used || vkeys) {
                    serverLog(LL_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lld slots HT.",j,used,vkeys,size);
                    /* dictPrintStats(server.dict); */
                }
            }
        }
    
        // 如果服务器不在哨兵模式下,那么周期性打印一些连接client的信息到日志中
        if (!server.sentinel_mode) {
            run_with_period(5000) {
                serverLog(LL_VERBOSE,
                    "%lu clients connected (%lu slaves), %zu bytes in use",
                    listLength(server.clients)-listLength(server.slaves),
                    listLength(server.slaves),
                    zmalloc_used_memory());
            }
        }
    
        // 执行client的周期性任务
        clientsCron();
    
        // 执行数据库的周期性任务
        databasesCron();
    
        // 如果当前没有正在进行RDB和AOF持久化操作,且AOF重写操作被提上了日程,那么在后台执行AOF的重写操作
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
            server.aof_rewrite_scheduled)
        {
            rewriteAppendOnlyFileBackground();
        }
    
        // 如果正在进行RDB或AOF重写等操作,那么等待接收子进程发来的信息
        if (server.rdb_child_pid != -1 || server.aof_child_pid != -1 ||
            ldbPendingChildren())
        {
            int statloc;
            pid_t pid;
    
            // 接收所有子进程发送的信号,非阻塞
            if ((pid = wait3(&statloc,WNOHANG,NULL)) != 0) {
                // 获取退出码
                int exitcode = WEXITSTATUS(statloc);
                int bysignal = 0;
    
                // 判断子进程是否因为信号而终止,是的话,取得子进程因信号而中止的信号码
                if (WIFSIGNALED(statloc)) bysignal = WTERMSIG(statloc);
    
                // 子进程没有退出,还在进行RDB或AOF重写等操作
                if (pid == -1) {
                    // 打印日志
                    serverLog(LL_WARNING,"wait3() returned an error: %s. "
                        "rdb_child_pid = %d, aof_child_pid = %d",
                        strerror(errno),
                        (int) server.rdb_child_pid,
                        (int) server.aof_child_pid);
                // RDB持久化完成
                } else if (pid == server.rdb_child_pid) {
                    // 将RDB文件写入磁盘或网络中
                    backgroundSaveDoneHandler(exitcode,bysignal);
                // AOF持久化完成
                } else if (pid == server.aof_child_pid) {
                    // 将重写缓冲区的命令追加AOF文件中,且进行同步操作
                    backgroundRewriteDoneHandler(exitcode,bysignal);
                // 其他子进程,打印日志
                } else {
                    if (!ldbRemoveChild(pid)) {
                        serverLog(LL_WARNING,
                            "Warning, detected child with unmatched pid: %ld",
                            (long)pid);
                    }
                }
                // 更新能否resize哈希的策略
                updateDictResizePolicy();
            }
    
        // 没有正在进行RDB或AOF重写等操作,那么检查是否需要执行
        } else {
            // 遍历save命令的参数数组
             for (j = 0; j < server.saveparamslen; j++) {
                struct saveparam *sp = server.saveparams+j;
    
                // 数据库的键被修改的次数大于SAVE命令参数指定的修改次数,且已经过了SAVE命令参数指定的秒数
                if (server.dirty >= sp->changes &&
                    server.unixtime-server.lastsave > sp->seconds &&
                    (server.unixtime-server.lastbgsave_try >
                     CONFIG_BGSAVE_RETRY_DELAY ||
                     server.lastbgsave_status == C_OK))
                {
                    serverLog(LL_NOTICE,"%d changes in %d seconds. Saving...",
                        sp->changes, (int)sp->seconds);
                    // 进行 BGSAVE 操作
                    rdbSaveBackground(server.rdb_filename);
                    break;
                }
             }
    
             // 是否触发AOF重写操作
             if (server.rdb_child_pid == -1 &&
                 server.aof_child_pid == -1 &&
                 server.aof_rewrite_perc &&
                 server.aof_current_size > server.aof_rewrite_min_size)
             {
                // 上一次重写后的大小
                long long base = server.aof_rewrite_base_size ?
                                server.aof_rewrite_base_size : 1;
                // AOF文件增长的百分比
                long long growth = (server.aof_current_size*100/base) - 100;
                // 大于设置的百分比100则进行AOF后台重写
                if (growth >= server.aof_rewrite_perc) {
                    serverLog(LL_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                    rewriteAppendOnlyFileBackground();
                }
             }
        }
    
        // 将AOF缓存冲洗到磁盘中
        if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);
    
        // 当AOF重写操作,同样将重写缓冲区的数据刷新到AOF文件中
        run_with_period(1000) {
            if (server.aof_last_write_status == C_ERR)
                flushAppendOnlyFile(0);
        }
    
        // 释放被设置为异步释放的client
        freeClientsInAsyncFreeQueue();
    
        // 解除client的暂停状态
        clientsArePaused(); /* Don't check return value, just use the side effect. */
    
        // 周期性执行复制的任务
        run_with_period(1000) replicationCron();
    
        /* Run the Redis Cluster cron. */
        // 周期性执行集群任务
        run_with_period(100) {
            if (server.cluster_enabled) clusterCron();
        }
    
        //周期性执行哨兵任务
        run_with_period(100) {
            if (server.sentinel_mode) sentinelTimer();
        }
    
        // 清理过期的被缓存的sockets连接
        run_with_period(1000) {
            migrateCloseTimedoutSockets();
        }
    
        // 如果 BGSAVE 被提上过日程,那么进行BGSAVE操作,因为AOF重写操作在更新
        // 注意:此代码必须在上面的replicationCron()调用之后,确保在重构此文件以保持此顺序时。 这是有用的,因为我们希望优先考虑RDB节省的复制
        if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
            server.rdb_bgsave_scheduled &&
            (server.unixtime-server.lastbgsave_try > CONFIG_BGSAVE_RETRY_DELAY ||
             server.lastbgsave_status == C_OK))
        {
            // 更新执行BGSAVE,成功则清除rdb_bgsave_scheduled标志
            if (rdbSaveBackground(server.rdb_filename) == C_OK)
                server.rdb_bgsave_scheduled = 0;
        }
    
        // 周期loop计数器加1
        server.cronloops++;
        // 返回周期,默认为100ms
        return 1000/server.hz;
    }

我们也是大致总结列出部分:

  • 主动删除过期的键(也可以在读数据库时被动删除)
  • 喂看门狗 watchdog
  • 更新一些统计值
  • 渐进式rehash
  • 触发 BGSAVE / AOF 的重写操作,并处理子进程的中断
  • 不同状态的client的超时
  • 复制重连
  • 等……

我们重点看两个函数,一个是关于 客户端资源管理clientsCron(),一个是关于 数据库资源管理databasesCron()

3.1客户端资源管理

服务器要定时检查client是否与服务器有交互,如果超过了设置的限制时间,则要释放client所占用的资源。具体的函数是clientsCronHandleTimeout(),它被clientsCron()函数所调用。

    // 检查超时,如果client中断超时返回非零值,函数获取当前时间作为参数因为他被一个循环中调用多次。所以调用gettimeofday()为每一次迭代都是昂贵的,而没有任何实际的效益
    // client被关闭则返回1,没有关闭返回0
    int clientsCronHandleTimeout(client *c, mstime_t now_ms) {
        // 当前时间,单位秒
        time_t now = now_ms/1000;
    
        // 当前时间 - client上一次和服务器交互的时间 如果大于 服务器中设置client超过的最大时间
        // 不检查这四类client的超时时间:slaves从节点服务器、masters主节点服务器、BLPOP被阻塞的client、订阅状态的client
        if (server.maxidletime &&
            !(c->flags & CLIENT_SLAVE) &&    /* no timeout for slaves */
            !(c->flags & CLIENT_MASTER) &&   /* no timeout for masters */
            !(c->flags & CLIENT_BLOCKED) &&  /* no timeout for BLPOP */
            !(c->flags & CLIENT_PUBSUB) &&   /* no timeout for Pub/Sub clients */
            (now - c->lastinteraction > server.maxidletime))
        {
            serverLog(LL_VERBOSE,"Closing idle client");
            freeClient(c);
            return 1;
        // 如果client处于BLPOP被阻塞
        } else if (c->flags & CLIENT_BLOCKED) {
            // 如果阻塞的client的超时时间已经到达
            if (c->bpop.timeout != 0 && c->bpop.timeout < now_ms) {
                // 回复client一个空回复
                replyToBlockedClientTimedOut(c);
                // 接触client的阻塞状态
                unblockClient(c);
            // 如果服务器处于集群模式
            } else if (server.cluster_enabled) {
                // 重定向client的阻塞到其他的服务器
                if (clusterRedirectBlockedClientIfNeeded(c))
                    // 解除阻塞
                    unblockClient(c);
            }
        }
        return 0;
    }

3.2 数据库资源管理

服务器要定时检查数据库的输入缓冲区是否可以resize,以节省内存资源。而resize输入缓冲区的两个条件:

  • 输入缓冲区的大小大于32K以及超过缓冲区的峰值的2倍。
  • client超过时间大于2秒,且输入缓冲区的大小超过1k

实现的函数是clientsCronResizeQueryBuffer(),被databasesCron()函数所调用。

    // resize客户端的输入缓冲区
    int clientsCronResizeQueryBuffer(client *c) {
        // 获取输入缓冲区的大小
        size_t querybuf_size = sdsAllocSize(c->querybuf);
        // 计算服务器对于client的空转时间,也就是client的超时时间
        time_t idletime = server.unixtime - c->lastinteraction;
    
        // resize输入缓冲区的两个条件:
        //      1. 输入缓冲区的大小大于32K以及超过缓冲区的峰值的2倍
        //      2. client超过时间大于2秒,且输入缓冲区的大小超过1k
        if (((querybuf_size > PROTO_MBULK_BIG_ARG) &&
             (querybuf_size/(c->querybuf_peak+1)) > 2) ||
             (querybuf_size > 1024 && idletime > 2))
        {
            // 只有输入缓冲区的未使用大小超过1k,则会释放未使用的空间
            if (sdsavail(c->querybuf) > 1024) {
                c->querybuf = sdsRemoveFreeSpace(c->querybuf);
            }
        }
        // 清空输入缓冲区的峰值
        c->querybuf_peak = 0;
        return 0;
    }

4. maxmemory的策略

Redis 服务器对内存使用会有一个server.maxmemory的限制,如果超过这个限制,就要通过删除一些键空间来释放一些内存,具体函数对应freeMemoryIfNeeded()Redis 单机服务器实现源码注释

释放内存时,可以指定不同的策略。策略保存在maxmemory_policy中,他可以指定以下的几个值:

    #define MAXMEMORY_VOLATILE_LRU 0
    #define MAXMEMORY_VOLATILE_TTL 1
    #define MAXMEMORY_VOLATILE_RANDOM 2
    #define MAXMEMORY_ALLKEYS_LRU 3
    #define MAXMEMORY_ALLKEYS_RANDOM 4
    #define MAXMEMORY_NO_EVICTION 5

可以看出主要分为三种,

  • LRU:优先删除最近最少使用的键。
  • TTL:优先删除生存时间最短的键。
  • RANDOM:随机删除。

ALLKEYSVOLATILE的不同之处就是要确定是从数据库的 键值对字典 还是 过期键字典 中删除。

了解了以上这些,我们贴出代码:

    // 按需释放内存空间
    int freeMemoryIfNeeded(void) {
        size_t mem_used, mem_tofree, mem_freed;
        int slaves = listLength(server.slaves);
        mstime_t latency, eviction_latency;
    
        // 计算出服务器总的内存使用量,但是有两部分要减去
        /*
            1、从节点的输出缓冲区
            2、AOF缓冲区
        */
        mem_used = zmalloc_used_memory();
        // 存在从节点
        if (slaves) {
            listIter li;
            listNode *ln;
    
            listRewind(server.slaves,&li);
            // 遍历从节点链表
            while((ln = listNext(&li))) {
                client *slave = listNodeValue(ln);
                // 获取当前从节点的输出缓冲区的大小,不包含静态的固定回复缓冲区,因为他总被分配
                unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
                // 减去当前从节点的输出缓冲区的大小
                if (obuf_bytes > mem_used)
                    mem_used = 0;
                else
                    mem_used -= obuf_bytes;
            }
        }
        // 如果开启了AOF操作
        if (server.aof_state != AOF_OFF) {
            // 减去AOF缓冲区的大小
            mem_used -= sdslen(server.aof_buf);
            // 减去AOF重写缓冲区的大小
            mem_used -= aofRewriteBufferSize();
        }
    
        // 如果没有超过服务器设置的最大内存限制,则返回C_OK
        if (mem_used <= server.maxmemory) return C_OK;
        // 如果内存回收策略为不回收,则返回C_ERR
        if (server.maxmemory_policy == MAXMEMORY_NO_EVICTION)
            return C_ERR; /* We need to free memory, but policy forbids. */
    
        // 计算需要回收的大小
        mem_tofree = mem_used - server.maxmemory;
        // 已回收的大小
        mem_freed = 0;
        // 设置回收延迟检测开始的时间
        latencyStartMonitor(latency);
        // 循环回收,直到到达需要回收大小
        while (mem_freed < mem_tofree) {
            int j, k, keys_freed = 0;
    
            // 遍历所有的数据库
            for (j = 0; j < server.dbnum; j++) {
                long bestval = 0; /* just to prevent warning */
                sds bestkey = NULL;
                dictEntry *de;
                redisDb *db = server.db+j;
                dict *dict;
                // 如果回收策略有ALLKEYS_LRU或RANDOM,从键值对字典中选择回收
                if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_LRU ||
                    server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM)
                {
                    // 则从键值对字典中选择回收的键。选择样品字典
                    dict = server.db[j].dict;
                } else {
                    // 否则从过期键字典中选择回收的键。选择样品字典
                    dict = server.db[j].expires;
                }
                if (dictSize(dict) == 0) continue;  //跳过空字典
    
                /* volatile-random and allkeys-random policy */
                // 如果回收策略有 ALLKEYS_RANDOM 或 VOLATILE_RANDOM,则是随机挑选
                if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_RANDOM ||
                    server.maxmemory_policy == MAXMEMORY_VOLATILE_RANDOM)
                {
                    // 随机返回一个key
                    de = dictGetRandomKey(dict);
                    bestkey = dictGetKey(de);
                }
    
                /* volatile-lru and allkeys-lru policy */
                // 如果回收策略有 ALLKEYS_LRU 或 VOLATILE_LRU,则使用LRU策略
                else if (server.maxmemory_policy == MAXMEMORY_ALLKEYS_LRU ||
                    server.maxmemory_policy == MAXMEMORY_VOLATILE_LRU)
                {
                    // 回收池
                    struct evictionPoolEntry *pool = db->eviction_pool;
    
                    while(bestkey == NULL) {
                        // evictionPoolPopulate()用于在每次我们想要过期一个键的时候,用几个节点填充evictionPool。 空闲时间小于当前key的之一的key被添加。 如果有free的节点,则始终添加key。 我们按升序插入key,所以空闲时间越短的键在左边,右边的空闲时间越长。
                        // 从样品字典dict中随机选择样品
                        evictionPoolPopulate(dict, db->dict, db->eviction_pool);
                        // 从空转时间最长的开始遍历
                        for (k = MAXMEMORY_EVICTION_POOL_SIZE-1; k >= 0; k--) {
                            // 跳过空位置
                            if (pool[k].key == NULL) continue;
                            // 从样品字典dict中查找当前key
                            de = dictFind(dict,pool[k].key);
    
                            // 从收回池中删除
                            sdsfree(pool[k].key);
                            // 释放位置
                            memmove(pool+k,pool+k+1,
                                sizeof(pool[0])*(MAXMEMORY_EVICTION_POOL_SIZE-k-1));
                            // 重置key和空转时间
                            pool[MAXMEMORY_EVICTION_POOL_SIZE-1].key = NULL;
                            pool[MAXMEMORY_EVICTION_POOL_SIZE-1].idle = 0;
    
                            // 如果从样品字典中可以找到,则保存键
                            if (de) {
                                bestkey = dictGetKey(de);
                                break;
                            // 没找到,则继续找下一个样品空间所保存的键
                            } else {
                                /* Ghost... */
                                continue;
                            }
                        }
                        // 如果当前选出的所有的样品都没找到,则重新选择一批样品,知道找到一个可以释放的键
                    }
                }
    
                /* volatile-ttl */
                // 如果回收策略有 VOLATILE_TTL,则选择生存时间最短的键
                else if (server.maxmemory_policy == MAXMEMORY_VOLATILE_TTL) {
                    // 抽样个数为maxmemory_samples个
                    for (k = 0; k < server.maxmemory_samples; k++) {
                        sds thiskey;
                        long thisval;
    
                        // 返回一个键,获取他的生存时间
                        de = dictGetRandomKey(dict);
                        thiskey = dictGetKey(de);
                        thisval = (long) dictGetVal(de);
    
                        // 如果当前键的生存时间更短,则保存
                        if (bestkey == NULL || thisval < bestval) {
                            bestkey = thiskey;
                            bestval = thisval;
                        }
                    }
                }
    
                /* Finally remove the selected key. */
                // 删除所有被选择的键
                if (bestkey) {
                    long long delta;
    
                    robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                    // 当一个键在主节点中过期时,主节点会发送del命令给从节点和AOF文件
                    propagateExpire(db,keyobj);
                    // 单独计算dbDelete()所释放的空间大小, 在AOF和复制链接中传播DEL的内存实际上大于我们释放的key的内存
                    // 但是无法解释,窦泽不会退出循环
                    // AOF和输出缓冲区的内存最终被释放,所以我们只关心键空间使用的内存
                    delta = (long long) zmalloc_used_memory();
                    // 设置删除key对象的开始时间
                    latencyStartMonitor(eviction_latency);
                    dbDelete(db,keyobj);
                    // 保存删除key对象时间
                    latencyEndMonitor(eviction_latency);
                    // 添加到延迟诊断字典中
                    latencyAddSampleIfNeeded("eviction-del",eviction_latency);
                    // 删除嵌套的延迟事件
                    latencyRemoveNestedEvent(latency,eviction_latency);
                    // 计算删除这个键的大小
                    delta -= (long long) zmalloc_used_memory();
                    // 更新内存释放量
                    mem_freed += delta;
                    // 服务器总的回收键的个数计数器加1
                    server.stat_evictedkeys++;
                    // 事件通知
                    notifyKeyspaceEvent(NOTIFY_EVICTED, "evicted",
                        keyobj, db->id);
                    // 释放键对象
                    decrRefCount(keyobj);
                    // 释放键的个数加1
                    keys_freed++;
    
                    // 如果有从节点,则刷新所有的输出缓冲区数据
                    if (slaves) flushSlavesOutputBuffers();
                }
            }
            // 如果所有数据库都没有释放键,返回C_ERR
            if (!keys_freed) {
                latencyEndMonitor(latency);
                latencyAddSampleIfNeeded("eviction-cycle",latency);
                return C_ERR; /* nothing to free... */
            }
        }
        // 计算回收延迟的时间
        latencyEndMonitor(latency);
        latencyAddSampleIfNeeded("eviction-cycle",latency);
        return C_OK;
    }

5. Redis服务器的main函数

Redis 服务器的mian()主要执行了一下操作:Redis 单机服务器实现源码注释

  • 初始化服务器状态
  • 载入服务器的配置
  • 初始化服务器数据结构
  • 载入持久化文件还原数据库状态
  • 执行事件循环
    int main(int argc, char **argv) {
        struct timeval tv;
        int j;
    
    #ifdef INIT_SETPROCTITLE_REPLACEMENT
        spt_init(argc, argv);
    #endif
        // 本函数用来配置地域的信息,设置当前程序使用的本地化信息,LC_COLLATE 配置字符串比较
        setlocale(LC_COLLATE,"");
        // 设置线程安全
        zmalloc_enable_thread_safeness();
        // 设置内存溢出的处理函数
        zmalloc_set_oom_handler(redisOutOfMemoryHandler);
        // 初始化随机数发生器
        srand(time(NULL)^getpid());
        // 保存当前信息
        gettimeofday(&tv,NULL);
        // 设置哈希函数的种子
        dictSetHashFunctionSeed(tv.tv_sec^tv.tv_usec^getpid());
        // 检查开启哨兵模式的两种方式
        server.sentinel_mode = checkForSentinelMode(argc,argv);
        // 初始化服务器配置
        initServerConfig();
    
        // 设置可执行文件的绝对路径
        server.executable = getAbsolutePath(argv[0]);
        // 分配执行executable文件的参数列表的空间
        server.exec_argv = zmalloc(sizeof(char*)*(argc+1));
        server.exec_argv[argc] = NULL;
        // 保存当前参数
        for (j = 0; j < argc; j++) server.exec_argv[j] = zstrdup(argv[j]);
    
        // 如果已开启哨兵模式
        if (server.sentinel_mode) {
            // 初始化哨兵的配置
            initSentinelConfig();
            initSentinel();
        }
    
        // 检查是否执行"redis-check-rdb"检查程序
        if (strstr(argv[0],"redis-check-rdb") != NULL)
            redis_check_rdb_main(argc,argv);    //该函数不会返回
    
        // 解析参数
        if (argc >= 2) {
            j = 1; /* First option to parse in argv[] */
            sds options = sdsempty();
            char *configfile = NULL;
    
            /* Handle special options --help and --version */
            // 指定了打印版本信息,然后退出
            if (strcmp(argv[1], "-v") == 0 ||
                strcmp(argv[1], "--version") == 0) version();
            // 执行帮助信息,然后退出
            if (strcmp(argv[1], "--help") == 0 ||
                strcmp(argv[1], "-h") == 0) usage();
            // 执行内存测试程序
            if (strcmp(argv[1], "--test-memory") == 0) {
                if (argc == 3) {
                    memtest(atoi(argv[2]),50);
                    exit(0);
                } else {
                    fprintf(stderr,"Please specify the amount of memory to test in megabytes.\n");
                    fprintf(stderr,"Example: ./redis-server --test-memory 4096\n\n");
                    exit(1);
                }
            }
    
            /* First argument is the config file name? */
            // 如果第1个参数不是'-',那么是配置文件
            if (argv[j][0] != '-' || argv[j][1] != '-') {
                configfile = argv[j];
                // 设置配置文件的绝对路径
                server.configfile = getAbsolutePath(configfile);
                /* Replace the config file in server.exec_argv with
                 * its absoulte path. */
                zfree(server.exec_argv[j]);
                // 设置可执行的参数列表
                server.exec_argv[j] = zstrdup(server.configfile);
                j++;
            }
    
            // 解析指定的对象
            while(j != argc) {
                // 如果是以'-'开头
                if (argv[j][0] == '-' && argv[j][1] == '-') {
                    /* Option name */
                    // 跳过"--check-rdb"
                    if (!strcmp(argv[j], "--check-rdb")) {
                        /* Argument has no options, need to skip for parsing. */
                        j++;
                        continue;
                    }
                    // 每个选项之间用'\n'隔开
                    if (sdslen(options)) options = sdscat(options,"\n");
                    // 将选项追加在sds中
                    options = sdscat(options,argv[j]+2);
                    // 选项和参数用 " "隔开
                    options = sdscat(options," ");
                } else {
                    /* Option argument */
                    // 追加选项参数
                    options = sdscatrepr(options,argv[j],strlen(argv[j]));
                    options = sdscat(options," ");
                }
                j++;
            }
            // 如果开启哨兵模式,哨兵模式配置文件不正确
            if (server.sentinel_mode && configfile && *configfile == '-') {
                serverLog(LL_WARNING,
                    "Sentinel config from STDIN not allowed.");
                serverLog(LL_WARNING,
                    "Sentinel needs config file on disk to save state.  Exiting...");
                exit(1);
            }
            // 重置save命令的参数
            resetServerSaveParams();
            // 载入配置文件
            loadServerConfig(configfile,options);
            sdsfree(options);
        } else {
            serverLog(LL_WARNING, "Warning: no config file specified, using the default config. In order to specify a config file use %s /path/to/%s.conf", argv[0], server.sentinel_mode ? "sentinel" : "redis");
        }
    
        // 是否被监视
        server.supervised = redisIsSupervised(server.supervised_mode);
        // 是否以守护进程的方式运行
        int background = server.daemonize && !server.supervised;
        if (background) daemonize();
    
        // 初始化服务器
        initServer();
        // 创建保存pid的文件
        if (background || server.pidfile) createPidFile();
        // 为服务器进程设置标题
        redisSetProcTitle(argv[0]);
        // 打印Redis的logo
        redisAsciiArt();
        // 检查backlog队列
        checkTcpBacklogSettings();
    
        // 如果不是哨兵模式
        if (!server.sentinel_mode) {
            /* Things not needed when running in Sentinel mode. */
            serverLog(LL_WARNING,"Server started, Redis version " REDIS_VERSION);
        #ifdef __linux__
            // 打印内存警告
            linuxMemoryWarnings();
        #endif
            // 从AOF文件或RDB文件载入数据
            loadDataFromDisk();
            // 如果开启了集群模式
            if (server.cluster_enabled) {
                // 集群模式下验证载入的数据
                if (verifyClusterConfigWithData() == C_ERR) {
                    serverLog(LL_WARNING,
                        "You can't have keys in a DB different than DB 0 when in "
                        "Cluster mode. Exiting.");
                    exit(1);
                }
            }
            // 打印端口号
            if (server.ipfd_count > 0)
                serverLog(LL_NOTICE,"The server is now ready to accept connections on port %d", server.port);
            // 打印本地套接字fd
            if (server.sofd > 0)
                serverLog(LL_NOTICE,"The server is now ready to accept connections at %s", server.unixsocket);
        } else {
            // 开启哨兵模式,哨兵模式和集群模式只能开启一种
            sentinelIsRunning();
        }
    
        /* Warning the user about suspicious maxmemory setting. */
        // 最大内存限制是否配置正确
        if (server.maxmemory > 0 && server.maxmemory < 1024*1024) {
            serverLog(LL_WARNING,"WARNING: You specified a maxmemory value that is less than 1MB (current value is %llu bytes). Are you sure this is what you really want?", server.maxmemory);
        }
    
        // 进入事件循环之前执行beforeSleep()函数
        aeSetBeforeSleepProc(server.el,beforeSleep);
        // 运行事件循环,一直到服务器关闭
        aeMain(server.el);
        // 服务器关闭,删除事件循环
        aeDeleteEventLoop(server.el);
        return 0;
    }

Java 面试宝典是大明哥全力打造的 Java 精品面试题,它是一份靠谱、强大、详细、经典的 Java 后端面试宝典。它不仅仅只是一道道面试题,而是一套完整的 Java 知识体系,一套你 Java 知识点的扫盲贴。

它的内容包括:

  • 大厂真题:Java 面试宝典里面的题目都是最近几年的高频的大厂面试真题。
  • 原创内容:Java 面试宝典内容全部都是大明哥原创,内容全面且通俗易懂,回答部分可以直接作为面试回答内容。
  • 持续更新:一次购买,永久有效。大明哥会持续更新 3+ 年,累计更新 1000+,宝典会不断迭代更新,保证最新、最全面。
  • 覆盖全面:本宝典累计更新 1000+,从 Java 入门到 Java 架构的高频面试题,实现 360° 全覆盖。
  • 不止面试:内容包含面试题解析、内容详解、知识扩展,它不仅仅只是一份面试题,更是一套完整的 Java 知识体系。
  • 宝典详情:https://www.yuque.com/chenssy/sike-java/xvlo920axlp7sf4k
  • 宝典总览:https://www.yuque.com/chenssy/sike-java/yogsehzntzgp4ly1
  • 宝典进展:https://www.yuque.com/chenssy/sike-java/en9ned7loo47z5aw

目前 Java 面试宝典累计更新 400+ 道,总字数 42w+。大明哥还在持续更新中,下图是大明哥在 2024-12 月份的更新情况:

想了解详情的小伙伴,扫描下面二维码加大明哥微信【daming091】咨询

同时,大明哥也整理一套目前市面最常见的热点面试题。微信搜[大明哥聊 Java]或扫描下方二维码关注大明哥的原创公众号[大明哥聊 Java] ,回复【面试题】 即可免费领取。

阅读全文