天天看點

Redis是如何存取資料的一:前言二:Redis 的資料庫三:資料庫的内部結構四:資料存取流程五:總結

一:前言

這段時間事情比較多,難得抽出時間,便接着上篇文章《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 源碼,與大家一起學習。