偷偷摘套内射激情视频,久久精品99国产国产精,中文字幕无线乱码人妻,中文在线中文a,性爽19p

Redis內(nèi)存回收機(jī)制,把我整懵了...

存儲(chǔ) 存儲(chǔ)軟件 Redis
之前看到過一道面試題:Redis 的過期策略都有哪些?內(nèi)存淘汰機(jī)制都有哪些?手寫一下 LRU 代碼實(shí)現(xiàn)?

之前看到過一道面試題:Redis 的過期策略都有哪些?內(nèi)存淘汰機(jī)制都有哪些?手寫一下 LRU 代碼實(shí)現(xiàn)?

[[278052]] 

圖片來自 Pexels

筆者結(jié)合在工作上遇到的問題學(xué)習(xí)分析,希望看完這篇文章能對(duì)大家有所幫助。

從一次不可描述的故障說起

問題描述:一個(gè)依賴于定時(shí)器任務(wù)的生成的接口列表數(shù)據(jù),時(shí)而有,時(shí)而沒有。

懷疑是 Redis 過期刪除策略

排查過程長(zhǎng),因?yàn)槭謩?dòng)執(zhí)行定時(shí)器,Set 數(shù)據(jù)沒有報(bào)錯(cuò),但是 Set 數(shù)據(jù)之后不生效。

Set 沒報(bào)錯(cuò),但是 Set 完再查的情況下沒數(shù)據(jù),開始懷疑 Redis 的過期刪除策略(準(zhǔn)確來說應(yīng)該是 Redis 的內(nèi)存回收機(jī)制中的數(shù)據(jù)淘汰策略觸發(fā)內(nèi)存上限淘汰數(shù)據(jù)),導(dǎo)致新加入 Redis 的數(shù)據(jù)都被丟棄了。

最終發(fā)現(xiàn)故障的原因是因?yàn)榕渲缅e(cuò)了,導(dǎo)致數(shù)據(jù)寫錯(cuò)地方,并不是 Redis 的內(nèi)存回收機(jī)制引起。

通過這次故障后思考總結(jié),如果下一次遇到類似的問題,在懷疑 Redis 的內(nèi)存回收之后,如何有效地證明它的正確性?如何快速證明猜測(cè)的正確與否?以及什么情況下懷疑內(nèi)存回收才是合理的呢?

下一次如果再次遇到類似問題,就能夠更快更準(zhǔn)地定位問題的原因。另外,Redis 的內(nèi)存回收機(jī)制原理也需要掌握,明白是什么,為什么。

花了點(diǎn)時(shí)間查閱資料研究 Redis 的內(nèi)存回收機(jī)制,并閱讀了內(nèi)存回收的實(shí)現(xiàn)代碼,通過代碼結(jié)合理論,給大家分享一下 Redis 的內(nèi)存回收機(jī)制。

為什么需要內(nèi)存回收?

原因有如下兩點(diǎn):

  • 在 Redis 中,Set 指令可以指定 Key 的過期時(shí)間,當(dāng)過期時(shí)間到達(dá)以后,Key 就失效了。
  • Redis 是基于內(nèi)存操作的,所有的數(shù)據(jù)都是保存在內(nèi)存中,一臺(tái)機(jī)器的內(nèi)存是有限且很寶貴的。

基于以上兩點(diǎn),為了保證 Redis 能繼續(xù)提供可靠的服務(wù),Redis 需要一種機(jī)制清理掉不常用的、無效的、多余的數(shù)據(jù),失效后的數(shù)據(jù)需要及時(shí)清理,這就需要內(nèi)存回收了。

Redis 的內(nèi)存回收機(jī)制

Redis 的內(nèi)存回收主要分為過期刪除策略和內(nèi)存淘汰策略兩部分。

過期刪除策略

刪除達(dá)到過期時(shí)間的 Key。

①定時(shí)刪除

對(duì)于每一個(gè)設(shè)置了過期時(shí)間的 Key 都會(huì)創(chuàng)建一個(gè)定時(shí)器,一旦到達(dá)過期時(shí)間就立即刪除。

該策略可以立即清除過期的數(shù)據(jù),對(duì)內(nèi)存較友好,但是缺點(diǎn)是占用了大量的 CPU 資源去處理過期的數(shù)據(jù),會(huì)影響 Redis 的吞吐量和響應(yīng)時(shí)間。

②惰性刪除

當(dāng)訪問一個(gè) Key 時(shí),才判斷該 Key 是否過期,過期則刪除。該策略能最大限度地節(jié)省 CPU 資源,但是對(duì)內(nèi)存卻十分不友好。

有一種極端的情況是可能出現(xiàn)大量的過期 Key 沒有被再次訪問,因此不會(huì)被清除,導(dǎo)致占用了大量的內(nèi)存。

在計(jì)算機(jī)科學(xué)中,懶惰刪除(英文:lazy deletion)指的是從一個(gè)散列表(也稱哈希表)中刪除元素的一種方法。

在這個(gè)方法中,刪除僅僅是指標(biāo)記一個(gè)元素被刪除,而不是整個(gè)清除它。被刪除的位點(diǎn)在插入時(shí)被當(dāng)作空元素,在搜索之時(shí)被當(dāng)作已占據(jù)。

③定期刪除

每隔一段時(shí)間,掃描 Redis 中過期 Key 字典,并清除部分過期的 Key。該策略是前兩者的一個(gè)折中方案,還可以通過調(diào)整定時(shí)掃描的時(shí)間間隔和每次掃描的限定耗時(shí),在不同情況下使得 CPU 和內(nèi)存資源達(dá)到最優(yōu)的平衡效果。

在 Redis 中,同時(shí)使用了定期刪除和惰性刪除。

過期刪除策略原理

為了大家聽起來不會(huì)覺得疑惑,在正式介紹過期刪除策略原理之前,先給大家介紹一點(diǎn)可能會(huì)用到的相關(guān) Redis 基礎(chǔ)知識(shí)。

①RedisDB 結(jié)構(gòu)體定義

我們知道,Redis 是一個(gè)鍵值對(duì)數(shù)據(jù)庫,對(duì)于每一個(gè) Redis 數(shù)據(jù)庫,Redis 使用一個(gè) RedisDB 的結(jié)構(gòu)體來保存,它的結(jié)構(gòu)如下:

  1. typedef struct redisDb { 
  2.         dict *dict;                 /* 數(shù)據(jù)庫的鍵空間,保存數(shù)據(jù)庫中的所有鍵值對(duì) */ 
  3.         dict *expires;              /* 保存所有過期的鍵 */ 
  4.         dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/ 
  5.         dict *ready_keys;           /* Blocked keys that received a PUSH */ 
  6.         dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */ 
  7.         int id;                     /* 數(shù)據(jù)庫ID字段,代表不同的數(shù)據(jù)庫 */ 
  8.         long long avg_ttl;          /* Average TTL, just for stats */ 
  9. } redisDb; 

從結(jié)構(gòu)定義中我們可以發(fā)現(xiàn),對(duì)于每一個(gè) Redis 數(shù)據(jù)庫,都會(huì)使用一個(gè)字典的數(shù)據(jù)結(jié)構(gòu)來保存每一個(gè)鍵值對(duì),dict 的結(jié)構(gòu)圖如下:

 

以上就是過期策略實(shí)現(xiàn)時(shí)用到比較核心的數(shù)據(jù)結(jié)構(gòu)。程序=數(shù)據(jù)結(jié)構(gòu)+算法,介紹完數(shù)據(jù)結(jié)構(gòu)以后,接下來繼續(xù)看看處理的算法是怎樣的。

②expires 屬性

RedisDB 定義的第二個(gè)屬性是 expires,它的類型也是字典,Redis 會(huì)把所有過期的鍵值對(duì)加入到 expires,之后再通過定期刪除來清理 expires 里面的值。

加入 expires 的場(chǎng)景有:

  • Set 指定過期時(shí)間 expire,如果設(shè)置 Key 的時(shí)候指定了過期時(shí)間,Redis 會(huì)將這個(gè) Key 直接加入到 expires 字典中,并將超時(shí)時(shí)間設(shè)置到該字典元素。
  • 調(diào)用 expire 命令,顯式指定某個(gè) Key 的過期時(shí)間。
  • 恢復(fù)或修改數(shù)據(jù),從 Redis 持久化文件中恢復(fù)文件或者修改 Key,如果數(shù)據(jù)中的 Key 已經(jīng)設(shè)置了過期時(shí)間,就將這個(gè) Key 加入到 expires 字典中。

以上這些操作都會(huì)將過期的 Key 保存到 expires。Redis 會(huì)定期從 expires 字典中清理過期的 Key。

③Redis 清理過期 Key 的時(shí)機(jī)

Redis 在啟動(dòng)的時(shí)候,會(huì)注冊(cè)兩種事件,一種是時(shí)間事件,另一種是文件事件。時(shí)間事件主要是 Redis 處理后臺(tái)操作的一類事件,比如客戶端超時(shí)、刪除過期 Key;文件事件是處理請(qǐng)求。

在時(shí)間事件中,Redis 注冊(cè)的回調(diào)函數(shù)是 serverCron,在定時(shí)任務(wù)回調(diào)函數(shù)中,通過調(diào)用 databasesCron 清理部分過期 Key。(這是定期刪除的實(shí)現(xiàn)。)

  1. int serverCron(struct aeEventLoop *eventLoop, long long id, void *clientData) 
  2.     … 
  3.     /* Handle background operations on Redis databases. */ 
  4.     databasesCron(); 
  5.     ... 

每次訪問 Key 的時(shí)候,都會(huì)調(diào)用 expireIfNeeded 函數(shù)判斷 Key 是否過期,如果是,清理 Key。(這是惰性刪除的實(shí)現(xiàn))

  1. robj *lookupKeyRead(redisDb *db, robj *key) { 
  2.     robj *val; 
  3.     expireIfNeeded(db,key); 
  4.     val = lookupKey(db,key); 
  5.      ... 
  6.     return val; 

每次事件循環(huán)執(zhí)行時(shí),主動(dòng)清理部分過期 Key。(這也是惰性刪除的實(shí)現(xiàn))

  1. void aeMain(aeEventLoop *eventLoop) { 
  2.     eventLoop->stop = 0; 
  3.     while (!eventLoop->stop) { 
  4.         if (eventLoop->beforesleep != NULL
  5.             eventLoop->beforesleep(eventLoop); 
  6.         aeProcessEvents(eventLoop, AE_ALL_EVENTS); 
  7.     } 
  8.  
  9. void beforeSleep(struct aeEventLoop *eventLoop) { 
  10.        ... 
  11.        /* Run a fast expire cycle (the called function will return 
  12.         - ASAP if a fast cycle is not needed). */ 
  13.        if (server.active_expire_enabled && server.masterhost == NULL
  14.            activeExpireCycle(ACTIVE_EXPIRE_CYCLE_FAST); 
  15.        ... 
  16.    } 

④過期策略的實(shí)現(xiàn)

我們知道,Redis 是以單線程運(yùn)行的,在清理 Key 時(shí)不能占用過多的時(shí)間和 CPU,需要在盡量不影響正常的服務(wù)情況下,進(jìn)行過期 Key 的清理。

過期清理的算法如下:

  • server.hz 配置了 serverCron 任務(wù)的執(zhí)行周期,默認(rèn)是 10,即 CPU 空閑時(shí)每秒執(zhí)行十次。
  • 每次清理過期 Key 的時(shí)間不能超過 CPU 時(shí)間的 25%:timelimit = 1000000*ACTIVE_EXPIRE_CYCLE_SLOW_TIME_PERC/server.hz/100。
  • 比如,如果 hz=1,一次清理的最大時(shí)間為 250ms,hz=10,一次清理的最大時(shí)間為 25ms。
  • 如果是快速清理模式(在 beforeSleep 函數(shù)調(diào)用),則一次清理的最大時(shí)間是 1ms。
  • 依次遍歷所有的 DB。
  • 從 DB 的過期列表中隨機(jī)取 20 個(gè) Key,判斷是否過期,如果過期,則清理。
  • 如果有 5 個(gè)以上的 Key 過期,則重復(fù)步驟 5,否則繼續(xù)處理下一個(gè) DB。
  • 在清理過程中,如果達(dá)到 CPU 的 25% 時(shí)間,退出清理過程。

從實(shí)現(xiàn)的算法中可以看出,這只是基于概率的簡(jiǎn)單算法,且是隨機(jī)的抽取,因此是無法刪除所有的過期 Key,通過調(diào)高 hz 參數(shù)可以提升清理的頻率,過期 Key 可以更及時(shí)的被刪除,但 hz 太高會(huì)增加 CPU 時(shí)間的消耗。

⑤刪除 Key

Redis 4.0 以前,刪除指令是 del,del 會(huì)直接釋放對(duì)象的內(nèi)存,大部分情況下,這個(gè)指令非常快,沒有任何延遲的感覺。

但是,如果刪除的 Key 是一個(gè)非常大的對(duì)象,比如一個(gè)包含了千萬元素的 Hash,那么刪除操作就會(huì)導(dǎo)致單線程卡頓,Redis 的響應(yīng)就慢了。

為了解決這個(gè)問題,在 Redis 4.0 版本引入了 unlink 指令,能對(duì)刪除操作進(jìn)行“懶”處理,將刪除操作丟給后臺(tái)線程,由后臺(tái)線程來異步回收內(nèi)存。

實(shí)際上,在判斷 Key 需要過期之后,真正刪除 Key 的過程是先廣播 expire 事件到從庫和 AOF 文件中,然后在根據(jù) Redis 的配置決定立即刪除還是異步刪除。

如果是立即刪除,Redis 會(huì)立即釋放 Key 和 Value 占用的內(nèi)存空間,否則,Redis 會(huì)在另一個(gè) BIO 線程中釋放需要延遲刪除的空間。

小結(jié):總的來說,Redis 的過期刪除策略是在啟動(dòng)時(shí)注冊(cè)了 serverCron 函數(shù),每一個(gè)時(shí)間時(shí)鐘周期,都會(huì)抽取 expires 字典中的部分 Key 進(jìn)行清理,從而實(shí)現(xiàn)定期刪除。

另外,Redis 會(huì)在訪問 Key 時(shí)判斷 Key 是否過期,如果過期了,就刪除,以及每一次 Redis 訪問事件到來時(shí),beforeSleep 都會(huì)調(diào)用 activeExpireCycle 函數(shù),在 1ms 時(shí)間內(nèi)主動(dòng)清理部分 Key,這是惰性刪除的實(shí)現(xiàn)。

Redis 結(jié)合了定期刪除和惰性刪除,基本上能很好的處理過期數(shù)據(jù)的清理,但是實(shí)際上還是有點(diǎn)問題的。

如果過期 Key 較多,定期刪除漏掉了一部分,而且也沒有及時(shí)去查,即沒有走惰性刪除,那么就會(huì)有大量的過期 Key 堆積在內(nèi)存中,導(dǎo)致 Redis 內(nèi)存耗盡。

當(dāng)內(nèi)存耗盡之后,有新的 Key 到來會(huì)發(fā)生什么事呢?是直接拋棄還是其他措施呢?有什么辦法可以接受更多的 Key?

內(nèi)存淘汰策略

Redis 的內(nèi)存淘汰策略,是指內(nèi)存達(dá)到 maxmemory 極限時(shí),使用某種算法來決定清理掉哪些數(shù)據(jù),以保證新數(shù)據(jù)的存入。

Redis 的內(nèi)存淘汰機(jī)制如下:

  • noeviction:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),新寫入操作會(huì)報(bào)錯(cuò)。
  • allkeys-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間(server.db[i].dict)中,移除最近最少使用的 Key(這個(gè)是最常用的)。
  • allkeys-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在鍵空間(server.db[i].dict)中,隨機(jī)移除某個(gè) Key。
  • volatile-lru:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,移除最近最少使用的 Key。
  • volatile-random:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,隨機(jī)移除某個(gè) Key。
  • volatile-ttl:當(dāng)內(nèi)存不足以容納新寫入數(shù)據(jù)時(shí),在設(shè)置了過期時(shí)間的鍵空間(server.db[i].expires)中,有更早過期時(shí)間的 Key 優(yōu)先移除。

在配置文件中,通過 maxmemory-policy 可以配置要使用哪一個(gè)淘汰機(jī)制。

①什么時(shí)候會(huì)進(jìn)行淘汰?

Redis 會(huì)在每一次處理命令的時(shí)候(processCommand 函數(shù)調(diào)用 freeMemoryIfNeeded)判斷當(dāng)前 Redis 是否達(dá)到了內(nèi)存的最大限制,如果達(dá)到限制,則使用對(duì)應(yīng)的算法去處理需要?jiǎng)h除的 Key。

偽代碼如下:

  1. int processCommand(client *c) 
  2.     ... 
  3.     if (server.maxmemory) { 
  4.         int retval = freeMemoryIfNeeded();   
  5.     } 
  6.     ... 

②LRU 實(shí)現(xiàn)原理

在淘汰 Key 時(shí),Redis 默認(rèn)最常用的是 LRU 算法(Latest Recently Used)。

Redis 通過在每一個(gè) redisObject 保存 lRU 屬性來保存 Key 最近的訪問時(shí)間,在實(shí)現(xiàn) LRU 算法時(shí)直接讀取 Key 的 lRU 屬性。

具體實(shí)現(xiàn)時(shí),Redis 遍歷每一個(gè) DB,從每一個(gè) DB 中隨機(jī)抽取一批樣本 Key,默認(rèn)是 3 個(gè) Key,再?gòu)倪@ 3 個(gè) Key 中,刪除最近最少使用的 Key。

實(shí)現(xiàn)偽代碼如下:

  1. keys = getSomeKeys(dict, sample) 
  2. key = findSmallestIdle(keys) 
  3. remove(key

3 這個(gè)數(shù)字是配置文件中的 maxmeory-samples 字段,也是可以設(shè)置采樣的大小,如果設(shè)置為 10,那么效果會(huì)更好,不過也會(huì)耗費(fèi)更多的 CPU 資源。

以上就是 Redis 內(nèi)存回收機(jī)制的原理介紹,了解了上面的原理介紹后,回到一開始的問題,在懷疑 Redis 內(nèi)存回收機(jī)制的時(shí)候能不能及時(shí)判斷故障是不是因?yàn)? Redis 的內(nèi)存回收機(jī)制導(dǎo)致的呢?

回到問題原點(diǎn)

如何證明故障是不是由內(nèi)存回收機(jī)制引起的?根據(jù)前面分析的內(nèi)容,如果 Set 沒有報(bào)錯(cuò),但是不生效,只有兩種情況:

  • 設(shè)置的過期時(shí)間過短,比如,1s。
  • 內(nèi)存超過了最大限制,且設(shè)置的是 noeviction 或者 allkeys-random。

因此,在遇到這種情況,首先看 Set 的時(shí)候是否加了過期時(shí)間,且過期時(shí)間是否合理,如果過期時(shí)間較短,那么應(yīng)該檢查一下設(shè)計(jì)是否合理。

如果過期時(shí)間沒問題,那就需要查看 Redis 的內(nèi)存使用率,查看 Redis 的配置文件或者在 Redis 中使用 Info 命令查看 Redis 的狀態(tài),maxmemory 屬性查看最大內(nèi)存值。

如果是 0,則沒有限制,此時(shí)是通過 total_system_memory 限制,對(duì)比 used_memory 與 Redis 最大內(nèi)存,查看內(nèi)存使用率。

如果當(dāng)前的內(nèi)存使用率較大,那么就需要查看是否有配置最大內(nèi)存,如果有且內(nèi)存超了,那么就可以初步判定是內(nèi)存回收機(jī)制導(dǎo)致 Key 設(shè)置不成功。

還需要查看內(nèi)存淘汰算法是否 noeviction 或者 allkeys-random,如果是,則可以確認(rèn)是 Redis 的內(nèi)存回收機(jī)制導(dǎo)致。

如果內(nèi)存沒有超,或者內(nèi)存淘汰算法不是上面的兩者,則還需要看看 Key 是否已經(jīng)過期,通過 TTL 查看 Key 的存活時(shí)間。

 

如果運(yùn)行了程序,Set 沒有報(bào)錯(cuò),則 TTL 應(yīng)該馬上更新,否則說明 Set 失敗,如果 Set 失敗了那么就應(yīng)該查看操作的程序代碼是否正確了。

總結(jié)

Redis 對(duì)于內(nèi)存的回收有兩種方式,一種是過期 Key 的回收,另一種是超過 Redis 的最大內(nèi)存后的內(nèi)存釋放。

對(duì)于第一種情況,Redis 會(huì)在:

  • 每一次訪問的時(shí)候判斷 Key 的過期時(shí)間是否到達(dá),如果到達(dá),就刪除 Key。
  • Redis 啟動(dòng)時(shí)會(huì)創(chuàng)建一個(gè)定時(shí)事件,會(huì)定期清理部分過期的 Key,默認(rèn)是每秒執(zhí)行十次檢查,每次過期 Key 清理的時(shí)間不超過 CPU 時(shí)間的 25%。

即若 hz=1,則一次清理時(shí)間最大為 250ms,若 hz=10,則一次清理時(shí)間最大為 25ms。

對(duì)于第二種情況,Redis 會(huì)在每次處理 Redis 命令的時(shí)候判斷當(dāng)前 Redis 是否達(dá)到了內(nèi)存的最大限制,如果達(dá)到限制,則使用對(duì)應(yīng)的算法去處理需要?jiǎng)h除的 Key。

看完這篇文章后,你能回答文章開頭的面試題了嗎?

 

責(zé)任編輯:武曉燕 來源: hoohack
相關(guān)推薦

2013-04-01 10:07:19

Java內(nèi)存回收機(jī)制

2011-01-18 14:06:58

JavaScriptweb

2011-07-04 13:12:04

JavaScript

2009-12-09 17:28:34

PHP垃圾回收機(jī)制

2012-08-13 10:19:03

IBMdW

2023-02-28 07:56:07

V8內(nèi)存管理

2010-09-26 16:42:04

JVM內(nèi)存組成JVM垃圾回收

2011-05-26 15:41:25

java虛擬機(jī)

2009-06-23 14:15:00

Java垃圾回收

2017-03-03 09:26:48

PHP垃圾回收機(jī)制

2017-08-17 15:40:08

大數(shù)據(jù)Python垃圾回收機(jī)制

2010-09-25 15:33:19

JVM垃圾回收

2024-05-14 08:20:59

線程CPU場(chǎng)景

2015-06-04 09:38:39

Java垃圾回收機(jī)

2017-06-12 17:38:32

Python垃圾回收引用

2011-06-28 12:39:34

Java垃圾回收

2011-07-04 16:48:56

JAVA垃圾回收機(jī)制GC

2021-05-27 21:47:12

Python垃圾回收

2010-09-16 15:10:24

JVM垃圾回收機(jī)制

2010-09-25 15:26:12

JVM垃圾回收
點(diǎn)贊
收藏

51CTO技術(shù)棧公眾號(hào)