redis過期鍵刪除策略


如果一個鍵過期了,那么它什么時候會被刪除呢?
這個問題有三種可能的答案,它們分別代表了三種不同的刪除策略:
定時刪除:在設置鍵的過期時間的同時,創建一個定時器( timer ). 讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
惰性刪除:放任鍵過期不管,但是每次從鍵空間中獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵;如果沒有過期,就返回該鍵。
定期刪除: 每隔一段時間,程序就對數據庫進行一次檢查,刪除里面的過期鍵。至於要刪除多少過期鍵,以及要檢查多少個數據庫, 則由算法決定。
在這三種策略中,第一種和第三種為主動刪除策略, 而第二種則為被動刪除策略。

定時刪除

定時刪除策略對內存是最友好的:通過使用定時器,定時刪除策略可以保證過期鍵會盡可能快地被刪除,並釋放過期鍵所占用的內存。另一方面,定時刪除策略的缺點是,它對CPU 時間是最不友好的:在過期鍵比較多的情況下,刪除過期鍵這一行為可能會占用相當一部分CPU 時間,在內存不緊張但是CPU 時間非常緊張的情況下.將CPU 時間用在刪除和當前任務無關的過期鍵上,無疑會對服務器的響應時間和吞吐量造成影響。
例如,如果正有大量的命令請求在等待服務器處理,並且服務器當前不缺少內存,那么服務器應該優先將CPU 時間用在處理客戶端的命令請求上面,而不是用在刪除過期鍵上面。除此之外,創建一個定時器需要用到Redis 服務器中的時間事件,而當前時間事件的實現方式一一無序鏈表,查找一個事件的時間復雜度為O(N)一並不能高效地處理大量時間事件。
因此,要讓服務器創建大量的定時器,從而實現定時刪除策略,在現階段來說並不現實。

惰性刪除

惰性刪除策略對CPU 時間來說是最友好的:程序只會在取出鍵時才對鍵進行過期檢查,這可以保證刪除過期鍵的操作只會在非做不可的情況下進行, 並且刪除的目標僅限於當前處理的鍵,這個策略不會在刪除其他無關的過期鍵上花費任何CPU時間。
惰性刪除策略的缺點是,它對內存是最不友好的: 如果一個鍵已經過期,而這個鍵又仍然保留在數據庫中,那么只要這個過期鍵不被刪除,它所占用的內存就不會釋放。在使用惰性刪除策略時,如果數據庫中有非常多的過期鍵,而這些過期鍵又恰好沒有被訪問到的話,那么它們也許永遠也不會被刪除(除非用戶手動執行FLUSHDB),我們甚至可以將這種情況看作是一種內存泄漏一一無用的垃圾數據占用了大量的內存,而服務器卻不會自己去釋放它們,這對於運行狀態非常依賴於內存的Redis服務器來說,肯定不是一個好消息。
舉個例子,對於一些和時間有關的數據,比如日志(log) ,在某個時間點之后,對它們的訪問就會大大減少,甚至不再訪問,如果這類過期數據大量地積壓在數據庫中,用戶以為服務器已經自動將它們刪除了,但實際上這些鍵仍然存在, 而且鍵所占用的內存也沒有釋放,那么造成的后果肯定是非常嚴重的。

定期刪除

從上面對定時刪除和惰性刪除的討論來看,這兩種刪除方式在單一使用時都有明顯的缺陷:
定時刪除占用太多CPU 時間,影響服務器的響應時間和吞吐量。惰性刪除浪費太多內存,有內存泄漏的危險。
定期刪除策略是前兩種策略的一種整合和折中:

定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU 時間的影響。除此之外,通過定期刪除過期鍵,定期刪除策略有效地減少了因為過期鍵而帶來的內存浪費。定期刪除策略的難點是確定刪除操作執行的時長和頻率:
如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至於將C P U 時間過多地消耗在刪除過期鍵上面。
如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策略一樣,出現浪費內存的情況。
因此,如果采用定期刪除策略的話,服務器必須根據情況,合理地設置刪除操作的執行時長和執行頻率。

Redis 的過期鍵刪除策略

我們討論了定時刪除、惰性刪除和定期刪除三種過期鍵刪除策略, Redis服務器實際使用的是惰性刪除和定期刪除兩種策略: 通過配合使用這兩種刪除策略,服務器可以很好地在合理使用CPU時間和避免浪費內存空間之間取得平衡。
接下來我們將對Redis服務器中惰性刪除和定期刪除的具體實現進行說明。

惰性刪除策略的實現

過期鍵的惰性刪除策略由db.c/expirelfNeeded 函數實現,所有讀寫數據庫的Redis命令在執行之前都會調用expirelfNeeded 函數對輸入鍵進行檢查:

如果輸入鍵已經過期,那么expirelfNeeded 函數將輸入鍵從數據庫中刪除。
如果輸入鍵未過期, 那么expirelfNeeded 函數不做動作。
命令調用expirelfNeeded 函數的過程如圖9- 15 所示。
expirelfNeeded 函數就像一個過濾器,它可以在命令真正執行之前,過搜、掉過期的輸入鍵,從而避免命令接觸到過期鍵。
另外,因為每個被訪問的鍵都可能因為過期而被expirelfNeeded 函數刪除,所以每個命令的實現函數都必須能同時處理鍵存在以及鍵不存在這兩種情況:
當鍵存在時,命令按照鍵存在的情況執行。
當鍵不存在或者鍵因為過期而被expirelfNeeded 函數刪除時,命令按照鍵不存在的情況執行。
舉個例子,圖9-16展示了GET 命令的執行過程,在這個執行過程中,命令需要判斷鍵是否存在以及鍵是否過期,然后根據判斷來執行合適的動作

定期刪除策略的實現

過期鍵的定期刪除策略由redis.c/activeExpireCycle 函數實現,每當Redis 的
服務器周期性操作redis.c/serverCron 函數執行時,activeExpireCycle 函數就會被調用, 它在規定的時間內,分多次遍歷服務器中的各個數據庫,從數據庫的expires 字
典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。

整個過程可以用偽代碼描述如下:

#默認每次檢查的數據庫數量
DEFAULT_DB_NUMBERS = 16
#默認每個數據庫檢查的鍵數量
DEFAULT_KEY_NUMBERS = 20
#全局變量,記錄檢查進度
current_db = 0
def activeExpireCycle():
#初始化要檢查的數據庫數量
#如果服務榕的數據庫數量比DEFAULT DB NUMBERS 要小
#那么以服務器的數據庫數量為准
if server.dbnum < DEFAULT_DB_NUMBERS :
db_numbers = server.dbnum
else :
db_numbers = DEFAULT_DB_NUMBERS
#遍歷各個數據庫
for i in range(db_numbers) :
#如果current_db 的值等於服務榕的數據庫數量
#這表示檢查程序已經遍歷了服務榕的所有數據庫一次
#將current_db重置為0 ,開始新的一輪遍歷
if current_db == server.dbnum:
current_db = 0
#獲取當前要處理的數據庫
redisDB = server.db[current db)
#將數據庫索引增1 ,指向下一個要處理的數據庫
current_db += 1
#檢查數據庫鍵
for j in range(DEFAULT_KEY_NUMBERS):
#如果數據庫中沒有一個鍵帶有過期時間,那么跳過這個數據庫
if redisDB.expires.size () == 0: break
# 隨機獲取一個帶有過期時間的鍵
key with_ttl = redisDb.expires.get_random_key ()
#檢查鍵是否過期,如果過期就刪除它
if is_expired (key with ttl):
delete_key (key_with_ttl )
#已達到時間上限,停止處理
if reach_time_limit(): return

activeExpireCycle 函數的工作模式可以總結如下:

函數每次運行時,都從一定數量的數據庫中取出一定數量的隨機鍵進行檢查, 並刪除其中的過期鍵。

全局變量current db 會記錄當前activeExpireCycle 函數檢查的進度,並在下一次activeExpireCycle 函數調用時,接着上一次的進度進行處理。比如說,如果當前activeExpireCycle 函數在遍歷10 號數據庫時返回了,那么下次activeExpireCycle 函數執行時,將從11號數據庫開始查找並刪除過期鍵。

隨着activeExpireCycle函數的不斷執行,服務器中的所有數據庫都會被檢查一遍,這時函數將current_db變量重置為0,然后再次開始新一輪的檢查工作。

 


免責聲明!

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



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