Redis 緩存失效和回收機制續

2、Redis Key失效機制redis

Redis的Key失效機制,主要藉助藉助EXPIRE命令:數據庫

EXPIRE key 30緩存

上面的命令即爲key設置30秒的過時時間,超過這個時間,咱們應該就訪問不到這個值了。接下來咱們繼續深刻探究這個問題,Redis緩存失效機制是如何實現的呢?服務器

 

惰性淘汰機制less

惰性淘汰機制即當客戶端請求操做某個key的時候,Redis會對客戶端請求操做的key進行有效期檢查,若是key過時才進行相應的處理,惰性淘汰機制也叫消極失效機制。dom

咱們看看t_string組件下面對get請求處理的服務端端執行堆棧:函數

getCommand 
     -> getGenericCommand 
            -> lookupKeyReadOrReply
                   -> lookupKeyRead 
                         -> expireIfNeedeoop

 

關鍵的地方是expireIfNeed,Redis對key的get操做以前會判斷key關聯的值是否失效,咱們看看expireIfNeeded的流程,大體以下:測試

一、從expires中查找key的過時時間,若是不存在說明對應key沒有設置過時時間,直接返回。this

二、若是是slave機器,則直接返回,由於Redis爲了保證數據一致性且實現簡單,將緩存失效的主動權交給Master機器,slave機器沒有權限將key失效。

三、若是當前是Master機器,且key過時,則master會作兩件重要的事情:1)將刪除命令寫入AOF文件。2)通知Slave當前key失效,能夠刪除了。

四、master從本地的字典中將key對於的值刪除。

 

惰性刪除策略流程:

1. 在進行get或setnx等操做時,先檢查key是否過時;

2. 若過時,刪除key,而後執行相應操做; 若沒過時,直接執行相應操做;

 

在redis源碼中,實現懶惰淘汰策略的是函數expireIfNeeded,全部讀寫數據庫命令在執行以前都會調用expireIfNeeded函數對輸入鍵進行檢查。若是過時就刪除,若是沒過時就正常訪問。

咱們看下expireIfNeeded函數在文件db.c中的具體實現:

int expireIfNeeded(redisDb *db, robj *key) {

    mstime_t when = getExpire(db,key);

    mstime_t now;

 

    if (when < 0) return 0; /* No expire for this key */

 

    /* Don't expire anything while loading. It will be done later. */

    if (server.loading) return 0;

 

    /* If we are in the context of a Lua script, we claim that time is

     * blocked to when the Lua script started. This way a key can expire

     * only the first time it is accessed and not in the middle of the

     * script execution, making propagation to slaves / AOF consistent.

     * See issue #1525 on Github for more information. */

    now = server.lua_caller ? server.lua_time_start : mstime();

 

    /* If we are running in the context of a slave, return ASAP:

     * the slave key expiration is controlled by the master that will

     * send us synthesized DEL operations for expired keys.

     *

     * Still we try to return the right information to the caller,

     * that is, 0 if we think the key should be still valid, 1 if

     * we think the key is expired at this time. */

         /*若是咱們正在slaves上執行讀寫命令,就直接返回,

          *由於slaves上的過時是由master來發送刪除命令同步給slaves刪除的,

          *slaves不會自主刪除*/

    if (server.masterhost != NULL) return now > when;

 

    /* Return when this key has not expired */

         /*只是回了一個判斷鍵是否過時的值,0表示沒有過時,1表示過時

          *可是並無作其餘與鍵值過時相關的操做

          *若是沒有過時,就返回當前鍵

          */

    if (now <= when) return 0;

 

    /* Delete the key */

         /*增長過時鍵個數*/

    server.stat_expiredkeys++;

         /*向AOF文件和節點傳播過時信息.當key過時時,DEL操做也會傳遞給全部的AOF文件和節點*/

    propagateExpire(db,key);

        /*發送事件通知,關於redis的鍵事件通知和鍵空間通知*/

    notifyKeyspaceEvent(NOTIFY_EXPIRED,

        "expired",key,db->id);

         /*將過時鍵從數據庫中刪除*/

    return dbDelete(db,key);

}

 

 

函數描述propagateExpire:

/* Propagate expires into slaves and the AOF file.

 * When a key expires in the master, a DEL operation for this key is sent

 * to all the slaves and the AOF file if enabled.

 *

 * This way the key expiry is centralized in one place, and since both

 * AOF and the master->slave link guarantee operation ordering, everything

 * will be consistent even if we allow write operations against expiring

 * keys. */

 

 

主動刪除機制

主動失效機制也叫積極失效機制,即服務端定時的去檢查失效的緩存,若是失效則進行相應的操做。

咱們都知道Redis是單線程的,基於事件驅動的,Redis中有個EventLoop,EventLoop負責對兩類事件進行處理:

一、一類是IO事件,這類事件是從底層的多路複用器分離出來的。

二、一類是定時事件,這類事件主要用來事件對某個任務的定時執行。

 

爲何講到Redis的單線程模型,由於Redis的主動失效機制邏輯是被當作一個定時任務來由主線程執行的,相關代碼以下:

if(aeCreateTimeEvent(server.el, 1, serverCron, NULL, NULL) == AE_ERR) {
        redisPanic("Can't create the serverCron time event.");
        exit(1);
    }

 

serverCron就是這個定時任務的函數指針,adCreateTimeEvent將serverCron任務註冊到EventLoop上面,並設置初始的執行時間是1毫秒以後。接下來,咱們想知道的東西都在serverCron裏面了。serverCron作的事情有點多,咱們只關心和本篇內容相關的部分,也就是緩存失效是怎麼實現的,我認爲看代碼作什麼事情,調用堆棧仍是比較直觀的:

aeProcessEvents
    ->processTimeEvents
        ->serverCron 
             -> databasesCron 
                   -> activeExpireCycle 
                           -> activeExpireCycleTryExpire

 

EventLoop經過對定時任務的處理,觸發對serverCron邏輯的執行,最終之執行key過時處理的邏輯,值得一提的是,activeExpireCycle邏輯只能由master來作。

 

咱們看下函數activeExpireCycle在server.c中的實現:

/* Try to expire a few timed out keys. The algorithm used is adaptive and

 * will use few CPU cycles if there are few expiring keys, otherwise

 * it will get more aggressive to avoid that too much memory is used by

 * keys that can be removed from the keyspace.

 *

 * 函數嘗試刪除數據庫中已通過期的鍵。
 * 當帶有過時時間的鍵比較少時,函數運行得比較保守,
 * 若是帶有過時時間的鍵比較多,那麼函數會以更積極的方式來刪除過時鍵,
 * 從而可能地釋放被過時鍵佔用的內存。

  *

 * No more than CRON_DBS_PER_CALL databases are tested at every

 * iteration.

 *

 * 每次循環中被測試的數據庫數目不會超過 REDIS_DBCRON_DBS_PER_CALL

  *

 * This kind of call is used when Redis detects that timelimit_exit is

 * true, so there is more work to do, and we do it more incrementally from

 * the beforeSleep() function of the event loop.

 *

 * 若是 timelimit_exit 爲真,那麼說明還有更多刪除工做要作,(在我看來timelimit_exit若是爲真的話那表示上一次刪除過時鍵時是由於刪除時間過長超時了才退出的,

   因此此次將刪除方法更加積極),那麼在 beforeSleep() 函數調用時,程序會再次執行這個函數。
 *

 * Expire cycle type:

 *

  * 過時循環的類型:

  *

 * If type is ACTIVE_EXPIRE_CYCLE_FAST the function will try to run a

 * "fast" expire cycle that takes no longer than EXPIRE_FAST_CYCLE_DURATION

 * microseconds, and is not repeated again before the same amount of time.

 *

   若是循環的類型爲ACTIVE_EXPIRE_CYCLE_FAST 
 那麼函數會以「快速過時」模式執行,
 執行的時間不會長過 EXPIRE_FAST_CYCLE_DURATION 毫秒,
 而且在 EXPIRE_FAST_CYCLE_DURATION 毫秒以內不會再從新執行。

  *

 * If type is ACTIVE_EXPIRE_CYCLE_SLOW, that normal expire cycle is

 * executed, where the time limit is a percentage of the REDIS_HZ period

 * as specified by the REDIS_EXPIRELOOKUPS_TIME_PERC define.

  

  * 若是循環的類型爲ACTIVE_EXPIRE_CYCLE_SLOW 
 那麼函數會以「正常過時」模式執行,
 函數的執行時限爲 REDIS_HS 常量的一個百分比,
 這個百分比由 REDIS_EXPIRELOOKUPS_TIME_PERC 定義。

*/

 

void activeExpireCycle(int type) {

    /* This function has some global state in order to continue the work

     * incrementally across calls. */

    // 共享變量,用來累積函數連續執行時的數據

    static unsigned int current_db = 0; /* Last DB tested. 正在測試的數據庫*/

    static int timelimit_exit = 0;      /* Time limit hit in previous call 上一次執行是否時間超時的提示 */

    static long long last_fast_cycle = 0; /* When last fast cycle ran. 上次快速模式執行的時間*/

 

    int j, iteration = 0;

    // 默認每次處理的數據庫數量

    int dbs_per_call = CRON_DBS_PER_CALL;

    // 函數開始的時間

    long long start = ustime(), timelimit;

 

    // 快速模式

    if (type == ACTIVE_EXPIRE_CYCLE_FAST) {

        /* Don't start a fast cycle if the previous cycle did not exited

         * for time limt. Also don't repeat a fast cycle for the same period

         * as the fast cycle total duration itself. */

        // 若是上次函數沒有觸發 timelimit_exit ,那麼不執行處理

        if (!timelimit_exit) return;

        // 若是距離上次執行未夠必定時間,那麼不執行處理

        if (start < last_fast_cycle + ACTIVE_EXPIRE_CYCLE_FAST_DURATION*2) return;

        // 運行到這裏,說明執行快速處理,記錄當前時間

        last_fast_cycle = start;

    }

 

    /* We usually should test CRON_DBS_PER_CALL per iteration, with

     * two exceptions:

     *

     * 通常狀況下,每次迭代(也就是每次調用這個函數)函數只處理 CRON_DBS_PER_CALL 個數據庫,

     * 除非:

     *

     * 1) Don't test more DBs than we have.

     *    當前數據庫的數量小於 REDIS_DBCRON_DBS_PER_CALL

     * 2) If last time we hit the time limit, we want to scan all DBs

     * in this iteration, as there is work to do in some DB and we don't want

     * expired keys to use memory for too much time.

     *     若是上次處理遇到了時間上限,那麼此次須要對全部數據庫進行掃描,

     *     這能夠避免過多的過時鍵佔用空間

     */

    if (dbs_per_call > server.dbnum || timelimit_exit)//以服務器的數據庫數量爲準

        dbs_per_call = server.dbnum;

 

    /* We can use at max ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC percentage of CPU time

     * per iteration. Since this function gets called with a frequency of

     * server.hz times per second, the following is the max amount of

     * microseconds we can spend in this function. */

    // 函數處理的微秒時間上限

    // ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC 默認爲 25 ,也便是 25 % 的 CPU 時間

    timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100;

    timelimit_exit = 0;

    if (timelimit <= 0) timelimit = 1;

 

    // 若是是運行在快速模式之下

    // 那麼最多隻能運行 FAST_DURATION 微秒

    // 默認值爲 1000 (微秒)

    if (type == ACTIVE_EXPIRE_CYCLE_FAST)

        timelimit = ACTIVE_EXPIRE_CYCLE_FAST_DURATION; /* in microseconds. */

 

    // 遍歷數據庫

    for (j = 0; j < dbs_per_call; j++) {

        int expired;

        // 指向要處理的數據庫

        redisDb *db = server.db+(current_db % server.dbnum);

 

        /* Increment the DB now so we are sure if we run out of time

         * in the current DB we'll restart from the next. This allows to

         * distribute the time evenly across DBs. */

        // 爲 currrnt_DB 計數器加一,若是進入 do 循環以後由於超時而跳出

        // 那麼下次會直接從下個 currrnt_DB 開始處理。這樣使得分配在每一個數據庫上處理時間比較平均

        current_db++;

 

        /* Continue to expire if at the end of the cycle more than 25%

         * of the keys were expired. */

        //若是每次循環清理的過時鍵是過時鍵的25%以上,那麼就繼續清理

        do {

            unsigned long num, slots;

            long long now, ttl_sum;

            int ttl_samples;

 

            /* If there is nothing to expire try next DB ASAP. */

            // 獲取數據庫中帶過時時間的鍵的數量

            // 若是該數量爲 0 ,直接跳過這個數據庫

            if ((num = dictSize(db->expires)) == 0) {

                db->avg_ttl = 0;

                break;

            }

            // 獲取數據庫中鍵值對的數量

            slots = dictSlots(db->expires);

            // 當前時間

            now = mstime();

 

            /* When there are less than 1% filled slots getting random

             * keys is expensive, so stop here waiting for better times...

             * The dictionary will be resized asap. */

            // 這個數據庫的使用率低於 1% ,掃描起來太費力了(大部分都會 MISS)

            // 跳過,等待字典收縮程序運行

            if (num && slots > DICT_HT_INITIAL_SIZE &&

                (num*100/slots < 1)) break;

 

            /* The main collection cycle. Sample random keys among keys

             * with an expire set, checking for expired ones.

             *

             * 樣本計數器

             */

            // 已處理過時鍵計數器

            expired = 0;

            // 鍵的總 TTL 計數器

            ttl_sum = 0;

            // 總共處理的鍵計數器

            ttl_samples = 0;

 

            // 每次最多隻能檢查 LOOKUPS_PER_LOOP 個鍵,默認是20

            if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)

                num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;

 

            // 開始遍歷數據庫

            while (num--) {

                dictEntry *de;

                long long ttl;

 

                // 從 expires 中隨機取出一個帶過時時間的鍵

                if ((de = dictGetRandomKey(db->expires)) == NULL) break;

                // 計算 TTL

                ttl = dictGetSignedIntegerVal(de)-now;

                // 若是鍵已通過期,那麼刪除它,並將 expired 計數器增一

                if (activeExpireCycleTryExpire(db,de,now)) expired++;

                if (ttl > 0) {

            /* We want the average TTL of keys yet not expired. */

                // 累積鍵的 TTL

                ttl_sum += ttl;

                // 累積處理鍵的個數

                ttl_samples++;

               }

            }

 

            /* Update the average TTL stats for this database. */

            // 爲這個數據庫更新平均 TTL 統計數據

            if (ttl_samples) {

                // 計算當前平均值

                long long avg_ttl = ttl_sum/ttl_samples;

                /* Do a simple running average with a few samples.

 

                 * We just use the current estimate with a weight of 2%

                 * and the previous estimate with a weight of 98%. */

                if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;

               

                db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);

            }

 

            /* We can't block forever here even if there are many keys to

             * expire. So after a given amount of milliseconds return to the

             * caller waiting for the other active expire cycle. */

            // 若是過時鍵太多的話,咱們不能用太長時間處理,因此這個函數執行必定時間以後就要返回,等待下一次循環

            // 更新遍歷次數

            iteration++;

 

 

 

            if ((iteration & 0xf) == 0) { /* check once every 16 iterations. */

                long long elapsed = ustime()-start;

 

                latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);

                if (elapsed > timelimit) timelimit_exit = 1;

            }

 

            // 已經超時了,返回

            if (timelimit_exit) return;

 

            /* We don't repeat the cycle if there are less than 25% of keys

             * found expired in the current DB. */

            // 若是刪除的過時鍵少於當前數據庫中過時鍵數量的 25 %,那麼再也不遍歷。固然若是超過了25%,那說明過時鍵還不少,繼續清理

        } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);

    }

}

 

 

函數activeExpireCycleTryExpire描述:

/* Helper function for the activeExpireCycle() function.

 * This function will try to expire the key that is stored in the hash table

 * entry 'de' of the 'expires' hash table of a Redis database.

 *

 * If the key is found to be expired, it is removed from the database and

 * 1 is returned. Otherwise no operation is performed and 0 is returned.

 *

 * When a key is expired, server.stat_expiredkeys is incremented.

 *

 * The parameter 'now' is the current time in milliseconds as is passed

 * to the function to avoid too many gettimeofday() syscalls. */

 

 

3、Redis內存管理

咱們在使用Redis,須要注意如下幾點:

  • 當某些緩存被刪除後Redis並非老是當即將內存歸還給操做系統。這並非redis所特有的,而是函數malloc()的特性。例如你緩存了6G的數據,而後刪除了2G數據,從操做系統看,redis可能仍然佔用了6G的內存,即便redis已經明確聲明只使用了3G的空間。這是由於redis使用的底層內存分配器不會這麼簡單的就把內存歸還給操做系統,多是由於已經刪除的key和沒有刪除的key在同一個頁面(page),這樣就不能把完整的一頁歸還給操做系統。

  • 內存分配器是智能的,能夠複用用戶已經釋放的內存。因此當使用的內存從6G下降到3G時,你能夠從新添加更多的key,而不須要再向操做系統申請內存。分配器將複用以前已經釋放的3G內存.

  • 當redis的peak內存很是高於平時的內存使用時,碎片所佔可用內存的比例就會波動很大。當前使用的內存除以實際使用的物理內存(RSS)就是fragmentation;由於RSS就是peak memory,因此當大部分key被釋放的時候,此時內存的mem_used / RSS就比較高。

     

若是 maxmemory 沒有設置,redis就會一直向OS申請內存,直到OS的全部內存都被使用完。因此一般建議設置上redis的內存限制。或許你也想設置 maxmemory-policy 的值爲 no-enviction。

設置了maxmemory後,當redis的內存達到內存限制後,再向redis發送寫指令,會返回一個內存耗盡的錯誤。錯誤一般會觸發一個應用程序錯誤,可是不會致使整臺機器宕掉。

若是redis沒有設置expire,它是否默認永不過時?若是實際內存超過你設置的最大內存,就會使用LRU刪除機制。

 

--EOF--

相關文章
相關標籤/搜索