一:前言
這段時間事情比較多,難得抽出時間,便接着上篇文章《Redis是如何建立連接配接和處理指令的》,繼續往下分析。Redis 本質就是資料庫,要想深入了解Redis,那資料存取這一塊肯定是大頭。不過得益于 Redis 優良簡潔的設計,資料存取倒沒有那麼複雜,源碼讀起來也比較輕松。
二:Redis 的資料庫
Redis 對資料庫進行了抽象,在 Redis 源碼中,承擔資料庫角色的叫 redisDb。我們暫且無需去了解 redisDb 的内部結構,我們可以站在一個更加宏觀的角度去初步了解它,這樣能得到一個更全局的認識。
Redis服務可以同時配置多個 redisDb,每個redisDb的資料是互相隔離的。那麼怎麼配置多個 redisDb 呢?有過 redis 實戰經驗的同學肯定會說,這太簡單了,我們隻需要在 redis 的配置檔案中配置 databases 即可。redis 預設配置的 redisDb 數量為 16。
Redis 用 redisServer 表示服務,redisServer 中有個數組 db,用來記錄所有的 redisDb。當 Redis 程序啟動後,便會在 initServer()中按照配置的 redisDb 數量,初始化好 Redis 服務的所有資料庫。
server.db = zmalloc(sizeof(redisDb)*server.dbnum);
既然初始化了多個資料庫,而每個資料庫之間的資料又是隔離的,那麼當用戶端發存取指令的時候,Redis 服務又怎麼知道使用那個資料庫呢?諸位請往下看。
在上一篇文章中,我有提到過,每當一個新的用戶端連接配接到 Redis 後,Redis 便會建立一個 client 對象來表示一個用戶端連接配接,後續收到該用戶端的所有指令,都會基于建立的 client 進行。
Redis 在為新連接配接建立 client 時,便會為其配置設定資料庫,即 redisDb。代碼如下所示:
client *createClient(int fd) {
client *c = zmalloc(sizeof(client));
......
selectDb(c,0);
......
}
selectDb(c,0)即為 client 配置設定 redisDb,第二個參數标志所配置設定的資料庫在Redis服務中的索引,即第幾個資料庫。selectDb()邏輯很簡單:
int selectDb(client *c, int id) {
if (id < 0 || id >= server.dbnum)
return C_ERR;
c->db = &server.db[id];
return C_OK;
}
現在我們知道了,事實上,所有用戶端預設使用的都是Redis服務中的第一個redisDb。那麼Redis 服務初始化這麼多資料庫幹嘛呢?不是白費資源嗎?
Redis 用戶端有個 select 指令,使用 select 指令就可以選擇使用那個 redisDb,這樣不同用戶端之間就實作了資料隔離。如調用 select 2,redis 服務在收到指令後,就會将該連接配接的資料庫切換到索引為 2 的 redisDb。
三:資料庫的内部結構
從宏觀角度認識 redisDb 之後,我們便可以進入 redisDb 内部一探究竟。
typedef struct redisDb {
dict *dict; /* The keyspace for this DB */
dict *expires; /* Timeout of keys with a timeout set */
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 */
int id; /* Database ID */
long long avg_ttl; /* Average TTL, just for stats */
list *defrag_later; /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;
乍一看,redisDb 内部包括了好幾個 dict,即字典。從注釋來看,這些字典各有各的用處,如 dict 用來存放鍵值對,expires 用來存放key的逾時時間。由此可見,Redis 存放資料的核心便是這些字典了。
typedef struct dictEntry {
void *key;
union {
void *val;
uint64_t u64;
int64_t s64;
double d;
} v;
struct dictEntry *next;
} dictEntry;
typedef struct dictType {
uint64_t (*hashFunction)(const void *key);
void *(*keyDup)(void *privdata, const void *key);
void *(*valDup)(void *privdata, const void *obj);
int (*keyCompare)(void *privdata, const void *key1, const void *key2);
void (*keyDestructor)(void *privdata, void *key);
void (*valDestructor)(void *privdata, void *obj);
} dictType;
/* This is our hash table structure. Every dictionary has two of this as we
* implement incremental rehashing, for the old to the new table. */
typedef struct dictht {
dictEntry **table;
unsigned long size;
unsigned long sizemask;
unsigned long used;
} dictht;
typedef struct dict {
dictType *type;
void *privdata;
dictht ht[2];
long rehashidx; /* rehashing not in progress if rehashidx == -1 */
unsigned long iterators; /* number of iterators currently running */
} dict;
dict 中利用 dictht 來存放資料,dictht 其實就是 HashTable,本質也是通過計算 key 的 hash 值,将資料分布到不同的桶之中。 這裡比較有趣的是,一個 dict 中有兩個 dictht,按道理隻要一個 dictht 用來存放資料不就夠了嗎?其實平時用來存放資料的也就是 ht[0],隻有當要進行 rehash 的時候,才會使用 ht[1],臨時作為一個新的HashTable,存放新增資料。ht[0]中的存量資料會 rehash 到 ht[1] 中,等到 rehash 完成,ht[0] 就會再指向 ht[1] 的 dictht,完成職責交換。
dictht 中存放着一個二維指針:dictEntry **table ,第一維指針用來指向 dictEntry 連結清單,第二維指針指向dictEntry 連結清單中的某個dictEntry,dictEntry本身是也一個連結清單,記錄着hash(key)相同的元素。
總結下,也就是說真正用來存放資料的就是 dictEntry,而 dictht 作為HashTable,将資料根據 key hash,存放到不同的 dictEntry 中,并通過 table 這個二維指針管理所有 dictEntry。
四:資料存取流程
聊完了資料庫,再來聊聊Redis 是如何從資料庫存取資料的。
Redis 一共支援 5 種基礎資料結構:
- string:字元串
- list:清單
- hash:字典
- set:集合
- zset:有序集合
今天我們從最簡單的資料結構 string 入手,窺探下 Redis 内部設計。
在 Redis 用戶端調用指令,Redis 服務收到指令後便會調用指令對應的處理函數,如調用 set a A ,Redis 對應的指令處理函數便為 t_string.c 中的 setCommand()。 setCommand()解析指令附帶标志後,便調用了 setGenericCommand()處理資料。
void setGenericCommand(client *c, int flags, robj *key, robj *val, robj *expire, int unit, robj *ok_reply, robj *abort_reply) {
......
setKey(c->db,key,val);
......
}
setGenericCommand()中用來存放資料就一行代碼,即 setKey(c->db,key,val),将資料存放到 client 對應的 redisDb 中。接下來的要看的邏輯,便就是 redisDb 如何存入 了。
void setKey(redisDb *db, robj *key, robj *val) {
if (lookupKeyWrite(db,key) == NULL) {
dbAdd(db,key,val);
} else {
dbOverwrite(db,key,val);
}
incrRefCount(val);
removeExpire(db,key);
signalModifiedKey(db,key);
}
setKey()首先會查詢資料庫中是否已存在相同的key,如果不存在,就調用 dbAdd()插入資料,否則調用 dbOverwrite()覆寫掉舊資料。
廢話不多說,我們直接看 dbAdd()插入資料的邏輯:
void dbAdd(redisDb *db, robj *key, robj *val) {
sds copy = sdsdup(key->ptr);
int retval = dictAdd(db->dict, copy, val);
......
}
上面邏輯主要分為兩步:
(1)調用sdsdup(),将 key 的 C 字元串轉化為 redis 自定義的 sds 字元串,之是以将字元串由普通的字元數組轉化為 sds,主要就是為了效率考慮,sds 規避了普通字元數組的很多問題。
(2)調用dictAdd(),将 sds 類型的key ,和 val 一起存入redisDb 的字典 dict 中。dictAdd()邏輯也不複雜,代碼如下:
int dictAdd(dict *d, void *key, void *val)
{
dictEntry *entry = dictAddRaw(d,key,NULL);
if (!entry) return DICT_ERR;
dictSetVal(d, entry, val);
return DICT_OK;
}
dictAdd()同樣分兩步走:
(1)從dict 中找到 key 對應的哈希桶。
(2)調用dictSetVal(),将 value 存放到哈希桶中。到此,就被成功的存儲到資料庫中了。至于從 Redis 中讀取資料,那就更加簡單了,也就是根據 key,從 redisDb 的 dict 中找到對應的 dictEntry,并傳回 dictEntry 中存放的 value。
五:總結
總結下上面的源碼分析:
(1)Redis 預設會建立 16 個資料庫:redisDb,每個資料庫之間資料隔離。
(2)Redis 預設為每個用戶端配置設定第 0 号索引的 redisDb,用戶端可以調用 select 指令切換需要使用的資料庫。
(3)redisDb 内部采用了 HashTable 結構存放資料。
今天我們通過最基本的資料結構 string 學習了Redis 存取資料的核心邏輯,其實 Redis 在資料存放過程中還有很多細節,如漸進式 rehash、過期 key 惰性删除等。有興趣的同學可以關注下我的專欄,後續文章中我将繼續分析 Redis 源碼,與大家一起學習。