Redis源码剖析(四)过期键的删除策略
Redis是支持時間事件的,所謂時間事件,是為某個鍵值對設置過期時間,時間一到,Redis會自動刪除該鍵值對。例如使用SET命令添加字符串類型的鍵值對
127.0.0.1:6379> SET blog redis ex 10 //添加鍵值對<blog, redis>,10秒后刪除 OK 127.0.0.1:6379> GET blog //添加后馬上查找,可以獲取redis "redis" 127.0.0.1:6379> GET blog //上趟廁所回來,發現找不到了 (nil)Redis是如何實現定時刪除的呢,在數據庫結構redisDb中,可以發現除了上篇提到的用于保存鍵值對的dict字典外,另有一個字典變量expires,實際上正是它保存著鍵和其過期時間(絕對時間)。當執行完SET命令后,兩個字典的數據分布為
//server.h typedef struct redisDb {dict *dict; /* 保存鍵值對的字典 */ dict *expires; /* 保存鍵和過期時間 */ int id; /* 數據庫唯一id */ ... } redisDb; dict字典 blog --> redis expires字典 blog --> blog的過期時間設置,獲取,刪除過期時間
以下鍵節點指字典中的哈希節點,保存鍵和值
設置鍵的過期時間
//db.c /* * 設置鍵的過期時間* db : 數據庫* key : 鍵* when : 過期時間(絕對時間)*/ void setExpire(redisDb *db, robj *key, long long when) {dictEntry *kde, *de;/* Reuse the sds from the main dict in the expire dict *//* 從數據字典中尋找鍵節點 */kde = dictFind(db->dict,key->ptr);serverAssertWithInfo(NULL,key,kde != NULL);/* 從時間字典中尋找鍵節點,如果不存在則創建一個 */de = dictReplaceRaw(db->expires,dictGetKey(kde));/* 設置鍵節點的值,值為過期時間(絕對時間) */dictSetSignedIntegerVal(de,when); }dictSetSignedIntegerVal是宏定義,設置鍵節點de的值為when。因為哈希節點中的值結構是聯合,可以存儲不同大小的數字,也可以通過void*指針存儲其它類型,這里過期時間是long long類型,所以可以存在int64_t類型上
//dict.h #define dictSetSignedIntegerVal(entry, _val_) \do { entry->v.s64 = _val_; } while(0)獲取鍵的過期時間
//db.c /* 返回鍵的過期時間 */ long long getExpire(redisDb *db, robj *key) {dictEntry *de;/* 從時間字典中查找匹配的鍵節點 */if (dictSize(db->expires) == 0 ||(de = dictFind(db->expires,key->ptr)) == NULL) return -1;serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);/* 返回鍵節點對應的值 */return dictGetSignedIntegerVal(de); }刪除鍵的過期時間
//db.c /* 移除鍵的過期時間 */ int removeExpire(redisDb *db, robj *key) {serverAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);/* 從時間字典中將鍵刪除 */return dictDelete(db->expires,key->ptr) == DICT_OK; }上面三個函數都是調用字典dict的接口,比較好理解
過期鍵刪除策略
對于過期鍵值對的刪除有三種策略,分別是
- 定時刪除,設置一個定時器和回調函數,時間一到就調用回調函數刪除鍵值對。優點是及時刪除,缺點是需要為每個鍵值對都設置定時器,比較麻煩(其實可以用timer_fd的,參考muduo定時任務的實現)
- 惰性刪除,只有當再次訪問該鍵時才判斷是否過期,如果過期將其刪除。優點是不需要為每個鍵值對進行時間監聽,缺點是如果這個鍵值對一直不被訪問,那么即使過期也會一直殘留在數據庫中,占用不必要的內存
- 周期刪除,每隔一段時間執行一次刪除過期鍵值對的操作。優點是既不需要監聽每個鍵值對導致占用CPU,也不會一直不刪除導致占用內存,缺點是不容易確定刪除操作的執行時長和頻率
Redis采用惰性刪除和周期刪除兩種策略,通過配合使用,服務器可以很好的合理使用CPU時間和避免內不能空間的浪費
惰性刪除
惰性刪除是指在對每一個鍵進行讀寫操作時,先判斷一下這個鍵是否已經過期,如果過期則將其刪除。該操作由expireIfNeeded函數完成
//db.c /* 判斷鍵key是否已過期,如果過期將其從數據庫中刪除 */ int expireIfNeeded(redisDb *db, robj *key) {/* 獲取鍵的過期時間*/mstime_t when = getExpire(db,key);mstime_t now;/* 該鍵沒有設置過期時間 */if (when < 0) return 0; /* No expire for this key *//* Don't expire anything while loading. It will be done later. */if (server.loading) return 0;/* 獲取當前時間,lua腳本相關 */now = server.lua_caller ? server.lua_time_start : mstime();if (server.masterhost != NULL) return now > when;/* 當前時間小于過期時間,該鍵沒有過期,不需要刪除 */if (now <= when) return 0;/* 執行到這里,說明這個鍵已過期,需要刪除 *//* 過期鍵數量加一 */server.stat_expiredkeys++;propagateExpire(db,key);notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",key,db->id);/* 從數據字典和時間字典中刪除(即從數據庫中刪除,因為該鍵在兩個字典中,所以需要刪除兩個) */return dbDelete(db,key); }expireIfNeeded函數只是判斷是否需要刪除鍵節點,實際刪除操作由dbDelete函數完成
該函數調用字典的刪除接口完成刪除操作,該接口在上一篇有提到過
//db.c /* 將鍵key從數據庫中刪除 */ int dbDelete(redisDb *db, robj *key) {/* Deleting an entry from the expires dict will not free the sds of* the key, because it is shared with the main dictionary. *//* 從時間字典中刪除 */if (dictSize(db->expires) > 0) dictDelete(db->expires,key->ptr);/* 從數據字典中刪除 */if (dictDelete(db->dict,key->ptr) == DICT_OK) {/* 集群相關 */if (server.cluster_enabled) slotToKeyDel(key);return 1;} else {return 0;} }周期刪除
Redis服務器會周期性地執行server.c/serverCron函數,在這個函數中執行的databasesCron函數會調用activeExpireCycle函數,這個函數在時間字典(expires)中隨機選擇若干鍵節點,判斷其是否過期,如果過期則將其刪除
//server.c /* 隨機選擇若干鍵節點判斷其是否過期,如果過期將其刪除 */ void activeExpireCycle(int type) {...while (num--) {dictEntry *de;long long ttl;/* 在時間字典中隨機選擇一個鍵節點 */if ((de = dictGetRandomKey(db->expires)) == NULL) break;/* 獲取鍵節點的值,即過期時間 */ttl = dictGetSignedIntegerVal(de)-now;/* 判斷是否過期,如果過期,刪除 */if (activeExpireCycleTryExpire(db,de,now)) expired++;if (ttl > 0) {/* We want the average TTL of keys yet not expired. */ttl_sum += ttl;ttl_samples++;}}... }隨機選擇鍵節點后,調用activeExpireCycleTryExpire判斷其是否過期
//server.c /* * 判斷鍵節點是否過期,如果過期將其從數據庫中刪除 * db : 數據庫* de : 鍵節點* now : 當前時間*/ int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {/* 獲取鍵的過期時間 */long long t = dictGetSignedIntegerVal(de);/* 判斷是否過期 */if (now > t) {/* 從鍵節點中取出鍵 */sds key = dictGetKey(de);/* 因為Redis中鍵默認都是sds存儲的,所以這里需要將其轉化為robj*格式以滿足函數傳參 */robj *keyobj = createStringObject(key,sdslen(key));propagateExpire(db,keyobj);/* 將鍵和其對應的值從數據庫中刪除 */dbDelete(db,keyobj);notifyKeyspaceEvent(NOTIFY_EXPIRED,"expired",keyobj,db->id);/* 鍵的引用計數減一,因為是剛創建的,所以引用計數就是1,這里會將keyobj對象刪除 */decrRefCount(keyobj);/* 過期鍵個數加一 */server.stat_expiredkeys++;return 1;} else {return 0;} }隨機選擇鍵節點
隨機選擇鍵節點是字典的接口,該函數利用隨機函數選擇一個下標,確保當前下標下存在鍵節點后進行第二次隨機,選擇該下標下的某個鍵節點返回,函數定義如下
/* 隨機返回一個鍵節點 */ dictEntry *dictGetRandomKey(dict *d) {dictEntry *he, *orighe;unsigned int h;int listlen, listele;/* 字典中沒有鍵節點,返回 */if (dictSize(d) == 0) return NULL;/* 處于rehash狀態,執行一步rehash */if (dictIsRehashing(d)) _dictRehashStep(d);/* 如果處于rehash狀態,那么隨機操作在ht[0]和ht[1]兩個字典中進行 *//* 隨機選擇一個下標,該下標下存在鍵節點 */if (dictIsRehashing(d)) {do {h = d->rehashidx + (random() % (d->ht[0].size +d->ht[1].size -d->rehashidx));he = (h >= d->ht[0].size) ? d->ht[1].table[h - d->ht[0].size] :d->ht[0].table[h];} while(he == NULL);} else {do {h = random() & d->ht[0].sizemask;he = d->ht[0].table[h];} while(he == NULL);}listlen = 0;orighe = he;/* 計算隨機的下標下的鍵節點個數 */while(he) {he = he->next;listlen++;}/* 隨機選擇一個鍵節點返回 */listele = random() % listlen;he = orighe;while(listele--) he = he->next;return he; }小結
Redis允許為每個鍵值對設置過期時間,時間一到會將其從數據庫中刪除。Redis內部采用惰性刪除和周期刪除兩種策略結合的方法刪除過期鍵,其中惰性刪除是指當訪問鍵時才判斷該鍵是否過期,周期刪除是每隔一定時間進行一次集中刪除,一次集中刪除隨機判斷一定數量的鍵是否過期并將過期鍵刪除
總結
以上是生活随笔為你收集整理的Redis源码剖析(四)过期键的删除策略的全部內容,希望文章能夠幫你解決所遇到的問題。
- 上一篇: Redis源码剖析(三)字典结构的设计与
- 下一篇: 每天一道LeetCode-----计算字