一文詳解Redis鍵過期策略
摘要:Redis採用的過期策略:惰性刪除+定期刪除。
本文分享自華為雲社群《Redis鍵過期策略詳解》,作者:JavaEdge。
1 設定帶過期時間的 key
# 時間複雜度:O(1),最常用方式 expire key seconds # 字串獨有方式 setex(String key, int seconds, String value)
除了string獨有設定過期時間的方法,其他型別都需依靠expire方法設定時間,若:
- 未設定時間,則快取永不過期
- 設定過期時間,但之後又想讓快取永不過期,使用persist
設定key的過期時間。超時後,將會自動刪除該key。在Redis的術語中一個key的相關超時是volatile的。
生存時間可以通過使用 DEL 命令來刪除整個 key 來移除,或者被 SET 和 GETSET 命令覆寫(overwrite)。這意味著,如果一個命令只是修改(alter)一個帶生存時間的 key 的值而不是用一個新的 key 值來代替(replace)它的話,那麼生存時間不會被改變。 如使用 INCR 遞增key的值,執行 LPUSH 將新值推到 list 中或用 HSET 改變hash的field,這些操作都使超時保持不變。
- 使用 PERSIST 命令可以清除超時,使其變成一個永久key
- 若 key 被 RENAME 命令修改,相關的超時時間會轉移到新key
- 若 key 被 RENAME 命令修改,比如原來就存在 Key_A,然後呼叫 RENAME Key_B Key_A 命令,這時不管原來 Key_A 是永久的還是設為超時的,都會由Key_B的有效期狀態覆蓋
注意,使用非正超時呼叫 EXPIRE/PEXPIRE 或具有過去時間的 EXPIREAT/PEXPIREAT 將導致key被刪除而不是過期(因此,發出的key事件將是 del,而不是過期)。
1.1 重新整理過期時間
對已經有過期時間的key執行EXPIRE操作,將會更新它的過期時間。有很多應用有這種業務場景,例如記錄會話的session。
1.2 Redis 之前的 2.1.3 的差異
在 Redis 版本之前 2.1.3 中,使用更改其值的命令更改具有過期集的金鑰具有完全刪除key的效果。由於現在修復的複製層中存在限制,因此需要此語義。
EXPIRE 將返回 0,並且不會更改具有超時集的鍵的超時。
1.3 返回值
- 1,如果成功設定過期時間。
- 0,如果key不存在或者不能設定過期時間。
1.4 示例
假設有一 Web 服務,對使用者最近訪問的最新 N 頁感興趣,這樣每個相鄰頁面檢視在上一個頁面之後不超過 60 秒。從概念上講,可以將這組頁面檢視視為使用者的導航會話,該會話可能包含有關ta當前正在尋找的產品的有趣資訊,以便你可以推薦相關產品。
可使用以下策略輕鬆在 Redis 中對此模式建模:每次使用者執行頁面檢視時,您都會呼叫以下命令:
MULTI RPUSH pagewviews.user:<userid> http://..... EXPIRE pagewviews.user:<userid> 60 EXEC
如果使用者空閒超過 60 秒,則將刪除該key,並且僅記錄差異小於 60 秒的後續頁面檢視。
此模式很容易修改,使用 INCR 而不是使用 RPUSH 的列表。
1.5 帶過期時間的 key
通常,建立 Redis 鍵時沒有關聯的存活時間。key將永存,除非使用者以顯式方式(例如 DEL 命令)將其刪除。EXPIRE 族的命令能夠將過期項與給定key關聯,但代價是該key使用的額外記憶體。當key具有過期集時,Redis 將確保在經過指定時間時刪除該key。
可使用 EXPIRE 和 PERSIST 命令(或其他嚴格命令)更新或完全刪除生存的關鍵時間。
1.6 過期精度
在 Redis 2.4 中,過期可能不準確,並且可能介於 0 到 1 秒之間。
由於 Redis 2.6,過期誤差從 0 到 1 毫秒。
1.7 過期和持久化
過期資訊的鍵儲存為絕對 Unix 時間戳(Redis 版本 2.6 或更高版本為毫秒)。這意味著即使 Redis 例項不處於活動狀態,時間也在流動。
要使過期工作良好,必須穩定計算機時間。若將 RDB 檔案從兩臺計算機上移動,其時鐘中具有大 desync,則可能會發生有趣的事情(如載入時載入到過期的所有key)。
即使執行時的例項,也始終會檢查計算機時鐘,例如,如果將一個key設定為 1000 秒,然後在將來設定計算機時間 2000 秒,則該key將立即過期,而不是持續 1000 秒。
2 Redis的key過期策略
- 被動方式 - 惰性刪除
- 主動方式 - 定期刪除
為保證 Redis 的高效能,所以不會單獨安排一個執行緒專門去刪除。
2.1 惰性刪除
key過期時不刪除,每次獲取key時,再去檢查是否過期。若過期,則刪除,返回null。
2.1.1 優點
刪除操作只發生在取key時,且只刪除當前key,所以對CPU時間佔用較少。此時刪除已非做不可,畢竟若還不刪除,就會獲取到已過期key。
當查詢該key時,Redis再很懶惰地檢查是否刪除。這和 Spring 的延遲初始化有著異曲同工之妙。
2.1.2 缺點
但這是不夠的,因為有過期key,永遠不會再訪問。若大量key在超出TTL後,很久一段時間內,都沒有被獲取過,則可能發生記憶體洩露(無用垃圾佔用了大量記憶體)。
無論如何,這些key都應過期,因此還需要定期 Redis 在具有過期集的key之間隨機測試幾個key。已過期的所有key將從key空間中刪除。
定時刪除
在設定key的過期時間的同時,為該key建立一個定時器,讓定時器在key的過期時間來臨時,對key進行刪除。
優點
保證記憶體被儘快釋放
缺點
- 若過期key很多,刪除這些key會佔用很多的CPU時間,在CPU時間緊張的情況下,CPU不能把所有的時間用來做要緊的事兒,還需要去花時間刪除這些key
- 定時器的建立耗時,若為每一個設定過期時間的key建立一個定時器(將會有大量的定時器產生),效能影響嚴重
所以沒人用
2.2 定期刪除
每隔一段時間執行一次刪除過期key操作。
優點
- 通過限制刪除操作的時長和頻率,來減少刪除操作對CPU時間的佔用–處理"定時刪除"的缺點
- 定期刪除過期key–處理"惰性刪除"的缺點
缺點
- 在記憶體友好方面,不如"定時刪除"
- 在CPU時間友好方面,不如"惰性刪除"
難點
- 合理設定刪除操作的執行時長(每次刪除執行多長時間)和執行頻率(每隔多長時間做一次刪除)(這個要根據伺服器執行情況來定了)
每秒 10 次:
- 測試 20 個帶有過期的隨機鍵
- 刪除找到的所有已過期key
- 如果超過 25% 的key已過期,從步驟 1 重新開始
這是個微不足道的概率演算法,假設樣本為整個key空間,繼續過期,直到可能過期的key百分比低於 25%。
這意味著在任何給定時刻,使用記憶體的已過期的最大鍵量等於最大寫入操作量/秒除以 4。
定期刪除流程
void databasesCron(void) { /* Expire keys by random sampling. Not required for slaves * as master will synthesize DELs for us. */ if (server.active_expire_enabled) { if (iAmMaster()) { activeExpireCycle(ACTIVE_EXPIRE_CYCLE_SLOW); } else { expireSlaveKeys(); } } #define ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP 20 /* Keys for each DB loop. */ void activeExpireCycle(int type) { config_keys_per_loop = ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP + ACTIVE_EXPIRE_CYCLE_KEYS_PER_LOOP/4*effort, // step1 for (j = 0; j < dbs_per_call && timelimit_exit == 0; j++) { /* 如果沒有什麼可以過期,請儘快嘗試下一個資料庫 */ // step2 if ((num = dictSize(db->expires)) == 0) { db->avg_ttl = 0; // step3 break; } } }
對指定N個庫的每個庫隨機刪除≤指定個數的過期K。
- 遍歷每個資料庫(redis.conf中配置的"database"數量,預設16)
- 檢查當前庫中的指定個數的key(預設每個庫檢查20個key,這相當於該迴圈執行20次,迴圈體為下邊的描述)
- 若當前庫沒有一個K設定TTL,直接執行下一個庫的遍歷
- 隨機獲取一個設定TTL的K,檢查其是否過期,若過期,則刪除
- 判斷定期刪除操作是否已達到指定時長,若已達到,直接退出定期刪除
定期刪除程式中有個全域性變數current_db,記錄下一個將要遍歷的庫。預設16個庫,這次定期刪除遍歷了10個,那此時current_db就是11,下一次定期刪除就從第11個庫開始遍歷,假設current_db等於15,那之後就再從0號庫開始遍歷(此時current_db==0)
Redis採用的過期策略
惰性刪除+定期刪除。
惰性刪除流程
在進行get或setnx等操作時,先檢查key是否過期:
- 若過期,刪除key,然後執行相應操作
- 若沒過期,直接執行相應操作
RDB處理過期key
過期key對RDB無影響:
- 從記憶體資料庫持久化資料到RDB檔案
持久化key之前,會檢查是否過期,過期的key不進入RDB檔案
- 從RDB檔案恢復資料到記憶體資料庫
資料載入資料庫之前,會對K進行過期檢查,若過期,不匯入資料庫(主庫情況)
AOF處理過期K
過期key對AOF沒有任何影響。
從記憶體資料庫持久化到AOF檔案
- 當key過期後,還沒有被刪除,此時進行執行持久化操作(該key不會進入aof檔案,因為沒有發生修改命令)
- 當key過期後,在發生刪除操作時,程式會向aof檔案追加一條del命令(在將來的以aof檔案恢復資料的時候該過期的鍵就會被刪掉)
AOF重寫
重寫時,會先判斷key是否過期,已過期的key不會重寫到aof檔案
2.3 在複製鏈路和 AOF 檔案中處理過期的方式
為了在不犧牲一致性的情況下獲得正確行為,當key過期時,DEL 操作將同時在 AOF 檔案中合成並獲取所有附加的從節點。這樣,過期的這個處理過程集中到主節點中,還沒有一致性錯誤的可能性。
但是,雖然連線到主節點的從節點不會獨立過期key(但會等待來自master的 DEL),但它們仍將使用資料集中現有過期的完整狀態,因此,當選擇slave作為master時,它將能夠獨立過期key,完全充當master。
預設每臺Redis伺服器有16個數據庫,預設使用0號資料庫,所有的操作都是對0號資料庫的操作
# 設定資料庫數量。預設為16個庫,預設使用DB 0,可使用"select 1"來選擇一號資料庫 # 注意:由於預設使用0號資料庫,那麼我們所做的所有的快取操作都存在0號資料庫上, # 當你在1號資料庫上去查詢的時候,就查不到之前set過的快取 # 若想將0號資料庫上的快取移動到1號資料庫,可以使用"move key 1" databases 16
- memcached只是用了惰性刪除,而redis同時使用了惰性刪除與定期刪除,這也是二者的一個不同點(可以看做是redis優於memcached的一點)
- 對於惰性刪除而言,並不是只有獲取key的時候才會檢查key是否過期,在某些設定key的方法上也會檢查(eg.setnx key2 value2:該方法類似於memcached的add方法,如果設定的key2已經存在,那麼該方法返回false,什麼都不做;如果設定的key2不存在,那麼該方法設定快取key2-value2。假設呼叫此方法的時候,發現redis中已經存在了key2,但是該key2已經過期了,如果此時不執行刪除操作的話,setnx方法將會直接返回false,也就是說此時並沒有重新設定key2-value2成功,所以對於一定要在setnx執行之前,對key2進行過期檢查)
可是,很多過期key,你沒及時去查,定期刪除也漏掉了,大量過期key堆積記憶體,Redis記憶體殆耗盡!
因此記憶體滿時,還需有記憶體淘汰機制!這就是 Redis 自己 主動刪除 資料了!