redis TTL實現(xiàn)原理

系列

redis數(shù)據(jù)淘汰原理
redis過期數(shù)據(jù)刪除策略
redis server事件模型
redis cluster mget 引發(fā)的討論
redis 3.x windows 集群搭建
redis 命令執(zhí)行過程
redis string底層數(shù)據(jù)結(jié)構(gòu)
redis list底層數(shù)據(jù)結(jié)構(gòu)
redis hash底層數(shù)據(jù)結(jié)構(gòu)
redis set底層數(shù)據(jù)結(jié)構(gòu)
redis zset底層數(shù)據(jù)結(jié)構(gòu)
redis 客戶端管理
redis 主從同步-slave端
redis 主從同步-master端
redis 主從超時檢測
redis aof持久化
redis rdb持久化
redis 數(shù)據(jù)恢復(fù)過程
redis TTL實現(xiàn)原理
redis cluster集群建立
redis cluster集群選主

TTL存儲的數(shù)據(jù)結(jié)構(gòu)

?redis針對TTL時間有專門的dict進行存儲,就是redisDb當中的dict *expires字段照棋,dict顧名思義就是一個hashtable饱溢,key為對應(yīng)的rediskey祈餐,value為對應(yīng)的TTL時間摄乒。
?dict的數(shù)據(jù)結(jié)構(gòu)中含有2個dictht對象蛹磺,主要是為了解決hash沖突過程中重新hash數(shù)據(jù)使用卸亮。
?dictEntry當中的dictEntry就是hashtable當中的hash桶叫榕,作用應(yīng)該不言自明了吧。

typedef struct redisDb {

    // 數(shù)據(jù)庫鍵空間详炬,保存著數(shù)據(jù)庫中的所有鍵值對
    dict *dict;                 /* The keyspace for this DB */

    // 鍵的過期時間盐类,字典的鍵為鍵,字典的值為過期事件 UNIX 時間戳
    dict *expires;              /* Timeout of keys with a timeout set */

    // 正處于阻塞狀態(tài)的鍵
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP) */

    // 可以解除阻塞的鍵
    dict *ready_keys;           /* Blocked keys that received a PUSH */

    // 正在被 WATCH 命令監(jiān)視的鍵
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */

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

    // 數(shù)據(jù)庫號碼
    int id;                     /* Database ID */

    // 數(shù)據(jù)庫的鍵的平均 TTL ,統(tǒng)計信息
    long long avg_ttl;          /* Average TTL, just for stats */

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

    // 類型特定函數(shù)
    dictType *type;

    // 私有數(shù)據(jù)
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 當 rehash 不在進行時在跳,值為 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在運行的安全迭代器的數(shù)量
    int iterators; /* number of iterators currently running */

} dict;
/*
 * 哈希表
 *
 * 每個字典都使用兩個哈希表枪萄,從而實現(xiàn)漸進式 rehash 。
 */
typedef struct dictht {
    
    // 哈希表數(shù)組
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩碼猫妙,用于計算索引值
    // 總是等于 size - 1
    unsigned long sizemask;

    // 該哈希表已有節(jié)點的數(shù)量
    unsigned long used;

} dictht;


TTL 設(shè)置過期時間

?TTL設(shè)置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

?從實際設(shè)置過期時間的實現(xiàn)函數(shù)來看瓷翻,相對時間的策略會有一個當前時間作為基準時間,絕對時間的策略會以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。
?中途考慮設(shè)置的過期時間是否已經(jīng)過期单旁,如果已經(jīng)過期那么在master就會刪除該數(shù)據(jù)并同步刪除動作到slave沪羔。
?正常的設(shè)置過期時間是通過setExpire方法保存到 dict *expires對象當中。

/* 
 *
 * 這個函數(shù)是 EXPIRE 象浑、 PEXPIRE 蔫饰、 EXPIREAT 和 PEXPIREAT 命令的底層實現(xiàn)函數(shù)。
 *
 * 命令的第二個參數(shù)可能是絕對值愉豺,也可能是相對值篓吁。
 * 當執(zhí)行 *AT 命令時, basetime 為 0 蚪拦,在其他情況下杖剪,它保存的就是當前的絕對時間。
 *
 * unit 用于指定 argv[2] (傳入過期時間)的格式驰贷,
 * 它可以是 UNIT_SECONDS 或 UNIT_MILLISECONDS 盛嘿,
 * basetime 參數(shù)則總是毫秒格式的。
 */
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 參數(shù)
    if (getLongLongFromObjectOrReply(c, param, &when, NULL) != REDIS_OK)
        return;

    // 如果傳入的過期時間是以秒為單位的括袒,那么將它轉(zhuǎn)換為毫秒
    if (unit == UNIT_SECONDS) when *= 1000;
    when += basetime;

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

    /* 
     * 在載入數(shù)據(jù)時次兆,或者服務(wù)器為附屬節(jié)點時,
     * 即使 EXPIRE 的 TTL 為負數(shù)锹锰,或者 EXPIREAT 提供的時間戳已經(jīng)過期芥炭,
     * 服務(wù)器也不會主動刪除這個鍵,而是等待主節(jié)點發(fā)來顯式的 DEL 命令恃慧。
     *
     * 程序會繼續(xù)將(一個可能已經(jīng)過期的 TTL)設(shè)置為鍵的過期時間园蝠,
     * 并且等待主節(jié)點發(fā)來 DEL 命令。
     */
    if (when <= mstime() && !server.loading && !server.masterhost) {

        // when 提供的時間已經(jīng)過期糕伐,服務(wù)器為主節(jié)點砰琢,并且沒在載入數(shù)據(jù)

        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 {

        // 設(shè)置鍵的過期時間
        // 如果服務(wù)器為附屬節(jié)點,或者服務(wù)器正在載入,
        // 那么這個 when 有可能已經(jīng)過期的
        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函數(shù)主要是對db->expires中的key對應(yīng)的dictEntry設(shè)置過期時間陪汽。

/*
 * 將鍵 key 的過期時間設(shè)為 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);

    // 根據(jù)鍵取出鍵的過期時間
    de = dictReplaceRaw(db->expires,dictGetKey(kde));

    // 設(shè)置鍵的過期時間
    // 這里是直接使用整數(shù)值來保存過期時間训唱,不是用 INT 編碼的 String 對象
    dictSetSignedIntegerVal(de,when);
}


TTL 獲取過期時間

?通過ttl或者pttl返回剩余過期時間的邏輯其實非常簡單,就是通過key去db->expires找到過期時間對象挚冤,然后與當前系統(tǒng)時間相比計算差值况增。

{"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 計算的是漸近秒數(shù)
        addReplyLongLong(c,output_ms ? ttl : ((ttl+500)/1000));
    }
}

/* 
 * 返回給定 key 的過期時間。
 *
 * 如果鍵沒有設(shè)置過期時間澜薄,那么返回 -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);
}
最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末肤京,一起剝皮案震驚了整個濱河市颊艳,隨后出現(xiàn)的幾起案子,更是在濱河造成了極大的恐慌忘分,老刑警劉巖棋枕,帶你破解...
    沈念sama閱讀 218,036評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件,死亡現(xiàn)場離奇詭異妒峦,居然都是意外死亡重斑,警方通過查閱死者的電腦和手機,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,046評論 3 395
  • 文/潘曉璐 我一進店門肯骇,熙熙樓的掌柜王于貴愁眉苦臉地迎上來窥浪,“玉大人,你說我怎么就攤上這事笛丙『螅” “怎么了?”我有些...
    開封第一講書人閱讀 164,411評論 0 354
  • 文/不壞的土叔 我叫張陵若债,是天一觀的道長符相。 經(jīng)常有香客問我,道長蠢琳,這世上最難降的妖魔是什么啊终? 我笑而不...
    開封第一講書人閱讀 58,622評論 1 293
  • 正文 為了忘掉前任,我火速辦了婚禮傲须,結(jié)果婚禮上蓝牲,老公的妹妹穿的比我還像新娘。我一直安慰自己泰讽,他們只是感情好例衍,可當我...
    茶點故事閱讀 67,661評論 6 392
  • 文/花漫 我一把揭開白布昔期。 她就那樣靜靜地躺著,像睡著了一般佛玄。 火紅的嫁衣襯著肌膚如雪硼一。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,521評論 1 304
  • 那天梦抢,我揣著相機與錄音般贼,去河邊找鬼。 笑死奥吩,一個胖子當著我的面吹牛哼蛆,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播霞赫,決...
    沈念sama閱讀 40,288評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼腮介,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了端衰?” 一聲冷哼從身側(cè)響起萤厅,我...
    開封第一講書人閱讀 39,200評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎靴迫,沒想到半個月后,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體楼誓,經(jīng)...
    沈念sama閱讀 45,644評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡玉锌,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,837評論 3 336
  • 正文 我和宋清朗相戀三年,在試婚紗的時候發(fā)現(xiàn)自己被綠了疟羹。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片主守。...
    茶點故事閱讀 39,953評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡,死狀恐怖榄融,靈堂內(nèi)的尸體忽然破棺而出参淫,到底是詐尸還是另有隱情,我是刑警寧澤愧杯,帶...
    沈念sama閱讀 35,673評論 5 346
  • 正文 年R本政府宣布涎才,位于F島的核電站,受9級特大地震影響力九,放射性物質(zhì)發(fā)生泄漏耍铜。R本人自食惡果不足惜,卻給世界環(huán)境...
    茶點故事閱讀 41,281評論 3 329
  • 文/蒙蒙 一跌前、第九天 我趴在偏房一處隱蔽的房頂上張望棕兼。 院中可真熱鬧,春花似錦抵乓、人聲如沸伴挚。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,889評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽茎芋。三九已至颅眶,卻和暖如春,著一層夾襖步出監(jiān)牢的瞬間败徊,已是汗流浹背帚呼。 一陣腳步聲響...
    開封第一講書人閱讀 33,011評論 1 269
  • 我被黑心中介騙來泰國打工, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留皱蹦,地道東北人煤杀。 一個月前我還...
    沈念sama閱讀 48,119評論 3 370
  • 正文 我出身青樓,卻偏偏與公主長得像沪哺,于是被迫代替她去往敵國和親沈自。 傳聞我的和親對象是個殘疾皇子,可洞房花燭夜當晚...
    茶點故事閱讀 44,901評論 2 355

推薦閱讀更多精彩內(nèi)容