Redis系列(四)--內(nèi)存淘汰機制(含單機版內(nèi)存優(yōu)化建議)

每臺redis的服務(wù)器的內(nèi)存都是有限的,而且也不是所有的內(nèi)存都用來存儲信息。而且redis的實現(xiàn)并沒有在內(nèi)存這塊做太多的優(yōu)化,所以實現(xiàn)者為了防止內(nèi)存過于飽和,采取了一些措施來管控內(nèi)存。

文章結(jié)構(gòu):(1)內(nèi)存策略;(2)內(nèi)存釋放機制原理;(3)項目中如何合理應(yīng)用淘汰策略;(4)單機版Redis內(nèi)存優(yōu)化注意點。


本系列:

(1) Redis系列(一)--安裝、helloworld以及讀懂配置文件

(2)Redis系列(二)--緩存設(shè)計(整表緩存以及排行榜緩存方案實現(xiàn))

(3) Redis系列(三)--過期策略

一、內(nèi)存策略:先來吃份官方文檔

最大內(nèi)存的設(shè)置是通過設(shè)置maxmemory來完成的,格式為maxmemory bytes ,當(dāng)目前使用的內(nèi)存超過了設(shè)置的最大內(nèi)存,就要進行內(nèi)存釋放了, 當(dāng)需要進行內(nèi)存釋放的時候,需要用某種策略對保存的的對象進行刪除。Redis有六種策略(默認(rèn)的策略是volatile-lru。)

redis中當(dāng)內(nèi)存超過限制時,按照配置的策略,淘汰掉相應(yīng)的key-value,使得內(nèi)存可以繼續(xù)留有足夠的空間保存新的數(shù)據(jù)。redis 確定驅(qū)逐某個鍵值對后,會刪除這個數(shù)據(jù)并,并將這個數(shù)據(jù)變更消息發(fā)布到本地(AOF 持久化)和從機(主從連接)。

(1)volatile-lru:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選最近最少使用的數(shù)據(jù)淘汰。

(2)volatile-ttl:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中挑選將要過期的數(shù)據(jù)淘汰

(3)volatile-random:從已設(shè)置過期時間的數(shù)據(jù)集(server.db[i].expires)中任意選擇數(shù)據(jù)淘汰

(4)allkeys-lru:從數(shù)據(jù)集(server.db[i].dict)中挑選最近最少使用的數(shù)據(jù)淘汰

(5)allkeys-random:從數(shù)據(jù)集(server.db[i].dict)中任意選擇數(shù)據(jù)淘汰

(6)no-enviction:禁止淘汰數(shù)據(jù)

除此之外還有一個配置項,就是maxmemory-samples,默認(rèn)值是3,因為上面的策略代碼實現(xiàn)的都是近似算法,所以不管是lru算法,還是ttl,都并不是在數(shù)據(jù)庫中所有的數(shù)據(jù)為基礎(chǔ)的算法,因為當(dāng)數(shù)據(jù)庫的數(shù)據(jù)很多的時候,這樣效率太低,所以代碼中都是基于maxmemory-samples個數(shù)據(jù)的近似算法。詳情請讀下文。

置換策略是如何工作的:

1)客戶端執(zhí)行一條新命令,導(dǎo)致數(shù)據(jù)庫需要增加數(shù)據(jù)(比如set key value)

2)Redis會檢查內(nèi)存使用,如果內(nèi)存使用超過maxmemory,就會按照置換策略刪除一些key

3)新的命令執(zhí)行成功

注意:

如果我們持續(xù)的寫數(shù)據(jù)會導(dǎo)致內(nèi)存達到或超出上限maxmemory,但是置換策略會將內(nèi)存使用降低到上限以下。

如果一次需要使用很多的內(nèi)存(比如一次寫入一個很大的set),那么,Redis的內(nèi)存使用可能超出最大內(nèi)存限制一段時間。

二、內(nèi)存釋放機制原理:

(1)概述:

當(dāng)mem_used內(nèi)存已經(jīng)超過maxmemory的設(shè)定,對于所有的讀寫請求,都會觸發(fā)redis.c/freeMemoryIfNeeded(void)函數(shù)以清理超出的內(nèi)存。注意這個清理過程是阻塞的,直到清理出足夠的內(nèi)存空間。所以如果在達到maxmemory并且調(diào)用方還在不斷寫入的情況下,可能會反復(fù)觸發(fā)主動清理策略,導(dǎo)致請求會有一定的延遲。

清理時會根據(jù)用戶配置的maxmemory-policy來做適當(dāng)?shù)那謇恚ㄒ话闶荓RU或TTL),這里的LRU或TTL策略并不是針對redis的所有key,而是以配置文件中的maxmemory-samples個key作為樣本池進行抽樣清理。

maxmemory-samples在redis-3.0.0中的默認(rèn)配置為5,如果增加,會提高LRU或TTL的精準(zhǔn)度,redis作者測試的結(jié)果是當(dāng)這個配置為10時已經(jīng)非常接近全量LRU的精準(zhǔn)度了,并且增加maxmemory-samples會導(dǎo)致在主動清理時消耗更多的CPU時間,有如下建議:

1)盡量不要觸發(fā)maxmemory,最好在mem_used內(nèi)存占用達到maxmemory的一定比例后,需要考慮調(diào)大hz以加快淘汰,或者進行集群擴容。

2)如果能夠控制住內(nèi)存,則可以不用修改maxmemory-samples配置;如果Redis本身就作為LRU cache服務(wù)(這種服務(wù)一般長時間處于maxmemory狀態(tài),由Redis自動做LRU淘汰),可以適當(dāng)調(diào)大maxmemory-samples。

(2)內(nèi)存管理源碼解析:參考博文

Redis釋放內(nèi)存是由函數(shù)freeMemoryIfNeeded完成的,redis用processCommand函數(shù)處理每條命令,函數(shù)中在真正處理命令之前都會調(diào)用freeMemoryIfNeeded函數(shù),這個函數(shù)會判斷當(dāng)前使用的內(nèi)存是否超過了最大使用內(nèi)存,如果超過,就會根據(jù)內(nèi)存釋放策略釋放內(nèi)存。

freeMemoryIfNeeded函數(shù)首先會計算出當(dāng)前使用了多少內(nèi)存,注意,這里并不會包括slaves 輸出緩存以及AOF緩存,源碼如下:

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. 
     */ 
     //計算占用內(nèi)存大小時,并不計算slave output buffer和aof buffer,因此maxmemory應(yīng)該比實際內(nèi)存小,為這兩個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);
    }
//判斷已經(jīng)使用內(nèi)存是否超過最大使用內(nèi)存,如果沒有超過就返回REDIS_OK,
    /* Check if we are over the memory limit. */
    if (mem_used <= server.maxmemory) return REDIS_OK;
//當(dāng)超過了最大使用內(nèi)存時,就要判斷此時redis到底采用的是那種內(nèi)存釋放策略,根據(jù)不同的策略,采取不同的手段。
//(1)首先判斷是否是為no-enviction策略,如果是,則返回REDIS_ERR,然后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;
    //(2)接下來就判斷淘汰策略是基于所有的鍵還是只是基于設(shè)置了過期時間的鍵,如果是針對所有的鍵,就從server.db[j].dict中取數(shù)據(jù),如果是針對設(shè)置了過期時間的鍵,就從server.db[j].expires中取數(shù)據(jù)。
    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;
    //(3)然后判斷是不是random策略,包括volatile-random 和allkeys-random,這兩種策略是最簡單的,就是在上面的數(shù)據(jù)集中隨便去一個鍵,然后刪掉。
            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;
//接著又判斷allkeys-random還是volatile-ttl策略
            /* 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
//然后就是判斷是lru策略還是ttl策略,如果是lru策略就采用lru近似算法
            /* 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維護的數(shù)據(jù)結(jié)構(gòu)里并沒有記錄該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淘汰算法一樣,都是非最優(yōu)解,lru算法是在相應(yīng)的dict中,選擇maxmemory_samples(默認(rèn)設(shè)置是3)份key,挑選其中l(wèi)ru的,進行淘汰
            }
//如果是ttl策略。ttl策略很簡單,就是取maxmemory_samples個鍵,然后比較他們的過期時間,然后從這些鍵中找到最快過期的那個鍵,就是我們將要刪除的鍵。
            /* 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實現(xiàn)和上邊一樣,都是挑選出maxmemory_samples份進行挑選
            }
//根據(jù)不同的策略,我們找到了將要刪除的鍵,下面就是將他們刪除的時候了,刪除選定的鍵值對
            /* Finally remove the selected key. */
            if (bestkey) {
                long long delta;

                robj *keyobj = createStringObject(bestkey,sdslen(bestkey));
                // 發(fā)布數(shù)據(jù)更新消息,主要是AOF 持久化和從機
                propagateExpire(db,keyobj); //將del命令擴散給slaves

    // 注意, propagateExpire() 可能會導(dǎo)致內(nèi)存的分配,
    // propagateExpire() 提前執(zhí)行就是因為redis 只計算
    // dbDelete() 釋放的內(nèi)存大小。倘若同時計算dbDelete()
    // 釋放的內(nèi)存和propagateExpire() 分配空間的大小,與此
    // 同時假設(shè)分配空間大于釋放空間,就有可能永遠退不出這個循環(huán)。
    // 下面的代碼會同時計算dbDelete() 釋放的內(nèi)存和propagateExpire() 分配空間的大小
                /* 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() 釋放內(nèi)存的大小
                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. */
                 // 將從機回復(fù)空間中的數(shù)據(jù)及時發(fā)送給從機
                if (slaves) flushSlavesOutputBuffers();
            }
        }//在所有的db中遍歷一遍,然后判斷刪除的key釋放的空間是否足夠,未能釋放空間,且此時redis 使用的內(nèi)存大小依舊超額,失敗返回
        if (!keys_freed) return REDIS_ERR; /* nothing to free... */
    }
    return REDIS_OK;
}

此函數(shù)是在執(zhí)行特定命令之前進行調(diào)用的,并且在當(dāng)前占用內(nèi)存低于限制后即返回OK。因此可能在后續(xù)執(zhí)行命令后,redis占用的內(nèi)存就超過了maxmemory的限制。因此,maxmemory是redis執(zhí)行命令所需保證的最大內(nèi)存占用,而非redis實際的最大內(nèi)存占用。(在不考慮slave buffer和aof buffer的前提下)。

**TTL 數(shù)據(jù)淘汰機制 **:

redis 數(shù)據(jù)集數(shù)據(jù)結(jié)構(gòu)中保存了鍵值對過期時間的表,即 redisDb.expires。

定義:

從過期時間的表中隨機挑選幾個鍵值對,取出其中 ttl 最大的鍵值對淘汰。同樣你會發(fā)現(xiàn),redis 并不是保證取得所有過期時間的表中最快過期的鍵值對,而只是隨機挑選的幾個鍵值對中的。

freeMemoryIfNeeded函數(shù)關(guān)于TTL的源碼:

//挑選將要過期的數(shù)據(jù)
else if (server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_TTL) {
 // server.maxmemory_samples 為隨機挑選鍵值對次數(shù)
    // 隨機挑選server.maxmemory_samples 個鍵值對,驅(qū)逐最快要過期的數(shù)據(jù)
    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;
        }
    }
}

**LRU 數(shù)據(jù)淘汰機制 **:

在服務(wù)器配置中保存了 lru 計數(shù)器 server.lrulock,會定時(redis 定時程序 serverCorn())更新,server.lrulock 的值是根據(jù) server.unixtime 計算出來的。另外,從 struct redisObject 中可以發(fā)現(xiàn),每一個 redis 對象都會設(shè)置相應(yīng)的 lru??梢韵胂蟮氖?,每一次訪問數(shù)據(jù)的時候,會更新 redisObject.lru。

LRU 數(shù)據(jù)淘汰機制定義:

在數(shù)據(jù)集中隨機挑選幾個鍵值對,取出其中 lru 最小的鍵值對淘汰。所以,你會發(fā)現(xiàn),redis 
并不是保證取得所有數(shù)據(jù)集中最近最少使用(LRU)的鍵值對,而只是隨機挑選的幾個鍵值對中的鍵值對。

freeMemoryIfNeeded函數(shù)關(guān)于LRU的源碼:

//不同的策略,操作的數(shù)據(jù)集不同
if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
    server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_RANDOM)
{
    dict = server.db[j].dict;
} else {//操作的是設(shè)置了過期時間的key集
    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 = dictGetKey(de);
}

/* volatile-lru and allkeys-lru policy */
//具體的LRU算法
else if (server.maxmemory_policy == REDIS_MAXMEMORY_ALLKEYS_LRU ||
    server.maxmemory_policy == REDIS_MAXMEMORY_VOLATILE_LRU)
{
    struct evictionPoolEntry *pool = db->eviction_pool;

    while(bestkey == NULL) {
        //選擇隨機樣式,并從樣本中作用LRU算法選擇需要淘汰的數(shù)據(jù)
        evictionPoolPopulate(dict, db->dict, db->eviction_pool);
        /* Go backward from best to worst element to evict. */
        for (k = REDIS_EVICTION_POOL_SIZE-1; k >= 0; k--) {
            if (pool[k].key == NULL) continue;
            de = dictFind(dict,pool[k].key);
            sdsfree(pool[k].key);
            //將pool+k+1之后的元素向前平移一個單位
            memmove(pool+k,pool+k+1,
                sizeof(pool[0])*(REDIS_EVICTION_POOL_SIZE-k-1));
            /* Clear the element on the right which is empty
             * since we shifted one position to the left.  */
            pool[REDIS_EVICTION_POOL_SIZE-1].key = NULL;
            pool[REDIS_EVICTION_POOL_SIZE-1].idle = 0;
            //選擇了需要淘汰的數(shù)據(jù)
            if (de) {
                bestkey = dictGetKey(de);
                break;
            } else {
                /* Ghost... */
                continue;
            }
        }
    }
}

LRU算法實現(xiàn)在evictionPoolPopulate方法內(nèi).(有興趣的朋友再進行相關(guān)查閱了解吧)


三、項目中如何合理應(yīng)用淘汰策略:

此部分部分轉(zhuǎn)載于此博主此博客

(1)合理設(shè)置maxmemory:

修改方式有兩種:

1. 通過CONFIG SET 設(shè)定:

127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "0"
127.0.0.1:6379> CONFIG SET maxmemory 80MB
OK
127.0.0.1:6379> CONFIG GET maxmemory
1) "maxmemory"
2) "83886080"

2.修改配置文件redis.conf:配置文件講解

maxmemory 80mb

注意:在64bit系統(tǒng)下,maxmemory設(shè)置為0表示不限制Redis內(nèi)存使用,在32bit系統(tǒng)下,maxmemory隱式不能超過3GB。

當(dāng)Redis內(nèi)存使用達到指定的限制時,就需要選擇一個置換的策略。

(2)置換策略的選擇:

當(dāng)Redis內(nèi)存使用達到maxmemory時,就會使用設(shè)置好的maxmemory-policy進行對老數(shù)據(jù)的置換。

設(shè)置maxmemory-policy的方法和設(shè)置maxmemory方法類似,通過redis.conf或是通過CONFIG SET動態(tài)修改。

如果沒有匹配到可以刪除的key,那么volatile-lru、volatile-random和volatile-ttl策略和noeviction替換策略一樣——不對任何key進行置換。

選擇合適的置換策略是很重要的,這主要取決于你的應(yīng)用的訪問模式,當(dāng)然你也可以動態(tài)的修改置換策略,并通過用Redis命令——INFO去輸出cache的命中率情況,進而可以對置換策略進行調(diào)優(yōu)。

針對一些策略所使用的場景:

1)allkeys-lru:如果我們的應(yīng)用對緩存的訪問符合冪律分布(也就是存在相對熱點數(shù)據(jù)),或者我們不太清楚我們應(yīng)用的緩存訪問分布狀況,我們可以選擇allkeys-lru策略。

在所有的key都是最近最經(jīng)常使用,那么就需要選擇allkeys-lru進行置換最近最不經(jīng)常使用的key,如果你不確定使用哪種策略。

設(shè)置是失效時間expire會占用一些內(nèi)存,而采用allkeys-lru就沒有必要設(shè)置失效時間,進而更有效的利用內(nèi)存

2)allkeys-random:如果我們的應(yīng)用對于緩存key的訪問概率相等,則可以使用這個策略。

如果所有的key的訪問概率都是差不多的,那么可以選用allkeys-random策略去置換數(shù)據(jù)。

3)volatile-ttl:這種策略使得我們可以向Redis提示哪些key更適合被eviction。

如果對數(shù)據(jù)有足夠的了解,能夠為key指定hint(通過expire/ttl指定),那么可以選擇volatile-ttl進行置換

4)volatile-lru策略和volatile-random策略適合我們將一個Redis實例既應(yīng)用于緩存和又應(yīng)用于持久化存儲的時候,然而我們也可以通過使用兩個Redis實例來達到相同的效果,值得一提的是將key設(shè)置過期時間實際上會消耗更多的內(nèi)存,因此我們建議使用allkeys-lru策略從而更有效率的使用內(nèi)存。


四、Redis內(nèi)存優(yōu)化注意點:(先針對單機版)

(1)Redis的編碼:此部分參考此博主此博客

概述:

很多數(shù)據(jù)類型都可以通過特殊編碼的方式來進行存儲空間的優(yōu)化。其中,Hash、List和由Integer組成的Sets都可以通過該方式來優(yōu)化存儲結(jié)構(gòu),以便占用更少的空間,在有些情況下,可以省去9/10的空間。

這些特殊編碼對于Redis的使用而言是完全透明的,事實上,它只是CPU和內(nèi)存之間的一個交易而言。如果內(nèi)存使用率方面高一些,那么在操作數(shù)據(jù)時消耗的CPU自然要多一些,反之亦然。在Redis中提供了一組配置參數(shù)用于設(shè)置與特殊編碼相關(guān)的各種閾值。

閾值詳細設(shè)置:

    #如果Hash中字段的數(shù)量小于該參數(shù)值,Redis將對該Key的Hash Value采用特殊編碼。
    hash-max-zipmap-entries 64
    #如果Hash中各個字段的最大長度不超過512字節(jié),Redis也將對該Key的Hash Value采用特殊編碼方式。
    hash-max-zipmap-value 512
    #下面兩個參數(shù)的含義基本等同于上面兩個和Hash相關(guān)的參數(shù),只是作用的對象類型為List。
    list-max-ziplist-entries 512
    list-max-ziplist-value 64
    #如果set中整型元素的數(shù)量不超過512時,Redis將會采用該特殊編碼。
    set-max-intset-entries 512
    #如果zset中的元素數(shù)量小于128或者各字段長度不超過64,redis會對zset采用特殊編碼 
    zset-max-ziplist-entries 128
    zset-max-ziplist-value 64

倘若某個已經(jīng)被編碼的值再經(jīng)過修改之后超過了配置信息中的最大限制,那么Redis會自動將其轉(zhuǎn)換為正常編碼格式,這一操作是非常快速的,但是如果反過來操作,將一個正常編碼的較大值轉(zhuǎn)換為特殊編碼,Redis的建議是,在正式做之前最好先簡單測試一下轉(zhuǎn)換效率,因為這樣的轉(zhuǎn)換往往是非常低效的。

(2)使用bit位級別操作和byte字節(jié)級別操作來減少不必要的內(nèi)存使用:

bit位級別操作:GETRANGE, SETRANGE, GETBIT and SETBIT

byte字節(jié)級別操作:GETRANGE and SETRANGE

Redis提供了GETRANGE/SETRANGE/GETBIT/SETBIT四個用于字符串類型Key/Value的命令。通過這些命令,我們便可以像操作數(shù)組那樣來訪問String類型的值數(shù)據(jù)了。比如唯一標(biāo)識用戶身份的ID,可能僅僅是String值的其中一段子字符串。這樣就可以通過GETRANGE/SETRANGE命令來方便的提取。再有就是可以使用BITMAP來表示用戶的性別信息,如1表示male,0表示female。用這種方式來表示100,000,000個用戶的性別信息時,也僅僅占用12MB的存儲空間,與此同時,在通過SETBIT/GETBIT命令進行數(shù)據(jù)遍歷也是非常高效的。

(3)盡可能地使用hashes哈希,因為小Hashes會被編碼成一個非常小的空間。

由于小的Hash類型數(shù)據(jù)占用的空間相對較少,因此我們在實際應(yīng)用時應(yīng)該盡可能的考慮使用Hash類型,比如用戶的注冊信息,這其中包括姓名、性別、email、年齡和口令等字段。我們當(dāng)然可以將這些信息以Key的形式進行存儲,而用戶填寫的信息則以String Value的形式存儲。然而Redis則更為推薦以Hash的形式存儲,以上信息則以Field/Value的形式表示。

現(xiàn)在我們就通過學(xué)習(xí)Redis的存儲機制來進一步證明這一說法。在該篇博客的開始處已經(jīng)提到了特殊編碼機制,其中有兩個和Hash類型相關(guān)的配置參數(shù):hash-max-zipmap-entries和hash-max-zipmap-value。至于它們的作用范圍前面已經(jīng)給出,這里就不再過多的贅述了。現(xiàn)在我們先假設(shè)存儲在Hash Value中的字段數(shù)量小于hash-max-zipmap-entries,而每個元素的長度又同時小于hash-max-zipmap-value。這樣每當(dāng)有新的Hash類型的Key/Value存儲時,Redis都會為Hash Value創(chuàng)建定長的空間,最大可預(yù)分配的字節(jié)數(shù)為:

 total_bytes = hash-max-zipmap-entries * hash-max-zipmap-value
 

這樣一來,Hash中所有字段的位置已經(jīng)預(yù)留,并且可以像訪問數(shù)組那樣隨機的訪問Field/Value,他們之間的步長間隔為hash-max-zipmap-value。只有當(dāng)Hash Value中的字段數(shù)量或某一新元素的長度分別超過以上兩個參數(shù)值時,Redis才會考慮將他們以Hash Table的方式進行重新存儲,否則將始終保持這種高效的存儲和訪問方式。不僅如此,由于每個Key都要存儲一些關(guān)聯(lián)的系統(tǒng)信息,如過期時間、LRU等,因此和String類型的Key/Value相比,Hash類型極大的減少了Key的數(shù)量(大部分的Key都以Hash字段的形式表示并存儲了),從而進一步優(yōu)化了存儲空間的使用效率。

(4)過期策略的合理設(shè)計:

請參考此博客點此處

(5)合理的內(nèi)存分配:

如果maxmemory沒有設(shè)置的Redis會繼續(xù)分配內(nèi)存,因為它認(rèn)為合適的,因此它可以(逐漸)吃了你的全部可用內(nèi)存。因此,通常建議配置一些限制。您可能還需要設(shè)置maxmemory策略,默認(rèn)的是:noeviction(這不是在一些舊版本的Redis的默認(rèn)值)。

這使得Redis的返回內(nèi)存不足的錯誤寫命令,如果當(dāng)它到達了極限 - 這反過來可能會導(dǎo)致應(yīng)用程序錯誤,但不會導(dǎo)致因為內(nèi)存饑餓而整機死亡。

(6)在存到Redis之前先把你的數(shù)據(jù)壓縮下。

(7)一些真正的實際Redis內(nèi)存設(shè)計方案會在本系列的后面寫出。比如:共享對象,【string】-->【hash】-->【segment-hash】的優(yōu)化


好了,Redis系列(四)--內(nèi)存淘汰機制(含單機版內(nèi)存優(yōu)化建議)講完了,這是項目過程中Redis內(nèi)存優(yōu)化的筆記,現(xiàn)在羅列給大家,這是積累的必經(jīng)一步,我會繼續(xù)出這個系列文章,分享經(jīng)驗給大家。歡迎在下面指出錯誤,共同學(xué)習(xí)??!你的點贊是對我最好的支持!!

更多內(nèi)容,可以訪問JackFrost的博客

最后編輯于
?著作權(quán)歸作者所有,轉(zhuǎn)載或內(nèi)容合作請聯(lián)系作者
【社區(qū)內(nèi)容提示】社區(qū)部分內(nèi)容疑似由AI輔助生成,瀏覽時請結(jié)合常識與多方信息審慎甄別。
平臺聲明:文章內(nèi)容(如有圖片或視頻亦包括在內(nèi))由作者上傳并發(fā)布,文章內(nèi)容僅代表作者本人觀點,簡書系信息發(fā)布平臺,僅提供信息存儲服務(wù)。

相關(guān)閱讀更多精彩內(nèi)容

友情鏈接更多精彩內(nèi)容