美文网首页工作生活
「Redis源码解读」—事件(三)时间事件

「Redis源码解读」—事件(三)时间事件

作者: wh4763 | 来源:发表于2019-07-05 02:28 被阅读0次

知识点

  1. 时间事件分为定时事件与周期事件;
  2. 时间事件与文件事件是合作关系,不会出现抢占的情况;
  3. 时间事件要比预设的更晚一些。
  • Redis的时间事件分为以下两类
    1.定时事件:让一段程序在指定的时间之后执行一次
    2.周期性事件:让一段程序每隔指定时间就执行一次

一个时间事件主要由以下三个属性组成:

  • id(全局唯一id)
  • when (毫秒时间戳,记录了时间事件的到达时间)
  • timeProc(时间事件处理器,当时间到达时,Redis就会调用相应的处理器来处理事件)
/* Time event structure
 *
 * 时间事件结构
 */
typedef struct aeTimeEvent {
    // 时间事件的唯一标识符
    long long id; /* time event identifier. */
    // 事件的到达时间,存贮的是UNIX的时间戳
    long when_sec; /* seconds */
    long when_ms; /* milliseconds */
    // 事件处理函数,当到达指定时间后调用该函数处理对应的问题
    aeTimeProc *timeProc;
    // 事件释放函数
    aeEventFinalizerProc *finalizerProc;
    // 多路复用库的私有数据
    void *clientData;
    // 指向下个时间事件结构,形成链表
    struct aeTimeEvent *next;
} aeTimeEvent;

在结构里面很容易的得出时间时间的结构是一个无序链表。
Redis将时间事件都放在一个无序链表中,每当时间事件执行器运行时,它就遍历整个链表,查找所有已到达的时间事件,并调用相应的事件处理器。

时间事件的API

创建新的时间事件,新的时间事件是总是处在时间事件的表头

long long aeCreateTimeEvent(aeEventLoop *eventLoop, long long milliseconds,
        aeTimeProc *proc, void *clientData,
        aeEventFinalizerProc *finalizerProc)
{
    // 更新时间计数器
    long long id = eventLoop->timeEventNextId++;
    // 创建时间事件结构
    aeTimeEvent *te;
    te = zmalloc(sizeof(*te));
    if (te == NULL) return AE_ERR;
    // 设置 ID
    te->id = id;
    // 设定处理事件的时间
    aeAddMillisecondsToNow(milliseconds,&te->when_sec,&te->when_ms);
    // 设置事件处理器
    te->timeProc = proc;
    te->finalizerProc = finalizerProc;
    // 设置私有数据
    te->clientData = clientData;
    // 将新事件放入表头
    te->next = eventLoop->timeEventHead;
    eventLoop->timeEventHead = te;
    return id;
}

删除给定ID的时间事件:

int aeDeleteTimeEvent(aeEventLoop *eventLoop, long long id)
{
    aeTimeEvent *te, *prev = NULL;
    // 遍历链表
    te = eventLoop->timeEventHead;
    while(te) {
        // 发现目标事件,删除
        if (te->id == id) {
            if (prev == NULL)
                eventLoop->timeEventHead = te->next;
            else
                prev->next = te->next;
            // 执行清理处理器
            if (te->finalizerProc)
                te->finalizerProc(eventLoop, te->clientData);
            // 释放时间事件
            zfree(te);
            return AE_OK;
        }
        prev = te;
        te = te->next;
    }
    return AE_ERR; /* NO event with the specified ID found */
}

返回距离当前时间最近的的时间事件

// 寻找里目前时间最近的时间事件
static aeTimeEvent *aeSearchNearestTimer(aeEventLoop *eventLoop)
{
    aeTimeEvent *te = eventLoop->timeEventHead;
    aeTimeEvent *nearest = NULL;
    while(te) {
        if (!nearest || te->when_sec < nearest->when_sec ||
                (te->when_sec == nearest->when_sec &&
                 te->when_ms < nearest->when_ms))
            nearest = te;
        te = te->next;
    }
    return nearest;
}

时间事件处理器

与文件事件不同,时间事件处理器只有一个,用来处理到达其指定时间的时间事件,并判断其是否需要继续循环,即周期事件:

static int processTimeEvents(aeEventLoop *eventLoop) {
    int processed = 0;
    aeTimeEvent *te;
    long long maxId;
    //获取当前时间
    time_t now = time(NULL);
    // 通过重置事件的运行时间,
    // 防止因时间穿插(skew)而造成的事件处理混乱
    if (now < eventLoop->lastTime) {
        te = eventLoop->timeEventHead;
        while(te) {
            te->when_sec = 0;
            te = te->next;
        }
    }
    // 更新最后一次处理时间事件的时间
    eventLoop->lastTime = now;
    // 遍历链表
    // 执行那些已经到达的事件
    te = eventLoop->timeEventHead;
    maxId = eventLoop->timeEventNextId-1;
    while(te) {
        long now_sec, now_ms;
        long long id;
        // 跳过无效事件
        if (te->id > maxId) {
            te = te->next;
            continue;
        }
        // 获取当前时间
        aeGetTime(&now_sec, &now_ms);
        // 如果当前时间等于或等于事件的执行时间,那么说明事件已到达,执行这个事件
        if (now_sec > te->when_sec ||
            (now_sec == te->when_sec && now_ms >= te->when_ms))
        {
            int retval;
            id = te->id;
            // 执行事件处理器,并获取返回值
            retval = te->timeProc(eventLoop, id, te->clientData);
            processed++;
            // 记录是否有需要循环执行这个事件时间
            if (retval != AE_NOMORE) {
                // 是的, retval 毫秒之后继续执行这个时间事件
                aeAddMillisecondsToNow(retval,&te->when_sec,&te->when_ms);
            } else {
                // 不,将这个事件删除
                aeDeleteTimeEvent(eventLoop, id);
            }
            // 因为执行事件之后,事件列表可能已经被改变了
            // 因此需要将 te 放回表头,继续开始执行事件
            te = eventLoop->timeEventHead;
        } else {
            te = te->next;
        }
    }
    return processed;
}

时间事件的调度

时间事件是与文件事件由同一个事件分派器来调度的,如果有时间事件存在的话,文件事件的阻塞时间将由最近的时间事件与当前时间的差来决定

int aeProcessEvents(aeEventLoop *eventLoop, int flags)
{
    int processed = 0, numevents;
    /* 没有事件 */
    if (!(flags & AE_TIME_EVENTS) && !(flags & AE_FILE_EVENTS)) return 0;
    /* Note that we want call select() even if there are no
     * file events to process as long as we want to process time
     * events, in order to sleep until the next time event is ready
     * to fire. */
    if (eventLoop->maxfd != -1 ||
        ((flags & AE_TIME_EVENTS) && !(flags & AE_DONT_WAIT))) {
        int j;
        aeTimeEvent *shortest = NULL;
        struct timeval tv, *tvp;
        // 获取最近的时间事件
        if (flags & AE_TIME_EVENTS && !(flags & AE_DONT_WAIT))
            shortest = aeSearchNearestTimer(eventLoop);
        if (shortest) {
            // 如果时间事件存在的话
            // 那么根据最近可执行时间事件和现在时间的时间差来决定文件事件的阻塞时间
            long now_sec, now_ms;
            /* Calculate the time missing for the nearest
             * timer to fire. */
            // 计算距今最近的时间事件还要多久才能达到
            // 并将该时间距保存在 tv 结构中
            aeGetTime(&now_sec, &now_ms);
            tvp = &tv;
            tvp->tv_sec = shortest->when_sec - now_sec;
            if (shortest->when_ms < now_ms) {
                tvp->tv_usec = ((shortest->when_ms+1000) - now_ms)*1000;
                tvp->tv_sec --;
            } else {
                tvp->tv_usec = (shortest->when_ms - now_ms)*1000;
            }
            // 时间差小于 0 ,说明事件已经可以执行了,将秒和毫秒设为 0 (不阻塞)
            if (tvp->tv_sec < 0) tvp->tv_sec = 0;
            if (tvp->tv_usec < 0) tvp->tv_usec = 0;
        } else {
            // 执行到这一步,说明没有时间事件
            // 那么根据 AE_DONT_WAIT 是否设置来决定是否阻塞,以及阻塞的时间长度
            /* If we have to check for events but need to return
             * ASAP because of AE_DONT_WAIT we need to set the timeout
             * to zero */
            if (flags & AE_DONT_WAIT) {
                // 设置文件事件不阻塞
                tv.tv_sec = tv.tv_usec = 0;
                tvp = &tv;
            } else {
                /* Otherwise we can block */
                // 文件事件可以阻塞直到有事件到达为止
                tvp = NULL; /* wait forever */
            }
        }
        .......
        // 处理文件事件,阻塞时间由 tvp 决定
       ......
    /* Check time events */
    // 执行时间事件
    if (flags & AE_TIME_EVENTS)
        processed += processTimeEvents(eventLoop);
    return processed; /* return the number of processed file/time events */
}

这样调度的好处是让时间事件不会频繁的查询,也不会让文件事件阻塞很长的时间。从代码中可以看出,时间事件的执行判断是其指定时间减去当前时间小于0时才执行的,这样一来时间事件的执行会比指定的要晚一些。

时间事件的应用

时间事件的最主要的应用是在redis服务器需要对自身的资源与配置进行定期的调整,从而确保服务器的长久运行,这些操作由redis.c中的serverCron函数实现,主要的工作有清除过期键值对、清理关闭连接失效的客户端、更新统计信息等等:

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;
}

相关文章

  • 「Redis源码解读」—事件(三)时间事件

    知识点 时间事件分为定时事件与周期事件; 时间事件与文件事件是合作关系,不会出现抢占的情况; 时间事件要比预设的更...

  • 「Redis源码解读」—事件(二)文件事件

    文件事件 事件类型在文件事件中分为两大类,可读事件(AE_READABLE)与可写(AE_WRITABLE)事件,...

  • Redis - 事件

    Redis - 事件 Redis服务器是一个事件驱动程序,服务器处理的事件分为时间事件和文件事件两类。 文件事件处...

  • redis-事件

    redis的主要事件源包括信号,网络,文件和时间事件,文件事件没看到用于做什么。 1、信号事件 redis忽略了S...

  • Redis文件事件和时间事件

    Redis服务器需要处理两类事件:文件事件和时间事件。 1、文件事件 Redis服务器通过套接字与客户端进行连接,...

  • Redis源码研究之命令处理与回复

    本文主要从源码角度说明Redis为客户端提供服务(包括命令处理与回复)的过程。 建议阅读: 1、Redis 事件...

  • 06. RxSwift源码解读:ControlEvent、Con

    今天带大家解读RxSwift中封装UI事件响应相关的源码: ControlEvent和ControlPropert...

  • Redis 事件

    Redis是事件驱动的服务器。事件是对操作的抽象,redis中的事件分为两类: 1)文件事件:redis通过soc...

  • Redis源码学习之事件

    事件    Redis服务器是一个事件驱动程序,服务器主要处理时间事件与文件事件。其中,时间事件是指服务器中的一些...

  • [redis 源码走读] 事件 - 文件事件

    redis 服务底层采用了异步事件管理(aeEventLoop):管理时间事件和文件事件。对于大量网络文件描述符(...

网友评论

    本文标题:「Redis源码解读」—事件(三)时间事件

    本文链接:https://www.haomeiwen.com/subject/ubejhctx.html