在面試中遇到美女面試官時,我們以為面試會比較容易過,也能好好表現自己技術的時候了。然而卻出現以下這一幕,當美女面試官聽說你使用過Redis時,那麼問題來了。
?面試官:Q1,你知道Redis設定key過期時間的命令嗎?
?你:你毫不猶豫的巴拉巴拉說了一堆命令,以及用法,比如expire 等等命令
(?這時候你想問得那麼簡單?但真的那麼簡單嗎?美女面試官停頓了一下,接著問)
?面試官:Q2,那你說說Redis是怎麼實現過期時間設定呢?以及怎麼判斷鍵過期的呢?
?你:(這時候想這還難不倒我),然後又巴拉巴拉的說一通,Redis的資料庫伺服器中redisDb資料結構以及過期時間的判定
(?你又在想應該不會問了吧,換個Redis的話題了吧,那你就錯了)
?面試官:(抬頭笑著看了看你)Q3,那你說說過期鍵的刪除策略以及Redis過期鍵的刪除策略以及實現?
?️你:這時你回答的就不那麼流暢了,有時頭腦還阻塞了。
(?這是你可能就有點蒙了,或者只知道一些過期鍵的刪除策略,但具體怎麼實現不知道呀,你以為面試官的提問這樣就完了嗎?)
?面試官:Q4,那你再說說其他環節中是怎麼處理過期鍵的呢(比如AOF、RDB)?
??你:...........
(?這更加尷尬了,知道的不全,也可能不知道,本來想好好表現,也想著面試比較簡單,沒想到會經歷這些)
為了避免這尷尬的場景出現,那現在需要你記錄下以下的內容,這樣就可以在美女面試官面前好好表現了。
1. Redis Expire Key基礎
redis資料庫在資料庫伺服器中使用了redisDb
資料結構,結構如下:
typedef struct redisDb {
dict *dict; /* 鍵空間 key space */
dict *expires; /* 過期字典 */
dict *blocking_keys; /* Keys with clients waiting for data (BLPOP) */
dict *ready_keys; /* Blocked keys that received a PUSH */
dict *watched_keys; /* WATCHED keys for MULTI/EXEC CAS */
struct evictionPoolEntry *eviction_pool; /* Eviction pool of keys */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
} redisDb;
複製程式碼
其中,
- 鍵空間(
key space
):dict字典用來儲存資料庫中的所有鍵值對 - 過期字典(
expires
):儲存資料庫中所有鍵的過期時間,過期時間用UNIX
時間戳表示,且值為long long
整數
1.1 設定過期時間命令
EXPIRE \<key> \<ttl>
:命令用於將鍵key的過期時間設定為ttl秒之後PEXPIRE \<key> \<ttl>
:命令用於將鍵key的過期時間設定為ttl毫秒之後EXPIREAT \<key> \<timesramp>
:命令用於將key的過期時間設定為timrestamp所指定的秒數時間戳PEXPIREAT \<key> \<timesramp>
:命令用於將key的過期時間設定為timrestamp所指定的毫秒數時間戳
設定過期時間:
redis> set Ccww 5 2 0
ok
redis> expire Ccww 5
ok
複製程式碼
使用redisDb結構儲存資料圖表示:
1.2過期時間儲存以及判定
過期鍵的判定,其實通過過期字典進行判定,步驟:
- 檢查給定鍵是否存在於過期字典,如果存在,取出鍵的過期時間
- 通過判斷當前UNIX時間戳是否大於鍵的過期時間,是的話,鍵已過期,相反則鍵未過期。
2. 過期鍵刪除策略
2.1 三種不同刪除策略
- 定時刪除:在設定鍵的過期時間的同時,建立一個定時任務,當鍵達到過期時間時,立即執行對鍵的刪除操作
- 惰性刪除:放任鍵過期不管,但在每次從鍵空間獲取鍵時,都檢查取得的鍵是否過期,如果過期的話,就刪除該鍵,如果沒有過期,就返回該鍵
- 定期刪除:每隔一點時間,程式就對資料庫進行一次檢查,刪除裡面的過期鍵,至於要刪除多少過期鍵,以及要檢查多少個資料庫,則由演算法決定。
2.2 三種刪除策略的優缺點
2.2.1 定時刪除
- 優點: 對記憶體友好,定時刪除策略可以保證過期鍵會盡可能快地被刪除,並釋放國期間所佔用的記憶體
- 缺點: 對cpu時間不友好,在過期鍵比較多時,刪除任務會佔用很大一部分cpu時間,在記憶體不緊張但cpu時間緊張的情況下,將cpu時間用在刪除和當前任務無關的過期鍵上,影響伺服器的響應時間和吞吐量
2.2.2 惰性刪除
- 優點: 對cpu時間友好,在每次從鍵空間獲取鍵時進行過期鍵檢查並是否刪除,刪除目標也僅限當前處理的鍵,這個策略不會在其他無關的刪除任務上花費任何cpu時間。
- 缺點: 對記憶體不友好,過期鍵過期也可能不會被刪除,導致所佔的記憶體也不會釋放。甚至可能會出現記憶體洩露的現象,當存在很多過期鍵,而這些過期鍵又沒有被訪問到,這會可能導致它們會一直儲存在記憶體中,造成記憶體洩露。
2.2.4 定期刪除
由於定時刪除會佔用太多cpu時間,影響伺服器的響應時間和吞吐量以及惰性刪除浪費太多記憶體,有記憶體洩露的危險,所以出現一種整合和折中這兩種策略的定期刪除策略。
- 定期刪除策略每隔一段時間執行一次刪除過期鍵操作,並通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響。
- 定時刪除策略有效地減少了因為過期鍵帶來的記憶體浪費。
定時刪除策略難點就是確定刪除操作執行的時長和頻率:
刪除操作執行得太頻繁。或者執行時間太長,定期刪除策略就會退化成為定時刪除策略,以至於將cpu時間過多地消耗在刪除過期鍵上。相反,則惰性刪除策略一樣,出現浪費記憶體的情況。 所以使用定期刪除策略,需要根據伺服器的情況合理地設定刪除操作的執行時長和執行頻率。
3. Redis的過期鍵刪除策略
Redis伺服器結合惰性刪除和定期刪除兩種策略一起使用,通過這兩種策略之間的配合使用,使得伺服器可以在合理使用CPU時間和浪費記憶體空間取得平衡點。
3.1 惰性刪除策略的實現
Redis在執行任何讀寫命令時都會先找到這個key,惰性刪除就作為一個切入點放在查詢key之前,如果key過期了就刪除這個key。
robj *lookupKeyRead(redisDb *db, robj *key) {
robj *val;
expireIfNeeded(db,key); // 切入點
val = lookupKey(db,key);
if (val == NULL)
server.stat_keyspace_misses++;
else
server.stat_keyspace_hits++;
return val;
}
複製程式碼
通過expireIfNeeded
函式對輸入鍵進行檢查是否刪除
int expireIfNeeded(redisDb *db, robj *key) {
/* 取出鍵的過期時間 */
mstime_t when = getExpire(db,key);
mstime_t now;
/* 沒有過期時間返回0*/
if (when < 0) return 0; /* No expire for this key */
/* 伺服器loading時*/
if (server.loading) return 0;
/* 根據一定規則獲取當前時間*/
now = server.lua_caller ? server.lua_time_start : mstime();
/* 如果當前的是從(Slave)伺服器
* 0 認為key為無效
* 1 if we think the key is expired at this time.
* */
if (server.masterhost != NULL) return now > when;
/* key未過期,返回 0 */
if (now <= when) return 0;
/* 刪除鍵 */
server.stat_expiredkeys++;
propagateExpire(db,key,server.lazyfree_lazy_expire);
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
return server.lazyfree_lazy_expire ? dbAsyncDelete(db,key) :
dbSyncDelete(db,key);
}
複製程式碼
3.2 定期刪除策略的實現
key的定期刪除會在Redis的週期性執行任務(serverCron
,預設每100ms執行一次)中進行,而且是發生Redis的master
節點,因為slave
節點會通過主節點的DEL命令同步過來達到刪除key的目的。
for (j = 0; j < dbs_per_call; j++) {
int expired;
redisDb *db = server.db+(current_db % server.dbnum);
current_db++;
/* 超過25%的key已過期,則繼續. */
do {
unsigned long num, slots;
long long now, ttl_sum;
int ttl_samples;
/* 如果該db沒有設定過期key,則繼續看下個db*/
if ((num = dictSize(db->expires)) == 0) {
db->avg_ttl = 0;
break;
}
slots = dictSlots(db->expires);
now = mstime();
/*但少於1%時,需要調整字典大小*/
if (num && slots > DICT_HT_INITIAL_SIZE &&
(num*100/slots < 1)) break;
expired = 0;
ttl_sum = 0;
ttl_samples = 0;
if (num > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP)
num = ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP;// 20
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++;
}
}
/* Update the average TTL stats for this database. */
if (ttl_samples) {
long long avg_ttl = ttl_sum/ttl_samples;
/樣本獲取移動平均值 */
if (db->avg_ttl == 0) db->avg_ttl = avg_ttl;
db->avg_ttl = (db->avg_ttl/50)*49 + (avg_ttl/50);
}
iteration++;
if ((iteration & 0xf) == 0) { /* 每迭代16次檢查一次 */
long long elapsed = ustime()-start;
latencyAddSampleIfNeeded("expire-cycle",elapsed/1000);
if (elapsed > timelimit) timelimit_exit = 1;
}
/* 超過時間限制則退出*/
if (timelimit_exit) return;
/* 在當前db中,如果少於25%的key過期,則停止繼續刪除過期key */
} while (expired > ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP/4);
}
複製程式碼
依次遍歷每個db(預設配置數是16),針對每個db,每次迴圈隨機選擇20個(ACTIVE_EXPIRE_CYCLE_LOOKUPS_PER_LOOP
)key判斷是否過期,如果一輪所選的key少於25%過期,則終止迭次,此外在迭代過程中如果超過了一定的時間限制則終止過期刪除這一過程。
4. AOF、RDB和複製功能對過期鍵的處理
4.1 RDB
生成RDB檔案
程式會資料庫中的鍵進行檢查,已過期的鍵不會儲存到新建立的RDB檔案中
載入RDB檔案
- 主服務載入RDB檔案,會對檔案中儲存的鍵進行檢查會忽略過期鍵載入未過期鍵
- 從伺服器載入RDB檔案,會載入檔案所儲存的所有鍵(過期和未過期的),但從主伺服器同步資料同時會清空從伺服器的資料庫。
4.2 AOF
- AOF檔案寫入:當過期鍵被刪除後,會在AOF檔案增加一條DEL命令,來顯式地記錄該鍵已被刪除。
- AOF重寫:已過期的鍵不會儲存到重寫的AOF檔案中
4.3 複製
當伺服器執行在複製模式下時,從伺服器的過期鍵刪除動作由主伺服器控制的,這樣的好處主要為了保持主從伺服器資料一致性:
- 主伺服器在刪除一個過期鍵之後,會顯式地向所有的從伺服器傳送一個DEL命令,告知從伺服器刪除這個過期鍵
- 從伺服器在執行客戶端傳送的讀取命令時,即使碰到過期鍵也不會將過期鍵刪除,不作任何處理。
- 只有接收到主伺服器 DEL命令後,從伺服器進行刪除處理。
最後可關注公眾號【Ccww筆記】,一起學習,每天會分享乾貨,還有學習視訊乾貨領取!