作為一種定期清理無效資料的重要機制,主鍵失效存在于大多數緩存系統中,reids也不例外。在redis提供的諸多指令中,expire、expireat、pexpire、pexpireat以及setex和psetex均可以用來設定一條key-value對的失效時間,而一條key-value對一旦被關聯了失效時間就會在到期後自動删除(或者說變得無法通路更為準确)。可以說,主鍵失效這個概念還是比較容易了解的,但是在具體實作到redis中又是如何呢?最近本部落客就對redis中的主鍵失效機制産生了幾個疑問,并根據這些疑問對其進行了仔細的探究,現總結所得如下,以飨各位看客。
一、除了調用persist指令外,還有沒有其他情況會撤銷一個主鍵的失效時間?答案是肯定的。首先,在通過del指令删除一個主鍵時,失效時間自然會被撤銷(這不是廢話麼,哈哈)。其次,在一個設定了失效時間的主鍵被更新覆寫時,該主鍵的失效時間也會被撤銷(這貌似也是廢話,哈哈)。但需要注意的是,這裡所說的是主鍵被更新覆寫,而不是主鍵對應的value被更新覆寫,是以set、mset或者是getset可能會導緻主鍵被更新覆寫,而像incr、decr、lpush、hset等都是更新主鍵對應的值,這類操作是不會觸碰主鍵的失效時間的。此外,還有一個特殊的指令就是rename,當我們使用rename對一個主鍵進行重命名後,之前關聯的失效時間會自動傳遞給新的主鍵,但是如果一個主鍵是被rename所覆寫的話(如主鍵hello可能會被指令rename
world hello所覆寫),這時被覆寫主鍵的失效時間會被自動撤銷,而新的主鍵則繼續保持原來主鍵的特性。
二、redis中的主鍵失效是如何實作的,即失效的主鍵是如何删除的?實際上,redis删除失效主鍵的方法主要有兩種:1)消極方法(passive
way),在主鍵被通路時如果發現它已經失效,那麼就删除它;2)積極方法(active
way),周期性地從設定了失效時間的主鍵中選擇一部分失效的主鍵删除。接下來我們就通過代碼來探究一下這兩種方法的具體實作,但在此之前,我們先看一看redis是如何管理和維護主鍵的吧(注:本博文中的源碼全部來自redis-2.6.12)。
代碼段一給出了redis中關于資料庫的結構體定義,這個結構體定義中除了id以外都是指向字典的指針,其中我們隻看dict和expires,前者用來維護一個redis資料庫中包含的所有key-value對(其結構可以了解為dict[key]:value,即主鍵與值之間的映射),後者則用于維護一個redis資料庫中設定了失效時間的主鍵(其結構可以了解為expires[key]:timeout,即主鍵與失效時間的映射)。當我們使用setex和psetex指令向系統插入資料時,redis首先将key和value添加到dict這個字典表中,然後将key和失效時間添加到expires這個字典表中。當我們使用expire、expireat、pexpire和pexpireat指令設定一個主鍵的失效時間時,redis首先到dict這個字典表中查找要設定的主鍵是否存在,如果存在就将這個主鍵和失效時間添加到expires這個字典表。簡單地總結來說就是,設定了失效時間的主鍵和具體的失效時間全部都維護在expires這個字典表中。
代碼段一:
typedef struct redisdb {
dict *dict;
dict *expires;
dict *blocking_keys;
dict *ready_keys;
dict *watched_keys;
int id;
} redisdb;
在大緻了解了redis是如何維護設定了失效時間的主鍵之後,我們就先來看一看redis是如何實作消極地删除失效主鍵的。代碼段二給出了一個名為expireifneeded的函數,這個函數在任何通路資料的函數中都會被調用,也就是說redis在實作get、mget、hget、lrange等所有涉及到讀取資料的指令時都會調用它,它存在的意義就是在讀取資料之前先檢查一下它有沒有失效,如果失效了就删除它。代碼段二中給出了expireifneeded函數的所有相關描述,這裡就不再重複它的實作方法了。這裡需要說明的是在expireifneeded函數中調用的另外一個函數propagateexpire,這個函數用來在正式删除失效主鍵之前廣播這個主鍵已經失效的資訊,這個資訊會傳播到兩個目的地:一個是發送到aof檔案,将删除失效主鍵的這一操作以del
key的标準指令格式記錄下來;另一個就是發送到目前redis伺服器的所有slave,同樣将删除失效主鍵的這一操作以del
key的标準指令格式告知這些slave删除各自的失效主鍵。從中我們可以知道,所有作為slave來運作的redis伺服器并不需要通過消極方法來删除失效主鍵,它們隻需要對master唯命是從就ok了!
代碼段二:
int expireifneeded(redisdb *db, robj *key) {
擷取主鍵的失效時間
long long when = getexpire(db,key);
假如失效時間為負數,說明該主鍵未設定失效時間(失效時間預設為-1),直接傳回0
if (when < 0) return 0;
假如redis伺服器正在從rdb檔案中加載資料,暫時不進行失效主鍵的删除,直接傳回0
if (server.loading) return 0;
假如目前的redis伺服器是作為slave運作的,那麼不進行失效主鍵的删除,因為slave
上失效主鍵的删除是由master來控制的,但是這裡會将主鍵的失效時間與目前時間進行
一下對比,以告知調用者指定的主鍵是否已經失效了
if (server.masterhost != null) {
return mstime() > when;
}
如果以上條件都不滿足,就将主鍵的失效時間與目前時間進行對比,如果發現指定的主鍵
還未失效就直接傳回0
if (mstime() <= when) return 0;
如果發現主鍵确實已經失效了,那麼首先更新關于失效主鍵的統計個數,然後将該主鍵失
效的資訊進行廣播,最後将該主鍵從資料庫中删除
server.stat_expiredkeys++;
propagateexpire(db,key);
return dbdelete(db,key);
}
代碼段三:
void propagateexpire(redisdb *db, robj *key) {
robj *argv[2];
shared.del是在redis伺服器啟動之初就已經初始化好的一個常用redis對象,即del指令
argv[0] = shared.del;
argv[1] = key;
incrrefcount(argv[0]);
incrrefcount(argv[1]);
檢查redis伺服器是否開啟了aof,如果開啟了就為失效主鍵記錄一條del日志
if (server.aof_state != redis_aof_off)
feedappendonlyfile(server.delcommand,db->id,argv,2);
檢查redis伺服器是否擁有slave,如果是就向所有slave發送del失效主鍵的指令,這就是
上面expireifneeded函數中發現自己是slave時無需主動删除失效主鍵的原因了,因為它
隻需聽從master發送過來的指令就ok了
if (listlength(server.slaves))
replicationfeedslaves(server.slaves,db->id,argv,2);
decrrefcount(argv[0]);
decrrefcount(argv[1]);
以上我們通過對expireifneeded函數的介紹了解了redis是如何以一種消極的方式删除失效主鍵的,但是僅僅通過這種方式顯然是不夠的,因為如果某些失效的主鍵遲遲等不到再次通路的話,redis就永遠不會知道這些主鍵已經失效,也就永遠也不會删除它們了,這無疑會導緻記憶體空間的浪費。是以,redis還準備了一招積極的删除方法,該方法利用redis的時間事件來實作,即每隔一段時間就中斷一下完成一些指定操作,其中就包括檢查并删除失效主鍵。這裡我們說的時間事件的回調函數就是servercron,它在redis伺服器啟動時建立,每秒的執行次數由宏定義redis_default_hz來指定,預設每秒鐘執行10次。代碼段四給出該時間事件建立時的程式代碼,該代碼在redis.c檔案的initserver函數中。實際上,servercron這個回調函數不僅要進行失效主鍵的檢查與删除,還要進行統計資訊的更新、用戶端連接配接逾時的控制、bgsave和aof的觸發等等,這裡我們僅關注删除失效主鍵的實作,也就是函數activeexpirecycle。
代碼段四:
if(aecreatetimeevent(server.el, 1, servercron, null, null) == ae_err) {
redispanic("create time event failed");
exit(1);
代碼段五給出了函數activeexpirecycle的實作及其較長的描述,其主要實作原理就是周遊處理redis伺服器中每個資料庫的expires字典表中,從中嘗試着随機抽樣redis_expirelookups_per_cron(預設值為10)個設定了失效時間的主鍵,檢查它們是否已經失效并删除掉失效的主鍵,如果失效的主鍵個數占本次抽樣個數的比例超過25%,redis會認為目前資料庫中的失效主鍵依然很多,是以它會繼續進行下一輪的随機抽樣和删除,直到剛才的比例低于25%才停止對目前資料庫的處理,轉向下一個資料庫。這裡我們需要注意的是,activeexpirecycle函數不會試圖一次性處理redis中的所有資料庫,而是最多隻處理redis_dbcron_dbs_per_call(預設值為16),此外activeexpirecycle函數還有處理時間上的限制,不是想執行多久就執行多久,凡此種種都隻有一個目的,那就是避免失效主鍵删除占用過多的cpu資源。代碼段五有對activeexpirecycle所有代碼的較長的描述,從中可以了解該函數的具體實作方法。
代碼段五:
void activeexpirecycle(void) {
因為每次調用activeexpirecycle函數不會一次性檢查所有redis資料庫,是以需要記錄下
每次函數調用處理的最後一個redis資料庫的編号,這樣下次調用activeexpirecycle函數
還可以從這個資料庫開始繼續處理,這就是current_db被聲明為static的原因,而另外一
個變量timelimit_exit是為了記錄上一次調用activeexpirecycle函數的執行時間是否達
到時間限制了,是以也需要聲明為static
static unsigned int current_db = 0;
static int timelimit_exit = 0;
unsigned int j, iteration = 0;
每次調用activeexpirecycle函數處理的redis資料庫個數為redis_dbcron_dbs_per_call
unsigned int dbs_per_call = redis_dbcron_dbs_per_call;
long long start = ustime(), timelimit;
如果目前redis伺服器中的資料庫個數小于redis_dbcron_dbs_per_call,則處理全部資料庫,
如果上一次調用activeexpirecycle函數的執行時間達到了時間限制,說明失效主鍵較多,也
會選擇處理全部資料庫
if (dbs_per_call > server.dbnum || timelimit_exit)
dbs_per_call = server.dbnum;
執行activeexpirecycle函數的最長時間(以微秒計),其中redis_expirelookups_time_perc
是機關時間内能夠配置設定給activeexpirecycle函數執行的cpu時間比例,預設值為25,server.hz
即為一秒内activeexpirecycle的調用次數,是以這個計算公式更明白的寫法應該是這樣的,即
(1000000 * (redis_expirelookups_time_perc / 100)) / server.hz
timelimit = 1000000*redis_expirelookups_time_perc/server.hz/100;
timelimit_exit = 0;
if (timelimit <= 0) timelimit = 1;
周遊處理每個redis資料庫中的失效資料
for (j = 0; j < dbs_per_call; j++) {
int expired;
redisdb *db = server.db+(current_db % server.dbnum);
此處立刻就将current_db加一,這樣可以保證即使這次無法在時間限制内删除完所有目前
資料庫中的失效主鍵,下一次調用activeexpirecycle一樣會從下一個資料庫開始處理,
進而保證每個資料庫都有被處理的機會
current_db++;
開始處理目前資料庫中的失效主鍵
do {
unsigned long num, slots;
long long now;
如果expires字典表大小為0,說明該資料庫中沒有設定失效時間的主鍵,直接檢查下
一資料庫
if ((num = dictsize(db->expires)) == 0) break;
slots = dictslots(db->expires);
now = mstime();
如果expires字典表不為空,但是其填充率不足1%,那麼随機選擇主鍵進行檢查的代價
會很高,是以這裡直接檢查下一資料庫
if (num && slots > dict_ht_initial_size &&
(num*100/slots < 1)) break;
expired = 0;
如果expires字典表中的entry個數不足以達到抽樣個數,則選擇全部key作為抽樣樣本
if (num > redis_expirelookups_per_cron)
num = redis_expirelookups_per_cron;
while (num--) {
dictentry *de;
long long t;
随機擷取一個設定了失效時間的主鍵,檢查其是否已經失效
if ((de = dictgetrandomkey(db->expires)) == null) break;
t = dictgetsignedintegerval(de);
if (now > t) {
發現該主鍵确實已經失效,删除該主鍵
sds key = dictgetkey(de);
robj *keyobj = createstringobject(key,sdslen(key));
同樣要在删除前廣播該主鍵的失效資訊
propagateexpire(db,keyobj);
dbdelete(db,keyobj);
decrrefcount(keyobj);
expired++;
server.stat_expiredkeys++;
}
}
每進行一次抽樣删除後對iteration加一,每16次抽樣删除後檢查本次執行時間是否
已經達到時間限制,如果已達到時間限制,則記錄本次執行達到時間限制并退出
iteration++;
if ((iteration & 0xf) == 0 &&
(ustime()-start) > timelimit)
{
timelimit_exit = 1;
return;
如果失效的主鍵數占抽樣數的百分比大于25%,則繼續抽樣删除過程
} while (expired > redis_expirelookups_per_cron/4);
)!
四、redis的主鍵失效機制會不會影響系統性能?通過以上對redis主鍵失效機制的介紹,我們知道雖然redis會定期地檢查設定了失效時間的主鍵并删除已經失效的主鍵,但是通過對每次處理資料庫個數的限制、activeexpirecycle函數在一秒鐘内執行次數的限制、配置設定給activeexpirecycle函數cpu時間的限制、繼續删除主鍵的失效主鍵數百分比的限制,redis已經大大降低了主鍵失效機制對系統整體性能的影響,但是如果在實際應用中出現大量主鍵在短時間内同時失效的情況還是會使得系統的響應能力降低,是以這種情況無疑應該避免。
<a href="http://www.cnblogs.com/tangtianfly/archive/2012/05/02/2479315.html">http://www.cnblogs.com/tangtianfly/archive/2012/05/02/2479315.html</a>
原文連結:[http://wely.iteye.com/blog/2361617]