資料庫鍵空間過期鍵處理 - 《Redis設計與實現》讀書筆記
切換資料庫
預設情況下,Redis客戶端的目標資料庫為0號資料庫
通過修改client.db指標,指向不同資料庫,從而實現切換目標資料庫的功能
客戶端切換目標資料庫命令: SELECT 資料庫序號
在處理多資料庫程式時,為了避免誤操作(特別像FLUSHDB這樣的危險命令),
最好先執行一個SELECT命令,顯式切換到指定的資料庫,然後再執行別的命令
資料庫鍵空間
- 鍵空間是一個字典,所有針對資料庫的操作實際上都是通過對鍵空間字典進行操作來實現的,
- 新增新鍵:將一個新鍵值對新增到鍵空間字典裡面
- 刪除鍵:在鍵空間裡面刪除鍵所對應的鍵值對物件
- 更新鍵:對鍵空間裡面鍵所對應的值物件進行更新
- 對鍵取值:在鍵空間中取出鍵所對應的值物件
- 讀寫鍵空間的維護操作
-
在讀取一個鍵之後(讀操作和寫操作都要對鍵進行讀取),伺服器會根據鍵是否存在來更新伺服器的鍵空間命中次數(keyspace_hits)和鍵空間不命中次數(keyspace_misses)
INFO stats 命令:檢視keyspace_hits、keyspace_misses屬性 -
在讀取一個鍵之後,伺服器會更新鍵的LRU時間
OBJECT idletime 命令:檢視鍵的空轉時長(閒置時間) -
如果伺服器在讀取一個鍵時發現鍵已經
過期
,那麼伺服器會先刪除
這個過期鍵,然後才執行餘下的其他操作 -
如果有客戶端使用watch命令監視了某個鍵,那麼伺服器在對被監視的鍵進行修改之後,會將這個鍵
標記為髒
-
伺服器每次修改一個鍵之後,都會對
髒鍵計數器
的值增1,這個計數器會觸發伺服器的持久化以及複製操作 -
如果伺服器開啟了資料庫通知功能,那麼在對鍵進行修改之後,伺服器將按配置傳送相應的資料庫通知
儲存過期時間
- 設定過期時間
生存時間: 鍵可以存在多久
expire命令: 將鍵key的生存時間設定為ttl秒
pexpire命令: 將鍵key的生存時間設定為ttl毫秒
過期時間: 鍵什麼時候會被刪除
expireat命令: 將鍵key的過期時間設定為timestamp所指定的秒數時間戳
pexpireat命令: 將鍵key的過期時間設定為timestamp所指定的毫秒數時間戳
實際上expire、pexpire、expireat三個命令底層都是用pexpireat來實現的
- 移除過期時間
persist命令: 移除一個鍵的過期時間,
pexpireat命令的反操作,解除過期字典中給定鍵和對應值的關聯
- 計算並返回剩餘生存時間
ttl命令: 以秒為單位返回鍵的剩餘生存時間
pttl命令: 以毫秒為單位返回鍵的剩餘生存時間
ttl、pttl命令若返回-1,表示鍵沒有設定過期時間
ttl、pttl命令若返回-2,表示鍵不存在於資料庫
- 過期鍵的判定
- 檢查給定的鍵
是否存在
於過期字典: 如果存在,那麼取得鍵的過期時間- 檢查當前UNIX時間戳
是否大於
鍵的過期時間: 如果是的話,那麼鍵已經過期; 否則的話,鍵未過期
過期鍵刪除策略
如果一個鍵過期了,有三種刪除策略:定時刪除、惰性刪除、定期刪除,
- 定時刪除: 對記憶體最友好,對CPU時間最不友好,佔用太多的CPU時間,影響伺服器的響應時間和吞吐量
- 惰性刪除: 對記憶體最不友好,對CPU時間最友好,浪費太多記憶體,有記憶體洩漏的危險
- 定期刪除:定時刪除和惰性刪除的一種整合和折中,通過限制刪除操作執行的時長和頻率來減少刪除操作對CPU時間的影響,通過定期刪除過期鍵有效地減少因為過期鍵而帶來的記憶體浪費
- Redis伺服器實際使用的是惰性刪除和定期刪除兩種策略
通過配合使用,伺服器可以很好地在合理使用CPU時間和避免浪費空間之間取得平衡
RDB持久化功能對過期鍵的處理
資料庫中包含過期鍵不會
對 生成新的RDB檔案
或者 載入RDB檔案的伺服器
造成影響
-
生成RDB檔案
在執行SAVE命令或者BGSAVE命令建立一個新的RDB檔案時,程式會對資料庫中的鍵進行檢查,已過期的鍵不會
被儲存到新建立的RDB檔案中 -
載入RDB檔案
-
如果伺服器以
主服器
模式執行,對檔案中儲存的鍵進行檢查,忽略過期鍵
,然後將未過期的鍵載入資料庫中 -
如果伺服器以
從伺服器
模式執行,對檔案中儲存的鍵進行檢查,全部鍵
都載入資料庫中,主從資料同步的時候才會刪除過期鍵
-
AOF持久化功能對過期鍵的處理
資料庫中包含過期鍵不會
對 AOF檔案
或者 AOF重寫
造成影響
-
AOF檔案寫入
當一個過期鍵被刪除之後,伺服器會追加一條DEL命令到現有AOF檔案的末尾,顯式地記錄該鍵已被刪除 -
AOF重寫
在執行AOF重寫的過程中,程式會對資料庫中的鍵進行檢查,已過期的鍵不會
被儲存到重寫後的AOF檔案中
複製功能對過期鍵的處理
當伺服器執行在複製模式下時,從伺服器的過期鍵刪除動作由主伺服器控制
,保證主從伺服器資料的一致性
-
主伺服器在刪除一個過期鍵之後,會
顯式
地向所有從伺服器傳送一個DEL命令,告知從伺服器刪除這個過期鍵 -
從伺服器
在執行客戶端傳送的讀命令時,即使碰到過期鍵也不會主動將過期鍵刪除
,而是繼續像處理為過期的鍵一樣來處理過期鍵 -
從伺服器只有在接到主伺服器發來的DEL命令之後,才會刪除過期鍵
資料庫通知
伺服器配置server.notify_keyspace_events 決定了伺服器所傳送通知的型別
- 鍵空間通知:某個鍵執行了什麼命令
- 鍵事件通知:某個命令被什麼鍵執行了
定義
// 自動間隔性儲存配置:記錄了儲存條件的秒數和修改數
struct saveparam {
// 秒數
time_t seconds;
// 修改數
int changes;
};
// 資料庫狀態結構
typedef struct redisDb {
// ...
// 資料庫鍵空間,儲存著資料庫中的所有鍵值對,與使用者所見的資料庫是直接對應的
dict *dict; /* The keyspace for this DB */
// 資料庫序號,序號範圍:[0, 配置檔案中的databases選項)
int id;
// 過期字典,儲存著資料庫中所有鍵的過期時間
// 過期字典的鍵是一個指標,指向鍵空間中的某個鍵物件(也就是某個資料庫鍵)
// 過期字典的值是一個long long型別的整數,儲存鍵所指向的資料庫鍵的過期時間(一個毫秒精度的UNIX時間戳)
dict *expires;
// ...
} redisDb;
// 伺服器狀態結構
struct redisServer {
// ...
// 資料庫陣列: 儲存著伺服器中的所有資料庫
redisDb *db;
// 資料庫數量,可通過配置檔案中的databases選項進行調整,預設為16
int dbnum;
// 伺服器所傳送通知的型別
int notify_keyspace_events;
// 自動間隔性儲存配置:記錄了儲存條件配置的陣列
struct saveparam *saveparams;
// 修改計數器:記錄距離上一次成功執行save命令 或者 bgsave命令之後,
// 伺服器對資料庫狀態(伺服器中所有的資料庫)進行了多少次修改(包括寫入、刪除、更新等操作)
long long dirty;
// 上一次執行儲存的時間: 是一個uninx時間戳,記錄伺服器上一次成功執行save命令 或者 bgsave命令的時間
time_t lastsave;
// AOF緩衝區:儲存對資料庫狀態修改的寫命令
sds aof_buf;
// AOF重寫緩衝區:儲存伺服器建立子程序之後對資料庫狀態修改的寫命令
list *aof_rewrite_buf_blocks;
// ...
};
// 客戶端狀態結構
typedef struct client {
// ...
// 當前客戶端目標資料庫指標:記錄客戶端當前正在使用的資料庫
redisDb *db;
// ...
} client;
/**
* 資料庫通知傳送
* type 當前想要傳送的通知的型別
* event 事件的名稱
* key 產生事件的鍵
* dbid 產生事件的資料庫號碼
*/
void notifyKeyspaceEvent(int type, char *event, robj *key, int dbid) {
sds chan;
robj *chanobj, *eventobj;
int len = -1;
char buf[24];
/* If any modules are interested in events, notify the module system now.
* This bypasses the notifications configuration, but the module engine
* will only call event subscribers if the event type matches the types
* they are interested in. */
// 如果任何模組對事件呼叫,立即通知模組系統。這將繞過通知配置,
// 但模組引擎將僅在事件型別與其允許的型別匹配時呼叫事件訂閱者。
moduleNotifyKeyspaceEvent(type, event, key, dbid);
/* If notifications for this class of events are off, return ASAP. */
// 如果給定的通知不是伺服器允許傳送的通知,那麼直接返回,不做任何動作
if (!(server.notify_keyspace_events & type)) return;
// 事件字串物件
eventobj = createStringObject(event,strlen(event));
/* __keyspace@<db>__:<key> <event> notifications. */
// 傳送鍵空間通知
if (server.notify_keyspace_events & NOTIFY_KEYSPACE) {
// 將通知傳送給頻道__keyspace@<dbid>__:<key>
// 內容為鍵所發生的事件<event>
// 構建頻道名字
chan = sdsnewlen("__keyspace@",11);
len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, key->ptr);
chanobj = createObject(OBJ_STRING, chan);
// 傳送通知
pubsubPublishMessage(chanobj, eventobj);
// 釋放頻道字串物件
decrRefCount(chanobj);
}
/* __keyevent@<db>__:<event> <key> notifications. */
// 傳送鍵事件通知
if (server.notify_keyspace_events & NOTIFY_KEYEVENT) {
// 將通知傳送給頻道__keyspace@<dbid>__:<event>
// 內容為鍵所發生的事件<key>
// 構建頻道名字
chan = sdsnewlen("__keyevent@",11);
if (len == -1) len = ll2string(buf,sizeof(buf),dbid);
chan = sdscatlen(chan, buf, len);
chan = sdscatlen(chan, "__:", 3);
chan = sdscatsds(chan, eventobj->ptr);
chanobj = createObject(OBJ_STRING, chan);
// 傳送通知
pubsubPublishMessage(chanobj, key);
// 釋放頻道字串物件
decrRefCount(chanobj);
}
// 釋放事件字串物件
decrRefCount(eventobj);
}
原始碼閱讀
- 伺服器:src/server.h 、 src/server.c
- 鍵的過期時間:src/expire.c
- 過期鍵惰性刪除策略:src/db.c/expireIfNeeded函式
- 過期鍵定期刪除策略:src/expire.c/activeExpireCycle函式
- 傳送通知: src/notify.c/notifyKeyspaceEvent函式