鍍金池/ 教程/ 大數據/ Redis 數據淘汰機制
Redis 數據淘汰機制
積分排行榜
小剖 Memcache
Redis 數據結構 intset
分布式鎖
從哪里開始讀起,怎么讀
Redis 數據結構 dict
不在浮沙筑高臺
Redis 集群(上)
Redis 監(jiān)視器
源碼閱讀工具
Redis 日志和斷言
內存數據管理
Redis 數據結構綜述
源碼日志
Web 服務器存儲 session
消息中間件
Redis 與 Lua 腳本
什么樣的源代碼適合閱讀
Redis 數據結構 sds
Memcached slab 分配策略
訂閱發(fā)布機制
Redis 是如何提供服務的
Redis 事務機制
Redis 集群(下)
主從復制
Redis 應用
RDB 持久化策略
Redis 數據遷移
Redis 事件驅動詳解
初探 Redis
Redis 與 Memcache
AOF 持久化策略
Redis 數據結構 redisOb
作者簡介
Redis 數據結構 ziplist
Redis 數據結構 skiplist
Redis 哨兵機制

Redis 數據淘汰機制

概述

在 Redis 中,允許用戶設置最大使用內存大小 server.maxmemory,在內存限定的情況下是很有用的。譬如,在一臺 8G 機子上部署了 4 個 Redis 服務點,每一個服務點分配 1G 的內存大小,減少內存緊張的情況,由此獲取更為穩(wěn)健的服務。Redis 內存數據集大小上升到一定大小的時候,就會施行數據淘汰策略。Redis 提供 6 種數據淘汰策略:

  1. volatile-lru:從已設置過期時間的數據集(server.db[i].expires)中挑選最近最少使用 的數據淘汰
  2. volatile-ttl:從已設置過期時間的數據集(server.db[i].expires)中挑選將要過期的數 據淘汰
  3. volatile-random:從已設置過期時間的數據集(server.db[i].expires)中任意選擇數據 淘汰
  4. allkeys-lru:從數據集(server.db[i].dict)中挑選最近最少使用的數據淘汰
  5. allkeys-random:從數據集(server.db[i].dict)中任意選擇數據淘汰
  6. no-enviction(驅逐):禁止驅逐數據

Redis 確定驅逐某個鍵值對后,會刪除這個數據,并將這個數據變更消息發(fā)布到本地(AOF 持久化)和從機(主從連接)。

LRU 數據淘汰機制

在服務器配置中保存了 lru 計數器 server.lrulock,會定時(Redis 定時程序serverCorn())更新,server.lrulock 的值是根據 server.unixtime 計算出來的。

// redisServer 保存了lru 計數器
struct redisServer {
...
unsigned lruclock:22; /* Clock incrementing every minute, for LRU */
...
};

另外,從 struct redisObject 中可以發(fā)現,每一個 Redis 對象都會設置相應的 lru,即最近訪問的時間??梢韵胂蟮氖?,每一次訪問數據的時候,會更新 redisObject.lru。

LRU 數據淘汰機制是這樣的:在數據集中隨機挑選幾個鍵值對,取出其中 lru 最大的鍵值對淘汰。所以,你會發(fā)現,Redis 并不是保證取得所有數據集中最近最少使用(LRU)的鍵值對,而只是隨機挑選的幾個鍵值對中的。

// 每一個redis 對象都保存了lru
#define REDIS_LRU_CLOCK_MAX ((1<<21)-1) /* Max value of obj->lru */
#define REDIS_LRU_CLOCK_RESOLUTION 10 /* LRU clock resolution in seconds */
typedef struct redisObject {
    // 剛剛好32 bits
    // 對象的類型,字符串/列表/集合/哈希表
    unsigned type:4;
    // 未使用的兩個位
    unsigned notused:2; /* Not used */
    // 編碼的方式,redis 為了節(jié)省空間,提供多種方式來保存一個數據
    // 譬如:“123456789” 會被存儲為整數123456789
    unsigned encoding:4;
    unsigned lru:22; /* lru time (relative to server.lruclock) */
    // 引用數
    int refcount;
    // 數據指針
    void *ptr;
} robj;

// redis 定時執(zhí)行程序。聯(lián)想:linux cron
int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) {
    ......
    /* We have just 22 bits per object for LRU information.
    * So we use an (eventually wrapping) LRU clock with 10 seconds resolution.
    * 2^22 bits with 10 seconds resolution is more or less 1.5 years.
    **
    Note that even if this will wrap after 1.5 years it's not a problem,
    * everything will still work but just some object will appear younger
    * to Redis. But for this to happen a given object should never be touched
    * for 1.5 years.
    **
    Note that you can change the resolution altering the
    * REDIS_LRU_CLOCK_RESOLUTION define.
    */
    updateLRUClock();
    ......
}
    // 更新服務器的lru 計數器
void updateLRUClock(void) {
    server.lruclock = (server.unixtime/REDIS_LRU_CLOCK_RESOLUTION) &
    REDIS_LRU_CLOCK_MAX;
}

TTL 數據淘汰機制

Redis 數據集數據結構中保存了鍵值對過期時間的表,即 redisDb.expires,在使用 SET 命令的時候,就有一個鍵值對超時時間的選項。和 LRU 數據淘汰機制類似,TTL 數據淘汰機制是這樣的:從過期時間 redisDB.expires 表中隨機挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣你會發(fā)現,Redis 并不是保證取得所有過期時間的表中最快過期的鍵值對,而只是隨機挑選的幾個鍵值對中的。

無論是什么機制,都是從所有的鍵值對中挑選合適的淘汰。

在哪里開始淘汰數據

Redis 每服務客戶端執(zhí)行一個命令的時候,會檢測使用的內存是否超額。如果超額,即進行數據淘汰。

// 執(zhí)行命令
int processCommand(redisClient *c) {
        ......
        // 內存超額
        /* 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;
        }
    }
    ......
}

這是我們之前講述過的命令處理函數。在處理命令處理函數的過程,會涉及到內存使用量的檢測,如果檢測到內存使用超額,會觸發(fā)數據淘汰機制。我們來看看淘汰機制觸發(fā)的函數 freeMemoryIfNeeded() 里面發(fā)生了什么。

// 如果需要,是否一些內存
int freeMemoryIfNeeded(void) {
    size_t mem_used, mem_tofree, mem_freed;
    int slaves = listLength(server.slaves);
    // redis 從機回復空間和AOF 內存大小不計算入redis 內存大小
    // 關于已使用內存大小是如何統(tǒng)計的,我們會其他章節(jié)講解,這里先忽略這個細節(jié)
    /* Remove the size of slaves output buffers and AOF buffer from the
    * count of used memory. */
    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;
    }
}
    // server.aof_buf && server.aof_rewrite_buf_blocks
    if (server.aof_state != REDIS_AOF_OFF) {
        mem_used -= sdslen(server.aof_buf);
        mem_used -= aofRewriteBufferSize();
    }
    // 內存是否超過設置大小
    /* Check if we are over the memory limit. */
    if (mem_used <= server.maxmemory) return REDIS_OK;
    // redis 中可以設置內存超額策略
    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. */
        mem_tofree = mem_used - server.maxmemory;
        mem_freed = 0;
    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;
            struct dictEntry *de;
            redisDb *db = server.db+j;
            dict *dict;
        // 不同的策略,選擇的數據集不一樣
        if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
            server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
        {
            dict = server.db[j].dict;
        } else {
            dict = server.db[j].expires;
        }
        // 數據集為空,繼續(xù)下一個數據集
        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);
        }
    // LRU 策略:挑選最近最少使用的數據
    /* volatile-lru and allkeys-lru policy */
        else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
            server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
        {
        // server.maxmemory_samples 為隨機挑選鍵值對次數
        // 隨機挑選server.maxmemory_samples 個鍵值對,驅逐最近最少使用的數據
        for (k = 0; k < server.maxmemory_samples; k++) {
            sds thiskey;
            long thisval;
            robj *o;
        // 隨機挑選鍵值對
            de = dictGetRandomKey(dict);
        // 獲取鍵
            thiskey = dictGetKey(de);
        /* When policy is volatile-lru we need an additional lookup
        * to locate the real key, as dict is set to db->expires. */
        if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            de = dictFind(db->dict, thiskey);
            o = dictGetVal(de);
            // 計算數據的空閑時間
            thisval = estimateObjectIdleTime(o);
            // 當前鍵值空閑時間更長,則記錄
            /* Higher idle time is better candidate for deletion */
        if (bestkey == NULL || thisval > bestval) {
            bestkey = thiskey;
            bestval = thisval;
            }
        }
    }
    // TTL 策略:挑選將要過期的數據
    /* volatile-ttl */
    else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
    // server.maxmemory_samples 為隨機挑選鍵值對次數
    // 隨機挑選server.maxmemory_samples 個鍵值對,驅逐最快要過期的數據
    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));
    // 發(fā)布數據更新消息,主要是AOF 持久化和從機
    propagateExpire(db,keyobj);
    // 注意, propagateExpire() 可能會導致內存的分配,
    // propagateExpire() 提前執(zhí)行就是因為redis 只計算
    // dbDelete() 釋放的內存大小。倘若同時計算dbDelete()
    // 釋放的內存和propagateExpire() 分配空間的大小,與此
    // 同時假設分配空間大于釋放空間,就有可能永遠退不出這個循環(huán)。
    // 下面的代碼會同時計算dbDelete() 釋放的內存和propagateExpire() 分配空間的大小// propagateExpire(db,keyobj);
    // delta = (long long) zmalloc_used_memory();
    // dbDelete(db,keyobj);
    // delta -= (long long) zmalloc_used_memory();
    // mem_freed += delta;
    /////////////////////////////////////////
/* 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. */
// 只計算dbDelete() 釋放內存的大小
    delta = (long long) zmalloc_used_memory();
    dbDelete(db,keyobj);
    delta -= (long long) zmalloc_used_memory();
    mem_freed += delta;
    server.stat_evictedkeys++;
    // 將數據的刪除通知所有的訂閱客戶端
    notifyKeyspaceEvent(REDIS_NOTIFY_EVICTED, "evicted",
    keyobj, db->id);
    decrRefCount(keyobj);
    keys_freed++;
    // 將從機回復空間中的數據及時發(fā)送給從機
/* 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();
    }
}
    // 未能釋放空間,且此時redis 使用的內存大小依舊超額,失敗返回
    if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}