亚洲视频二区_亚洲欧洲日本天天堂在线观看_日韩一区二区在线观看_中文字幕不卡一区

公告:魔扣目錄網(wǎng)為廣大站長(zhǎng)提供免費(fèi)收錄網(wǎng)站服務(wù),提交前請(qǐng)做好本站友鏈:【 網(wǎng)站目錄:http://www.430618.com 】, 免友鏈快審服務(wù)(50元/站),

點(diǎn)擊這里在線咨詢(xún)客服
新站提交
  • 網(wǎng)站:51998
  • 待審:31
  • 小程序:12
  • 文章:1030137
  • 會(huì)員:747

“叮……”,美好的周六就這么被一陣釘釘消息吵醒了。

業(yè)務(wù)組的同學(xué)告訴我說(shuō)很多用戶(hù)的帳號(hào)今天被強(qiáng)制下線。我們的帳號(hào)系統(tǒng)正常的邏輯是用戶(hù)登錄一次后,token的有效期可以維持一天的時(shí)間。現(xiàn)在的問(wèn)題是用戶(hù)大概每10分鐘左右就需要重新登錄一次。這種情況一般有兩種原因:

  • 1、token生成時(shí)出問(wèn)題。
  • 2、驗(yàn)證token時(shí)出現(xiàn)問(wèn)題。

通過(guò)檢查日志,我發(fā)現(xiàn)是驗(yàn)證token時(shí),redis中已經(jīng)沒(méi)有對(duì)應(yīng)的token了。并且確定了生成新的token時(shí),set到Redis中的有效期是正確的,那么就基本可以確定是Redis的問(wèn)題了。

于是又去檢查了Redis的監(jiān)控,發(fā)現(xiàn)在那段時(shí)間Redis由于內(nèi)存占用過(guò)高強(qiáng)制清理了幾次key。但從日志上來(lái)看,這段時(shí)間并沒(méi)有出現(xiàn)流量暴漲的情況,而且Redis中key的數(shù)量也沒(méi)有顯著增加。那是什么原因?qū)е翿edis內(nèi)存占用過(guò)高呢?確定了Redis內(nèi)存升高不是我們?cè)斐傻闹螅覀冇致?lián)系了業(yè)務(wù)組的同學(xué)協(xié)助他們,他們表示最近確實(shí)有上線,并且新上線的功能有使用到Redis。但我仍然感覺(jué)很奇怪,為什么Redis中的key沒(méi)有增多,并且沒(méi)看到有其他業(yè)務(wù)的key。經(jīng)過(guò)一番詢(xún)問(wèn),才了解到,業(yè)務(wù)組同學(xué)使用的是這個(gè)Redis的db1,而我用的(和剛查的)是db0。這里確實(shí)是我在排查問(wèn)題時(shí)出現(xiàn)了疏忽。

那么Redis的不同db之間會(huì)互相影響嗎?通常情況下,我們使用不同的db進(jìn)行數(shù)據(jù)隔離,這沒(méi)問(wèn)題。但Redis進(jìn)行清理時(shí),并不是只清理數(shù)據(jù)量占用最大的那個(gè)db,而是會(huì)對(duì)所有的db進(jìn)行清理。在這之前我并不是很了解這方面知識(shí),這里也只是根據(jù)現(xiàn)象進(jìn)行的猜測(cè)。

好奇心驅(qū)使我來(lái)驗(yàn)證一下這個(gè)想法。于是我決定直接來(lái)看Redis的源碼。清理key相關(guān)的代碼在evict.c文件中。

Redis中會(huì)保存一個(gè)“過(guò)期key池”,這個(gè)池子中存放了一些可能會(huì)被清理的key。其中保存的數(shù)據(jù)結(jié)構(gòu)如下:

struct evictionPoolEntry {
    unsigned long long idle;    /* Object idle time (inverse frequency for LFU) */
    sds key;                    /* Key name. */
    sds cached;                 /* Cached SDS object for key name. */
    int dbid;                   /* Key DB number. */
};

其中idle是對(duì)象空閑時(shí)間,在Reids中,key的過(guò)期算法有兩種:一種是近似LRU,一種是LFU。默認(rèn)使用的是近似LRU。

近似LRU

在解釋近似LRU之前,先來(lái)簡(jiǎn)單了解一下LRU。當(dāng)Redis的內(nèi)存占用超過(guò)我們?cè)O(shè)置的maxmemory時(shí),會(huì)把長(zhǎng)時(shí)間沒(méi)有使用的key清理掉。按照LRU算法,我們需要對(duì)所有key(也可以設(shè)置成只淘汰有過(guò)期時(shí)間的key)按照空閑時(shí)間進(jìn)行排序,然后淘汰掉空閑時(shí)間最大的那部分?jǐn)?shù)據(jù),使得Redis的內(nèi)存占用降到一個(gè)合理的值。

LRU算法的缺點(diǎn)是,我們需要維護(hù)一個(gè)全部(或只有過(guò)期時(shí)間)key的列表,還要按照最近使用時(shí)間排序。這會(huì)消耗大量?jī)?nèi)存,并且每次使用key時(shí)更新排序也會(huì)占用額外的CPU資源。對(duì)于Redis這樣對(duì)性能要求很高的系統(tǒng)來(lái)說(shuō)是不被允許的。

因此,Redis采用了一種近似LRU的算法。當(dāng)Redis接收到新的寫(xiě)入命令,而內(nèi)存又不夠時(shí),就會(huì)觸發(fā)近似LRU算法來(lái)強(qiáng)制清理一些key。具體清理的步驟是,Redis會(huì)對(duì)key進(jìn)行采樣,通常是取5個(gè),然后會(huì)把過(guò)期的key放到我們上面說(shuō)的“過(guò)期池”中,過(guò)期池中的key是按照空閑時(shí)間來(lái)排序的,Redis會(huì)優(yōu)先清理掉空閑時(shí)間最長(zhǎng)的key,直到內(nèi)存小于maxmemory。

近似LRU算法的清理效果圖如圖(圖片來(lái)自Redis官方文檔)

Redis如何清除過(guò)期key? 一篇文章帶你走近源碼!

 

這么說(shuō)可能不夠清楚,我們直接上代碼。

源碼分析

Redis如何清除過(guò)期key? 一篇文章帶你走近源碼!

 

上圖展示了代碼中近似LRU算法的主要邏輯調(diào)用路徑。

其中主要邏輯是在freeMemoryIfNeeded函數(shù)中

首先調(diào)用getMaxmemoryState函數(shù)判斷當(dāng)前內(nèi)存的狀態(tài)

int getMaxmemoryState(size_t *total, size_t *logical, size_t *tofree, float *level) {
    size_t mem_reported, mem_used, mem_tofree;

    mem_reported = zmalloc_used_memory();
    if (total) *total = mem_reported;

    int return_ok_asap = !server.maxmemory || mem_reported <= server.maxmemory;
    if (return_ok_asap && !level) return C_OK;

    mem_used = mem_reported;
    size_t overhead = freeMemoryGetNotCountedMemory();
    mem_used = (mem_used > overhead) ? mem_used-overhead : 0;

    if (level) {
        if (!server.maxmemory) {
            *level = 0;
        } else {
            *level = (float)mem_used / (float)server.maxmemory;
        }
    }

    if (return_ok_asap) return C_OK;

    if (mem_used <= server.maxmemory) return C_OK;

    mem_tofree = mem_used - server.maxmemory;

    if (logical) *logical = mem_used;
    if (tofree) *tofree = mem_tofree;

    return C_ERR;
}

如果使用內(nèi)存低于maxmemory的話(huà),就返回C_OK,否則返回C_ERR。另外,這個(gè)函數(shù)還通過(guò)傳遞指針型的參數(shù)來(lái)返回一些額外的信息。

  • total:已使用的字節(jié)總數(shù),無(wú)論是C_OK還是C_ERR都有效。
  • logical:已使用的內(nèi)存減去slave或AOF緩沖區(qū)后的大小,只有返回C_ERR時(shí)有效。
  • tofree:需要釋放的內(nèi)存大小,只有返回C_ERR時(shí)有效。
  • level:已使用內(nèi)存的比例,通常是0到1之間,當(dāng)超出內(nèi)存限制時(shí),就大于1。無(wú)論是C_OK還是C_ERR都有效。

判斷完內(nèi)存狀態(tài)以后,如果內(nèi)存沒(méi)有超過(guò)使用限制就會(huì)直接返回,否則就繼續(xù)向下執(zhí)行。此時(shí)我們已經(jīng)知道需要釋放多少內(nèi)存空間了,下面就開(kāi)始進(jìn)行釋放內(nèi)存的操作了。每次釋放內(nèi)存都會(huì)記錄釋放內(nèi)存的大小,直到釋放的內(nèi)存不小于tofree。

首先根據(jù)maxmemory_policy進(jìn)行判斷,對(duì)于不同的清除策略有不同的實(shí)現(xiàn)方法,我們來(lái)看LRU的具體實(shí)現(xiàn)。

for (i = 0; i < server.dbnum; i++) {
  db = server.db+i;
  dict = (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) ?
    db->dict : db->expires;
  if ((keys = dictSize(dict)) != 0) {
    evictionPoolPopulate(i, dict, db->dict, pool);
    total_keys += keys;
  }
}

首先是填充“過(guò)期池”,這里遍歷了每一個(gè)db(驗(yàn)證了我最開(kāi)始的想法),調(diào)用evictionPoolPopulate函數(shù)進(jìn)行填充。

void evictionPoolPopulate(int dbid, dict *sampledict, dict *keydict, struct evictionPoolEntry *pool) {
    int j, k, count;
    dictEntry *samples[server.maxmemory_samples];

    count = dictGetSomeKeys(sampledict,samples,server.maxmemory_samples);
    for (j = 0; j < count; j++) {
        unsigned long long idle;
        sds key;
        robj *o;
        dictEntry *de;

        de = samples[j];
        key = dictGetKey(de);
				/* some code */
        if (server.maxmemory_policy & MAXMEMORY_FLAG_LRU) {
            idle = estimateObjectIdleTime(o);
        }

        /* some code */
        k = 0;
        while (k < EVPOOL_SIZE &&
               pool[k].key &&
               pool[k].idle < idle) k++;
        if (k == 0 && pool[EVPOOL_SIZE-1].key != NULL) {
            continue;
        } else if (k < EVPOOL_SIZE && pool[k].key == NULL) {
        } else {
            if (pool[EVPOOL_SIZE-1].key == NULL) {
                sds cached = pool[EVPOOL_SIZE-1].cached;
                memmove(pool+k+1,pool+k,
                    sizeof(pool[0])*(EVPOOL_SIZE-k-1));
                pool[k].cached = cached;
            } else {
                k--;
                sds cached = pool[0].cached; /* Save SDS before overwriting. */
                if (pool[0].key != pool[0].cached) sdsfree(pool[0].key);
                memmove(pool,pool+1,sizeof(pool[0])*k);
                pool[k].cached = cached;
            }
        }
        /* some code */
    }
}

由于篇幅原因,我截取了部分代碼,通過(guò)這段代碼我們可以看到,Redis首先是采樣了一部分key,這里采樣數(shù)量maxmemory_samples通常是5,我們也可以自己設(shè)置,采樣數(shù)量越大,結(jié)果就越接近LRU算法的結(jié)果,帶來(lái)的影響是性能隨之變差。

采樣之后我們需要獲得每個(gè)key的空閑時(shí)間,然后將其填充到“過(guò)期池”中的指定位置。這里“過(guò)期池”是按照空閑時(shí)間從小到大排序的,也就是說(shuō),idle大大key排在最右邊。

填充完“過(guò)期池”之后,會(huì)從后向前獲取到最適合清理的key。

/* Go backward from best to worst element to evict. */
for (k = EVPOOL_SIZE-1; k >= 0; k--) {
  if (pool[k].key == NULL) continue;
  bestdbid = pool[k].dbid;

  if (server.maxmemory_policy & MAXMEMORY_FLAG_ALLKEYS) {
    de = dictFind(server.db[pool[k].dbid].dict,
                  pool[k].key);
  } else {
    de = dictFind(server.db[pool[k].dbid].expires,
                  pool[k].key);
  }
  /* some code */
  if (de) {
    bestkey = dictGetKey(de);
    break;
  }
}

找到需要?jiǎng)h除的key后,就需要根據(jù)設(shè)置清理策略進(jìn)行同步/異步清理。

if (server.lazyfree_lazy_eviction)
  dbAsyncDelete(db,keyobj);
else
  dbSyncDelete(db,keyobj)

最后記下本次清理的空間大小,用來(lái)在循環(huán)條件判斷是否要繼續(xù)清理。

delta -= (long long) zmalloc_used_memory();
mem_freed += delta;

清理策略

最后我們來(lái)看一下Redis支持的幾種清理策略

  • noeviction:不會(huì)繼續(xù)處理寫(xiě)請(qǐng)求(DEL可以繼續(xù)處理)。
  • allkeys-lru:對(duì)所有key的近似LRU
  • volatile-lru:使用近似LRU算法淘汰設(shè)置了過(guò)期時(shí)間的key
  • allkeys-random:從所有key中隨機(jī)淘汰一些key
  • volatile-random:對(duì)所有設(shè)置了過(guò)期時(shí)間的key隨機(jī)淘汰
  • volatile-ttl:淘汰有效期最短的一部分key

Redis4.0開(kāi)始支持了LFU策略,和LRU類(lèi)似,它分為兩種:

  • volatile-lfu:使用LFU算法淘汰設(shè)置了過(guò)期時(shí)間的key
  • allkeys-lfu:從全部key中進(jìn)行淘汰,使用LFU

寫(xiě)在最后

現(xiàn)在我知道了Redis在內(nèi)存達(dá)到上限時(shí)做了哪些事了。以后出問(wèn)題時(shí)也就不會(huì)只檢查自己的db了。

關(guān)于這次事故的后續(xù)處理,我首先是讓業(yè)務(wù)同學(xué)回滾了代碼,然后讓他們使用一個(gè)單獨(dú)的Redis,這樣業(yè)務(wù)再出現(xiàn)類(lèi)似問(wèn)題就不會(huì)影響到我們的帳號(hào)服務(wù)了,整體的影響范圍也會(huì)變得更加可控。

原文鏈接:https://www.cnblogs.com/Jackeyzhe/p/12616624.html

分享到:
標(biāo)簽:Redis key
用戶(hù)無(wú)頭像

網(wǎng)友整理

注冊(cè)時(shí)間:

網(wǎng)站:5 個(gè)   小程序:0 個(gè)  文章:12 篇

  • 51998

    網(wǎng)站

  • 12

    小程序

  • 1030137

    文章

  • 747

    會(huì)員

趕快注冊(cè)賬號(hào),推廣您的網(wǎng)站吧!
最新入駐小程序

數(shù)獨(dú)大挑戰(zhàn)2018-06-03

數(shù)獨(dú)一種數(shù)學(xué)游戲,玩家需要根據(jù)9

答題星2018-06-03

您可以通過(guò)答題星輕松地創(chuàng)建試卷

全階人生考試2018-06-03

各種考試題,題庫(kù),初中,高中,大學(xué)四六

運(yùn)動(dòng)步數(shù)有氧達(dá)人2018-06-03

記錄運(yùn)動(dòng)步數(shù),積累氧氣值。還可偷

每日養(yǎng)生app2018-06-03

每日養(yǎng)生,天天健康

體育訓(xùn)練成績(jī)?cè)u(píng)定2018-06-03

通用課目體育訓(xùn)練成績(jī)?cè)u(píng)定