redis源碼筆記 - 有關LRU cache相關的代碼


redis可以被作為類似memcached的應用級緩存使用,在內存超過限制時,按照配置的策略,淘汰掉相應的kv,使得內存可以繼續留有足夠的空間保存新的數據。

redis的conf文件中有對該機制的一份很好的解釋:

194 # Don't use more memory than the specified amount of bytes.
195 # When the memory limit is reached Redis will try to remove keys
196 # accordingly to the eviction policy selected (see maxmemmory-policy).
197 #
198 # If Redis can't remove keys according to the policy, or if the policy is
199 # set to 'noeviction', Redis will start to reply with errors to commands
200 # that would use more memory, like SET, LPUSH, and so on, and will continue
201 # to reply to read-only commands like GET.
202 #
203 # This option is usually useful when using Redis as an LRU cache, or to set
204 # an hard memory limit for an instance (using the 'noeviction' policy).
205 #
206 # WARNING: If you have slaves attached to an instance with maxmemory on,
207 # the size of the output buffers needed to feed the slaves are subtracted
208 # from the used memory count, so that network problems / resyncs will
209 # not trigger a loop where keys are evicted, and in turn the output
210 # buffer of slaves is full with DELs of keys evicted triggering the deletion
211 # of more keys, and so forth until the database is completely emptied.
212 #
213 # In short... if you have slaves attached it is suggested that you set a lower
214 # limit for maxmemory so that there is some free RAM on the system for slave
215 # output buffers (but this is not needed if the policy is 'noeviction').
216 #
217 # maxmemory <bytes>

注意,在redis按照master-slave使用時,其maxmeory應設置的比實際物理內存稍小一些,給slave output buffer留有足夠的空間。

redis支持如下五種緩存淘汰策略:

219 # MAXMEMORY POLICY: how Redis will select what to remove when maxmemory
220 # is reached? You can select among five behavior:
221 # 
222 # volatile-lru -> remove the key with an expire set using an LRU algorithm
223 # allkeys-lru -> remove any key accordingly to the LRU algorithm
224 # volatile-random -> remove a random key with an expire set
225 # allkeys->random -> remove a random key, any key
226 # volatile-ttl -> remove the key with the nearest expire time (minor TTL)
227 # noeviction -> don't expire at all, just return an error on write operations

注釋已經解釋的很清楚了,不再贅述。

其緩存管理功能,由redis.c文件中的freeMemoryIfNeeded函數實現。如果maxmemory被設置,則在每次進行命令執行之前,該函數均被調用,用以判斷是否有足夠內存可用,釋放內存或返回錯誤。如果沒有找到足夠多的內存,程序主邏輯將會阻止設置了REDIS_COM_DENYOOM flag的命令執行,對其返回command not allowed when used memory > 'maxmemory'的錯誤消息。

具體代碼如下:

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. */ 計算占用內存大小時,並不計算slave output buffer和aof buffer,因此maxmemory應該比實際內存小,為這兩個buffer留足空間。
    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.appendonly) {
        mem_used -= sdslen(server.aofbuf);
        mem_used -= sdslen(server.bgrewritebuf);
    }

    /* Check if we are over the memory limit. */
    if (mem_used <= server.maxmemory) return REDIS_OK;

    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;
            }
            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 = dictGetEntryKey(de);
            }//如果是random delete,則從dict中隨機選一個key

            /* volatile-lru and allkeys-lru policy */
            else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
                server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
            {
                for (k = 0; k < server.maxmemory_samples; k++) {
                    sds thiskey;
                    long thisval;
                    robj *o;

                    de = dictGetRandomKey(dict);
                    thiskey = dictGetEntryKey(de);
                    /* When policy is volatile-lru we need an additonal 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); //因為dict->expires維護的數據結構里並沒有記錄該key的最后訪問時間
                    o = dictGetEntryVal(de);
                    thisval = estimateObjectIdleTime(o);

                    /* Higher idle time is better candidate for deletion */
                    if (bestkey == NULL || thisval > bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }//為了減少運算量,redis的lru算法和expire淘汰算法一樣,都是非最優解,lru算法是在相應的dict中,選擇maxmemory_samples(默認設置是3)份key,挑選其中lru的,進行淘汰
            }

            /* volatile-ttl */
            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 = dictGetEntryKey(de);
                    thisval = (long) dictGetEntryVal(de);

                    /* Expire sooner (minor expire unix timestamp) is better
                     * candidate for deletion */
                    if (bestkey == NULL || thisval < bestval) {
                        bestkey = thiskey;
                        bestval = thisval;
                    }
                }//注意ttl實現和上邊一樣,都是挑選出maxmemory_samples份進行挑選
            }

            /* Finally remove the selected key. */
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                propagateExpire(db,keyobj); //將del命令擴散給slaves
                /* 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. */
                delta = (long long) zmalloc_used_memory();
                dbDelete(db,keyobj);
                delta -= (long long) zmalloc_used_memory();
                mem_freed += delta;
                server.stat_evictedkeys++;
                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();
            }
        }//在所有的db中遍歷一遍,然后判斷刪除的key釋放的空間是否足夠
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}

注意,此函數是在執行特定命令之前進行調用的,並且在當前占用內存低於限制后即返回OK。因此可能在后續執行命令后,redis占用的內存就超過了maxmemory的限制。因此,maxmemory是redis執行命令所需保證的最大內存占用,而非redis實際的最大內存占用。(在不考慮slave buffer和aof buffer的前提下)


免責聲明!

本站轉載的文章為個人學習借鑒使用,本站對版權不負任何法律責任。如果侵犯了您的隱私權益,請聯系本站郵箱yoyou2525@163.com刪除。



 
粵ICP備18138465號   © 2018-2026 CODEPRJ.COM