在 Redis 中,鍵的過期時間設計與實現是一個重要的功能,這使得 Redis 可以自動刪除在指定時間後不再需要的鍵。下面詳細介紹 Redis 過期時間的設計和實現,包括設定過期時間、過期鍵的儲存結構、過期鍵的刪除策略等。
1. 設定過期時間
Redis 提供了多個命令來設定鍵的過期時間,如 EXPIRE
、PEXPIRE
、EXPIREAT
和 PEXPIREAT
。這些命令可以以秒或毫秒為單位設定鍵的過期時間,也可以設定具體的過期時間點。
EXPIRE key seconds
PEXPIRE key milliseconds
EXPIREAT key timestamp
PEXPIREAT key milliseconds-timestamp
示例:
void expireCommand(client *c) { long long seconds; if (getLongLongFromObjectOrReply(c, c->argv[2], &seconds, NULL) != C_OK) return; setExpire(c, c->db, c->argv[1], mstime() + seconds*1000); addReply(c, shared.cone); }
2. 過期鍵的儲存結構
每個 Redis 資料庫例項(redisDb
)中都有一個名為 expires
的字典,用於儲存鍵的過期時間。這個字典將鍵指標對映到以毫秒為單位的到期時間點。
typedef struct redisDb { dict *dict; // 主字典,儲存所有鍵值對 dict *expires; // 過期字典,儲存鍵的過期時間 ... } redisDb;
3. 設定過期時間
透過 setExpire
函式設定鍵的過期時間。如果鍵已經存在於 expires
字典中,則更新其過期時間;否則,將其新增到 expires
字典中。
void setExpire(client *c, redisDb *db, robj *key, long long when) { dictEntry *de = dictFind(db->dict, key->ptr); if (de == NULL) return; /* Set the new expire time */ if (dictAdd(db->expires, dictGetKey(de), (void*)when) == DICT_ERR) { dictReplace(db->expires, dictGetKey(de), (void*)when); } }
4. 刪除過期鍵的策略
Redis 採用了以下三種策略來刪除過期鍵:
惰性刪除(Lazy Deletion) :每次訪問鍵時檢查其是否過期,如果已過期則刪除。這樣只在訪問鍵時才進行過期檢查,節省了資源。
robj *lookupKeyRead(redisDb *db, robj *key) { robj *val; expireIfNeeded(db,key); // 檢查並刪除過期鍵 val = lookupKey(db,key,LOOKUP_NONE); return val ? val : NULL; }
定期刪除(Periodic Deletion) :Redis 會週期性地隨機抽取一定數量的鍵進行過期檢查,並刪除其中已過期的鍵。這一過程由後臺任務定期執行,確保儘可能多的過期鍵被及時刪除。
int activeExpireCycle(int type) { unsigned int current_db = server.dbnum; long long start = ustime(); long long timelimit = 1000000; // 1秒 int dbs_per_call = CRON_DBS_PER_CALL; current_db = server.current_db; while(dbs_per_call--) { redisDb *db = server.db + (current_db % server.dbnum); activeExpireCycleTryExpire(db, cycle_tickets); current_db++; } long long elapsed = ustime()-start; return elapsed > timelimit; }
主動刪除(Active Expiration) :在記憶體使用接近最大限制時,會觸發主動刪除策略,透過掃描所有庫的鍵刪除過期資料,以確保記憶體使用量保持在設定範圍內。
void evictExpiredKeys() { for (int j = 0; j < server.dbnum; j++) { redisDb *db = server.db+j; scanDatabaseForExpiredKeys(db); } }
Redis 預設採用以下兩種刪除過期鍵策略:
惰性刪除(Lazy Deletion) :每次訪問某個鍵時檢查其是否過期,如果過期則刪除。
定期刪除(Periodic Deletion) :後臺任務定期掃描資料庫中的鍵,隨機抽取部分鍵進行過期檢查並刪除其中已過期的鍵。
5. 檢查並刪除過期鍵
expireIfNeeded
函式用於檢查某個鍵是否過期,如果過期則刪除該鍵。
int expireIfNeeded(redisDb *db, robj *key) { mstime_t when = getExpire(db, key); if (when < 0) return 0; if (mstime() > when) { server.stat_expiredkeys++; propagateExpire(db,key); dbDelete(db,key); return 1; } else { return 0; } }
getExpire
:從expires
字典中獲取鍵的過期時間。mstime
:返回當前的毫秒時間戳。如果鍵已過期,則呼叫
dbDelete
刪除該鍵,並增加統計計數器stat_expiredkeys
。
6. 獲取過期時間
getExpire
函式用於獲取鍵的過期時間,如果鍵沒有設定過期時間則返回 -1。
mstime_t getExpire(redisDb *db, robj *key) { dictEntry *de; if (dictSize(db->expires) == 0 || (de = dictFind(db->expires, key->ptr)) == NULL) return -1; return (mstime_t)dictGetSignedIntegerVal(de); }
總結
Redis 的過期時間設計與實現包括以下幾個關鍵點:
設定過期時間:透過 EXPIRE、PEXPIRE 等命令設定鍵的過期時間,並將過期時間儲存在
expires
字典中。過期字典:每個資料庫例項都有一個
expires
字典,用於儲存鍵的過期時間。刪除策略:
惰性刪除:每次訪問鍵時檢查其是否過期,如果已過期則刪除。
定期刪除:透過後臺任務週期性地檢測並刪除過期鍵。
主動刪除:在記憶體使用接近最大限制時觸發,掃描所有鍵並刪除過期鍵。
定期刪除activeExpireCycle函式詳細解析
void activeExpireCycle(int type) { static unsigned int current_db = 0; // 記錄上一次處理的資料庫索引 static int timelimit_exit = 0; // 用於指示是否超出時間限制 unsigned int j; // 每次要處理的資料庫數量 unsigned int dbs_per_call = CRON_DBS_PER_CALL; long long start = ustime(); // 開始時間 long long timelimit; // 時間限制 if (type == ACTIVE_EXPIRE_CYCLE_FAST) { /* Fast cycle: 1 ms */ timelimit = 1000; } else { /* Slow cycle: 25% CPU time p. DB / Configurable percentage. */ timelimit = server.hz < 100 ? 1000 : 10; if (server.active_expire_effort != 1) timelimit *= server.active_expire_effort-1; timelimit /= server.dbnum; timelimit_exit = 0; } for (j = 0; j < dbs_per_call; j++) { redisDb *db = server.db + (current_db % server.dbnum); current_db++; int expired, sampled; do { long now = mstime(); expireEntry *de; dictEntry *d; /* Sample a few keys in the database */ expired = 0; sampled = 0; while ((de = dictGetRandomKey(db->expires)) != NULL && mstime() - now < timelimit) { long long ttl = dictGetSignedIntegerVal(de) - mstime(); if (ttl < 0) { d = dictFind(db->dict, dictGetKey(de)); dbDelete(db, dictGetKey(d)); server.stat_expiredkeys++; expired++; } sampled++; } } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2); elapsed = ustime() - start; if (elapsed > timelimit) { timelimit_exit = 1; break; } } }
關鍵步驟解析
1. 初始化變數
static unsigned int current_db = 0; static int timelimit_exit = 0; unsigned int j; unsigned int dbs_per_call = CRON_DBS_PER_CALL; long long start = ustime(); long long timelimit;
current_db
:靜態變數,用於記錄上一次處理的資料庫索引。timelimit_exit
:用於指示是否耗盡了時間配額,防止無限迴圈。dbs_per_call
:每次掃描的資料庫數量,通常由配置決定。start
:記錄開始執行此函式的時間戳。timelimit
:本次呼叫允許消耗的最大時間(以微秒為單位)。
2. 確定時間限制
if (type == ACTIVE_EXPIRE_CYCLE_FAST) { timelimit = 1000; // 快速模式:1 毫秒 } else { timelimit = server.hz < 100 ? 1000 : 10; if (server.active_expire_effort != 1) timelimit *= server.active_expire_effort - 1; timelimit /= server.dbnum; timelimit_exit = 0; }
如果是快速模式,時間限制為 1 毫秒。
如果是慢速模式,時間限制根據 Redis 配置和當前伺服器負載情況計算。
server.active_expire_effort
引數可以調整過期鍵清理的力度。
3. 遍歷資料庫
每次呼叫 activeExpireCycle
時,會遍歷一定數量的資料庫,並在每個資料庫中隨機抽取鍵進行過期檢查和刪除。
for (j = 0; j < dbs_per_call; j++) { redisDb *db = server.db + (current_db % server.dbnum); current_db++; int expired, sampled; do { long now = mstime(); expireEntry *de; dictEntry *d; expired = 0; sampled = 0; while ((de = dictGetRandomKey(db->expires)) != NULL && mstime() - now < timelimit) { long long ttl = dictGetSignedIntegerVal(de) - mstime(); if (ttl < 0) { d = dictFind(db->dict, dictGetKey(de)); dbDelete(db, dictGetKey(d)); server.stat_expiredkeys++; expired++; } sampled++; } } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2); elapsed = ustime() - start; if (elapsed > timelimit) { timelimit_exit = 1; break; } }
關鍵點解析:
選擇資料庫:
redisDb *db = server.db + (current_db % server.dbnum); current_db++;
使用迴圈方式選擇下一個要檢查的資料庫例項。
current_db
記錄上一次處理的資料庫索引,透過取模操作確保索引在有效範圍內。初始化變數:
int expired, sampled; expired = 0; sampled = 0;
過期檢查迴圈:
while ((de = dictGetRandomKey(db->expires)) != NULL && mstime() - now < timelimit) { long long ttl = dictGetSignedIntegerVal(de) - mstime(); if (ttl < 0) { d = dictFind(db->dict, dictGetKey(de)); dbDelete(db, dictGetKey(d)); server.stat_expiredkeys++; expired++; } sampled++; }
從
expires
字典中隨機獲取一個鍵de
。檢查當前時間是否超過了本次週期的時間限制
timelimit
。如果鍵已經過期(
ttl < 0
),則刪除該鍵,並增加已過期鍵的計數器expired
。增加已檢查鍵的計數器
sampled
。多輪過期檢查:
do { ... } while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP / 2);
如果在一輪檢查中刪除的過期鍵數量超過預設值的一半,則繼續下一輪檢查。
4. 時間限制檢查
在每次處理完一個數據庫後,檢查是否超出時間限制:
elapsed = ustime() - start; if (elapsed > timelimit) { timelimit_exit = 1; break; }
計算已耗費的時間
elapsed
。如果已耗費時間超過
timelimit
,設定timelimit_exit
為 1 並跳出迴圈。
總結
Redis 的 activeExpireCycle
函式透過以下步驟實現定期刪除過期鍵:
初始化變數並確定時間限制:根據當前的模式(快速或慢速)和配置引數,計算本次函式呼叫的時間限制。
遍歷資料庫:迴圈遍歷一定數量的資料庫。
過期檢查與刪除:從每個資料庫中隨機抽取鍵,檢查其是否過期並進行刪除,直到達到時間限制或刪除了一定數量的過期鍵。
時間限制檢查:確保函式不會超出規定的時間配額,以避免影響 Redis 的其他操作。
AOF、RDB和複製功能對過期鍵的處理
在 Redis 中,AOF(Append Only File)、RDB(Redis DataBase)和複製(Replication)功能對過期鍵的處理方式有所不同。下面詳細介紹這些機制如何處理過期鍵:
AOF 持久化
記錄過期時間:
在 AOF 檔案中,除了寫入每個鍵值的設定操作外,還會寫入
EXPIRE
或PEXPIRE
命令來記錄鍵的過期時間。重寫(Rewrite)過程:
當 AOF 檔案需要重寫時,Redis 會檢查每個鍵,如果鍵已過期,則不會將其寫入新的 AOF 檔案。
載入 AOF 檔案:
當 Redis 重啟並載入 AOF 檔案時,會執行檔案中的所有命令,包括設定鍵值和設定過期時間的命令。如果某些鍵已經過期,這些鍵會立即被刪除。
RDB 持久化
儲存快照:
當 Redis 建立 RDB 快照時,它會將所有鍵及其剩餘的過期時間一起儲存到快照檔案中。
載入快照:
當 Redis 從 RDB 檔案恢復資料時,會載入所有鍵值對,同時載入它們的過期時間。如果某個鍵在載入時已經過期,Redis 會立即將其刪除。
複製(Replication)
主從同步:
在主從複製架構中,主節點會將過期鍵的刪除操作傳播給從節點。
如果一個鍵在主節點上過期並被刪除,主節點會向從節點發送
DEL
操作,從而在從節點上也刪除該鍵。延遲過期:
從節點可能因為網路延遲等原因,對過期鍵的處理會稍有滯後,但最終主從節點的資料將保持一致。
總結
AOF:
透過記錄
EXPIRE
/PEXPIRE
命令來處理過期時間。在重寫過程中跳過已過期的鍵。
載入時刪除已過期的鍵。
RDB:
將過期時間與鍵值一起儲存。
載入時立即刪除已過期的鍵。
複製:
主節點將刪除過期鍵的操作同步到從節點。
確保主從節點資料的一致性。