Redis 源碼研究之數(shù)據(jù)淘汰機制

本文主要介紹Redis的幾種數(shù)據(jù)淘汰機制。

I测僵、上帝視角

由于Redis是內(nèi)存型數(shù)據(jù)庫缕探,其允許用戶設(shè)置最大使用內(nèi)存大小為maxmemory缅疟,在內(nèi)存有限的情況下,為減少內(nèi)存緊張的情況淮菠,當內(nèi)存數(shù)據(jù)集大小上升至一定值時男公,就會實施數(shù)據(jù)淘汰機制。

Redis提供了以下幾種數(shù)據(jù)淘汰策略:
1合陵、 volatile-lru:從設(shè)置過期的數(shù)據(jù)集中淘汰最少使用的數(shù)據(jù)枢赔;
2、volatile-ttl:從設(shè)置過期的數(shù)據(jù)集中淘汰即將過期的數(shù)據(jù)(離過期時間最近)拥知;
3踏拜、volatile-random:從設(shè)置過期的數(shù)據(jù)集中隨機選取數(shù)據(jù)淘汰;
4低剔、allkeys-lru:從所有 數(shù)據(jù)集中選取使用最少的數(shù)據(jù)速梗;
5、allkeys-random:從所有數(shù)據(jù)集中任意選取數(shù)據(jù)淘汰襟齿;
6姻锁、no-envicition:不進行淘汰;

II猜欺、LRU數(shù)據(jù)淘汰

1位隶、redisServer中保存了lru計數(shù)器server.lrulock,會定時更新开皿,這是根據(jù)server.unixtime計算出來的:

// redisServer 保存了lru 計數(shù)器
/*src/redis.h/redisServer*/
struct redisServer {
...
unsigned lruclock:22; /* Clock incrementing every minute, for LRU */
...
};  

2涧黄、LRU數(shù)據(jù)淘汰機制使這樣的:從數(shù)據(jù)集中隨機挑選幾個鍵值對篮昧,取出其中l(wèi)ru最大的鍵值對淘汰。

III弓熏、TTL數(shù)據(jù)淘汰

1恋谭、TTL淘汰機制使從過期時間redisDB.expires表中隨機挑選幾個鍵值對,取出其中ttl最大的鍵值對淘汰挽鞠。

IV疚颊、淘汰發(fā)生

1、Redis服務(wù)器沒執(zhí)行一個命令信认,都會檢測內(nèi)存材义,判斷是否需要進行數(shù)據(jù)淘汰:

// 執(zhí)行命令
/*src/redis.cprocessCommand*/
int processCommand(redisClient *c) {
        ......
        // 內(nèi)存超額
        /* Handle the maxmemory directive.
        **
        First we try to free some memory if possible (if there are volatile
        * keys in the dataset). If there are not the only thing we can do
        * is returning an error. */
        if (server.maxmemory) {
                int retval = freeMemoryIfNeeded();
        if ((c->cmd->flags & REDIS_CMD_DENYOOM) && retval == REDIS_ERR) {
                flagTransaction(c);
                addReply(c, shared.oomerr);
                return REDIS_OK;
        }
    }
    ......
}  

2、這其中主要調(diào)用了freeMemoryIfNeeded函數(shù)嫁赏,它完成了完整的數(shù)據(jù)淘汰機制:

int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);

    /* Remove the size of slaves output buffers and AOF buffer from the
     * count of used memory. */
    // 計算出 Redis 目前占用的內(nèi)存總數(shù)其掂,但有兩個方面的內(nèi)存不會計算在內(nèi):
    // 1)從服務(wù)器的輸出緩沖區(qū)的內(nèi)存
    // 2)AOF 緩沖區(qū)的內(nèi)存
    mem_used = zmalloc_used_memory();
    if (slaves) {
        listIter li;
        listNode *ln;

        listRewind(server.slaves,&li);
        while((ln = listNext(&li))) {
            redisClient *slave = listNodeValue(ln);
            unsigned long obuf_bytes = getClientOutputBufferMemoryUsage(slave);
            if (obuf_bytes > mem_used)
                mem_used = 0;
            else
                mem_used -= obuf_bytes;
        }
    }
    if (server.aof_state != REDIS_AOF_OFF) {
        mem_used -= sdslen(server.aof_buf);
        mem_used -= aofRewriteBufferSize();
    }

    /* Check if we are over the memory limit. */
    // 如果目前使用的內(nèi)存大小比設(shè)置的 maxmemory 要小,那么無須執(zhí)行進一步操作
    if (mem_used <= server.maxmemory) return REDIS_OK;

    // 如果占用內(nèi)存比 maxmemory 要大潦蝇,但是 maxmemory 策略為不淘汰款熬,那么直接返回
    if (server.maxmemory_policy == REDIS_MAXMEMORY_NO_EVICTION)
        return REDIS_ERR; /* We need to free memory, but policy forbids. */

    /* Compute how much memory we need to free. */
    // 計算需要釋放多少字節(jié)的內(nèi)存
    mem_tofree = mem_used - server.maxmemory;

    // 初始化已釋放內(nèi)存的字節(jié)數(shù)為 0
    mem_freed = 0;

    // 根據(jù) maxmemory 策略,
    // 遍歷字典攘乒,釋放內(nèi)存并記錄被釋放內(nèi)存的字節(jié)數(shù)
    while (mem_freed < mem_tofree) {
        int j, k, keys_freed = 0;

        // 遍歷所有字典
        for (j = 0; j < server.dbnum; j++) {
            long bestval = 0; /* just to prevent warning */
            sds bestkey = NULL;
            dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;

            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
            {
                // 如果策略是 allkeys-lru 或者 allkeys-random 
                // 那么淘汰的目標為所有數(shù)據(jù)庫鍵
                dict = server.db[j].dict;
            } else {
                // 如果策略是 volatile-lru 贤牛、 volatile-random 或者 volatile-ttl 
                // 那么淘汰的目標為帶過期時間的數(shù)據(jù)庫鍵
                dict = server.db[j].expires;
            }

            // 跳過空字典
            if (dictSize(dict) == 0) continue;

            /* volatile-random and allkeys-random policy */
            // 如果使用的是隨機策略,那么從目標字典中隨機選出鍵
            if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_RANDOM)
            {
                de = dictGetRandomKey(dict);
                bestkey = dictGetKey(de);
            }

            /* volatile-lru and allkeys-lru policy */
            // 如果使用的是 LRU 策略则酝,
            // 那么從一集 sample 鍵中選出 IDLE 時間最長的那個鍵
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                struct evictionPoolEntry *pool = db->eviction_pool;

                while(bestkey == NULL) {
                    //隨機取一集鍵值對
                    evictionPoolPopulate(dict, db->dict, db->eviction_pool);
                    /* Go backward from best to worst element to evict. */
                    for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
                        if (pool[k].key == NULL) continue;
                        de = dictFind(dict,pool[k].key);

                        /* Remove the entry from the pool. */
                        sdsfree(pool[k].key);
                        /* Shift all elements on its right to left. */
                        memmove(pool+k,pool+k+1,
                            sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
                        /* Clear the element on the right which is empty
                         * since we shifted one position to the left.  */
                        pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
                        pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;

                        /* If the key exists, is our pick. Otherwise it is
                         * a ghost and we need to try the next element. */
                        if (de) {
                            bestkey = dictGetKey(de);
                            break;
                        } else {
                            /* Ghost... */
                            continue;
                        }
                    }
                }
            }

            /* volatile-ttl */
            // 策略為 volatile-ttl 殉簸,從一集 sample 鍵中選出過期時間距離當前時間最接近的鍵
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetKey(de);
                    thisval = (long) dictGetVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better
                     * candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }
            }

            /* Finally remove the selected key. */
            // 刪除被選中的鍵
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                propagateExpire(db,keyobj);
                /* We compute the amount of memory freed by dbDelete() alone.
                 * It is possible that actually the memory needed to propagate
                 * the DEL in AOF and replication link is greater than the one
                 * we are freeing removing the key, but we can't account for
                 * that otherwise we would never exit the loop.
                 *
                 * AOF and Output buffer memory will be freed eventually so
                 * we only care about memory used by the key space. */
                // 計算刪除鍵所釋放的內(nèi)存數(shù)量
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                
                // 對淘汰鍵的計數(shù)器增一
                server.stat_evictedkeys++;

                notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
                    keyobj, db->id);
                decrRefCount(keyobj);
                keys_freed++;

                /* When the memory to free starts to be big enough, we may
                 * start spending so much time here that is impossible to
                 * deliver data to the slaves fast enough, so we force the
                 * transmission here inside the loop. */
                if (slaves) flushSlavesOutputBuffers();
            }
        }

        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }

    return REDIS_OK;
}

【參考】
[1] 《Redis設(shè)計與實現(xiàn)》
[2] 《Redis源碼日志》

?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
  • 序言:七十年代末,一起剝皮案震驚了整個濱河市沽讹,隨后出現(xiàn)的幾起案子般卑,更是在濱河造成了極大的恐慌,老刑警劉巖爽雄,帶你破解...
    沈念sama閱讀 218,284評論 6 506
  • 序言:濱河連續(xù)發(fā)生了三起死亡事件蝠检,死亡現(xiàn)場離奇詭異,居然都是意外死亡挚瘟,警方通過查閱死者的電腦和手機蝇率,發(fā)現(xiàn)死者居然都...
    沈念sama閱讀 93,115評論 3 395
  • 文/潘曉璐 我一進店門,熙熙樓的掌柜王于貴愁眉苦臉地迎上來刽沾,“玉大人,你說我怎么就攤上這事排拷〔嗬欤” “怎么了?”我有些...
    開封第一講書人閱讀 164,614評論 0 354
  • 文/不壞的土叔 我叫張陵监氢,是天一觀的道長布蔗。 經(jīng)常有香客問我藤违,道長,這世上最難降的妖魔是什么纵揍? 我笑而不...
    開封第一講書人閱讀 58,671評論 1 293
  • 正文 為了忘掉前任顿乒,我火速辦了婚禮,結(jié)果婚禮上泽谨,老公的妹妹穿的比我還像新娘璧榄。我一直安慰自己,他們只是感情好吧雹,可當我...
    茶點故事閱讀 67,699評論 6 392
  • 文/花漫 我一把揭開白布骨杂。 她就那樣靜靜地躺著,像睡著了一般雄卷。 火紅的嫁衣襯著肌膚如雪搓蚪。 梳的紋絲不亂的頭發(fā)上,一...
    開封第一講書人閱讀 51,562評論 1 305
  • 那天丁鹉,我揣著相機與錄音妒潭,去河邊找鬼。 笑死揣钦,一個胖子當著我的面吹牛雳灾,可吹牛的內(nèi)容都是我干的。 我是一名探鬼主播拂盯,決...
    沈念sama閱讀 40,309評論 3 418
  • 文/蒼蘭香墨 我猛地睜開眼佑女,長吁一口氣:“原來是場噩夢啊……” “哼!你這毒婦竟也來了谈竿?” 一聲冷哼從身側(cè)響起团驱,我...
    開封第一講書人閱讀 39,223評論 0 276
  • 序言:老撾萬榮一對情侶失蹤,失蹤者是張志新(化名)和其女友劉穎空凸,沒想到半個月后嚎花,有當?shù)厝嗽跇淞掷锇l(fā)現(xiàn)了一具尸體,經(jīng)...
    沈念sama閱讀 45,668評論 1 314
  • 正文 獨居荒郊野嶺守林人離奇死亡呀洲,尸身上長有42處帶血的膿包…… 初始之章·張勛 以下內(nèi)容為張勛視角 年9月15日...
    茶點故事閱讀 37,859評論 3 336
  • 正文 我和宋清朗相戀三年紊选,在試婚紗的時候發(fā)現(xiàn)自己被綠了。 大學時的朋友給我發(fā)了我未婚夫和他白月光在一起吃飯的照片道逗。...
    茶點故事閱讀 39,981評論 1 348
  • 序言:一個原本活蹦亂跳的男人離奇死亡兵罢,死狀恐怖,靈堂內(nèi)的尸體忽然破棺而出滓窍,到底是詐尸還是另有隱情卖词,我是刑警寧澤,帶...
    沈念sama閱讀 35,705評論 5 347
  • 正文 年R本政府宣布吏夯,位于F島的核電站此蜈,受9級特大地震影響即横,放射性物質(zhì)發(fā)生泄漏。R本人自食惡果不足惜裆赵,卻給世界環(huán)境...
    茶點故事閱讀 41,310評論 3 330
  • 文/蒙蒙 一东囚、第九天 我趴在偏房一處隱蔽的房頂上張望。 院中可真熱鬧战授,春花似錦页藻、人聲如沸。這莊子的主人今日做“春日...
    開封第一講書人閱讀 31,904評論 0 22
  • 文/蒼蘭香墨 我抬頭看了看天上的太陽。三九已至钉跷,卻和暖如春弥鹦,著一層夾襖步出監(jiān)牢的瞬間,已是汗流浹背爷辙。 一陣腳步聲響...
    開封第一講書人閱讀 33,023評論 1 270
  • 我被黑心中介騙來泰國打工彬坏, 沒想到剛下飛機就差點兒被人妖公主榨干…… 1. 我叫王不留,地道東北人膝晾。 一個月前我還...
    沈念sama閱讀 48,146評論 3 370
  • 正文 我出身青樓栓始,卻偏偏與公主長得像,于是被迫代替她去往敵國和親血当。 傳聞我的和親對象是個殘疾皇子幻赚,可洞房花燭夜當晚...
    茶點故事閱讀 44,933評論 2 355

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