【Redis5源碼學習】淺析redis命令之exists篇

baiyanredis

命令語法

命令含義:判斷鍵是否存在。若是過時則不存在,不過時則存在
命令格式:segmentfault

EXISTS key1 key2 ... keyN

命令實戰:數組

127.0.0.1:6379> set key1 value1
OK
127.0.0.1:6379> exists key1
(integer) 1

返回值: 存在鍵的數量函數

源碼分析

exists命令對應的處理函數是existsCommand():源碼分析

void existsCommand(client *c) {
    long long count = 0;
    int j;

    for (j = 1; j < c->argc; j++) {
        // 去鍵空間字典尋找給定key是否存在,存在則count++
        if (lookupKeyRead(c->db,c->argv[j])) count++; 
    }
    // 返回找到key的數量
    addReplyLongLong(c,count);
}

一樣地,咱們使用gdb -p來觀察exists命令的執行過程,gdb的過程這裏再也不贅述。咱們在existsCommand處打一個斷點,而後在客戶端中執行命令:學習

127.0.0.1:6379> exists key1

觀察服務端,已經執行到咱們的斷點處了:ui

Breakpoint 1, existsCommand (c=0x7fb459ca3540) at db.c:496
496    void existsCommand(client *c) {
(gdb) n
500        for (j = 1; j < c->argc; j++) {
(gdb)
497        long long count = 0;
(gdb)
501            if (lookupKeyRead(c->db,c->argv[j])) count++;
(gdb) s

咱們看到,在入口函數existsCommand()中,遍歷了全部的命令參數,即咱們傳入的key1,這裏會調用lookupKeyRead()函數,去鍵空間中查找鍵是否過時,若是過時,則返回0,不存在。反之鍵存在,返回1。跟進這個函數調用:spa

lookupKeyRead (key=0x7fb462229ad0, db=0x7fb46221a800) at db.c:144
144        return lookupKeyReadWithFlags(db,key,LOOKUP_NONE);
(gdb) s
lookupKeyReadWithFlags (db=0x7fb46221a800, key=0x7fb462229ad0, flags=flags@entry=0) at db.c:100
100    robj *lookupKeyReadWithFlags(redisDb *db, robj *key, int flags) {
(gdb) n
103        if (expireIfNeeded(db,key) == 1) {
(gdb) n
133        val = lookupKey(db,key,flags);
(gdb) n

這個函數直接調用了lookupKeyReadWithFlags(),而後在lookupKeyReadWithFlags函數內部,調用了咱們熟悉的expireIfNeeded(),顯然在咱們調試過程當中,並無進這個if,由於咱們的鍵並無過時,因此確定返回0。因爲鍵並無過時,那麼在這裏應該直接返回了。可是因爲它是一個通用查找函數,還須要返回查找後的值,因此繼續調用lookupKey()函數,去鍵空間中查找key1對應的值value1。繼續往下執行:指針

133        val = lookupKey(db,key,flags);
(gdb) n
134        if (val == NULL)
(gdb) p val
$1 = (robj *) 0x7fb46220e100
(gdb) p *val
$2 = {type = 0, encoding = 8, lru = 8745377, refcount = 1, ptr = 0x7fb46220e113}
137            server.stat_keyspace_hits++;
(gdb)
139    }
(gdb)
existsCommand (c=0x7fb459ca3540) at db.c:501
501            if (lookupKeyRead(c->db,c->argv[j])) count++;

咱們看到,在查找完成以後,會判斷是否爲NULL,顯然這裏不會爲NULL。打印val的值,是一個redisObj結構,這裏ptr指向的sds就是咱們的value1了。而後,redis會統計一個數據,是在鍵空間中查找到value的次數,這裏因爲咱們找到了,將其++,而後函數會將查找到的value1返回,最外層判斷不爲NULL,count++,命令執行結束。調試

擴展

字典的查找

在exists命令執行過程當中,一個核心的函數調用就是lookupKey()。這個函數查找一個鍵對應的value值。若是存在,返回該value值,不然返回NULL:

robj *lookupKey(redisDb *db, robj *key, int flags) {
    dictEntry *de = dictFind(db->dict,key->ptr); // 找到當前key-value對所在的dictEntry
    if (de) {
        robj *val = dictGetVal(de); // 是一個宏,直接返回dictEntry中的val字段
        ...
        return val;
    } else {
        return NULL;
    }
}

這個函數會首先調用dictFind(),直接返回這個key所在dict中的dictEntry。咱們跟進這個函數:

static dictEntry *dictFind(dict *ht, const void *key) {
    dictEntry *he;  // dictEntry結構的指針
    unsigned int h; // 存儲哈希值

    if (ht->size == 0) return NULL; // 字典爲空,直接返回不存在
    h = dictHashKey(ht, key) & ht->sizemask; // 計算哈希值
    he = ht->table[h]; // 訪問字典對應哈希值位置上的元素,它是一個指針,指向dictEntry結構
    // 遍歷鏈地址法解決衝突的鏈表
    while(he) {
        if (dictCompareHashKeys(ht, key, he->key)) // 挨個比較當前dictEntry的key值是否等於咱們要找的key值
            return he; // 找到了,直接返回
        he = he->next; // 沒找到,繼續日後遍歷鏈表
    }
    return NULL; // 遍歷到鏈表尾部尚未找到,說明沒有該元素
}

在說明該函數查找流程以前,咱們回顧一下字典的存儲結構:

代碼中的he = ht->table[h]訪問的就是咱們**table這個指針。它是一個二級指針,能夠當作一個一維數組,每一個數組中的元素都是一個dictEntry的指針。咱們訪問到了第h個(計算後的哈希值)元素的bucket位置上,它也是一個dictEntry指針,指向真正存儲key-value對的結構。因爲須要解決哈希衝突問題,因此一個bucket後面會以鏈地址法,經過next指針字段,掛接多個dictEntry,這就是上面代碼爲何須要遍歷的緣由。每遍歷一個dictEntry,咱們都要比較一下當前dictEntry上的key值是否與咱們要比較的key值是否相等,若是相等就找到了咱們要找的key-value對。反之若是遍歷到鏈表尾部都沒找到,那就說明沒有這個key-value對了:

咱們回顧一下dictEntry的結構:

typedef struct dictEntry {
    void *key; // 指向存儲key值的結構
    union {
        void *val; // 指向存儲value值的結構
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 鏈地址法解決衝突的指針,指向下一個dictEntry結構
} dictEntry;

在找完以後,該函數會返回一個dictEntry結構的指針,咱們調用dictGetVal宏,就能訪問到dictEntry中的value值啦:

#define dictGetVal(he) ((he)->v.val)

咱們看到,這個宏訪問了dictEntry的val字段,成功拿到了當前key對應的value值。

參考資料

【Redis5源碼學習】2019-04-19 字典dict

相關文章
相關標籤/搜索