19-Redis 的過期策略和內存淘汰機制有什麼區別?

Redis 和 MySQL 是面試繞不過的兩座大山,他們一個是關係型數據庫的代表(MySQL),一個是鍵值數據庫以及緩存中間件的一哥。尤其 Redis 幾乎是所有互聯網公司都在用的技術,比如國內的 BATJ、新浪、360、小米等公司;國外的微軟、Twitter、Stack Overflow、GitHub、暴雪等公司。我從業了十幾年,就職過 4、5 家公司,有的公司用 MySQL、有的用 SQL Server、甚至還有的用 Oracle 和 DB2,但緩存無一例外使用的都是 Redis,從某種程度上來講 Redis 是普及率最高的技術,沒有之一。

我們本課時的面試題是,Redis 是如何處理過期數據的?當內存不夠用時 Redis 又是如何處理的?

典型回答

我們在新增 Redis 緩存時可以設置緩存的過期時間,該時間保證了數據在規定的時間內失效,可以藉助這個特性來實現很多功能。比如,存儲一定天數的用戶(登錄)會話信息,這樣在一定範圍內用戶不用重複登錄了,但爲了安全性,需要在一定時間之後重新驗證用戶的信息。因此,我們可以使用 Redis 設置過期時間來存儲用戶的會話信息。

對於已經過期的數據,Redis 將使用兩種策略來刪除這些過期鍵,它們分別是惰性刪除和定期刪除。

惰性刪除是指 Redis 服務器不主動刪除過期的鍵值,而是當訪問鍵值時,再檢查當前的鍵值是否過期,如果過期則執行刪除並返回 null 給客戶端;如果沒過期則正常返回值信息給客戶端。

它的優點是不會浪費太多的系統資源,只是在每次訪問時才檢查鍵值是否過期。缺點是刪除過期鍵不及時,造成了一定的空間浪費。

惰性刪除的源碼位於 src/db.c 文件的 expireIfNeeded 方法中,如下所示:

int expireIfNeeded(redisDb *db, robj *key) {
    // 判斷鍵是否過期
    if (!keyIsExpired(db,key)) return 0;
    if (server.masterhost != NULL) return 1;
    /* 刪除過期鍵 */
    // 增加過期鍵個數
    server.stat_expiredkeys++;
    // 傳播鍵過期的消息
    propagateExpire(db,key,server.lazyfree_lazy_expire);
    notifyKeyspaceEvent(NOTIFY_EXPIRED,
        "expired",key,db->id);
    // server.lazyfree_lazy_expire 爲 1 表示異步刪除,否則則爲同步刪除
    return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
                                         dbSyncDelete(db,key);
}
// 判斷鍵是否過期
int keyIsExpired(redisDb *db, robj *key) {
    mstime_t when = getExpire(db,key);
    if (when < 0) return 0; 
    if (server.loading) return 0;
    mstime_t now = server.lua_caller ? server.lua_time_start : mstime();
    return now > when;
}
// 獲取鍵的過期時間
long long getExpire(redisDb *db, robj *key) {
    dictEntry *de;
    if (dictSize(db->expires) == 0 ||
       (de = dictFind(db->expires,key->ptr)) == NULL) return -1;
    serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
    return dictGetSignedIntegerVal(de);
}

惰性刪除的執行流程如下圖所示:
在這裏插入圖片描述
除了惰性刪除之外,Redis 還提供了定期刪除功能以彌補惰性刪除的不足。

定期刪除是指 Redis 服務器每隔一段時間會檢查一下數據庫,看看是否有過期鍵可以被清除。

默認情況下 Redis 定期檢查的頻率是每秒掃描 10 次,用於定期清除過期鍵。當然此值還可以通過配置文件進行設置,在 redis.conf 中修改配置“hz”即可,默認的值爲“hz 10”。

小貼士:定期刪除的掃描並不是遍歷所有的鍵值對,這樣的話比較費時且太消耗系統資源。Redis服務器採用的是隨機抽取形式,每次從過期字典中,取出 20個鍵進行過期檢測,過期字典中存儲的是所有設置了過期時間的鍵值對。如果這批隨機檢查的數據中有 25% 的比例過期,那麼會再抽取 20個隨機鍵值進行檢測和刪除,並且會循環執行這個流程,直到抽取的這批數據中過期鍵值小於 25%,此次檢測纔算完成。

定期刪除的源碼在 expire.c 文件的 activeExpireCycle 方法中,如下所示:

void activeExpireCycle(int type) {
    static unsigned int current_db = 0; /* 上次定期刪除遍歷到的數據庫ID */
    static int timelimit_exit = 0;      
    static long long last_fast_cycle = 0; /* 上次執行定期刪除的時間點 */
    int j, iteration = 0;
    int dbs_per_call = CRON_DBS_PER_CALL; // 需要遍歷數據庫的數量
    long long start = ustime(), timelimit, elapsed;
    if (clientsArePaused()) return;
    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {
        if (!timelimit_exit) return;
        // ACTIVE_EXPIRE_CYCLE_FAST_DURATION 快速定期刪除的執行時長
        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;
        last_fast_cycle = start;
    }
    if (dbs_per_call > server.dbnum || timelimit_exit)
        dbs_per_call = server.dbnum;
    // 慢速定期刪除的執行時長
    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;
    timelimit_exit = 0;
    if (timelimit <= 0) timelimit = 1;
    if (type == ACTIVE_EXPIRE_CYCLE_FAST)
        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* 刪除操作花費的時間 */
    long total_sampled = 0;
    long total_expired = 0;
    for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) {
        int expired;
        redisDb *db = server.db+(current_db % server.dbnum);
        current_db++;
        do {
            // .......
            expired = 0;
            ttl_sum = 0;
            ttl_samples = 0;
            // 每個數據庫中檢查的鍵的數量
            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;
            // 從數據庫中隨機選取 num 個鍵進行檢查
            while (num--) {
                dictEntry *de;
                long long ttl;
                if ((de = dictGetRandomKey(db->expires)) == NULL) break;
                ttl = dictGetSignedInteger
                // 過期檢查,並對過期鍵進行刪除
                if (activeExpireCycleTryExpire(db,de,now)) expired++;
                if (ttl > 0) {
                    ttl_sum += ttl;
                    ttl_samples++;
                }
                total_sampled++;
            }
            total_expired += expired;
            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);
            }
            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */
                elapsed = ustime()-start;
                if (elapsed > timelimit) {
                    timelimit_exit = 1;
                    server.stat_expired_time_cap_reached_count++;
                    break;
                }
            }
            /* 判斷過期鍵刪除數量是否超過 25% */
        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
    }
    // .......
}

定期刪除的執行流程,如下圖所示:
在這裏插入圖片描述

小貼士:Redis 服務器爲了保證過期刪除策略不會導致線程卡死,會給過期掃描增加了最大執行時間爲 25ms。

以上是 Redis 服務器對待過期鍵的處理方案,當 Redis 的內存超過最大允許的內存之後,Redis 會觸發內存淘汰策略,這和過期策略是完全不同的兩個概念,經常有人把二者搞混,這兩者一個是在正常情況下清除過期鍵,一個是在非正常情況下爲了保證 Redis 順利運行的保護策略。

當 Redis 內存不夠用時,Redis 服務器會根據服務器設置的淘汰策略,刪除一些不常用的數據,以保證 Redis 服務器的順利運行。

考點分析

本課時的面試題並非 Redis 的入門級面試題,需要面試者對 Redis 有一定的瞭解才能對答如流,並且 Redis 的過期淘汰策略和內存淘汰策略的概念比較類似,都是用於淘汰數據的。因此很多人會把二者當成一回事,但其實並不是,這個面試者特別注意一下,和此知識點相關的面試題還有以下這些:

  • Redis 內存淘汰策略有哪些?
  • Redis 有哪些內存淘汰算法?

知識擴展

Redis 內存淘汰策略
我們可以使用 config get maxmemory-policy 命令,來查看當前 Redis 的內存淘汰策略,示例代碼如下:

127.0.0.1:6379> config get maxmemory-policy
1) "maxmemory-policy"
2) "noeviction"

從上面的結果可以看出,當前 Redis 服務器設置的是“noeviction”類型的內存淘汰策略,那麼這表示什麼含義呢?Redis 又有幾種內存淘汰策略呢?

在 4.0 版本之前 Redis 的內存淘汰策略有以下 6 種。

  • noeviction:不淘汰任何數據,當內存不足時,執行緩存新增操作會報錯,它是 Redis 默認內存淘汰策略。
  • allkeys-lru:淘汰整個鍵值中最久未使用的鍵值。
  • allkeys-random:隨機淘汰任意鍵值。
  • volatile-lru:淘汰所有設置了過期時間的鍵值中最久未使用的鍵值。
  • volatile-random:隨機淘汰設置了過期時間的任意鍵值。
  • volatile-ttl:優先淘汰更早過期的鍵值。
    可以看出我們上面示例使用的是 Redis 默認的內存淘汰策略“noeviction”。

而在 Redis 4.0 版本中又新增了 2 種淘汰策略:

  • volatile-lfu,淘汰所有設置了過期時間的鍵值中最少使用的鍵值;
  • allkeys-lfu,淘汰整個鍵值中最少使用的鍵值。

小貼士:從以上內存淘汰策略中可以看出,allkeys-xxx 表示從所有的鍵值中淘汰數據,而 volatile-xxx表示從設置了過期鍵的鍵值中淘汰數據。

這個內存淘汰策略我們可以通過配置文件來修改,redis.conf 對應的配置項是“maxmemory-policy noeviction”,只需要把它修改成我們需要設置的類型即可。

需要注意的是,如果使用修改 redis.conf 的方式,當設置完成之後需要重啓 Redis 服務器才能生效。

還有另一種簡單的修改內存淘汰策略的方式,我們可以使用命令行工具輸入“config set maxmemory-policy noeviction”來修改內存淘汰的策略,這種修改方式的好處是執行成功之後就會生效,無需重啓 Redis 服務器。但它的壞處是不能持久化內存淘汰策略,每次重啓 Redis 服務器之後設置的內存淘汰策略就會丟失。

Redis 內存淘汰算法

內存淘汰算法主要包含兩種:LRU 淘汰算法和 LFU 淘汰算法。

LRU( Least Recently Used,最近最少使用)淘汰算法:是一種常用的頁面置換算法,也就是說最久沒有使用的緩存將會被淘汰。

LRU 是基於鏈表結構實現的,鏈表中的元素按照操作順序從前往後排列,最新操作的鍵會被移動到表頭,當需要進行內存淘汰時,只需要刪除鏈表尾部的元素即可。

Redis 使用的是一種近似 LRU 算法,目的是爲了更好的節約內存,它的實現方式是給現有的數據結構添加一個額外的字段,用於記錄此鍵值的最後一次訪問時間。Redis 內存淘汰時,會使用隨機採樣的方式來淘汰數據,它是隨機取 5 個值 (此值可配置) ,然後淘汰最久沒有使用的數據。

LFU(Least Frequently Used,最不常用的)淘汰算法:最不常用的算法是根據總訪問次數來淘汰數據的,它的核心思想是“如果數據過去被訪問多次,那麼將來被訪問的頻率也更高”。

LFU 相對來說比 LRU 更“智能”,因爲它解決了使用頻率很低的緩存,只是最近被訪問了一次就不會被刪除的問題。如果是使用 LRU 類似這種情況數據是不會被刪除的,而使用 LFU 的話,這個數據就會被刪除。

Redis 內存淘汰策略使用了 LFU 和近 LRU 的淘汰算法,具體使用哪種淘汰算法,要看服務器是如何設置內存淘汰策略的,也就是要看“maxmemory-policy”的值是如何設置的。

小結

本課時我們講了 Redis 的過期刪除策略:惰性刪除 + 定期刪除;還講了 Redis 的內存淘汰策略,它和過期策略是完全不同的兩個概念,內存淘汰策略是當內存不夠用時纔會觸發的一種機制,它在 Redis 4.0 之後提供了 8 種內存淘汰策略,這些淘汰策略主要使用了近 LRU 淘汰算法和 LFU 淘汰算法。

發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章