前言
最近在需求開發(fā)中又用到了我們熟知的Redis字符串操作SET命令,可以設置指定key的值value及該key的生存時間(Time To Live,TTL)。相關命令的語法如下:
set key value [EX seconds] [PX milliseconds] [NX|XX] //TTL由EX指定秒或PX指定毫秒
expire key seconds //expire指定TTL,單位為秒
pexpire key milliseconds //pexpire指定TTL,單位為毫秒
expireat key timestamp //expireat指定過期時間戳,單位為秒
pexpireat key milliseconds-timestamp //pexpireat指定過期時間戳,單位為毫秒
TTL key //key的剩余生存時間,單位是秒
PTTL key //key的剩余生存時間,單位是毫秒
PERSIST key //移除key的過期時間
這些命令用起來挺熟練,可轉念一想,Redis中鍵的自動過期是如何實現(xiàn)的呢?在翻閱資料及源碼的基礎上,本文主要從過期時間處理、自動刪除過期鍵策略等方面簡要介紹該功能的實現(xiàn)。
鍵的過期時間處理
設置過期時間
前言中提到,Redis有四個不同命令可以用于設置鍵的生存時間或過期時間。
可以通過EXPIRE或PEXPIRE命令設置該key的生存時間(Time To Live,TTL),在經(jīng)過指定秒或毫秒后,Redis服務器就會自動刪除生存時間為0的鍵key。
同時可以使用EXPIREAT或PEXPIREAT命令給鍵key設置過期時間(expire time)。
雖然命令形式多樣,但實際上EXPIRE、PEXPIRE、EXPIREAT三個命令都是使用PEXPIREAT命令來實現(xiàn)的,轉換方法很簡單,就是將過期時間換算成時間戳,并保持時間單位統(tǒng)一。
保存過期時間
redisDb結構的expires字典保存了數(shù)據(jù)庫中所有鍵的過期時間,被稱為“過期字典”。
- 過期字典是一個指針,指向鍵空間中的某個對象(也即時某個數(shù)據(jù)庫鍵)。
- 過期字典的值是一個long long類型的整數(shù),保存了鍵所指向的數(shù)據(jù)庫鍵的過期時間(一個毫秒精度的UNIX時間戳)。
移除過期時間
PERSIST命令可以移除一個鍵的過期時間,實際就是PEXPIREAT命令的反操作:PERSIST命令在過期字典中查找給定的鍵,并解除鍵和值(過期時間)在過期字典中的關聯(lián)。
計算并返回剩余生存時間
可以使用TTL或PTTL命令查找給定鍵key的剩余生存時間(key距離被服務器刪除還剩多少秒/毫秒),兩個命令都是通過計算鍵的過期時間和當前時間的差值實現(xiàn)的。
過期鍵的判定
Redis通過查詢過期字典的方式檢查一個給定鍵是否過期:
- 檢查給定鍵是否存在于過期字典:如果存在,那么取得鍵的過期時間;
- 檢查當前UNIX時間戳是否大于鍵的過期時間:如果是的話,name鍵已過期;否則鍵未過期。
過期鍵刪除策略
常見的三種刪除策略對比
| 刪除策略 | 實現(xiàn) | 優(yōu)點 | 缺點 |
|---|---|---|---|
| 定時刪除 | 設置鍵的過期時間的同時,創(chuàng)建一個定時器(Timer),讓定時器在鍵的過期時間來臨時,立即執(zhí)行對鍵的刪除操作。 | 內存占用率低,通過使用定時器,可以保證過期鍵會盡可能快地被刪除,并釋放過期鍵所占的內存。 | 占用較多cpu時間,影響服務器的響應時間和吞吐量。 |
| 惰性刪除 | 放任鍵過期不管,但是每次獲取鍵時,都檢查鍵是否已過期,如果過期則刪除該鍵;否則返回該鍵。 | cpu占用率低,只會在取出鍵時才進行過期檢查,可以保證刪除的目標僅限于當前的鍵,不會在其它過期鍵上花費任何cpu時間。 | 浪費內存,有內存泄漏的風險。 |
| 定期刪除 | 每隔一段時間就對數(shù)據(jù)庫做一次過期鍵的刪除。但每次要刪除多少過期鍵、要檢查多少個db,則由算法決定。 | 是前兩種策略的整合和折中,減少了內存和cpu的無謂占用。 | 難以確定刪除操作執(zhí)行的時長和頻率。 |
Redis的過期鍵刪除策略
Redis服務器實際使用的是惰性刪除和定期刪除兩種策略:通過配合使用這兩種策略,服務器可以很好地在合理使用cpu和避免浪費內存空間之間取得平衡。
惰性刪除策略的實現(xiàn)
過期鍵的惰性刪除策略由db.c/expireIfNeeded函數(shù)實現(xiàn),所有讀寫數(shù)據(jù)庫的Redis命令在執(zhí)行之前都會調用expireIfNeeded函數(shù)對輸入鍵進行檢查:
- 如果輸入鍵已經(jīng)過期,那么expireIfNeeded函數(shù)將輸入鍵從數(shù)據(jù)庫中刪除。
- 如果輸入鍵未過期,那么expireIfNeeded函數(shù)不做動作。
expireIfNeeded函數(shù)就像一個過濾器,它可以在命令真正執(zhí)行之前,過濾掉過期的輸入鍵,從而避免命令接觸到過期鍵。另外,因為每個被訪問的鍵都可能因為過期而被expireIfNeeded函數(shù)刪除,所以每個命令的實現(xiàn)函數(shù)都必須能同時處理鍵存在和不存在的情況:
- 當鍵存在時,命令按照鍵存在的情況執(zhí)行。
- 當鍵不存在或者鍵因為過期而被expireIfNeeded函數(shù)刪除時,命令按照鍵不存在的情況執(zhí)行。
定期刪除策略的實現(xiàn)
過期鍵的定期刪除策略由redis.c/activeExpireCycle函數(shù)實現(xiàn),每當Redis的服務器周期性操作redis.c/serverCron函數(shù)執(zhí)行時,activeExpireCycle函數(shù)就會被調用,它在規(guī)定的時間內,分多次遍歷服務器中的各個數(shù)據(jù)庫,從數(shù)據(jù)庫的expires字典中隨機檢查一部分鍵的過期時間,并刪除其中的過期鍵。
activeExpireCycle函數(shù)的工作模式可以總結如下:
- 函數(shù)每次運行時,都從一定數(shù)量的數(shù)據(jù)庫(取min(默認16,實際數(shù)量))中取出一定數(shù)量的隨機鍵(默認20)進行檢查,并刪除其中的過期鍵。
- 全局變量current_db會記錄當前activeExpireCycle函數(shù)檢查的進度,并在下一次activeExpireCycle函數(shù)調用時,接著上一次的進度進行處理。
- 隨著activeExpireCycle函數(shù)的不斷執(zhí)行,服務器中的所有db都會被檢查一遍,這時函數(shù)將current_db變量重置為0,然后進行新一輪的定期刪除。
小結
本文對Redis中鍵過期功能的實現(xiàn)做了一個簡要介紹,相信讀者看完之后會對大致的實現(xiàn)方案有所了解,但更多細節(jié)推薦閱讀《Redis涉及與實現(xiàn)》,當然想自己去研究源碼更好啦。