1. 程式人生 > >Redis原始碼剖析和註釋(十一)--- 雜湊鍵命令的實現(t_hash)

Redis原始碼剖析和註釋(十一)--- 雜湊鍵命令的實現(t_hash)

Redis 雜湊鍵命令實現(t_hash)

1. 雜湊命令介紹

Redis 所有雜湊命令如下表所示:Redis 雜湊命令詳解

序號 命令及描述
1 HDEL key field2 [field2]:刪除一個或多個雜湊表字段
2 HEXISTS key field:檢視雜湊表 key 中,指定的欄位是否存在。
3 HGET key field:獲取儲存在雜湊表中指定欄位的值。
4 HGETALL key:獲取在雜湊表中指定 key 的所有欄位和值
5 HINCRBY key field increment:為雜湊表 key 中的指定欄位的整數值加上增量 increment 。
6 HINCRBYFLOAT key field increment:為雜湊表 key 中的指定欄位的浮點數值加上增量 increment 。
7 HKEYS key:獲取所有雜湊表中的欄位
8 HLEN key:獲取雜湊表中欄位的數量
9 HMGET key field1 [field2]:獲取所有給定欄位的值
10 HMSET key field1 value1 [field2 value2 ]:同時將多個 field-value (域-值)對設定到雜湊表 key 中。
11 HSET key field value:將雜湊表 key 中的欄位 field 的值設為 value 。
12 HSETNX key field value:只有在欄位 field 不存在時,設定雜湊表字段的值。
13 HVALS key:獲取雜湊表中所有值
14 HSCAN key cursor [MATCH pattern][COUNT count]: 迭代雜湊表中的鍵值對。

2. 雜湊型別的實現

之前在redis物件系統原始碼剖析和註釋中提到,一個雜湊型別的物件的編碼有兩種,分別是OBJ_ENCODING_ZIPLIST和OBJ_ENCODING_HT。

編碼—encoding 物件—ptr
OBJ_ENCODING_ZIPLIST 壓縮列表實現的雜湊物件
OBJ_ENCODING_HT 字典實現的雜湊物件

但是預設建立的雜湊型別的物件編碼為OBJ_ENCODING_ZIPLIST,OBJ_ENCODING_HT型別編碼是通過達到配置的閾值條件後,進行轉換得到的。

閾值條件為:

/* redis.conf檔案中的閾值 */
hash-max-ziplist-value 64 // ziplist中最大能存放的值長度
hash-max-ziplist-entries 512 // ziplist中最多能存放的entry節點數量

一個雜湊物件的結構定義如下:

typedef struct redisObject {
    //物件的資料型別,字串物件應該為 OBJ_HASH
    unsigned type:4;        
    //物件的編碼型別,分別為 OBJ_ENCODING_ZIPLIST 或 OBJ_ENCODING_HT
    unsigned encoding:4;
    //暫且不關心該成員
    unsigned lru:LRU_BITS; /* lru time (relative to server.lruclock) */
    //引用計數
    int refcount;
    //指向底層資料實現的指標,指向一個dict的字典結構
    void *ptr;
} robj;

例如,我們建立一個 user:info 雜湊鍵,有三個欄位,分別是name,sex,passwd。

127.0.0.1:6379> HMSET user:info name Mike sex male passwd 123456
OK
127.0.0.1:6379> HGETALL user:info
1) "name"
2) "Mike"
3) "sex"
4) "male"
5) "passwd"
6) "123456"

我們以此為例,檢視redis的雜湊物件的空間結構。

根據這些資訊的大小,redis應該為其建立一個編碼為OBJ_ENCODING_ZIPLIST的雜湊物件。如下圖所示:

這裡寫圖片描述

壓縮列表中的entry節點,兩兩組成一個鍵值對。

如果這個雜湊物件所儲存的鍵值對或者ziplist的長度超過配置的限制,則會轉換為字典結構,這寫閾值條件上面已經列出,而為了說明編碼為 OBJ_ENCODING_HT 型別的雜湊物件,我們仍用上面的 user:info 物件來表示一個字典結構的雜湊物件,雜湊物件中的鍵值對都是字串型別的物件。如下圖:

這裡寫圖片描述

和列表資料型別一樣,雜湊資料型別基於ziplist和hash table進行封裝,實現了雜湊資料型別的介面:

/* Hash data type */
// 轉換一個雜湊物件的編碼型別,enc指定新的編碼型別
void hashTypeConvert(robj *o, int enc);
// 檢查一個數字物件的長度判斷是否需要進行型別的轉換,從ziplist轉換到ht型別
void hashTypeTryConversion(robj *subject, robj **argv, int start, int end);
// 對鍵和值的物件嘗試進行優化編碼以節約記憶體
void hashTypeTryObjectEncoding(robj *subject, robj **o1, robj **o2);
// 從一個雜湊物件中返回field對應的值物件
robj *hashTypeGetObject(robj *o, robj *key);
// 判斷field物件是否存在在o物件中
int hashTypeExists(robj *o, robj *key);
//  將field-value新增到雜湊物件中,返回1,如果field存在更新新的值,返回0
int hashTypeSet(robj *o, robj *key, robj *value);
// 從一個雜湊物件中刪除field,成功返回1,沒找到field返回0
int hashTypeDelete(robj *o, robj *key);
// 返回雜湊物件中的鍵值對個數
unsigned long hashTypeLength(robj *o);
// 返回一個初始化的雜湊型別的迭代器
hashTypeIterator *hashTypeInitIterator(robj *subject);
// 釋放雜湊型別迭代器空間
void hashTypeReleaseIterator(hashTypeIterator *hi);
// 講雜湊型別迭代器指向雜湊物件中的下一個節點
int hashTypeNext(hashTypeIterator *hi);
// 從ziplist型別的雜湊型別迭代器中獲取對應的field或value,儲存在引數中
void hashTypeCurrentFromZiplist(hashTypeIterator *hi, int what, unsigned char **vstr, unsigned int *vlen, long long *vll);
// 從ziplist型別的雜湊型別迭代器中獲取對應的field或value,儲存在引數中
void hashTypeCurrentFromHashTable(hashTypeIterator *hi, int what, robj **dst);
// 從雜湊型別的迭代器中獲取鍵或值
robj *hashTypeCurrentObject(hashTypeIterator *hi, int what);
// 以寫操作在資料庫中查詢對應key的雜湊物件,如果不存在則建立
robj *hashTypeLookupWriteOrCreate(client *c, robj *key);

這些函式介面的註釋請上github檢視:雜湊型別函式介面的註釋

3. 雜湊型別的迭代器

和列表型別一樣,雜湊資料型別也實現自己的迭代器,而且也是基於ziplist和字典結構的迭代器封裝而成。

typedef struct {
    robj *subject;              // 雜湊型別迭代器所屬的雜湊物件
    int encoding;               // 雜湊物件的編碼型別

    // 用ziplist編碼
    unsigned char *fptr, *vptr; // 指向當前的key和value節點的地址,ziplist型別編碼時使用

    // 用於字典編碼
    dictIterator *di;           // 迭代HT型別的雜湊物件時的字典迭代器
    dictEntry *de;              // 指向當前的雜湊表節點
} hashTypeIterator;

#define OBJ_HASH_KEY 1          // 雜湊鍵
#define OBJ_HASH_VALUE 2        // 雜湊值
  • 建立一個迭代器
// 返回一個初始化的雜湊型別的迭代器
hashTypeIterator *hashTypeInitIterator(robj *subject) {
    // 分配空間初始化成員
    hashTypeIterator *hi = zmalloc(sizeof(hashTypeIterator));
    hi->subject = subject;
    hi->encoding = subject->encoding;

    // 根據不同的編碼設定不同的成員
    if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
        hi->fptr = NULL;
        hi->vptr = NULL;
    } else if (hi->encoding == OBJ_ENCODING_HT) {
        // 初始化一個字典迭代器返回給di成員
        hi->di = dictGetIterator(subject->ptr);
    } else {
        serverPanic("Unknown hash encoding");
    }

    return hi;
}
  • 釋放迭代器
// 釋放雜湊型別迭代器空間
void hashTypeReleaseIterator(hashTypeIterator *hi) {
    // 如果是字典,則需要先釋放字典迭代器的空間
    if (hi->encoding == OBJ_ENCODING_HT) {
        dictReleaseIterator(hi->di);
    }

    zfree(hi);
}
  • 迭代
/* Move to the next entry in the hash. Return C_OK when the next entry
 * could be found and C_ERR when the iterator reaches the end. */
//講雜湊型別迭代器指向雜湊物件中的下一個節點
int hashTypeNext(hashTypeIterator *hi) {
    // 迭代ziplist
    if (hi->encoding == OBJ_ENCODING_ZIPLIST) {
        unsigned char *zl;
        unsigned char *fptr, *vptr;

        // 備份迭代器的成員資訊
        zl = hi->subject->ptr;
        fptr = hi->fptr;
        vptr = hi->vptr;

        // field的指標為空,則指向第一個entry,只在第一次執行時,初始化指標
        if (fptr == NULL) {
            /* Initialize cursor */
            serverAssert(vptr == NULL);
            fptr = ziplistIndex(zl, 0);
        } else {
            /* Advance cursor */
            // 獲取value節點的下一個entry地址,即為下一個field的地址
            serverAssert(vptr != NULL);
            fptr = ziplistNext(zl, vptr);
        }
        // 迭代完畢或返回C_ERR
        if (fptr == NULL) return C_ERR;

        /* Grab pointer to the value (fptr points to the field) */
        // 儲存下一個value的地址
        vptr = ziplistNext(zl, fptr);
        serverAssert(vptr != NULL);

        /* fptr, vptr now point to the first or next pair */
        // 更新迭代器的成員資訊
        hi->fptr = fptr;
        hi->vptr = vptr;

    // 如果是迭代字典
    } else if (hi->encoding == OBJ_ENCODING_HT) {
        // 得到下一個字典節點的地址
        if ((hi->de = dictNext(hi->di)) == NULL) return C_ERR;
    } else {
        serverPanic("Unknown hash encoding");
    }
    return C_OK;
}

4. 雜湊命令的實現

上面都給出了雜湊型別的介面,所以雜湊型別命令實現很容易看懂,而且雜湊型別命令沒有阻塞版的。

  • Hgetall一類命令的底層實現

HKEYS、HVALS、HGETALL

void genericHgetallCommand(client *c, int flags) {
    robj *o;
    hashTypeIterator *hi;
    int multiplier = 0;
    int length, count = 0;

    // 以寫操作取出雜湊物件,若失敗,或取出的物件不是雜湊型別的物件,則傳送0後直接返回
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptymultibulk)) == NULL
        || checkType(c,o,OBJ_HASH)) return;

    // 計算一對鍵值對要返回的個數
    if (flags & OBJ_HASH_KEY) multiplier++;
    if (flags & OBJ_HASH_VALUE) multiplier++;

    // 計算整個雜湊物件中的所有鍵值對要返回的個數
    length = hashTypeLength(o) * multiplier;
    addReplyMultiBulkLen(c, length);        //發get到的個數給client

    // 建立一個雜湊型別的迭代器並初始化
    hi = hashTypeInitIterator(o);
    // 迭代所有的entry節點
    while (hashTypeNext(hi) != C_ERR) {
        // 如果取雜湊鍵
        if (flags & OBJ_HASH_KEY) {
            // 儲存當前迭代器指向的鍵
            addHashIteratorCursorToReply(c, hi, OBJ_HASH_KEY);
            count++;    //更新計數器
        }
        // 如果取雜湊值
        if (flags & OBJ_HASH_VALUE) {
            // 儲存當前迭代器指向的值
            addHashIteratorCursorToReply(c, hi, OBJ_HASH_VALUE);
            count++;    //更新計數器
        }
    }

    //釋放迭代器
    hashTypeReleaseIterator(hi);
    serverAssert(count == length);
}
  • HSTRLEN 命令實現

Redis 3.2版本以上新加入的

void hstrlenCommand(client *c) {
    robj *o;

    // 以寫操作取出雜湊物件,若失敗,或取出的物件不是雜湊型別的物件,則傳送0後直接返回
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,OBJ_HASH)) return;

    // 傳送field物件的值的長度給client
    addReplyLongLong(c,hashTypeGetValueLength(o,c->argv[2]));
}
  • HDEL命令實現
void hdelCommand(client *c) {
    robj *o;
    int j, deleted = 0, keyremoved = 0;

    // 以寫操作取出雜湊物件,若失敗,或取出的物件不是雜湊型別的物件,則傳送0後直接返回
    if ((o = lookupKeyWriteOrReply(c,c->argv[1],shared.czero)) == NULL ||
        checkType(c,o,OBJ_HASH)) return;

    // 遍歷所有的欄位field
    for (j = 2; j < c->argc; j++) {
        // 從雜湊物件中刪除當前欄位
        if (hashTypeDelete(o,c->argv[j])) {
            deleted++;  //更新刪除的個數

            // 如果雜湊物件為空,則刪除該物件
            if (hashTypeLength(o) == 0) {
                dbDelete(c->db,c->argv[1]);
                keyremoved = 1; //設定刪除標誌
                break;
            }
        }
    }

    // 只要刪除了欄位
    if (deleted) {
        // 傳送訊號表示鍵被改變
        signalModifiedKey(c->db,c->argv[1]);
        // 傳送"hdel"事件通知
        notifyKeyspaceEvent(NOTIFY_HASH,"hdel",c->argv[1],c->db->id);

        // 如果雜湊物件被刪除
        if (keyremoved)
            // 傳送"hdel"事件通知
            notifyKeyspaceEvent(NOTIFY_GENERIC,"del",c->argv[1],
                                c->db->id);
        server.dirty += deleted;    // 更新髒鍵
    }
    addReplyLongLong(c,deleted);    //傳送刪除的個數給client
}
  • HINCRBYFLOAT 命令的實現
void hincrbyfloatCommand(client *c) {
    double long value, incr;
    robj *o, *current, *new, *aux;

    // 得到一個long double型別的增量increment
    if (getLongDoubleFromObjectOrReply(c,c->argv[3],&incr,NULL) != C_OK) return;
    // 以寫方式取出雜湊物件,失敗則直接返回
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    // 返回field在雜湊物件o中的值物件
    if ((current = hashTypeGetObject(o,c->argv[2])) != NULL) {

        //從值物件中得到一個long double型別的value,如果不是浮點數的值,則傳送"hash value is not a valid float"資訊給client
        if (getLongDoubleFromObjectOrReply(c,current,&value,
            "hash value is not a valid float") != C_OK) {
            decrRefCount(current);  //取值成功,釋放臨時的value物件空間,直接返回
            return;
        }
        decrRefCount(current);  //取值失敗也要釋放空間
    } else {
        value = 0;  //如果沒有值,則設定為預設的0
    }

    value += incr;  //備份原先的值
    // 將value轉換為字串型別的物件
    new = createStringObjectFromLongDouble(value,1);
    //將鍵和值物件的編碼進行優化,以節省空間,是以embstr或raw或整型儲存
    hashTypeTryObjectEncoding(o,&c->argv[2],NULL);
    // 設定原來的key為新的值物件
    hashTypeSet(o,c->argv[2],new);
    // 講新的值物件傳送給client
    addReplyBulk(c,new);
     // 修改資料庫的鍵則傳送訊號,傳送"hincrbyfloat"事件通知,更新髒鍵
    signalModifiedKey(c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_HASH,"hincrbyfloat",c->argv[1],c->db->id);
    server.dirty++;

    /* Always replicate HINCRBYFLOAT as an HSET command with the final value
     * in order to make sure that differences in float pricision or formatting
     * will not create differences in replicas or after an AOF restart. */
    // 用HSET命令代替HINCRBYFLOAT,以防不同的浮點精度造成的誤差
    // 建立HSET字串物件
    aux = createStringObject("HSET",4);
    // 修改HINCRBYFLOAT命令為HSET物件
    rewriteClientCommandArgument(c,0,aux);
    // 釋放空間
    decrRefCount(aux);
    // 修改increment為新的值物件new
    rewriteClientCommandArgument(c,3,new);
    // 釋放空間
    decrRefCount(new);
}
  • HSCAN 命令實現
// HSCAN key cursor [MATCH pattern] [COUNT count]
// HSCAN 命令實現
void hscanCommand(client *c) {
    robj *o;
    unsigned long cursor;

    // 獲取scan命令的遊標cursor
    if (parseScanCursorOrReply(c,c->argv[2],&cursor) == C_ERR) return;
    // 以寫操作取出雜湊物件,若失敗,或取出的物件不是雜湊型別的物件,則傳送0後直接返回
    if ((o = lookupKeyReadOrReply(c,c->argv[1],shared.emptyscan)) == NULL ||
        checkType(c,o,OBJ_HASH)) return;
    // 呼叫底層實現
    scanGenericCommand(c,o,cursor);
}