1. 程式人生 > >《redis學習》-- 快取淘汰策略

《redis學習》-- 快取淘汰策略

redis

快取淘汰策略

  • 認識

    • 最大快取

    在 redis 中,允許使用者設定最大使用記憶體大小 server.maxmemory,預設為0,沒有指定最大快取,如果有新的資料新增,超過最大記憶體,則會使redis崩潰,所以一定要設定。redis 記憶體資料集大小上升到一定大小的時候,就會實行資料淘汰策略。

    • 主鍵失效

    作為一種定期清理無效資料的重要機制,在 Redis 提供的諸多命令中,EXPIRE、EXPIREAT、PEXPIRE、PEXPIREAT 以及 SETEX 和 PSETEX 均可以用來設定一條 Key-Value 對的失效時間,而一條 Key-Value 對一旦被關聯了失效時間就會在到期後自動刪除(或者說變得無法訪問更為準確)

    • 淘汰機制

    隨著不斷的向redis中儲存資料,當記憶體剩餘空間無法滿足新增的資料時,redis 內就會施行資料淘汰策略,清除一部分內容然後保證新的資料可以儲存到記憶體中。

    記憶體淘汰機制是為了更好的使用記憶體,用一定得miss來換取記憶體的利用率,保證redis快取中儲存的都是熱點資料。

  • 設定過期時間

    可以給redis中的key設定過期時間,當key過期的時候(生存期為0),它會被刪除。

    • 對key進行覆蓋會修改資料的生存時間,如set和getSet命令

    • 修改key對應的value和使用另外相同的key和value來覆蓋以後,當前資料的生存時間不同。

      如對一個 key 執行INCR命令,對一個列表進行LPUSH命令,或者對一個雜湊表執行HSET命令,這類操作都不會修改 key 本身的生存時間。

    • 使用rename對一個 key 進行改名,那麼改名後的 key 的生存時間和改名前一樣。

    • 使用persist命令可以在不刪除 key 的情況下,移除 key 的生存時間,讓 key 重新成為一個persistent key
    • 使用expire命令可以更新key的生存時間
  • 淘汰策略

    • redis淘汰策略配置:maxmemory-policy voltile-lru,支援熱配置

    • redis 提供 6種資料淘汰策略:

      1. voltile-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(驅逐):禁止驅逐資料
  • 策略規則

    1. 如果資料呈現冪律分佈,也就是一部分資料訪問頻率高,一部分資料訪問頻率低,則使用allkeys-lru
    2. 如果資料呈現平等分佈,也就是所有的資料訪問頻率都相同,則使用allkeys-random
    3. volatile-lru策略和volatile-random策略適合我們將一個Redis例項既應用於快取和又應用於持久化儲存的時候,然而我們也可以通過使用兩個Redis例項來達到相同的效果,
    4. 將key設定過期時間實際上會消耗更多的記憶體,因此我們建議使用allkeys-lru策略從而更有效率的使用記憶體
  • 非精準的LRU

    上面提到的LRU(Least Recently Used)策略,實際上Redis實現的LRU並不是可靠的LRU,也就是名義上我們使用LRU演算法淘汰鍵,但是實際上被淘汰的鍵並不一定是真正的最久沒用的,這裡涉及到一個權衡的問題,如果需要在全部鍵空間內搜尋最優解,則必然會增加系統的開銷,Redis是單執行緒的,也就是同一個例項在每一個時刻只能服務於一個客戶端,所以耗時的操作一定要謹慎 。為了在一定成本內實現相對的LRU,早期的Redis版本是基於取樣的LRU,也就是放棄全部鍵空間內搜尋解改為取樣空間搜尋最優解。自從Redis3.0版本之後,Redis作者對於基於取樣的LRU進行了一些優化,目的是在一定的成本內讓結果更靠近真實的LRU。

失效的內部實現

Redis 刪除失效主鍵的方法主要有兩種:

  • 消極方法(passive way),在主鍵被訪問時如果發現它已經失效,那麼就刪除它

  • 積極方法(active way),週期性地從設定了失效時間的主鍵中選擇一部分失效的主鍵刪除

  • 主動刪除:當前已用記憶體超過maxmemory限定時,觸發主動清理策略,該策略由啟動引數的配置決定

    主鍵具體的失效時間全部都維護在expires這個字典表中。

    typedef struct redisDb {
      dict *dict; //key-value
      dict *expires;  //維護過期key
      dict *blocking_keys;
      dict *ready_keys;
      dict *watched_keys;
      int id;
    } redisDb;

passive way 消極方法

  • 在passive way 中, redis在實現GET、MGET、HGET、LRANGE等所有涉及到讀取資料的命令時都會呼叫 expireIfNeeded,它存在的意義就是在讀取資料之前先檢查一下它有沒有失效,如果失效了就刪除它。
  • expireIfNeeded函式中呼叫的另外一個函式propagateExpire,這個函式用來在正式刪除失效主鍵之前廣播這個主鍵已經失效的資訊,這個資訊會傳播到兩個目的地:

    1. 一個是傳送到AOF檔案,將刪除失效主鍵的這一操作以DEL Key的標準命令格式記錄下來;
    2. 另一個就是傳送到當前Redis伺服器的所有Slave,同樣將刪除失效主鍵的這一操作以DEL Key的標準命令格式告知這些Slave刪除各自的失效主鍵。從中我們可以知道,所有作為Slave來執行的Redis伺服器並不需要通過消極方法來刪除失效主鍵,它們只需要對Master唯命是從就OK了!

程式碼段二:

int expireIfNeeded(redisDb *db, robj *key) {
    獲取主鍵的失效時間
    long long when = getExpire(db,key);
    假如失效時間為負數,說明該主鍵未設定失效時間(失效時間預設為-1),直接返回0
    if (when < 0) return 0;
    假如Redis伺服器正在從RDB檔案中載入資料,暫時不進行失效主鍵的刪除,直接返回0
    if (server.loading) return 0;
    假如當前的Redis伺服器是作為Slave執行的,那麼不進行失效主鍵的刪除,因為Slave
    上失效主鍵的刪除是由Master來控制的,但是這裡會將主鍵的失效時間與當前時間進行
    一下對比,以告知呼叫者指定的主鍵是否已經失效了
    if (server.masterhost != NULL) {
        return mstime() > when;
    }
    如果以上條件都不滿足,就將主鍵的失效時間與當前時間進行對比,如果發現指定的主鍵
    還未失效就直接返回0
    if (mstime() <= when) return 0;
    如果發現主鍵確實已經失效了,那麼首先更新關於失效主鍵的統計個數,然後將該主鍵失
    效的資訊進行廣播,最後將該主鍵從資料庫中刪除
    server.stat_expiredkeys++;
    propagateExpire(db,key);
    return dbDelete(db,key);
}

void propagateExpire(redisDb *db, robj *key) {
    robj *argv[2];
    shared.del是在Redis伺服器啟動之初就已經初始化好的一個常用Redis物件,即DEL命令
    argv[0] = shared.del;
    argv[1] = key;
    incrRefCount(argv[0]);
    incrRefCount(argv[1]);
    檢查Redis伺服器是否開啟了AOF,如果開啟了就為失效主鍵記錄一條DEL日誌
    if (server.aof_state != REDIS_AOF_OFF)
        feedAppendOnlyFile(server.delCommand,db->id,argv,2);
    檢查Redis伺服器是否擁有Slave,如果是就向所有Slave傳送DEL失效主鍵的命令,這就是
    上面expireIfNeeded函式中發現自己是Slave時無需主動刪除失效主鍵的原因了,因為它
    只需聽從Master傳送過來的命令就OK了
    if (listLength(server.slaves))
        replicationFeedSlaves(server.slaves,db->id,argv,2);
    decrRefCount(argv[0]);
    decrRefCount(argv[1]);
}

Active Way

消極方法的缺點是,如果key 遲遲不被訪問,就會佔用很多記憶體空間. 所以就產生的積極的方式(Active Way):此方法利用了redis的時間事件,即每隔一段時間就中斷一下完成一些指定操作,其中就包括檢查並刪除失效主鍵。

  • 時間事件

建立時間事件, 回撥函式就是serverCron,它在Redis伺服器啟動時建立,每秒的執行次數由巨集定義REDIS_DEFAULT_HZ來指定,預設每秒鐘執行10次。

  • 使用activeExpireCycle 清除失效key

其實現原理是從Redis中每個資料庫的expires字典表中,隨機抽樣REDIS_EXPIRELOOKUPS_PER_CRON(預設值為10)個設定了失效時間的主鍵,檢查它們是否已經失效並刪除掉失效的主鍵,如果失效主鍵個數佔本次抽樣個數的比例超過25%,它會繼續進行下一輪的隨機抽樣和刪除,直到剛才的比例低於25%才停止對當前資料庫的處理,轉向下一個資料庫。

注意,activeExpireCycle函式不會試圖一次性處理Redis中的所有資料庫,而是最多隻處理REDIS_DBCRON_DBS_PER_CALL(預設值為16),此外activeExpireCycle函式還有處理時間上的限制,不是想執行多久就執行多久,凡此種種都只有一個目的,那就是避免失效主鍵刪除佔用過多的CPU資源。