Redis(四):Key读写及过期策略

DB结构体
  Redis默认有16个数据库,存储数据前必须先通过SELECT INDEX来指定DB(默认index为0,DB结构体对应server.h/redisDb),DB主要存储并维护键值对信息。值得注意的是Redis目前没有命令可以获取当前正在操作的库,所以比较好的做法是每次操作前select。

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 */
    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
} redisDb;

dict字典又称键空间,存储了所有键值对,值是五大类型之一;expires存储的是设置了过期时间的key与其失效时间的键值对; 

Key读写
  当用户读取某个Key时,如果dict中不存在该Key,则miss次数+1并返回客户端空;否则hit次数+1并且更新键的最后一次使用时间,通过这个值可以计算出key的闲置时间,闲置时间的长短又会影响回收策略,如果Key已失效则同时清除dict与expires中的键值对,并向Slave发送DEL命令。通过info stats可以查看hist和miss等信息,通过Object idletime key可以查看key的闲置时间。
  当写入某个Key时,如果dict中已经存在该key则进行对应值的更新,否则存入新的键值对。对设置了过期时间的Key写入时同时也存入一份到expires字典当中,删除时同样也清除expires中对应键值对。


Key过期时间设置
   redis有四个命令expire、pexpire、expireat、pexpireat可以绑定key的过期时间(persist可以解除绑定)。expire和pexpire设置key可以存活的时间,expireat和pexpireat 设置key失效的时间点,前者以秒为单位,后者以毫秒为单位,前三个命令底层使用的都是pexpireat。

Key过期清理策略

  1. 定时清理:在设置k/v对时,同时开启一个定时器,或将失效时间点注入某个定时器,在有效时间到达后移除该键值对。这种方式一般能尽快释放被占用的内存,但对CPU性能消耗很严重,特别是在大量数据或大量连续请求的情况下,根本没这么多CPU资源拿来消耗,实际中并不常见。
  2. 惰性清理:在客户端请求某个key时才检测其有效性。这种方式不会像定时清理一样大量消耗CPU资源,但对于内存却可能产生内存泄漏,如果客户端永远不请求那些失效的key,那么这些key就会一直驻留在内存中,永远无法释放(flush等操作除外)。
  3. 定期清理:周期性的检测key的有效性(不一定是全量key),这种方式即不会大量消耗CPU资源,也不会导致内存泄漏,但key基本上不会被及时清理,实际中清理的时机与频率并不容易确定。

Redis综合采用了惰性清理与定期清理策略,关于惰性清理在上方的已经说明了,这里分析一下定期清理的源码,在3.2.8版本中源码位于server.c/activeExpireCycle,这个函数通过周期性调用serverCron().databasesCron()来间接调用,频率默认为
#define CONFIG_DEFAULT_HZ   10    /* Time interrupt calls/sec. */ 每秒10次,源码如下:

/*
 *type=ACTIVE_EXPIRE_CYCLE_FAST表示快清理; type=ACTIVE_EXPIRE_CYCLE_SLOW表示慢清理;
 */
void activeExpireCycle(int type) {
    
    static unsigned int current_db = 0; /* 当前清理的DB */
    static int timelimit_exit = 0;      /* 执行时长是否已到 */
    static long long last_fast_cycle = 0; /* 上次运行时间. */

    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL; /* 默认请理DB数量:16*/
    long long start = ustime(), timelimit;

    if (clientsArePaused()) return;

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        /* 上次快清理尚未结束,本次不开启 */
        if (!timelimit_exit) return;
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }

    /* 确定DB数量 */
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;

    /* 慢清理的执行时长: 1000000 * 25/10/100 = 25000 == 25毫秒 */    
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;

    /* 快清理执行时长:1毫秒 */
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

    /* 迭代DB */
    for (j = 0; j < dbs_per_call; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);

        /* DB索引加1,指向下一个要处理的DB */
        current_db++;

        /* 从当前DB中随机抽取最多20个key
         * 1:获取每个key的ttl,算出平均ttl
         * 2:清理失效的key,并记录失效key的个数,如果失效key的个数不超过1/4(少于5个),则认为当前DB暂不需要清理,执行下个DB的处理。
         * 3:每执行16次,则检测是否执行时长已到,执行时间已达上限则退出本次清理。等待下次周期性调用
         */
        do {
            unsigned long num, slots;
            long long now, ttl_sum;
            int ttl_samples;
           
            if ((num = dictSize(db->expires)) == 0) {
                db->avg_ttl = 0;
                break;
            }
            slots = dictSlots(db->expires);
            now = mstime();
          
            if (num && slots > DICT_HT_INITIAL_SIZE &&
                (num*100/slots < 1)) break;
        
            expired = 0;
            ttl_sum = 0;
            ttl_samples = 0;

            /* 随机抽取最多20个key */
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;           
            while (num--) {
                dictEntry *de;
                long long ttl;

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedIntegerVal(de)-now;
                if (activeExpireCycleTryExpire(db,de,now)) expired++;/* 记录失效个数 */
                if (ttl > 0) {                   
                    ttl_sum += ttl;
                    ttl_samples++;
                }
            }
          
            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);
            }

            /* 每执行16次检测一次执行时长 */
            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;
            /* 失效个数>1/4则继续,否则结束对当前DB的清理. */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
}

基本上这个函数的工作模式可以概括如下: 
1)迭代一定数量的DB,最大数量16。
2)从每个DB中随机抽取最多20个key
         * 1:获取每个key的ttl,用于统计求平均ttl
         * 2:清理失效的key,并记录失效key的个数,如果失效key的个数不超过1/4(少于5个),则认为当前DB暂不需要清理,执行下个DB的处理。
         * 3:为了防止长时间的阻塞,每执行16次检测是否执行时长已到,执行时间已达上限则退出本次清理,等待下次周期性调用。但当DB数量不足16个时,此步不会被执行,此时即使执行时长已到,也不会结束,直到最后清理完成才会终止。

RDB和AOF对过期Key的处理
  
RDB和AOF重写本质上都是存储当前内存中的数据,只不过RDB以数据文件的形式存储,而重写AOF则是以命令形式存储,但是对于内存中那些已经失效的Key,两者都不会存储,对于RDB来说只需要保存可用数据,没必要在加载后再进行清理无效数据,对于AOF来说只需要保存最小指令集,也没必要保存写入和删除两条指令。但正常的AOF对于失效Key,会追加一条Del指令到aof文件中。

主从节点对过期的Key的处理
  对于从节点而言由于它的数据来源于主节点,为了保持与主节点状态一致,在Master发送删除指令之前,它不会采用任何策略来清理失效的key, 但针对失效Key的所有请求都会返回空。之所以返回空,是因为从节点在key失效但尚未接收到或丢失主节点del命令的情况下,需要与主节点保持一致的响应。


总结:

 

  • Redis数据库主要由dict和expires两个字典构成,其中dict字典负责保存键值对,键是一个字符串,值是五大类型之一;expires字典则负责保存键与过期时间对。所以对键的操作都是建立在字典操作之上
  • Redis使用惰性删除和定期删除两种策略来删除过期的键
  • 一个失效键被删除之后,会追加一条DEL到现有AOF文件中,但对于RDB或AOF重写都不会包含已经过期的键
  • 主节点删除某个键时,会向所有从节点发送一条DEL命令;对于从节点来说,为了保持与主节点的一致性,在未接收到DEL命令前不会采用任何策略删除它,但对失效key的所有请求都返回空。

你可能感兴趣的:(Java高级,Redis,NoSql,Java中间件)