標籤:
這個問題有三種可能的答案,它們分別代表了三種不同的刪除策略:
·定時刪除:在設定鍵的到期時間的同時,建立一個定時器(timer),讓定時器在鍵的到期時間來臨時,立即執行對鍵的刪除操作。
·惰性刪除:放任鍵到期不管,但是每次從鍵空間中擷取鍵時,都檢查取得的鍵是否到期,如果到期的話,就刪除該鍵;如果沒有到期,就返回該鍵。
·定期刪除:每隔一段時間,程式就對資料庫進行一次檢查,刪除裡面的到期鍵。至於要刪除多少到期鍵,以及要檢查多少個資料庫,則由演算法決定。
在這三種策略中,第一種和第三種為主動刪除策略,而第二種則為被動刪除策略。
定時刪除
定時刪除策略對記憶體是最友好的:通過使用定時器,定時刪除策略可以保證到期鍵會儘可能快地被刪除,並釋放到期鍵所佔用的記憶體。
另一方面,定時刪除策略的缺點是,它對CPU時間是最不友好的:在到期鍵比較多的情況下,刪除到期鍵這一行為可能會佔用相當一部分CPU時間,在記憶體不緊張但是CPU時間非常緊張的情況下,將CPU時間用在刪除和當前任務無關的到期鍵上,無疑會對伺服器的回應時間和輸送量造成影響。
例如,如果正有大量的命令請求在等待伺服器處理,並且伺服器當前不缺少記憶體,那麼伺服器應該優先將CPU時間用在處理用戶端的命令請求上面,而不是用在刪除到期鍵上面。
除此之外,建立一個定時器需要用到Redis伺服器中的時間事件,而目前時間事件的實現方式——無序鏈表,尋找一個事件的時間複雜度為O(N)——並不能高效地處理大量時間事件。
因此,要讓伺服器建立大量的定時器,從而實現定時刪除策略,在現階段來說並不現實。
惰性刪除
惰性刪除策略對CPU時間來說是最友好的:程式只會在取出鍵時才對鍵進行到期檢查,這可以保證刪除到期鍵的操作只會在非做不可的情況下進行,並且刪除的目標僅限於當前處理的鍵,這個策略不會在刪除其他無關的到期鍵上花費任何CPU時間。
惰性刪除策略的缺點是,它對記憶體是最不友好的:如果一個鍵已經到期,而這個鍵又仍然保留在資料庫中,那麼只要這個到期鍵不被刪除,它所佔用的記憶體就不會釋放。
在使用惰性刪除策略時,如果資料庫中有非常多的到期鍵,而這些到期鍵又恰好沒有被訪問到的話,那麼它們也許永遠也不會被刪除(除非使用者手動執行FLUSHDB),我們甚至可以將這種情況看作是一種記憶體流失——無用的垃圾資料佔用了大量的記憶體,而伺服器卻不會自己去釋放它們,這對於運行狀態非常依賴於記憶體的Redis伺服器來說,肯定不是一個好訊息。
舉個例子,對於一些和時間有關的資料,比如日誌(log),在某個時間點之後,對它們的訪問就會大大減少,甚至不再訪問,如果這類到期資料大量地積壓在資料庫中,使用者以為伺服器已經自動將它們刪除了,但實際上這些鍵仍然存在,而且鍵所佔用的記憶體也沒有釋放,那麼造成的後果肯定是非常嚴重的。
定期刪除
從上面對定時刪除和惰性刪除的討論來看,這兩種刪除方式在單一使用時都有明顯的缺陷:
·定時刪除佔用太多CPU時間,影響伺服器的回應時間和輸送量。
·惰性刪除浪費太多記憶體,有記憶體流失的危險。
定期刪除策略是前兩種策略的一種整合和折中:
·定期刪除策略每隔一段時間執行一次刪除到期鍵操作,並通過限制刪除操作執行的時間長度和頻率來減少刪除操作對CPU時間的影響。
·除此之外,通過定期刪除到期鍵,定期刪除策略有效地減少了因為到期鍵而帶來的記憶體浪費。
定期刪除策略的痛點是確定刪除操作執行的時間長度和頻率:
·如果刪除操作執行得太頻繁,或者執行的時間太長,定期刪除策略就會退化成定時刪除策略,以至於將CPU時間過多地消耗在刪除到期鍵上面。
·如果刪除操作執行得太少,或者執行的時間太短,定期刪除策略又會和惰性刪除策略一樣,出現浪費記憶體的情況。
因此,如果採用定期刪除策略的話,伺服器必鬚根據情況,合理地設定刪除操作的執行時間長度和執行頻率。
到期鍵的定期刪除策略由redis.c/activeExpireCycle函數實現,每當Redis的伺服器周期性操作redis.c/serverCron函數執行時,activeExpireCycle函數就會被調用,它在規定的時間內,分多次遍曆伺服器中的各個資料庫,從資料庫的expires字典中隨機檢查一部分鍵的到期時間,並刪除其中的到期鍵。
整個過程可以用虛擬碼描述如下:
# 預設每次檢查的資料庫數量DEFAULT_DB_NUMBERS = 16# 預設每個資料庫檢查的鍵數量DEFAULT_KEY_NUMBERS = 20# 全域變數,記錄檢查進度current_db = 0def 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,然後再次開始新一輪的檢查工作。
redis的逾時刪除策略