Redis 缓存失效和回收机制续

2、Redis Key失效机制redis

Redis的Key失效机制,主要借助借助EXPIRE命令:数据库

EXPIRE key 30缓存

上面的命令即为key设置30秒的过时时间,超过这个时间,咱们应该就访问不到这个值了。接下来咱们继续深刻探究这个问题,Redis缓存失效机制是如何实现的呢?服务器

 

惰性淘汰机制less

惰性淘汰机制即当客户端请求操做某个key的时候,Redis会对客户端请求操做的key进行有效期检查,若是key过时才进行相应的处理,惰性淘汰机制也叫消极失效机制。dom

咱们看看t_string组件下面对get请求处理的服务端端执行堆栈:函数

getCommand 
     -> getGenericCommand 
            -> lookupKeyReadOrReply
                   -> lookupKeyRead 
                         -> expireIfNeedeoop

 

关键的地方是expireIfNeed,Redis对key的get操做以前会判断key关联的值是否失效,咱们看看expireIfNeeded的流程,大体以下:测试

一、从expires中查找key的过时时间,若是不存在说明对应key没有设置过时时间,直接返回。this

二、若是是slave机器,则直接返回,由于Redis为了保证数据一致性且实现简单,将缓存失效的主动权交给Master机器,slave机器没有权限将key失效。

三、若是当前是Master机器,且key过时,则master会作两件重要的事情:1)将删除命令写入AOF文件。2)通知Slave当前key失效,能够删除了。

四、master从本地的字典中将key对于的值删除。

 

惰性删除策略流程:

1. 在进行get或setnx等操做时,先检查key是否过时;

2. 若过时,删除key,而后执行相应操做; 若没过时,直接执行相应操做;

 

在redis源码中,实现懒惰淘汰策略的是函数expireIfNeeded,全部读写数据库命令在执行以前都会调用expireIfNeeded函数对输入键进行检查。若是过时就删除,若是没过时就正常访问。

咱们看下expireIfNeeded函数在文件db.c中的具体实现:

int expireIfNeeded(redisDb *db, robj *key) {

    mstime_t when = getExpire(db,key);

    mstime_t now;

 

    if (when < 0) return 0; /* No expire for this key */

 

    /* Don't expire anything while loading. It will be done later. */

    if (server.loading) return 0;

 

    /* If we are in the context of a Lua script, we claim that time is

     * blocked to when the Lua script started. This way a key can expire

     * only the first time it is accessed and not in the middle of the

     * script execution, making propagation to slaves / AOF consistent.

     * See issue #1525 on Github for more information. */

    now = server.lua_caller ? server.lua_time_start : mstime();

 

    /* If we are running in the context of a slave, return ASAP:

     * the slave key expiration is controlled by the master that will

     * send us synthesized DEL operations for expired keys.

     *

     * Still we try to return the right information to the caller,

     * that is, 0 if we think the key should be still valid, 1 if

     * we think the key is expired at this time. */

         /*若是咱们正在slaves上执行读写命令,就直接返回,

          *由于slaves上的过时是由master来发送删除命令同步给slaves删除的,

          *slaves不会自主删除*/

    if (server.masterhost != NULL) return now > when;

 

    /* Return when this key has not expired */

         /*只是回了一个判断键是否过时的值,0表示没有过时,1表示过时

          *可是并无作其余与键值过时相关的操做

          *若是没有过时,就返回当前键

          */

    if (now <= when) return 0;

 

    /* Delete the key */

         /*增长过时键个数*/

    server.stat_expiredkeys++;

         /*向AOF文件和节点传播过时信息.当key过时时,DEL操做也会传递给全部的AOF文件和节点*/

    propagateExpire(db,key);

        /*发送事件通知,关于redis的键事件通知和键空间通知*/

    notifyKeyspaceEvent(NOTIFY_EXPIRED,

        "expired",key,db->id);

         /*将过时键从数据库中删除*/

    return dbDelete(db,key);

}

 

 

函数描述propagateExpire:

/* Propagate expires into slaves and the AOF file.

 * When a key expires in the master, a DEL operation for this key is sent

 * to all the slaves and the AOF file if enabled.

 *

 * This way the key expiry is centralized in one place, and since both

 * AOF and the master->slave link guarantee operation ordering, everything

 * will be consistent even if we allow write operations against expiring

 * keys. */

 

 

主动删除机制

主动失效机制也叫积极失效机制,即服务端定时的去检查失效的缓存,若是失效则进行相应的操做。

咱们都知道Redis是单线程的,基于事件驱动的,Redis中有个EventLoop,EventLoop负责对两类事件进行处理:

一、一类是IO事件,这类事件是从底层的多路复用器分离出来的。

二、一类是定时事件,这类事件主要用来事件对某个任务的定时执行。

 

为何讲到Redis的单线程模型,由于Redis的主动失效机制逻辑是被当作一个定时任务来由主线程执行的,相关代码以下:

if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }

 

serverCron就是这个定时任务的函数指针,adCreateTimeEvent将serverCron任务注册到EventLoop上面,并设置初始的执行时间是1毫秒以后。接下来,咱们想知道的东西都在serverCron里面了。serverCron作的事情有点多,咱们只关心和本篇内容相关的部分,也就是缓存失效是怎么实现的,我认为看代码作什么事情,调用堆栈仍是比较直观的:

aeProcessEvents
    ->processTimeEvents
        ->serverCron 
             -> databasesCron 
                   -> activeExpireCycle 
                           -> activeExpireCycleTryExpire

 

EventLoop经过对定时任务的处理,触发对serverCron逻辑的执行,最终之执行key过时处理的逻辑,值得一提的是,activeExpireCycle逻辑只能由master来作。

 

咱们看下函数activeExpireCycle在server.c中的实现:

/* Try to expire a few timed out keys. The algorithm used is adaptive and

 * will use few CPU cycles if there are few expiring keys, otherwise

 * it will get more aggressive to avoid that too much memory is used by

 * keys that can be removed from the keyspace.

 *

 * 函数尝试删除数据库中已通过期的键。
 * 当带有过时时间的键比较少时,函数运行得比较保守,
 * 若是带有过时时间的键比较多,那么函数会以更积极的方式来删除过时键,
 * 从而可能地释放被过时键占用的内存。

  *

 * No more than CRON_DBS_PER_CALL databases are tested at every

 * iteration.

 *

 * 每次循环中被测试的数据库数目不会超过 REDIS_DBCRON_DBS_PER_CALL

  *

 * This kind of call is used when Redis detects that timelimit_exit is

 * true, so there is more work to do, and we do it more incrementally from

 * the beforeSleep() function of the event loop.

 *

 * 若是 timelimit_exit 为真,那么说明还有更多删除工做要作,(在我看来timelimit_exit若是为真的话那表示上一次删除过时键时是由于删除时间过长超时了才退出的,

   因此此次将删除方法更加积极),那么在 beforeSleep() 函数调用时,程序会再次执行这个函数。
 *

 * Expire cycle type:

 *

  * 过时循环的类型:

  *

 * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a

 * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION

 * microseconds, and is not repeated again before the same amount of time.

 *

   若是循环的类型为ACTIVE_EXPIRE_CYCLE_FAST 
 那么函数会以“快速过时”模式执行,
 执行的时间不会长过 EXPIRE_FAST_CYCLE_DURATION 毫秒,
 而且在 EXPIRE_FAST_CYCLE_DURATION 毫秒以内不会再从新执行。

  *

 * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is

 * executed, where the time limit is a percentage of the REDIS_HZ period

 * as specified by the REDIS_EXPIRELOOKUPS_TIME_PERC define.

  

  * 若是循环的类型为ACTIVE_EXPIRE_CYCLE_SLOW 
 那么函数会以“正常过时”模式执行,
 函数的执行时限为 REDIS_HS 常量的一个百分比,
 这个百分比由 REDIS_EXPIRELOOKUPS_TIME_PERC 定义。

*/

 

void activeExpireCycle(int type) {

    /* This function has some global state in order to continue the work

     * incrementally across calls. */

    // 共享变量,用来累积函数连续执行时的数据

    static unsigned int current_db = 0; /* Last DB tested. 正在测试的数据库*/

    static int timelimit_exit = 0;      /* Time limit hit in previous call 上一次执行是否时间超时的提示 */

    static long long last_fast_cycle = 0; /* When last fast cycle ran. 上次快速模式执行的时间*/

 

    int j, iteration = 0;

    // 默认每次处理的数据库数量

    int dbs_per_call = CRON_DBS_PER_CALL;

    // 函数开始的时间

    long long start = ustime(), timelimit;

 

    // 快速模式

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {

        /* Don't start a fast cycle if the previous cycle did not exited

         * for time limt. Also don't repeat a fast cycle for the same period

         * as the fast cycle total duration itself. */

        // 若是上次函数没有触发 timelimit_exit ,那么不执行处理

        if (!timelimit_exit) return;

        // 若是距离上次执行未够必定时间,那么不执行处理

        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;

        // 运行到这里,说明执行快速处理,记录当前时间

        last_fast_cycle = start;

    }

 

    /* We usually should test CRON_DBS_PER_CALL per iteration, with

     * two exceptions:

     *

     * 通常状况下,每次迭代(也就是每次调用这个函数)函数只处理 CRON_DBS_PER_CALL 个数据库,

     * 除非:

     *

     * 1) Don't test more DBs than we have.

     *    当前数据库的数量小于 REDIS_DBCRON_DBS_PER_CALL

     * 2) If last time we hit the time limit, we want to scan all DBs

     * in this iteration, as there is work to do in some DB and we don't want

     * expired keys to use memory for too much time.

     *     若是上次处理遇到了时间上限,那么此次须要对全部数据库进行扫描,

     *     这能够避免过多的过时键占用空间

     */

    if (dbs_per_call > server.dbnum || timelimit_exit)//以服务器的数据库数量为准

        dbs_per_call = server.dbnum;

 

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time

     * per iteration. Since this function gets called with a frequency of

     * server.hz times per second, the following is the max amount of

     * microseconds we can spend in this function. */

    // 函数处理的微秒时间上限

    // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默认为 25 ,也便是 25 % 的 CPU 时间

    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;

    timelimit_exit = 0;

    if (timelimit <= 0) timelimit = 1;

 

    // 若是是运行在快速模式之下

    // 那么最多只能运行 FAST_DURATION 微秒

    // 默认值为 1000 (微秒)

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)

        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

 

    // 遍历数据库

    for (j = 0; j < dbs_per_call; j++) {

        int expired;

        // 指向要处理的数据库

        redisDb *db = server.db+(current_db % server.dbnum);

 

        /* Increment the DB now so we are sure if we run out of time

         * in the current DB we'll restart from the next. This allows to

         * distribute the time evenly across DBs. */

        // 为 currrnt_DB 计数器加一,若是进入 do 循环以后由于超时而跳出

        // 那么下次会直接从下个 currrnt_DB 开始处理。这样使得分配在每一个数据库上处理时间比较平均

        current_db++;

 

        /* Continue to expire if at the end of the cycle more than 25%

         * of the keys were expired. */

        //若是每次循环清理的过时键是过时键的25%以上,那么就继续清理

        do {

            unsigned long num, slots;

            long long now, ttl_sum;

            int ttl_samples;

 

            /* If there is nothing to expire try next DB ASAP. */

            // 获取数据库中带过时时间的键的数量

            // 若是该数量为 0 ,直接跳过这个数据库

            if ((num = dictSize(db->expires)) == 0) {

                db->avg_ttl = 0;

                break;

            }

            // 获取数据库中键值对的数量

            slots = dictSlots(db->expires);

            // 当前时间

            now = mstime();

 

            /* When there are less than 1% filled slots getting random

             * keys is expensive, so stop here waiting for better times...

             * The dictionary will be resized asap. */

            // 这个数据库的使用率低于 1% ,扫描起来太费力了(大部分都会 MISS)

            // 跳过,等待字典收缩程序运行

            if (num && slots > DICT_HT_INITIAL_SIZE &&

                (num*100/slots < 1)) break;

 

            /* The main collection cycle. Sample random keys among keys

             * with an expire set, checking for expired ones.

             *

             * 样本计数器

             */

            // 已处理过时键计数器

            expired = 0;

            // 键的总 TTL 计数器

            ttl_sum = 0;

            // 总共处理的键计数器

            ttl_samples = 0;

 

            // 每次最多只能检查 LOOKUPS_PER_LOOP 个键,默认是20

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)

                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

 

            // 开始遍历数据库

            while (num--) {

                dictEntry *de;

                long long ttl;

 

                // 从 expires 中随机取出一个带过时时间的键

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;

                // 计算 TTL

                ttl = dictGetSignedIntegerVal(de)-now;

                // 若是键已通过期,那么删除它,并将 expired 计数器增一

                if (activeExpireCycleTryExpire(db,de,now)) expired++;

                if (ttl > 0) {

            /* We want the average TTL of keys yet not expired. */

                // 累积键的 TTL

                ttl_sum += ttl;

                // 累积处理键的个数

                ttl_samples++;

               }

            }

 

            /* Update the average TTL stats for this database. */

            // 为这个数据库更新平均 TTL 统计数据

            if (ttl_samples) {

                // 计算当前平均值

                long long avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.

 

                 * We just use the current estimate with a weight of 2%

                 * and the previous estimate with a weight of 98%. */

                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;

               

                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);

            }

 

            /* We can't block forever here even if there are many keys to

             * expire. So after a given amount of milliseconds return to the

             * caller waiting for the other active expire cycle. */

            // 若是过时键太多的话,咱们不能用太长时间处理,因此这个函数执行必定时间以后就要返回,等待下一次循环

            // 更新遍历次数

            iteration++;

 

 

 

            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */

                long long elapsed = ustime()-start;

 

                latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

                if (elapsed > timelimit) timelimit_exit = 1;

            }

 

            // 已经超时了,返回

            if (timelimit_exit) return;

 

            /* We don't repeat the cycle if there are less than 25% of keys

             * found expired in the current DB. */

            // 若是删除的过时键少于当前数据库中过时键数量的 25 %,那么再也不遍历。固然若是超过了25%,那说明过时键还不少,继续清理

        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);

    }

}

 

 

函数activeExpireCycleTryExpire描述:

/* Helper function for the activeExpireCycle() function.

 * This function will try to expire the key that is stored in the hash table

 * entry 'de' of the 'expires' hash table of a Redis database.

 *

 * If the key is found to be expired, it is removed from the database and

 * 1 is returned. Otherwise no operation is performed and 0 is returned.

 *

 * When a key is expired, server.stat_expiredkeys is incremented.

 *

 * The parameter 'now' is the current time in milliseconds as is passed

 * to the function to avoid too many gettimeofday() syscalls. */

 

 

3、Redis内存管理

咱们在使用Redis,须要注意如下几点:

  • 当某些缓存被删除后Redis并非老是当即将内存归还给操做系统。这并非redis所特有的,而是函数malloc()的特性。例如你缓存了6G的数据,而后删除了2G数据,从操做系统看,redis可能仍然占用了6G的内存,即便redis已经明确声明只使用了3G的空间。这是由于redis使用的底层内存分配器不会这么简单的就把内存归还给操做系统,多是由于已经删除的key和没有删除的key在同一个页面(page),这样就不能把完整的一页归还给操做系统。

  • 内存分配器是智能的,能够复用用户已经释放的内存。因此当使用的内存从6G下降到3G时,你能够从新添加更多的key,而不须要再向操做系统申请内存。分配器将复用以前已经释放的3G内存.

  • 当redis的peak内存很是高于平时的内存使用时,碎片所占可用内存的比例就会波动很大。当前使用的内存除以实际使用的物理内存(RSS)就是fragmentation;由于RSS就是peak memory,因此当大部分key被释放的时候,此时内存的mem_used / RSS就比较高。

     

若是 maxmemory 没有设置,redis就会一直向OS申请内存,直到OS的全部内存都被使用完。因此一般建议设置上redis的内存限制。或许你也想设置 maxmemory-policy 的值为 no-enviction。

设置了maxmemory后,当redis的内存达到内存限制后,再向redis发送写指令,会返回一个内存耗尽的错误。错误一般会触发一个应用程序错误,可是不会致使整台机器宕掉。

若是redis没有设置expire,它是否默认永不过时?若是实际内存超过你设置的最大内存,就会使用LRU删除机制。

 

--EOF--

相关文章
相关标签/搜索