Redis源码分析(十二)——列表类型t_list

本文深入探讨了Redis中列表类型(t_list)的实现原理及其客户端阻塞机制。重点介绍了列表类型的底层数据结构、编码方式及客户端如何在特定条件下进入阻塞状态。此外,文章还详细解释了解除客户端阻塞的过程。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

继前面对字符串、哈希表等类型的分析,今天主要分析了列表类型(t_list)。列表类型与前面所分析类型的实现是一致的,都主要是对底层实现数据结构函数的封装调用以实现该类型的各种操作命令,当然这涉及到与客户端、服务器的数据交互和各种维护设计。

值得注意的是:列表类型较不同的是实现了客户端阻塞原语(BLPOP/BRPO/BRPOPLPUSH三个命令):即当这三个命令所操作的键在数据库中不存在或者键值列表为空时将会使得该客户端进入阻塞状态,只有当所需的键值被后续PUSH进入数据库时才会解除阻塞。 而这个过程正是将要分析的主要部分。


疑惑为什么只有在操作列表类型时才对客户端对进行阻塞呢(是因为:比如在哈希表类型的操纵命令中实现了在key存在和不存在 的两种对应命令,比如:HSET / HSETNX 吗?)


列表的编码方式:为REDIS_ENCODING_ZIPLIST和REDIS_ENCODING_LINKEDLIST两种方式。创建新的列表时默认使用压缩列表编码,当以下任一条件满足时,列表将会被转换为双链表编码方式:

** 试图往列表中添加一个字符串,该字符串的长度大于server.list_max_ziplist_value(默认为       64)

**ziplist包含的节点数超过server.lsit_max_ziplist_entries(默认为512)


客户端阻塞:

BLPOP/BRPO/BRPOPLPUSH三个命令可能会造成客户端的阻塞,阻塞条件:

**这有这些命令作用的key在数据库中不存在

**这些命令作用的key的列表为空

否则,则三个命令将执行与其对应的非阻塞版本的命令,如下图:


客户端的阻塞过程:

**客户端的状设为“正在阻塞”,并记录阻塞这个客户端的各个键,以及阻塞的最长时间等数据。

**将客户端的信息记录到server.db[i]->blocking_key(其中i为客户端的数据库编号)    (这步是后面解除阻塞的关键)

**继续维护客户端与服务器的连接,但不再向客户端传送任何信息,造成客户端的阻塞


数据库结构:

//数据库 结构(
typedef struct redisDb {
    dict *dict;              //保存着数据库的所有键值对 的字典(该属性也被称为键空间:key space) /* The keyspace for this DB */
    dict *expires;          // 保存键的过期信息 的字典    /* Timeout of keys with a timeout set */
    dict *blocking_keys;      //阻塞键字典: 该字典的key为那造成某个客户端阻塞的键,而值是链表:记录因为该key所阻塞的客户端(阻塞的客户端可能不止一个)  /* Keys with clients waiting for data (BLPOP) */
    dict *ready_keys;       //用于解除客户端阻塞 :当一个key被添加进数据库时,如果当前有客户端正在因为这个key而被阻塞,则将该key放入到ready_keys这里,用于解除该key所阻塞的客户端     /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                        //数据库号码
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;



阻塞泛型命令

/* Blocking RPOP/LPOP :弹出给定key的列表的头或者尾元素(有可能阻塞)*/
//RPOP(从尾部弹出)LPOP(从头部弹出)的阻塞函数: 如果给定的所有key都不存在,或者其的值list都为空,则阻塞。
void blockingPopGenericCommand(redisClient *c, int where) {
    robj *o;
    mstime_t timeout;
    int j;

    // 取出 timeout 参数
    if (getTimeoutFromObjectOrReply(c,c->argv[c->argc-1],&timeout,UNIT_SECONDS)
        != REDIS_OK) return;

    // 遍历所有列表键
    for (j = 1; j < c->argc-1; j++) {

        // 取出列表键值
        o = lookupKeyWrite(c->db,c->argv[j]);

        // 有非空列表?
        if (o != NULL) {
            if (o->type != REDIS_LIST) {
                addReply(c,shared.wrongtypeerr);
                return;
            } else {
                // 非空列表且类型为REDIS_LIST	
                if (listTypeLength(o) != 0) {
                    /* Non empty list, this is like a non normal [LR]POP. */
                    char *event = (where == REDIS_HEAD) ? "lpop" : "rpop";

                    // 弹出值
                    robj *value = listTypePop(o,where);

                    redisAssert(value != NULL);

                    // 回复客户端
                    addReplyMultiBulkLen(c,2);
                    // 回复弹出元素的列表
                    addReplyBulk(c,c->argv[j]);
                    // 回复弹出值
                    addReplyBulk(c,value);

                    decrRefCount(value);

                    notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,
                                        c->argv[j],c->db->id);

                    // 删除空列表
                    if (listTypeLength(o) == 0) {
                        dbDelete(c->db,c->argv[j]);
                        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",
                                            c->argv[j],c->db->id);
                    }

                    signalModifiedKey(c->db,c->argv[j]);

                    server.dirty++;

                    /* Replicate it as an [LR]POP instead of B[LR]POP. */
                    // 传播一个 [LR]POP 而不是 B[LR]POP
                    rewriteClientCommandVector(c,2,
                        (where == REDIS_HEAD) ? shared.lpop : shared.rpop,
                        c->argv[j]);
                    return;
                }
            }
        }
    }

    /* If we are inside a MULTI/EXEC and the list is empty the only thing
     * we can do is treating it as a timeout (even with timeout 0). */
    // 如果命令在一个事务中执行,那么为了不产生死等待
    // 服务器只能向客户端发送一个空回复
    if (c->flags & REDIS_MULTI) {
        addReply(c,shared.nullmultibulk);
        return;
    }

    /* If the list is empty or the key does not exists we must block */
    // 所有输入列表键都不存在,只能阻塞了
    blockForKeys(c, c->argv + 1, c->argc - 2, timeout, NULL);
}

客户端阻塞函数:

/* This is how the current blocking POP works, we use BLPOP as example:
 *
 * 以下是目前的阻塞 POP 操作的运作方法,以 BLPOP 作为例子:
 *
 * - If the user calls BLPOP and the key exists and contains a non empty list
 *   then LPOP is called instead. So BLPOP is semantically the same as LPOP
 *   if blocking is not required.
 *
 * - 如果用户调用 BLPOP ,并且列表非空,那么程序执行 LPOP 。
 *   因此,当列表非空时,调用 BLPOP 等于调用 LPOP。
 *
 * - If instead BLPOP is called and the key does not exists or the list is
 *   empty we need to block. In order to do so we remove the notification for
 *   new data to read in the client socket (so that we'll not serve new
 *   requests if the blocking request is not served). Also we put the client
 *   in a dictionary (db->blocking_keys) mapping keys to a list of clients
 *   blocking for this keys.
 *
 * - 当 BLPOP 对一个空键执行时,客户端才会被阻塞:
 *   服务器不再对这个客户端发送任何数据,
 *   对这个客户端的状态设为“被阻塞“,直到解除阻塞为止。
 *   并且客户端会被加入到一个以阻塞键为 key ,
 *   以被阻塞客户端为 value 的字典 db->blocking_keys 中。
 *
 * - If a PUSH operation against a key with blocked clients waiting is
 *   performed, we mark this key as "ready", and after the current command,
 *   MULTI/EXEC block, or script, is executed, we serve all the clients waiting
 *   for this list, from the one that blocked first, to the last, accordingly
 *   to the number of elements we have in the ready list.
 *
 * - 当有 PUSH 命令作用于一个造成客户端阻塞的键时,
 *   程序将这个键标记为“就绪”,并且在执行完这个命令、事务、或脚本之后,
 *   程序会按“先阻塞先服务”的顺序,将列表的元素返回给那些被阻塞的客户端,
 *   被解除阻塞的客户端数量取决于 PUSH 命令推入的元素数量。
 */

/* Set a client in blocking mode for the specified key, with the specified
 * timeout */
// 根据给定数量的 key ,对给定客户端进行阻塞
// 参数:
// keys    任意多个 key
// numkeys keys 的键数量(当有多个阻塞key时,则该客户端将被添加到各个key的客户端链表中)
// timeout 阻塞的最长时限
// target  在解除阻塞时,将结果保存到这个 key 对象,而不是返回给客户端
//         只用于 BRPOPLPUSH 命令
void blockForKeys(redisClient *c, robj **keys, int numkeys, mstime_t timeout, robj *target) {
    dictEntry *de;
    list *l;
    int j;

    // 设置阻塞状态的超时和目标选项
    c->bpop.timeout = timeout;

    // target 在执行 RPOPLPUSH 命令时使用
    c->bpop.target = target;

    if (target != NULL) incrRefCount(target);

    // 关联阻塞客户端和键的相关信息
    for (j = 0; j < numkeys; j++) {

        /* If the key already exists in the dict ignore it. */
        // c->bpop.keys 是一个集合(值为 NULL 的字典)
        // 它记录所有造成客户端阻塞的键
        // 以下语句在键不存在于集合的时候,将它添加到集合
        if (dictAdd(c->bpop.keys,keys[j],NULL) != DICT_OK) continue;

        incrRefCount(keys[j]);

        /* And in the other "side", to map keys -> clients */
        // c->db->blocking_keys 字典的键为造成客户端阻塞的键
        // 而值则是一个链表,链表中包含了所有被阻塞的客户端
        // 以下程序将阻塞键和被阻塞客户端关联起来
        de = dictFind(c->db->blocking_keys,keys[j]);
        if (de == NULL) {
            // 链表不存在,新创建一个,并将它关联到字典中
            int retval;

            /* For every key we take a list of clients blocked for it */
            l = listCreate();
            retval = dictAdd(c->db->blocking_keys,keys[j],l);
            incrRefCount(keys[j]);
            redisAssertWithInfo(c,keys[j],retval == DICT_OK);
        } else {
            l = dictGetVal(de);
        }
        // 将客户端填接到被阻塞客户端的各key的链表尾部中
        listAddNodeTail(l,c);
    }
    blockClient(c,REDIS_BLOCKED_LIST);//阻塞客户端
}

客户端阻塞后,客户端数据库的阻塞字典(其中每个键即为导致客户端阻塞的key,每个键值为一个链表:节点为该key所阻塞的客户端):



解除阻塞:

当客户端被阻塞后,有三种方法可以使其脱离阻塞:

**被动脱离:有其他客户端为造成阻塞的键推入了新的元素

**主动脱离:达到执行阻塞原语时设定的最大阻塞时间

**强制脱离:客户端强制终止和服务器的连接,或服务器停机

在这主要分析主动和被动脱离方式。

被动脱离方式:

当LPUSH/RPUSH/LINSERT这三个命令添加了新的元素到阻塞键的列表时,可以让相应的客户端脱离阻塞(取消阻塞的客户端数量取决于推入的元素数量)。当有新元素添加时,底层由pushGenericCommand函数实现:


pushGenericCommand函数:

//PUSH:推入新元素
void pushGenericCommand(redisClient *c, int where) {

    int j, waiting = 0, pushed = 0;

    // 试着从数据库中取出准备推入的键的列表对象
    robj *lobj = lookupKeyWrite(c->db,c->argv[1]);

    // 如果列表对象不存在,那么可能有客户端在等待这个键的出现(可能有客户端因此键而阻塞)
    int may_have_waiting_clients = (lobj == NULL);

    if (lobj && lobj->type != REDIS_LIST) {
        addReply(c,shared.wrongtypeerr);
        return;
    }

    // 将列表状态设置为就绪
    if (may_have_waiting_clients) signalListAsReady(c,c->argv[1]);

    // 遍历所有输入值,并将它们添加到给定键的列表中
    for (j = 2; j < c->argc; j++) {

        // 编码值
        c->argv[j] = tryObjectEncoding(c->argv[j]);

        // 如果列表对象不存在,那么创建一个,并关联到数据库
        if (!lobj) {
            lobj = createZiplistObject();
            dbAdd(c->db,c->argv[1],lobj);
        }

        // 将值推入到列表
        listTypePush(lobj,c->argv[j],where);

        pushed++;
    }

    // 返回添加的节点数量
    addReplyLongLong(c, waiting + (lobj ? listTypeLength(lobj) : 0));

    // 如果至少有一个元素被成功推入,那么执行以下代码
    if (pushed) {
        char *event = (where == REDIS_HEAD) ? "lpush" : "rpush";

        // 发送键修改信号
        signalModifiedKey(c->db,c->argv[1]);

        // 发送事件通知
        notifyKeyspaceEvent(REDIS_NOTIFY_LIST,event,c->argv[1],c->db->id);
    }

    server.dirty += pushed;
}


当阻塞字典中的某个key列表被某个客户端推入数据库时,将把该key以及该客户端的数据库创建一个readyList结构,然后将该结构存放数据库的ready_keys字典中(以备在服务器的例行运行函数调用解除阻塞时查阅),其结构如下:


到此取消客户端阻塞的准备已经就绪,接下来由函数handleClientsBlockedOnLists解除阻塞客户端:

/* This function should be called by Redis every time a single command,
 * a MULTI/EXEC block, or a Lua script, terminated its execution after
 * being called by a client.
 *
 * 这个函数会在 Redis 每次执行完单个命令、事务块或 Lua 脚本之后调用。
 *
 * All the keys with at least one client blocked that received at least
 * one new element via some PUSH operation are accumulated into
 * the server.ready_keys list. This function will run the list and will
 * serve clients accordingly. Note that the function will iterate again and
 * again as a result of serving BRPOPLPUSH we can have new blocking clients
 * to serve because of the PUSH side of BRPOPLPUSH. 
 *
 * 对所有阻塞某个客户端的 key 来说,只要这个 key 被执行了某种 PUSH 操作
 * 那么这个 key 就会被放到 serve.ready_keys 去(用该key以及当前客户端的数据库 构造一个readyList结构,再将该结构放入到serve.ready_keys的列表中去)。
 * 
 * 这个函数会遍历整个 serve.ready_keys 链表,
 * 并将里面的 key 的元素弹出给相应的被该key阻塞客户端,
 * 从而解除客户端的阻塞状态。
 *
 * 函数会一次又一次地进行迭代,
 * 因此它在执行 BRPOPLPUSH 命令的情况下也可以正常获取到正确的正被阻塞客户端(如果阻塞该客户端的key正好是BRPOPLPUSH 命令的目标key列表)。
 **处理正在被ready_keys列表中的key阻塞的客户端:
 */
void handleClientsBlockedOnLists(void) {

    // 遍历整个 ready_keys:服务器中的一个字典,专门存放阻塞客户端的key(在后来push进来时就添加到该字典中,key值为链表)。 链表
    while(listLength(server.ready_keys) != 0) {
        list *l;

        /* Point server.ready_keys to a fresh list and save the current one
         * locally. This way as we run the old list we are free to call
         * signalListAsReady() that may push new elements in server.ready_keys
         * when handling clients blocked into BRPOPLPUSH. */
        // 备份旧的 ready_keys ,再给服务器端赋值一个新的
        l = server.ready_keys;
        server.ready_keys = listCreate();

        while(listLength(l) != 0) {

            // 取出备份 ready_keys 中的首个链表节点(一个readyList结构)
            listNode *ln = listFirst(l);

            // 指向 readyList 结构
            readyList *rl = ln->value;

            /* First of all remove this key from db->ready_keys so that
             * we can safely call signalListAsReady() against this key. */
            // 从 ready_keys 中移除就绪的 key
            dictDelete(rl->db->ready_keys,rl->key);

            /* If the key exists and it's a list, serve blocked clients
             * with data. */
            // 获取键对象,这个对象应该是非空的,并且是列表
            robj *o = lookupKeyWrite(rl->db,rl->key);
            if (o != NULL && o->type == REDIS_LIST) {
                dictEntry *de;

                /* We serve clients in the same order they blocked for
                 * this key, from the first blocked to the last. */
                // 取出所有被这个 key 阻塞的客户端链表(先阻塞先服务)
                de = dictFind(rl->db->blocking_keys,rl->key);
                if (de) {
                    list *clients = dictGetVal(de);
                    int numclients = listLength(clients);//链表长度 即:被该key阻塞的客户端的数量

                    while(numclients--) {
                        // 取出客户端
                        listNode *clientnode = listFirst(clients);
                        redisClient *receiver = clientnode->value;

                        // 设置弹出的目标对象(只在 BRPOPLPUSH 时使用)
                        robj *dstkey = receiver->bpop.target;

                        // 从列表中弹出元素
                        // 弹出的位置取决于是执行 BLPOP 还是 BRPOP 或者 BRPOPLPUSH
                        int where = (receiver->lastcmd &&
                                     receiver->lastcmd->proc == blpopCommand) ?
                                    REDIS_HEAD : REDIS_TAIL;
                        robj *value = listTypePop(o,where);

                        // 还有元素可弹出(非 NULL)
                        if (value) {
                            /* Protect receiver->bpop.target, that will be
                             * freed by the next unblockClient()
                             * call. */
                            if (dstkey) incrRefCount(dstkey);

                            // 取消客户端的阻塞状态
                            unblockClient(receiver);

                            // 将值 value 推入到造成客户度 receiver 阻塞的 key 上
                            if (serveClientBlockedOnList(receiver,
                                rl->key,dstkey,rl->db,value,
                                where) == REDIS_ERR)
                            {
                                /* If we failed serving the client we need
                                 * to also undo the POP operation. */
                                    listTypePush(o,value,where);
                            }

                            if (dstkey) decrRefCount(dstkey);
                            decrRefCount(value);
                        } else {
                            // 如果执行到这里,表示还有至少一个客户端被键所阻塞
                            // 这些客户端要等待对键的下次 PUSH
                            break;
                        }
                    }
                }
                
                // 如果列表元素已经为空,那么从数据库中将它删除
                if (listTypeLength(o) == 0) dbDelete(rl->db,rl->key);
                /* We don't call signalModifiedKey() as it was already called
                 * when an element was pushed on the list. */
            }

            /* Free this item. */
            decrRefCount(rl->key);
            zfree(rl);
            listDelNode(l,ln);
        }
        listRelease(l); /* We have the new list on place at this point. */
    }
}
在解除阻塞是采取先阻塞先服务的策略依次解除阻塞阻塞字典中的客户端:当添加一个新的客户端进入阻塞字典时链表时将该客户端加入到链表尾部;而当处理取消阻塞时则从链表最前面开始取消阻塞,从而形成一个FIFO队列。


第二种解除阻塞的方法: 阻塞时间达到所设定的最大阻塞时间。

每次Redis的服务器例行运行函数(serverCron)执行时,程序都会检查所有连接到服务器的客户端,查看那些处于正在阻塞状态的客户端的最大阻塞时间是否已经过期,如果是,则给客户端返回一个空值,然后撤销对客户端的阻塞(当然serverCron程序还执行很多其他的例行操作:clientCron()databaseCron()等函数)  。


serverCron():

<span style="color:#3366ff;">/* This is our timer interrupt, called server.hz times per second.
 *
 * 这是 Redis 的时间中断器,每秒调用 server.hz 次。
 *
 * Here is where we do a number of things that need to be done asynchronously.
 * For instance:
 *在这里我们可以异步操作:
 *例如:
 *
 * - Active expired keys collection (it is also performed in a lazy way on
 *   lookup).
 *   主动清除过期键。
 *
 * - Software watchdog.
 *   更新软件 watchdog 的信息。
 *
 * - Update some statistic.
 *   更新统计信息。
 *
 * - Incremental rehashing of the DBs hash tables.
 *   对数据库进行渐增式 Rehash
 *
 * - Triggering BGSAVE / AOF rewrite, and handling of terminated children.
 *   触发 BGSAVE 或者 AOF 重写,并处理之后由 BGSAVE 和 AOF 重写引发的子进程停止。
 *
 * - Clients timeout of different kinds.
 *   处理客户端超时。
 *
 * - Replication reconnection.
 *   复制重连
 *
 * - Many more...
 *   等等。。。
 *
 * Everything directly called here will be called server.hz times per second,
 * so in order to throttle execution of things we want to do less frequently
 * a macro is used: run_with_period(milliseconds) { .... }
 *
 * 因为 serverCron 函数中的所有代码都会每秒调用 server.hz 次,
 * 为了对部分代码的调用次数进行限制,
 * 使用了一个宏 run_with_period(milliseconds) { ... } ,
 * 这个宏可以将被包含代码的执行次数降低为每 milliseconds 执行一次。
 */
//服务器例行时间中断函数:
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    int j;
    REDIS_NOTUSED(eventLoop);
    REDIS_NOTUSED(id);
    REDIS_NOTUSED(clientData);

    /* Software watchdog: deliver the SIGALRM that will reach the signal
     * handler if we don't return here fast enough. */
    if (server.watchdog_period) watchdogScheduleSignal(server.watchdog_period);

    /* Update the time cache. */
    updateCachedTime();

    // 记录服务器执行命令的次数
    run_with_period(100) trackOperationsPerSecond();

    /* We have just REDIS_LRU_BITS bits per object for LRU information.
     * So we use an (eventually wrapping) LRU clock.
     *
     * Note that even if the counter wraps it's not a big problem,
     * everything will still work but some object will appear younger
     * to Redis. However for this to happen a given object should never be
     * touched for all the time needed to the counter to wrap, which is
     * not likely.
     *
     * 即使服务器的时间最终比 1.5 年长也无所谓,
     * 对象系统仍会正常运作,不过一些对象可能会比服务器本身的时钟更年轻。
     * 不过这要这个对象在 1.5 年内都没有被访问过,才会出现这种现象。
     *
     * Note that you can change the resolution altering the
     * REDIS_LRU_CLOCK_RESOLUTION define.
     *
     * LRU 时间的精度可以通过修改 REDIS_LRU_CLOCK_RESOLUTION 常量来改变。
     */
    server.lruclock = getLRUClock();

    /* Record the max memory used since the server was started. */
    // 记录服务器的内存峰值
    if (zmalloc_used_memory() > server.stat_peak_memory)
        server.stat_peak_memory = zmalloc_used_memory();

    /* Sample the RSS here since this is a relatively slow call. */
    server.resident_set_size = zmalloc_get_rss();

    /* We received a SIGTERM, shutting down here in a safe way, as it is
     * not ok doing so inside the signal handler. */
    // 服务器进程收到 SIGTERM 信号,关闭服务器
    if (server.shutdown_asap) {

        // 尝试关闭服务器
        if (prepareForShutdown(0) == REDIS_OK) exit(0);

        // 如果关闭失败,那么打印 LOG ,并移除关闭标识
        redisLog(REDIS_WARNING,"SIGTERM received but errors trying to shut down the server, check the logs for more information");
        server.shutdown_asap = 0;
    }

    /* Show some info about non-empty databases */
    // 打印数据库的键值对信息
    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);

            // 用 LOG 打印数量
            if (used || vkeys) {
                redisLog(REDIS_VERBOSE,"DB %d: %lld keys (%lld volatile) in %lld slots HT.",j,used,vkeys,size);
                /* dictPrintStats(server.dict); */
            }
        }
    }

    /* Show information about connected clients */
    // 如果服务器没有运行在 SENTINEL 模式下,那么打印客户端的连接信息
    if (!server.sentinel_mode) {
        run_with_period(5000) {
            redisLog(REDIS_VERBOSE,
                "%lu clients connected (%lu slaves), %zu bytes in use",
                listLength(server.clients)-listLength(server.slaves),
                listLength(server.slaves),
                zmalloc_used_memory());
        }
    }

    /* We need to do a few operations on clients asynchronously. */
    // 检查客户端,关闭超时客户端,并释放客户端多余的缓冲区
    clientsCron();

    /* Handle background operations on Redis databases. */
    // 对数据库执行各种操作
    databasesCron();

    /* Start a scheduled AOF rewrite if this was requested by the user while
     * a BGSAVE was in progress. */
    // 如果 BGSAVE 和 BGREWRITEAOF 都没有在执行
    // 并且有一个 BGREWRITEAOF 在等待,那么执行 BGREWRITEAOF
    if (server.rdb_child_pid == -1 && server.aof_child_pid == -1 &&
        server.aof_rewrite_scheduled)
    {
        rewriteAppendOnlyFileBackground();
    }

    /* Check if a background saving or AOF rewrite in progress terminated. */
    // 检查 BGSAVE 或者 BGREWRITEAOF 是否已经执行完毕
    if (server.rdb_child_pid != -1 || server.aof_child_pid != -1) {
        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);

            // BGSAVE 执行完毕
            if (pid == server.rdb_child_pid) {
                backgroundSaveDoneHandler(exitcode,bysignal);

            // BGREWRITEAOF 执行完毕
            } else if (pid == server.aof_child_pid) {
                backgroundRewriteDoneHandler(exitcode,bysignal);

            } else {
                redisLog(REDIS_WARNING,
                    "Warning, detected child with unmatched pid: %ld",
                    (long)pid);
            }
            updateDictResizePolicy();
        }
    } else {

        /* If there is not a background saving/rewrite in progress check if
         * we have to save/rewrite now */
        // 既然没有 BGSAVE 或者 BGREWRITEAOF 在执行,那么检查是否需要执行它们

        // 遍历所有保存条件,看是否需要执行 BGSAVE 命令
         for (j = 0; j < server.saveparamslen; j++) {
            struct saveparam *sp = server.saveparams+j;

            /* Save if we reached the given amount of changes,
             * the given amount of seconds, and if the latest bgsave was
             * successful or if, in case of an error, at least
             * REDIS_BGSAVE_RETRY_DELAY seconds already elapsed. */
            // 检查是否有某个保存条件已经满足了
            if (server.dirty >= sp->changes &&
                server.unixtime-server.lastsave > sp->seconds &&
                (server.unixtime-server.lastbgsave_try >
                 REDIS_BGSAVE_RETRY_DELAY ||
                 server.lastbgsave_status == REDIS_OK))
            {
                redisLog(REDIS_NOTICE,"%d changes in %d seconds. Saving...",
                    sp->changes, (int)sp->seconds);
                // 执行 BGSAVE
                rdbSaveBackground(server.rdb_filename);
                break;
            }
         }

         /* Trigger an AOF rewrite if needed */
        // 出发 BGREWRITEAOF
         if (server.rdb_child_pid == -1 &&
             server.aof_child_pid == -1 &&
             server.aof_rewrite_perc &&
             // AOF 文件的当前大小大于执行 BGREWRITEAOF 所需的最小大小
             server.aof_current_size > server.aof_rewrite_min_size)
         {
            // 上一次完成 AOF 写入之后,AOF 文件的大小
            long long base = server.aof_rewrite_base_size ?
                            server.aof_rewrite_base_size : 1;

            // AOF 文件当前的体积相对于 base 的体积的百分比
            long long growth = (server.aof_current_size*100/base) - 100;

            // 如果增长体积的百分比超过了 growth ,那么执行 BGREWRITEAOF
            if (growth >= server.aof_rewrite_perc) {
                redisLog(REDIS_NOTICE,"Starting automatic rewriting of AOF on %lld%% growth",growth);
                // 执行 BGREWRITEAOF
                rewriteAppendOnlyFileBackground();
            }
         }
    }

    // 根据 AOF 政策,
    // 考虑是否需要将 AOF 缓冲区中的内容写入到 AOF 文件中
    /* AOF postponed flush: Try at every cron cycle if the slow fsync
     * completed. */
    if (server.aof_flush_postponed_start) flushAppendOnlyFile(0);

    /* AOF write errors: in this case we have a buffer to flush as well and
     * clear the AOF error in case of success to make the DB writable again,
     * however to try every second is enough in case of 'hz' is set to
     * an higher frequency. */
    run_with_period(1000) {
        if (server.aof_last_write_status == REDIS_ERR)
            flushAppendOnlyFile(0);
    }

    /* Close clients that need to be closed asynchronous */
    // 关闭那些需要异步关闭的客户端
    freeClientsInAsyncFreeQueue();

    /* Clear the paused clients flag if needed. */
    clientsArePaused(); /* Don't check return value, just use the side effect. */

    /* Replication cron function -- used to reconnect to master and
     * to detect transfer failures. */
    // 复制函数
    // 重连接主服务器、向主服务器发送 ACK 、判断数据发送失败情况、断开本服务器超时的从服务器,等等
    run_with_period(1000) replicationCron();

    /* Run the Redis Cluster cron. */
    // 如果服务器运行在集群模式下,那么执行集群操作
    run_with_period(100) {
        if (server.cluster_enabled) clusterCron();
    }

    /* Run the Sentinel timer if we are in sentinel mode. */
    // 如果服务器运行在 sentinel 模式下,那么执行 SENTINEL 的主函数
    run_with_period(100) {
        if (server.sentinel_mode) sentinelTimer();
    }

    /* Cleanup expired MIGRATE cached sockets. */
    // 集群。。。TODO
    run_with_period(1000) {
        migrateCloseTimedoutSockets();
    }

    // 增加 loop 计数器
    server.cronloops++;

    return 1000/server.hz;
}</span>






评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值