Redis数据类型 — Hash

目录

Hash内部实现

源码片段

Hset执行函数

创建hash对象

尝试转换存储空间的编码

遍历把键值对根据存储空间格式进行存储起来


Hash 是一个键值对(key - value)集合,Hash 特别适合用于存储对象。

Hash内部实现

Hash 类型的底层数据结构是由压缩列表或哈希表实现的,Hash结构默认采用ZipList编码,用以节省内存。 ZipList中相邻的两个entry 分别保存field和value

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。

在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由listpack数据结构来实现了

Redis数据类型 — Hash_第1张图片

从底层结构来看,只要Zset把SkipList去掉,Hash底层采用的编码与Zset基本一致

Hash结构与Redis中的Zset非常类似:

  • 都是键值存储

  • 都需求根据键获取值

  • 键必须唯一

区别如下:

  • zset的键是member,值是score;hash的键和值都是任意值

  • zset要根据score排序;hash则无需排序

源码片段

Hset执行函数

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;
    }
    //判断hash的key是否存在,不存在则创建一个新的,默认是ZipList编码
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    //判断是否需要把ZipList转为Dict
    hashTypeTryConversion(o,c->argv,2,c->argc-1);
    //依次写入field-value对,并执行hset命令
    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. 
     HMSET(已弃用)和 HSET 返回值不同*/
    char *cmdname = c->argv[0]->ptr;
     //找命令是hset还是hmset,
    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 += (c->argc - 2)/2;
}

创建hash对象

//取出或新创建哈希对象,如果是新建相当于创建了一个Dict
robj *hashTypeLookupWriteOrCreate(client *c, robj *key) {
	//首先根据key去取
    robj *o = lookupKeyWrite(c->db,key);
    if (checkType(c,o,OBJ_HASH)) return NULL;
	//如果不存在则会新创建一个hash对象
    if (o == NULL) {
        o = createHashObject();
        dbAdd(c->db,key,o);
    }
    return o;
}

robj *createHashObject(void){
    //默认采用ZipList编码,申请ZipList内存空间
    unsigned char *zl = ziplistNew();
    robj *o = createObject(OBJ_HASH, zl);

    //设置编码
    o->encoding = OBJ_ENCODING_ZIPLIST;
    return o;
}

尝试转换存储空间的编码

当数据量较大时,Hash结构会转为HT编码,也就是Dict,触发条件有两个:

  • ZipList中的元素数量超过了hash-max-ziplist-entries(默认512)

  • ZipList中的任意entry大小超过了hash-max-ziplist-value(默认64字节)

当满足上面两个条件其中之⼀的时候,Redis就使⽤dict字典来实现hash。 Redis的hash之所以这样设计,是因为当ziplist变得很⼤的时候,它有如下几个缺点:

  • 每次插⼊或修改引发的realloc操作会有更⼤的概率造成内存拷贝,从而降低性能。

  • ⼀旦发生内存拷贝,内存拷贝的成本也相应增加,因为要拷贝更⼤的⼀块数据。

  • 当ziplist数据项过多的时候,在它上⾯查找指定的数据项就会性能变得很低,因为ziplist上的查找需要进行遍历。

总之,ziplist本来就设计为连续的内存空间,这种结构并不擅长做修改操作。⼀旦数据发⽣改动,就会引发内存realloc,可能导致内存拷贝。

void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;
    size_t sum = 0;

    //如果对象不是 ziplist 编码,那么直接返回
    if (o->encoding != OBJ_ENCODING_ZIPLIST) return;

    /*遍历所有对象,一个一个比较长度,字符串值是否超过指定长度,记住这里是比较链表
      中每一个值的长度,如果有一个大于server.hash_max_ziplist_value,则把ziplist
      转换成hashtable*/
    for (i = start; i <= end; i++) {
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_ziplist_value)
        {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            break;
        }
        sum += len;        
    }
    //Ziplist大小超过1G,也转为哈希表
    if(!ziplistSafeToAdd(o->ptr, sum))
        hashTypeConvert(o, OBJ_ENCODING_HT);
}

遍历把键值对根据存储空间格式进行存储起来

int hashTypeSet(robj *o, sds field, sds value, int flags){
    int update = 0;
    //判断是否为ZipList编码
    if(o->encoding == OBJ_ENCODING_ZIPLIST){
        unsigned char *zl , *fptr, *vptr;
        zl = o->ptr;
        //查询head指针
        fptr = ziplistIndex(zl, ZIPLIST_HEAD);
        if(fptr != NULL){
            //head不为空,说明ZipList不为空,开始查找key
            fptr = ziplistFind(zl, fptr, (unsigned char*) field, sdslen(field), 1);
            if (fptr != NULL) {
                /* Grab pointer to the value (fptr points to the field)
                抓取指向值的指针(fptr 指向字段)*/
                vptr = ziplistNext(zl, fptr);
                serverAssert(vptr != NULL);
                update = 1;

                /* Replace value
                替换掉值*/
                zl = ziplistReplace(zl, vptr, (unsigned char*)value,
                        sdslen(value));
            }
        }
        //创建新的而不是修改旧的value值
        if (!update) {
            /* Push new field/value pair onto the tail of the ziplist
            将新的字段/值对推送到 ziplist 的尾部*/
            zl = ziplistPush(zl, (unsigned char*)field, sdslen(field),
                    ZIPLIST_TAIL);
            zl = ziplistPush(zl, (unsigned char*)value, sdslen(value),
                    ZIPLIST_TAIL);
        }
        o->ptr = zl;

        /*再检查ziplist是否需要转换为哈希表 ,这里比较的是整个ziplist的长度,大于某个值则整个                  
        ziplist转换成hashtable存储*/
        if (hashTypeLength(o) > serverR.hash_max_ziplist_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        //哈希编码, 直接添加到hash表或覆盖
        dictEntry *de = dictFind(o->ptr,field);
        if (de) {
            //把键值对中旧的value替换掉
            sdsfree(dictGetVal(de));
            if (flags & HASH_SET_TAKE_VALUE) {
                dictGetVal(de) = value;
                value = NULL;
            } else {
                dictGetVal(de) = sdsdup(value);
            }
            update = 1;
        } else { 
            //往hashTable中加入新的键值对
            sds f,v;
            if (flags & HASH_SET_TAKE_FIELD) {
                f = field;
                field = NULL;
            } else {
                f = sdsdup(field);
            }
            if (flags & HASH_SET_TAKE_VALUE) {
                v = value;
                value = NULL;
            } else {
                v = sdsdup(value);
            }
            dictAdd(o->ptr,f,v);
        }
    } else {
        serverPanic("Unknown hash encoding");
    }

    /* Free SDS strings we did not referenced elsewhere if the flags
     * want this function to be responsible. */
    if (flags & HASH_SET_TAKE_FIELD && field) sdsfree(field);
    if (flags & HASH_SET_TAKE_VALUE && value) sdsfree(value);
    return update;
}

你可能感兴趣的:(Redis,redis,数据库)