2016-1-7 by Atlas
基礎指令篇提到過EXPIRE、PEXPIRE、EXPIREAT、PEXPIREAT四個過期鍵的命令,表達過期鍵刪除策略前先重溫一下這個四個命令的詳細過程。
* 設置過期時間
- EXPIRE ---轉換成---> PEXPIRE
def EXPIRE(key,ttl_in_sec):
// 將TTL從秒轉換成毫秒
ttl_in_ms = sec_to_ms(ttl_in_sec)
// 調用PEXPIRE
PEXPIRE(key,ttl_in_ms)
- PEXPIRE ---轉換成---> PEXPIREAT
def PEXPIRE(key,ttl_in_ms):
// 獲取以毫秒計算的當前UNIX時間戳
now_ms = get_current_unix_timestamp_in_ms()
// 當前時間加上TTL,得出毫秒格式的鍵過期時間
PEXPIREAT(key,now_ms+tll_in_ms)
- EXPIREAT ---轉換成---> PEXPIREAT
def EXPIREAT(key,expire_time_in_sec):
// 將過期時間從秒轉換成毫秒
expire_time_in_ms = sec_to_ms(expire_time_in_sec)
// 調用PEXPIREAT
PEXPIREAT(key,expire_time_in_ms)
EXPIRE、PEXPIRE、EXPIREAT都適配成PEXPIREAT,由PEXPIREAT函數具體實現鍵的過期操作。
def PEXPIREAT(key,expire_time_in_ms):
// 如果給定的鍵不存在於鍵空間,那麼不能設置過期時間
if key not in redisDb.dict:
return 0
// 在過期字典中關聯鍵和過期時間
redisDb.expires[key} = expire_time_in_ms
// 過期時間設置成功
return 1
* 過期鍵刪除策略
redis默認刪除策略組合是(惰性刪除 + 定期刪除)。
- 定時刪除
策略:在設置鍵的過期時間的同時,創建一個定時器,讓定時器在鍵的過期時間來臨時,立即執行對鍵的刪除操作。
優點:對內存友好,保證過期鍵會儘可能快地被刪除,並釋放過期鍵所佔用的內存。
缺點:對CPU時間不友好,佔用太多CPU時間,影響服務器的響應時間和吞吐量。
- 惰性刪除
策略:放任過期鍵不管,每次從鍵空間讀寫操作時,都檢查鍵是否過期,如果過期,刪除該鍵,如果沒有過期,返回該鍵。
優點:對CPU時間友好,讀寫操作鍵時纔對鍵進行過期檢查,刪除過期鍵的操作只會在非做不可的情況下進行。
缺點:對內存不友好,只要鍵不刪除,就不會釋放內存,浪費太多內存,有內存泄漏風險。
實現:所有讀寫數據庫的redis命令在執行前都會調用expireIfNeeded函數對輸入鍵進行檢查,如果輸入鍵已經過期,那麼expireIfNeeded函數就將過期鍵刪除;如果輸入鍵未過期,那麼expireIfNeeded函數不作爲。
- 定期刪除
策略:對定時刪除策略和惰性刪除策略的一種整合和折中。每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響;通過定期刪除過期鍵,有效減少了因爲過期鍵而帶來的內存浪費。
難點:確定刪除操作執行的時長和頻率。執行太頻繁,執行時間過長,就會退化成定時刪除策略;執行得太少,執行時間太短,又會和惰性刪除策略一樣存在內存浪費的情況。
redis服務器使用惰性刪除和定期刪除兩種策略,通過配合使用,很好地在合理使用CPU時間和避免浪費內存之間取得平衡。
實現:
1)在規定的時間內,分多次遍歷服務器中的各個數據庫;
2)從數據庫的expires字典中隨機檢查一部分鍵的過期時間;
3)刪除其中的過期鍵。
僞代碼:
# 默認每次檢查的數據庫數量
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
* RDB功能對過期鍵的處理
如果服務器開啓RDB功能。
- 生成RDB文件時:
在執行SAVE命令或者BGSAVE命令創建一個新的RDB文件時,程序會對數據庫中的鍵進行檢查,已過期的鍵不會被保存到新創建的RDB文件中。- 載入RDB文件時:
1)如果服務器以主服務器模式運行,那麼載入RDB文件時,程序會對文件中保存的鍵進行檢查,未過期的鍵會被載入到數據庫中,而過期的鍵則被忽略,所以過期鍵對載入RDB文件的主服務器不會造成影響。
2)如果服務器以從服務器模式運行,那麼載入RDB文件時,文件中保存的所有鍵,不論是否過期,都會被載入到數據庫中。不過,主服務器在進行數據同步的時候,從服務器的數據庫就會被清空,所以一般來講,過期鍵對載入RDB文件的從服務器也不會造成影響。
* AOF功能對過期鍵的處理
如果服務器以AOF持久化模式運行。
如果數據庫中的某個鍵已經過期,但它還沒有被惰性刪除或者定期刪除,那麼AOF文件不會因爲這個過期鍵而產生任何影響。
當過期鍵被惰性刪除或者定期刪除之後,程序會向AOF文件中追加一條DEL命令,來顯示地記錄該鍵已被刪除。
AOF重寫的過程中,程序會對數據庫中的鍵進行檢查,已經過期的鍵不會被保存到重寫後的AOF文件中。
* 複製功能對過期鍵的處理
主服務器在刪除一個過期鍵之後,會顯示地向所有的從服務器發送一個DEL命令,告知從服務器刪除這個過期鍵。
從服務器在執行客戶端發送的讀命令時,即使碰到過期鍵也不會將過期鍵刪除,而是繼續像處理未過期的鍵一樣來處理過期鍵。
從服務器只有接收到主服務器發來的DEL命令後,纔會刪除過期鍵。
參考文獻:《redis設計與實現》