Redis原始碼閱讀筆記--資料庫redisDb
一. 資料庫
Redis的資料庫使用字典作為底層實現,資料庫的增、刪、查、改都是構建在字典的操作之上的。
redis伺服器將所有資料庫都儲存在伺服器狀態結構redisServer(redis.h/redisServer)的db陣列(應該是一個連結串列)裡:
struct redisServer {
//..
// 資料庫陣列,儲存著伺服器中所有的資料庫
redisDb *db;
//..
}
在初始化伺服器時,程式會根據伺服器狀態的dbnum屬性來決定應該建立多少個數據庫:
struct redisServer {
// ..
//伺服器中資料庫的數量
int dbnum;
//..
}
dbnum屬性的值是由伺服器配置的database選項決定的,預設值為16;
二、切換資料庫原理
每個Redis客戶端都有自己的目標資料庫,每當客戶端執行資料庫的讀寫命令時,目標資料庫就會成為這些命令的操作物件。
127.0.0.1:6379> set msg 'Hello world'
OK
127.0.0.1:6379> get msg
"Hello world"
127.0.0.1:6379> select 2
OK
127.0.0.1:6379[2]> get msg
(nil)
127.0.0.1:6379[2]>
在伺服器內部,客戶端狀態redisClient結構(redis.h/redisClient)的db屬性記錄了客戶端當前的目標資料庫,這個屬性是一個指向redisDb結構(redis.h/redisDb)的指標:
typedef struct redisClient {
//..
// 客戶端當前正在使用的資料庫
redisDb *db;
//..
} redisClient;
redisClient.db指標指向redisServer.db陣列中的一個元素,而被指向的元素就是當前客戶端的目標資料庫。
我們就可以通過修改redisClient指標,讓他指向伺服器中的不同資料庫,從而實現切換資料庫的功能–這就是select命令的實現原理。
實現程式碼:
int selectDb(redisClient *c, int id) {
// 確保 id 在正確範圍內
if (id < 0 || id >= server.dbnum)
return REDIS_ERR;
// 切換資料庫(更新指標)
c->db = &server.db[id];
return REDIS_OK;
}
三、資料庫的鍵空間
1、資料庫的結構(我們只分析鍵空間和鍵過期時間)
typedef struct redisDb {
// 資料庫鍵空間,儲存著資料庫中的所有鍵值對
dict *dict; /* The keyspace for this DB */
// 鍵的過期時間,字典的鍵為鍵,字典的值為過期事件 UNIX 時間戳
dict *expires; /* Timeout of keys with a timeout set */
// 資料庫號碼
int id; /* Database ID */
// 資料庫的鍵的平均 TTL ,統計資訊
long long avg_ttl; /* Average TTL, just for stats */
//..
} redisDb
上圖是一個RedisDb的示例,該資料庫存放有五個鍵值對,分別是sRedis,INums,hBooks,SortNum和sNums,它們各自都有自己的值物件,另外,其中有三個鍵設定了過期時間,當前資料庫是伺服器的第0號資料庫。現在,我們就從原始碼角度分析這個資料庫結構:
我們知道,Redis是一個鍵值對資料庫伺服器,伺服器中的每一個數據庫都是一個redis.h/redisDb結構,其中,結構中的dict字典儲存了資料庫中所有的鍵值對,我們就將這個字典成為鍵空間。
Redis資料庫的資料都是以鍵值對的形式存在,其充分利用了字典高效索引的特點。
a、鍵空間的鍵就是資料庫中的鍵,一般都是字串物件;
b、鍵空間的值就是資料庫中的值,可以是5種類型物件(字串、列表、雜湊、集合和有序集合)之一。
資料庫的鍵空間結構分析完了,我們先看看資料庫的初始化。
2、鍵空間的初始化
在redis.c中,我們可以找到鍵空間的初始化操作:
//建立並初始化資料庫結構
for (j = 0; j < server.dbnum; j++) {
// 建立每個資料庫的鍵空間
server.db[j].dict = dictCreate(&dbDictType,NULL);
// ...
// 設定當前資料庫的編號
server.db[j].id = j;
}
初始化之後就是對鍵空間的操作了。
3、鍵空間的操作
我先把一些常見的鍵空間操作函式列出來:
// 從資料庫中取出鍵key的值物件,若不存在就返回NULL
robj *lookupKey(redisDb *db, robj *key);
/* 先刪除過期鍵,以讀操作的方式從資料庫中取出指定鍵對應的值物件
* 並根據是否成功找到值,更新伺服器的命中或不命中資訊,
* 如不存在則返回NULL,底層呼叫lookupKey函式 */
robj *lookupKeyRead(redisDb *db, robj *key);
/* 先刪除過期鍵,以寫操作的方式從資料庫中取出指定鍵對應的值物件
* 如不存在則返回NULL,底層呼叫lookupKey函式,
* 不會更新伺服器的命中或不命中資訊
*/
robj *lookupKeyWrite(redisDb *db, robj *key);
/* 先刪除過期鍵,以讀操作的方式從資料庫中取出指定鍵對應的值物件
* 如不存在則返回NULL,底層呼叫lookupKeyRead函式
* 此操作需要向客戶端回覆
*/
robj *lookupKeyReadOrReply(redisClient *c, robj *key, robj *reply);
/* 先刪除過期鍵,以寫操作的方式從資料庫中取出指定鍵對應的值物件
* 如不存在則返回NULL,底層呼叫lookupKeyWrite函式
* 此操作需要向客戶端回覆
*/
robj *lookupKeyWriteOrReply(redisClient *c, robj *key, robj *reply);
/* 新增元素到指定資料庫 */
void dbAdd(redisDb *db, robj *key, robj *val);
/* 重寫指定鍵的值 */
void dbOverwrite(redisDb *db, robj *key, robj *val);
/* 設定指定鍵的值 */
void setKey(redisDb *db, robj *key, robj *val);
/* 判斷指定鍵是否存在 */
int dbExists(redisDb *db, robj *key);
/* 隨機返回資料庫中的鍵 */
robj *dbRandomKey(redisDb *db);
/* 刪除指定鍵 */
int dbDelete(redisDb *db, robj *key);
/* 清空所有資料庫,返回鍵值對的個數 */
long long emptyDb(void(callback)(void*));
下面我選取幾個比較典型的操作函式分析一下:
查詢鍵值對函式–lookupKey
robj *lookupKey(redisDb *db, robj *key) {
// 查詢鍵空間
dictEntry *de = dictFind(db->dict,key->ptr);
// 節點存在
if (de) {
// 取出該鍵對應的值
robj *val = dictGetVal(de);
// 更新時間資訊
if (server.rdb_child_pid == -1 && server.aof_child_pid == -1)
val->lru = LRU_CLOCK();
// 返回值
return val;
} else {
// 節點不存在
return NULL;
}
}
新增鍵值對–dbAdd
新增鍵值對使我們經常使用到的函式,底層由dbAdd()函式實現,傳入的引數是待新增的資料庫,鍵物件和值物件,原始碼如下:
void dbAdd(redisDb *db, robj *key, robj *val) {
// 複製鍵名
sds copy = sdsdup(key->ptr);
// 嘗試新增鍵值對
int retval = dictAdd(db->dict, copy, val);
// 如果鍵已經存在,那麼停止
redisAssertWithInfo(NULL,key,retval == REDIS_OK);
// 如果開啟了叢集模式,那麼將鍵儲存到槽裡面
if (server.cluster_enabled) slotToKeyAdd(key);
}
好了,關於鍵空間操作函式就分析到這,其他函式(在檔案db.c中)大家可以自己去分析,有問題的話可以回帖,我們可以一起討論!
四、資料庫的過期鍵操作
在前面我們說到,redisDb結構中有一個expires指標(概況圖可以看上圖),該指標指向一個字典結構,字典中儲存了所有鍵的過期時間,該字典稱為過期字典。
過期字典的初始化:
// 建立並初始化資料庫結構
for (j = 0; j < server.dbnum; j++) {
// 建立每個資料庫的過期時間字典
server.db[j].expires = dictCreate(&keyptrDictType,NULL);
// 設定當前資料庫的編號
server.db[j].id = j;
// ..
}
a、過期字典的鍵是一個指標,指向鍵空間中的某一個鍵物件(就是某一個數據庫鍵);
b、過期字典的值是一個long long型別的整數,這個整數儲存了鍵所指向的資料庫鍵的時間戳–一個毫秒精度的unix時間戳。
下面我們就來分析過期鍵的處理函式:
1、過期鍵處理函式
設定鍵的過期時間–setExpire()
/*
* 將鍵 key 的過期時間設為 when
*/
void setExpire(redisDb *db, robj *key, long long when) {
dictEntry *kde, *de;
// 從鍵空間中取出鍵key
kde = dictFind(db->dict,key->ptr);
// 如果鍵空間找不到該鍵,報錯
redisAssertWithInfo(NULL,key,kde != NULL);
// 向過期字典中新增該鍵
de = dictReplaceRaw(db->expires,dictGetKey(kde));
// 設定鍵的過期時間
// 這裡是直接使用整數值來儲存過期時間,不是用 INT 編碼的 String 物件
dictSetSignedIntegerVal(de,when);
}
獲取鍵的過期時間–getExpire()
long long getExpire(redisDb *db, robj *key) {
dictEntry *de;
// 如果過期鍵不存在,那麼直接返回
if (dictSize(db->expires) == 0 ||
(de = dictFind(db->expires,key->ptr)) == NULL) return -1;
redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
// 返回過期時間
return dictGetSignedIntegerVal(de);
}
刪除鍵的過期時間–removeExpire()
// 移除鍵 key 的過期時間
int removeExpire(redisDb *db, robj *key) {
// 確保鍵帶有過期時間
redisAssertWithInfo(NULL,key,dictFind(db->dict,key->ptr) != NULL);
// 刪除過期時間
return dictDelete(db->expires,key->ptr) == DICT_OK;
}
2、過期鍵刪除策略
通過前面的介紹,大家應該都知道資料庫鍵的過期時間都儲存在過期字典裡,那假如一個鍵過期了,那麼這個過期鍵是什麼時候被刪除的呢?現在來看看redis的過期鍵的刪除策略:
a、定時刪除:在設定鍵的過期時間的同時,建立一個定時器,在定時結束的時候,將該鍵刪除;
b、惰性刪除:放任鍵過期不管,在訪問該鍵的時候,判斷該鍵的過期時間是否已經到了,如果過期時間已經到了,就執行刪除操作;
c、定期刪除:每隔一段時間,對資料庫中的鍵進行一次遍歷,刪除過期的鍵。
其中定時刪除可以及時刪除資料庫中的過期鍵,並釋放過期鍵所佔用的記憶體,但是它為每一個設定了過期時間的鍵都開了一個定時器,使的cpu的負載變高,會對伺服器的響應時間和吞吐量造成影響。
惰性刪除有效的克服了定時刪除對CPU的影響,但是,如果一個過期鍵很長時間沒有被訪問到,且若存在大量這種過期鍵時,勢必會佔用很大的記憶體空間,導致記憶體消耗過大。
定時刪除可以算是上述兩種策略的折中。設定一個定時器,每隔一段時間遍歷資料庫,刪除其中的過期鍵,有效的緩解了定時刪除對CPU的佔用以及惰性刪除對記憶體的佔用。
在實際應用中,Redis採用了惰性刪除和定時刪除兩種策略來對過期鍵進行處理,上面提到的lookupKeyWrite等函式中就利用到了惰性刪除策略,定時刪除策略則是在根據伺服器的例行處理程式serverCron來執行刪除操作,該程式每100ms呼叫一次。
惰性刪除函式–expireIfNeeded()
原始碼如下:
/* 檢查key是否已經過期,如果是的話,將它從資料庫中刪除
* 並將刪除命令寫入AOF檔案以及附屬節點(主從複製和AOF持久化相關)
* 返回0代表該鍵還沒有過期,或者沒有設定過期時間
* 返回1代表該鍵因為過期而被刪除
*/
int expireIfNeeded(redisDb *db, robj *key) {
// 獲取該鍵的過期時間
mstime_t when = getExpire(db,key);
mstime_t now;
// 該鍵沒有設定過期時間
if (when < 0) return 0;
// 伺服器正在載入資料的時候,不要處理
if (server.loading) return 0;
// lua指令碼相關
now = server.lua_caller ? server.lua_time_start : mstime();
// 主從複製相關,附屬節點不主動刪除key
if (server.masterhost != NULL) return now > when;
// 該鍵還沒有過期
if (now <= when) return 0;
// 刪除過期鍵
server.stat_expiredkeys++;
// 將刪除命令傳播到AOF檔案和附屬節點
propagateExpire(db,key);
// 傳送鍵空間操作時間通知
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",key,db->id);
// 將該鍵從資料庫中刪除
return dbDelete(db,key);
}
定期刪除策略
過期鍵的定期刪除策略由redis.c/activeExpireCycle()函式實現,伺服器週期性地操作redis.c/serverCron()(每隔100ms執行一次)時,會呼叫activeExpireCycle()函式,分多次遍歷伺服器中的各個資料庫,從資料庫中的expires字典中隨機檢查一部分鍵的過期時間,並刪除其中的過期鍵。
刪除過期鍵的操作由activeExpireCycleTryExpire函式(activeExpireCycle()呼叫了該函式)執行,其原始碼如下:
/* 檢查鍵的過期時間,如過期直接刪除*/
int activeExpireCycleTryExpire(redisDb *db, dictEntry *de, long long now) {
// 獲取過期時間
long long t = dictGetSignedIntegerVal(de);
if (now > t) {
// 執行到此說明過期
// 建立該鍵的副本
sds key = dictGetKey(de);
robj *keyobj = createStringObject(key,sdslen(key));
// 將刪除命令傳播到AOF和附屬節點
propagateExpire(db,keyobj);
// 在資料庫中刪除該鍵
dbDelete(db,keyobj);
// 傳送事件通知
notifyKeyspaceEvent(NOTIFY_EXPIRED,
"expired",keyobj,db->id);
// 臨時鍵物件的引用計數減1
decrRefCount(keyobj);
// 伺服器的過期鍵計數加1
// 該引數影響每次處理的資料庫個數
server.stat_expiredkeys++;
return 1;
} else {
return 0;
}
}
刪除過期鍵對AOF、RDB和主從複製都有影響,等到了介紹相關功能時再討論。
今天就先到這裡~