redis TTL实现原理

TTL存储的数据结构

 redis针对TTL时间有专门的dict进行存储,就是redisDb当中的dict *expires字段,dict顾名思义就是一个hashtable,key为对应的rediskey,value为对应的TTL时间。
 dict的数据结构中含有2个dictht对象,主要是为了解决hash冲突过程中重新hash数据使用。
 dictEntry当中的dictEntry就是hashtable当中的hash桶,作用应该不言自明了吧。

typedef struct redisDb {

    // 数据库键空间,保存着数据库中的所有键值对
    dict *dict;                 /* The keyspace for this DB */

    // 键的过期时间,字典的键为键,字典的值为过期事件 UNIX 时间戳
    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 */

    // 正在被 WATCH 命令监视的键
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

    struct evictionPoolEntry *eviction_pool;    /* Eviction pool of keys */

    // 数据库号码
    int id;                     /* Database ID */

    // 数据库的键的平均 TTL ,统计信息
    long long avg_ttl;          /* Average TTL, just for stats */

} redisDb;
/*
 * 字典
 */
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;
/*
 * 哈希表
 *
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

TTL 设置过期时间

 TTL设置key过期时间的方法主要是下面4个:

  • expire 按照相对时间且以秒为单位的过期策略
  • expireat 按照绝对时间且以秒为单位的过期策略
  • pexpire 按照相对时间且以毫秒为单位的过期策略
  • pexpireat 按照绝对时间且以毫秒为单位的过期策略
{"expire",expireCommand,3,"w",0,NULL,1,1,1,0,0},
{"expireat",expireatCommand,3,"w",0,NULL,1,1,1,0,0},
{"pexpire",pexpireCommand,3,"w",0,NULL,1,1,1,0,0},
{"pexpireat",pexpireatCommand,3,"w",0,NULL,1,1,1,0,0},

expire expireat pexpire pexpireat

 从实际设置过期时间的实现函数来看,相对时间的策略会有一个当前时间作为基准时间,绝对时间的策略会以0作为一个基准时间。

void expireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_SECONDS);
}

void expireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_SECONDS);
}

void pexpireCommand(redisClient *c) {
    expireGenericCommand(c,mstime(),UNIT_MILLISECONDS);
}

void pexpireatCommand(redisClient *c) {
    expireGenericCommand(c,0,UNIT_MILLISECONDS);
}

 整个过期时间最后都会换算到绝对时间进行存储,通过公式基准时间+过期时间来进行计算。
 对于相对时间而言基准时间就是当前时间,对于绝对时间而言相对时间就是0。
 中途考虑设置的过期时间是否已经过期,如果已经过期那么在master就会删除该数据并同步删除动作到slave。
 正常的设置过期时间是通过setExpire方法保存到 dict *expires对象当中。

/* 
 *
 * 这个函数是 EXPIRE 、 PEXPIRE 、 EXPIREAT 和 PEXPIREAT 命令的底层实现函数。
 *
 * 命令的第二个参数可能是绝对值,也可能是相对值。
 * 当执行 *AT 命令时, basetime 为 0 ,在其他情况下,它保存的就是当前的绝对时间。
 *
 * unit 用于指定 argv[2] (传入过期时间)的格式,
 * 它可以是 UNIT_SECONDS 或 UNIT_MILLISECONDS ,
 * basetime 参数则总是毫秒格式的。
 */
void expireGenericCommand(redisClient *c, long long basetime, int unit) {
    robj *key = c->argv[1], *param = c->argv[2];
    long long when; /* unix time in milliseconds when the key will expire. */

    // 取出 when 参数
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;

    // 如果传入的过期时间是以秒为单位的,那么将它转换为毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;

    /* No key, return zero. */
    // 取出键
    if (lookupKeyRead(c->db,key) == NULL) {
        addReply(c,shared.czero);
        return;
    }

    /* 
     * 在载入数据时,或者服务器为附属节点时,
     * 即使 EXPIRE 的 TTL 为负数,或者 EXPIREAT 提供的时间戳已经过期,
     * 服务器也不会主动删除这个键,而是等待主节点发来显式的 DEL 命令。
     *
     * 程序会继续将(一个可能已经过期的 TTL)设置为键的过期时间,
     * 并且等待主节点发来 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {

        // when 提供的时间已经过期,服务器为主节点,并且没在载入数据

        robj *aux;

        redisAssertWithInfo(c,key,dbDelete(c->db,key));
        server.dirty++;

        /* Replicate/AOF this as an explicit DEL. */
        // 传播 DEL 命令
        aux = createStringObject("DEL",3);

        rewriteClientCommandVector(c,2,aux,key);
        decrRefCount(aux);

        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"del",key,c->db->id);

        addReply(c, shared.cone);

        return;
    } else {

        // 设置键的过期时间
        // 如果服务器为附属节点,或者服务器正在载入,
        // 那么这个 when 有可能已经过期的
        setExpire(c->db,key,when);

        addReply(c,shared.cone);

        signalModifiedKey(c->db,key);
        notifyKeyspaceEvent(REDIS_NOTIFY_GENERIC,"expire",key,c->db->id);

        server.dirty++;

        return;
    }
}

 setExpire函数主要是对db->expires中的key对应的dictEntry设置过期时间。

/*
 * 将键 key 的过期时间设为 when
 */
void setExpire(redisDb *db, robj *key, long long when) {

    dictEntry *kde, *de;

    /* Reuse the sds from the main dict in the expire dict */
    // 取出键
    kde = dictFind(db->dict,key->ptr);

    redisAssertWithInfo(NULL,key,kde != NULL);

    // 根据键取出键的过期时间
    de = dictReplaceRaw(db->expires,dictGetKey(kde));

    // 设置键的过期时间
    // 这里是直接使用整数值来保存过期时间,不是用 INT 编码的 String 对象
    dictSetSignedIntegerVal(de,when);
}

TTL 获取过期时间

 通过ttl或者pttl返回剩余过期时间的逻辑其实非常简单,就是通过key去db->expires找到过期时间对象,然后与当前系统时间相比计算差值。

{"ttl",ttlCommand,2,"r",0,NULL,1,1,1,0,0},
{"pttl",pttlCommand,2,"r",0,NULL,1,1,1,0,0},

void ttlCommand(redisClient *c) {
    ttlGenericCommand(c, 0);
}

void pttlCommand(redisClient *c) {
    ttlGenericCommand(c, 1);
}

/*
 * 返回键的剩余生存时间。
 *
 * output_ms 指定返回值的格式:
 *
 *  - 为 1 时,返回毫秒
 *
 *  - 为 0 时,返回秒
 */
void ttlGenericCommand(redisClient *c, int output_ms) {
    long long expire, ttl = -1;

    /* If the key does not exist at all, return -2 */
    // 取出键
    if (lookupKeyRead(c->db,c->argv[1]) == NULL) {
        addReplyLongLong(c,-2);
        return;
    }

    /* The key exists. Return -1 if it has no expire, or the actual
     * TTL value otherwise. */
    // 取出过期时间
    expire = getExpire(c->db,c->argv[1]);

    if (expire != -1) {
        // 计算剩余生存时间
        ttl = expire-mstime();
        if (ttl < 0) ttl = 0;
    }

    if (ttl == -1) {
        // 键是持久的
        addReplyLongLong(c,-1);
    } else {
        // 返回 TTL 
        // (ttl+500)/1000 计算的是渐近秒数
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

/* 
 * 返回给定 key 的过期时间。
 *
 * 如果键没有设置过期时间,那么返回 -1 。
 */
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;

    /* No expire? return ASAP */
    // 获取键的过期时间
    // 如果过期时间不存在,那么直接返回
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;

    /* The entry was found in the expire dict, this means it should also
     * be present in the main dict (safety check). */
    redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);

    // 返回过期时间
    return dictGetSignedIntegerVal(de);
}
    原文作者:晴天哥_374
    原文地址: https://www.jianshu.com/p/53083f5f2ddc
    本文转自网络文章,转载此文章仅为分享知识,如有侵权,请联系博主进行删除。
点赞