Redis之内存管理之过期策略(五)

139 阅读5分钟

携手创作,共同成长!这是我参与「掘金日新计划 · 8 月更文挑战」的第13天,点击查看活动详情

redis是一个基于内存的存储系统,所以其内存受机器的物理内存影响.参考redis官网

image.png 如果配置是0,那么默认是电脑的内存,如果是32bit 隐式大小为3G。 maxmemory redis占用的最大内存 maxmemory 100mb.如果我们不淘汰,那么它的数据就会满,满了肯定就不能再放数据,发挥 不了redis的作用! 所以我们需要通过一定的方式去保证我们Redis的可用 性。

Redis过期使用

在redis中对某个key设置过期时间如下:

  1. pexpire key millisecond
  2. expire key seconds 前者是设置毫秒,后者是设置秒,那么获取某个key的过期时间如下:
  3. ttl key
  4. pttl key 前者获取毫秒,后者是获取秒.

过期原理

那么什么是过期策略。首先,我们知道Redis有一个特性,就是Redis中的 数据我都是可以设置过期时间的,如果时间到了,这个数据就会从我们的 Redis中删除。 那么过期策略,就是讲的我怎么把Redis中过期的数据从我们Redis服务中 移除的

惰性过期

惰性过期是指: 当我们访问某个key时,如果key已经过期那么就直接删除,删除可以是同步或者异步删除.具体配置方式依赖redis.conf中

lazyfree-lazy-user-del no

但是这种却对内存非常不友好。因为如果没有再次访问,该过期删除的就可能 一直堆积在内存里面!从而不会被清除,占用大量内存。所以存在定期过期的策略进行删除.

定期过期

那么多久去清除一次,我们在讲Rehash的时候,有个方法是serverCron ,执行频率根据redis.conf中的hz配置

# The range is between 1 and 500, however a value over 100 is usually not
# a good idea. Most users should use the default of 10 and raise this up to
# 100 only in environments where very low latency is required.
hz 10

定时任务入口位于server.c

void databasesCron(void) {
    /* Expire keys by random sampling. Not required for slaves
     * as master will synthesize DELs for us. */
    if (server.active_expire_enabled) {
        if (iAmMaster()) {
            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW);
        } else {
            expireSlaveKeys();
        }
    }

当前节点为master时触发过期操作.过期key位于之前提到的RedisDb中

typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

expires中存储了所有的过期key以及其过期时间.以下代码为具体过期逻辑逻辑

void activeExpireCycle(int type) {
 
    unsigned long
    effort = server.active_expire_effort-1, /* Rescale from 0 to 9. */
    // 20
    config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP +
                           ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort,
    config_cycle_fast_duration = ACTIVE_EXPIRE_CYCLE_FAST_DURATION +
                                 ACTIVE_EXPIRE_CYCLE_FAST_DURATION/4*effort,
                                 // 25+2*1
    config_cycle_slow_time_perc = ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC +
                                  2*effort,
                                  // 10
    config_cycle_acceptable_stale = ACTIVE_EXPIRE_CYCLE_ACCEPTABLE_STALE-
                                    effort;


    static long long last_fast_cycle = 0; /* When last fast cycle ran. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL; //16
    long long start = ustime(), timelimit, elapsed;


    if (checkClientPauseTimeoutAndReturnIfPaused()) return;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {

        if (!timelimit_exit &&
            server.stat_expired_stale_perc < config_cycle_acceptable_stale)
            return;

        if (start < last_fast_cycle + (long long)config_cycle_fast_duration*2)
            return;

        last_fast_cycle = start;
    }

    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

   
     // 25*100000/
    timelimit = config_cycle_slow_time_perc*1000000/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = config_cycle_fast_duration; /* in microseconds. */

    long total_sampled = 0;
    long total_expired = 0;

// dbs_per_call=16 遍历16个库
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        /* Expired and checked in a single loop. */
        unsigned long expired, sampled;

        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. */
        current_db++;

      
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
            iteration++;

       
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();

            if (slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;

            expired = 0;
            sampled = 0;
            ttl_sum = 0;
            ttl_samples = 0;

          // 不能超过20
            if (num > config_keys_per_loop)
            // 20
                num = config_keys_per_loop;
            long max_buckets = num*20;
            long checked_buckets = 0;

            while (sampled < num && checked_buckets < max_buckets) {
                for (int table = 0; table < 2; table++) {
                    if (table == 1 && !dictIsRehashing(db->expires)) break;

                    unsigned long idx = db->expires_cursor;
                    idx &= db->expires->ht[table].sizemask;
                    dictEntry *de = db->expires->ht[table].table[idx];
                    long long ttl;

                    /* Scan the current bucket of the current table. */
                    checked_buckets++;
                    while(de) {
                        /* Get the next entry now since this entry may get
                         * deleted. */
                        dictEntry *e = de;
                        de = de->next;

                        ttl = dictGetSignedIntegerVal(e)-now;
                        if (activeExpireCycleTryExpire(db,e,now)) expired++;
                        if (ttl > 0) {
                            /* We want the average TTL of keys yet
                             * not expired. */
                            ttl_sum += ttl;
                            ttl_samples++;
                        }
                        sampled++;
                    }
                }
                db->expires_cursor++;
            }
            total_expired += expired;
            total_sampled += sampled;

            /* Update the average TTL stats for this database. */
            if (ttl_samples) {
                long long avg_ttl = ttl_sum/ttl_samples;

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

     
            if ((iteration & 0xf) == 0) { 
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }

        } while (sampled == 0 ||
                 (expired*100/sampled) > config_cycle_acceptable_stale);
    }

    elapsed = ustime()-start;
    server.stat_expire_cycle_time_used += elapsed;
    latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

    double current_perc;
    if (total_sampled) {
        current_perc = (double)total_expired/total_sampled;
    } else
        current_perc = 0;
    server.stat_expired_stale_perc = (current_perc*0.05)+
                                     (server.stat_expired_stale_perc*0.95);
}

根据上述代码存在三个循环.影响循环因素:

  • timelimit_exit 前15次过循环中单次过期耗时(微秒)超过timelimit
  • 每个db最多400个哈希桶checked_buckets < max_buckets
  • 每个db单次扫描哈希桶中key的数量最多20个.sampled < num
  • 假设某个db中连续出现400个空桶,那么只能等待超过指定耗时才能结束该任务.
  1. 定时serverCron方法去执行清理,执行频率根据redis.conf中的hz配置 的值
  2. 执行清理的时候,不是去扫描所有的key,而是去扫描所有设置了过期 时间的key(redisDb.expires)
  3. 如果每次去把所有过期的key都拿过来,那么假如过期的key很多,就会 很慢,所以也不是一次性拿取所有的key
  4. 根据hash桶的维度去扫描key(扫描游标为expires_cursor),扫到20(可配)个key为止。假如第一个桶 是15个key ,没有满足20,继续扫描第二个桶,第二个桶20个key,由 于是以hash桶的维度扫描的,所以第二个扫到了就会全扫,总共扫描 35个key
  5. 找到扫描的key里面过期的key,并进行删除
  6. 如果取了400个空桶,或者扫描的删除比例跟扫描的总数超过10%,继续 执行4、5步。
  7. 也不能无限的循环,循环16次后回去检测时间,超过指定时间会跳出。

结语: 不积跬步无以至千里.