Redis原理-源码解析:数据结构3 hash

  created  by  鱼鱼 {{tag}}
创建于 2020年11月29日 18:21:41 最后修改于 2020年11月29日 18:35:29

    所有原理实现基于Redis版本6.0.9。

    hash在Redis中可以认为是套了一层的string,当然,对hash来说没有数字类型。让我们依旧通过基本命令看看hash的基本数据结构实现。

hash的相关指令实现

void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;

    if ((c->argc % 2) == 1) {
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);
        return;
    }
    //在这一步会判断并在为空时进行创建
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    //此方法检查并转换类型
    hashTypeTryConversion(o,c->argv,2,c->argc-1);

    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

    /* HMSET (deprecated) and HSET return value is different. */
    char *cmdname = c->argv[0]->ptr;
    if (cmdname[1] == 's' || cmdname[1] == 'S') {
        /* HSET */
        addReplyLongLong(c, created);
    } else {
        /* HMSET */
        addReply(c, shared.ok);
    }
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
    server.dirty++;
}
//创建hash
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
    robj *o = lookupKeyWrite(c->db,key);
    if (checkType(c,o,OBJ_HASH)) return NULL;

    if (o == NULL) {
        o = createHashObject();
        dbAdd(c->db,key,o);
    }
    return o;
}

robj *createHashObject(void) {
    //其实创建了一个ziplist
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

    在set方法中我们看到了hash的初始创建过程,一个hash最开始是zipist。

/* Check the length of a number of objects to see if we need to convert a
 * ziplist to a real hash. Note that we only check string encoded objects
 * as their string length can be queried in constant time. */
 //在ziplist中有元素超长的时候转化为hash
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;

    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

    for (i = start; i <= end; i++) {
    //server.hash_max_ziplist_value是一个可配置项,默认64,即数组中有key或value长度大于64字节的时候会被转换为新格式->hashtasble
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
        {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            break;
        }
    }
}

void hashTypeConvert(robj *o, int enc) {
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        //转换位hashtable
        hashTypeConvertZiplist(o, enc);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        serverPanic("Not implemented");
    } else {
        serverPanic("Unknown hash encoding");
    }
}

void hashTypeConvertZiplist(robj *o, int enc) {
    serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);

    if (enc == OBJ_ENCODING_ZIPLIST) {
        /* Nothing to do... */

    } else if (enc == OBJ_ENCODING_HT) {
        hashTypeIterator *hi;
        dict *dict;
        int ret;

        hi = hashTypeInitIterator(o);
        //利用dict数据格式
        dict = dictCreate(&hashDictType, NULL);

        while (hashTypeNext(hi) != C_ERR) {
            sds key, value;
            
            key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
            value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
            ret = dictAdd(dict, key, value);
            if (ret != DICT_OK) {
                serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
                    o->ptr,ziplistBlobLen(o->ptr));
                serverPanic("Ziplist corruption detected");
            }
        }
        hashTypeReleaseIterator(hi);
        zfree(o->ptr);
        o->encoding = OBJ_ENCODING_HT;
        o->ptr = dict;
    } else {
        serverPanic("Unknown hash encoding");
    }
}

    想要了解ziplist可以看Redis原理-源码解析:数据结构2 list ,是为节省内存而生的链表格式。所以其实在使用ziplist时其查询的时间复杂度不是遵循hash的近似O(1),而是O(n),但是在数据量不大时,这种性能的损失微乎其微,并且能预见到大多数使用hash的场景都不会存储过多的字段。所以优先使用了更节省内存空间的ziplist。当元素过多或是过长时, 就会升级为dict数据格式,这也是Redis key所采用的存储方式,实现接近于hashtable。

dict

    dict是一种hashtable的实现,看一下dict的定义:

typedef struct dict {    
dictType *type; ///指向dictType的指针,这里面包含着对节点的各种操作    
void *privdata; ///私有数据,保存着dictType结构中函数的参数    
dictht ht[2];  ///一个dict中有两张哈希表    
long rehashidx; ///如果rehashidx == -1,则没有进行rehash,标识rehash的进度   
unsigned long iterators;///当前正在运行的迭代器数    
} dict;

//这是我们的哈希表结构。 对于我们的旧表到新表,在实现增量重新哈希处理时,每个字典都有两个。    
// 这个是hash表定义的数据结构    
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry {
    void *key;///字典的key值    
    union { ///定义value的共用体,他可以是一个指针、uint64、int64、double   
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;  ///该节点的下一个节点(拉链法hash冲突)    
} dictEntry;

数据结构

    整体数据结构如下图(图片来源Redis 数据结构 dict - Redis 源码日志 - 极客学院Wiki):

    dictht才是真是正存储数据的地方。每次插入数据会对key进行hash演算,并插入对应的bucket,这样每次都对key进行hash然后查找的时间复杂度得以被压缩为近似O(1),当产生hash冲突时采用拉链法,即使用一个链表存储bucket相同的数据。

rehash&两组dictht

    我们知道 当table中的数据个数/bucket数量的值越大,一个hash表便越容易产生冲突,当这一值达到Redis的扩容因子时(默认为1),Redis便会对bucket进行扩容操作,尽可能的避免冲突,提升查询性能。

    rehash是一个不能被忽略的步骤,因其执行时间是无法预料的,可能涉及到大量数据的搬移,为了不使相关命令阻塞,Redis将一个dict写为两个dictht就是为rehash准备的。当触发rehash后,Redis会逐步进行数据的迁移(包括在每次执行相关命令时和定时执行)。其间会先为另一个空的dictht分配两倍的bucket内存用于扩容,然后再一步步迁移数据,所以在dict进行rehash操作时,需要通过迁移进度来判断要查询的数据在哪个表中,这样做会牺牲一些额外空间,但是换来了免于阻塞的rehash过程。

    ps:这里采用hash算法是murmurhash,拉链冲突采用头插法

评论区
评论
{{comment.creator}}
{{comment.createTime}} {{comment.index}}楼
评论

Redis原理-源码解析:数据结构3 hash

Redis原理-源码解析:数据结构3 hash

    所有原理实现基于Redis版本6.0.9。

    hash在Redis中可以认为是套了一层的string,当然,对hash来说没有数字类型。让我们依旧通过基本命令看看hash的基本数据结构实现。

hash的相关指令实现

void hsetCommand(client *c) {
    int i, created = 0;
    robj *o;

    if ((c->argc % 2) == 1) {
        addReplyErrorFormat(c,"wrong number of arguments for '%s' command",c->cmd->name);
        return;
    }
    //在这一步会判断并在为空时进行创建
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    //此方法检查并转换类型
    hashTypeTryConversion(o,c->argv,2,c->argc-1);

    for (i = 2; i < c->argc; i += 2)
        created += !hashTypeSet(o,c->argv[i]->ptr,c->argv[i+1]->ptr,HASH_SET_COPY);

    /* HMSET (deprecated) and HSET return value is different. */
    char *cmdname = c->argv[0]->ptr;
    if (cmdname[1] == 's' || cmdname[1] == 'S') {
        /* HSET */
        addReplyLongLong(c, created);
    } else {
        /* HMSET */
        addReply(c, shared.ok);
    }
    signalModifiedKey(c,c->db,c->argv[1]);
    notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
    server.dirty++;
}
//创建hash
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
    robj *o = lookupKeyWrite(c->db,key);
    if (checkType(c,o,OBJ_HASH)) return NULL;

    if (o == NULL) {
        o = createHashObject();
        dbAdd(c->db,key,o);
    }
    return o;
}

robj *createHashObject(void) {
    //其实创建了一个ziplist
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

    在set方法中我们看到了hash的初始创建过程,一个hash最开始是zipist。

/* Check the length of a number of objects to see if we need to convert a
 * ziplist to a real hash. Note that we only check string encoded objects
 * as their string length can be queried in constant time. */
 //在ziplist中有元素超长的时候转化为hash
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;

    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

    for (i = start; i <= end; i++) {
    //server.hash_max_ziplist_value是一个可配置项,默认64,即数组中有key或value长度大于64字节的时候会被转换为新格式->hashtasble
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
        {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            break;
        }
    }
}

void hashTypeConvert(robj *o, int enc) {
    if (o->encoding == OBJ_ENCODING_ZIPLIST) {
        //转换位hashtable
        hashTypeConvertZiplist(o, enc);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        serverPanic("Not implemented");
    } else {
        serverPanic("Unknown hash encoding");
    }
}

void hashTypeConvertZiplist(robj *o, int enc) {
    serverAssert(o->encoding == OBJ_ENCODING_ZIPLIST);

    if (enc == OBJ_ENCODING_ZIPLIST) {
        /* Nothing to do... */

    } else if (enc == OBJ_ENCODING_HT) {
        hashTypeIterator *hi;
        dict *dict;
        int ret;

        hi = hashTypeInitIterator(o);
        //利用dict数据格式
        dict = dictCreate(&hashDictType, NULL);

        while (hashTypeNext(hi) != C_ERR) {
            sds key, value;
            
            key = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_KEY);
            value = hashTypeCurrentObjectNewSds(hi,OBJ_HASH_VALUE);
            ret = dictAdd(dict, key, value);
            if (ret != DICT_OK) {
                serverLogHexDump(LL_WARNING,"ziplist with dup elements dump",
                    o->ptr,ziplistBlobLen(o->ptr));
                serverPanic("Ziplist corruption detected");
            }
        }
        hashTypeReleaseIterator(hi);
        zfree(o->ptr);
        o->encoding = OBJ_ENCODING_HT;
        o->ptr = dict;
    } else {
        serverPanic("Unknown hash encoding");
    }
}

    想要了解ziplist可以看Redis原理-源码解析:数据结构2 list ,是为节省内存而生的链表格式。所以其实在使用ziplist时其查询的时间复杂度不是遵循hash的近似O(1),而是O(n),但是在数据量不大时,这种性能的损失微乎其微,并且能预见到大多数使用hash的场景都不会存储过多的字段。所以优先使用了更节省内存空间的ziplist。当元素过多或是过长时, 就会升级为dict数据格式,这也是Redis key所采用的存储方式,实现接近于hashtable。

dict

    dict是一种hashtable的实现,看一下dict的定义:

typedef struct dict {    
dictType *type; ///指向dictType的指针,这里面包含着对节点的各种操作    
void *privdata; ///私有数据,保存着dictType结构中函数的参数    
dictht ht[2];  ///一个dict中有两张哈希表    
long rehashidx; ///如果rehashidx == -1,则没有进行rehash,标识rehash的进度   
unsigned long iterators;///当前正在运行的迭代器数    
} dict;

//这是我们的哈希表结构。 对于我们的旧表到新表,在实现增量重新哈希处理时,每个字典都有两个。    
// 这个是hash表定义的数据结构    
typedef struct dictht {
    dictEntry **table;
    unsigned long size;
    unsigned long sizemask;
    unsigned long used;
} dictht;

typedef struct dictEntry {
    void *key;///字典的key值    
    union { ///定义value的共用体,他可以是一个指针、uint64、int64、double   
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;  ///该节点的下一个节点(拉链法hash冲突)    
} dictEntry;

数据结构

    整体数据结构如下图(图片来源Redis 数据结构 dict - Redis 源码日志 - 极客学院Wiki):

    dictht才是真是正存储数据的地方。每次插入数据会对key进行hash演算,并插入对应的bucket,这样每次都对key进行hash然后查找的时间复杂度得以被压缩为近似O(1),当产生hash冲突时采用拉链法,即使用一个链表存储bucket相同的数据。

rehash&两组dictht

    我们知道 当table中的数据个数/bucket数量的值越大,一个hash表便越容易产生冲突,当这一值达到Redis的扩容因子时(默认为1),Redis便会对bucket进行扩容操作,尽可能的避免冲突,提升查询性能。

    rehash是一个不能被忽略的步骤,因其执行时间是无法预料的,可能涉及到大量数据的搬移,为了不使相关命令阻塞,Redis将一个dict写为两个dictht就是为rehash准备的。当触发rehash后,Redis会逐步进行数据的迁移(包括在每次执行相关命令时和定时执行)。其间会先为另一个空的dictht分配两倍的bucket内存用于扩容,然后再一步步迁移数据,所以在dict进行rehash操作时,需要通过迁移进度来判断要查询的数据在哪个表中,这样做会牺牲一些额外空间,但是换来了免于阻塞的rehash过程。

    ps:这里采用hash算法是murmurhash,拉链冲突采用头插法


Redis原理-源码解析:数据结构3 hash 2020-11-29鱼鱼

{{commentTitle}}

评论   ctrl+Enter 发送评论