redis源码学习-t_hash篇

t_hash介绍

t_hash,负责处理key:[{field,value}]键值对,常用来存储对象。底层在6之前使用使用ziplist 和 dict,在6之后变为listpack和dict。本篇针对6之后的底层实现。listpack与dict分别在


两篇中详解,不熟悉的同学可以先看这两篇文章。

t_hash详解

t_hash编码方式

t_hash的两种编码方式,分别为

#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */
#define OBJ_ENCODING_HT 2      /* Encoded as hash table */

在6之后,取决于redis.conf参数中的

# Hashes are encoded using a memory efficient data structure when they have a
# small number of entries, and the biggest entry does not exceed a given
# threshold. These thresholds can be configured using the following directives.
hash-max-listpack-entries 512
hash-max-listpack-value 64

解释来说

  1. listpack编码条件:哈希对象保存的所有键值对的键和值的字符串长度都小于 hash-max-listpack-value字节.且哈希对象保存的键值对数量小于 hash-max-listpack-entries 个
  2. hashtable编码条件:哈希对象保存的所有键值对的键和值的字符串长度都大于等于 hash-max-listpack-value字节或哈希对象保存的键值对数量大于等于 hash-max-listpack-entries 个
    需要注意的是,编码方式一旦变化那么不会根据t_hash的缩小再变回来。

t_hash中负责处理的具体命令

  1. hdel:删除一个或多个哈希表字段
  2. hexists:查看哈希表 key 中指定的字段是否存在
  3. hget:获取存储在哈希表中指定字段的值
  4. hgetall:获取在哈希表中指定 key 的所有字段和值
  5. hincrby:为哈希表 key 中的指定字段的整数加减
  6. hincrbyloat:为哈希表 key 中的指定字段的浮点数加减
  7. hkeys:获取所有哈希表中的字段
  8. hlen:获取哈希表中字段的数量
  9. hmget:获取所有给定字段的值
  10. hmset:同时将多个 field-value 对设置到 key 中
  11. hset:: 将一个field-value 对设置到 key 中
  12. hsetnx:只有在字段 field 不存在时,将一个field-value 对设置到 key 中
  13. hvals:获取哈希表中所有值
  14. hscan:迭代哈希表中的键值对

t_hash源码分析

与其他容器不太相同的是,thash中由于复杂性,取消掉了GenericCommand的特殊命名包装,整个t_hash仅有使用了一个scanGenericCommand函数,通用函数为hashTypeSet,hashTypeDelete等
hashTypeSet:

int hashTypeSet(robj *o, sds field, sds value, int flags) {
    int update = 0;

    if (o->encoding == OBJ_ENCODING_LISTPACK) {
        unsigned char *zl, *fptr, *vptr;

        zl = o->ptr;
        fptr = lpFirst(zl);
        //获取紧凑列表的头
        if (fptr != NULL) {
            //查看是否存在field
            fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) {
                /* Grab pointer to the value (fptr points to the field) */
                 //存在的话,那么取next
                vptr = lpNext(zl, fptr);
                serverAssert(vptr != NULL);
                update = 1;

                /* Replace value */
               //与ziplist不同,listpack中本身实现了replace操作,无需delet后删除
                zl = lpReplace(zl, &vptr, (unsigned char*)value, sdslen(value));
            }
        }

        if (!update) {
           //非更新操作的话那么直接插入就好,需要按顺序插入键和值
            /* Push new field/value pair onto the tail of the listpack */
            zl = lpAppend(zl, (unsigned char*)field, sdslen(field));
            zl = lpAppend(zl, (unsigned char*)value, sdslen(value));
        }
        o->ptr = zl;
        //这里是检查转换hash的操作,假如此次的插入导致触发了hash-max-listpack-entries或hash-max-listpack-value上限,那么需要整体转换
        /* Check if the listpack needs to be converted to a hash table */
        if (hashTypeLength(o) > server.hash_max_listpack_entries)
            hashTypeConvert(o, OBJ_ENCODING_HT);
    } else if (o->encoding == OBJ_ENCODING_HT) {
        //hash结构的话相对简单些,本身有hash映射
        dictEntry *de = dictFind(o->ptr,field);
        if (de) {
            sdsfree(dictGetVal(de));
            if (flags & HASH_SET_TAKE_VALUE) {
                dictGetVal(de) = value;
                value = NULL;
            } else {
                dictGetVal(de) = sdsdup(value);
            }
            update = 1;
        } else {
            //不存在的话需要插入操作,dict中的插入会有概率触发渐进式rehash
            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;
}

从这里看出,从listpack转换为hashtable出现在set的时候
hashTypeDelete:

/* Delete an element from a hash.
 * Return 1 on deleted and 0 on not found. */
int hashTypeDelete(robj *o, sds field) {
    int deleted = 0;
    
    if (o->encoding == OBJ_ENCODING_LISTPACK) {
        //listpack中的delete为减法,所以不存在出现编码转换的问题,这里仅仅调用listpack中的方法即可
        unsigned char *zl, *fptr;

        zl = o->ptr;
        fptr = lpFirst(zl);
        if (fptr != NULL) {
            fptr = lpFind(zl, fptr, (unsigned char*)field, sdslen(field), 1);
            if (fptr != NULL) {
                /* Delete both of the key and the value. */
                //lpDeleteRangeWithEntry在本质上是至空操作,在listpack详解中讲到 ,lp的delete与replace为同一个底层功能
                zl = lpDeleteRangeWithEntry(zl,&fptr,2);
                o->ptr = zl;
                deleted = 1;
            }
        }
    } else if (o->encoding == OBJ_ENCODING_HT) {
        if (dictDelete((dict*)o->ptr, field) == C_OK) {
            deleted = 1;

            /* Always check if the dictionary needs a resize after a delete. */
            if (htNeedsResize(o->ptr)) dictResize(o->ptr);
        }

    } else {
        serverPanic("Unknown hash encoding");
    }
    return deleted;
}

从这里看出,t_hash在hashtable的编码下,是不会尝试回到listpack的
hsetCommand:

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;
    }
    //假如key不存在,需要先创建key
    if ((o = hashTypeLookupWriteOrCreate(c,c->argv[1])) == NULL) return;
    //只检查sds的长度,尝试将listpack直接转换为hash结构(如果条件满足的话)
    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 */
        //由于hmset的返回相对较长,需要特殊处理
        addReplyLongLong(c, created);
    } else {
        /* HMSET */
        addReply(c, shared.ok);
    }
     // 发送键修改通知
    signalModifiedKey(c,c->db,c->argv[1]);
    //自己做pub/sub的通知,没有托管给通用函数
    notifyKeyspaceEvent(NOTIFY_HASH,"hset",c->argv[1],c->db->id);
    server.dirty += (c->argc - 2)/2;
}
void hashTypeTryConversion(robj *o, robj **argv, int start, int end) {
    int i;

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

    for (i = start; i <= end; i++) {
       //只检查sds的长度,尝试将listpack直接转换为hash结构(如果条件满足的话)
        if (sdsEncodedObject(argv[i]) &&
            sdslen(argv[i]->ptr) > server.hash_max_listpack_value)
        {
            hashTypeConvert(o, OBJ_ENCODING_HT);
            break;
        }
    }
}

一般判断Command是否独立完成功能,未使用通用函数的一个方式是看这个函数负不负责pub/sub的方法即notifyKeyspaceEvent,一般自己调用notifyKeyspaceEvent就说明此Command函数核心功能都在自己。
其余的command函数相对比一样的简单明了,这里不做详细分析

t_hash总结

得益于底层数据结构完善的封装,t_hash在处理具体的command过程中相对较少的有着自己的逻辑处理,大部分是直接调用底层数据结构的函数。
变动较大的是,t_hash终于摆脱了ziplist的连锁更新噩梦,采用了listpack作为底层编码结构之一,在调用上响应相对来说会快一些。但无论是listpack还是ziplist,在t_hash中始终是小对象结构才会存储的,整体的响应优化在大结构体上是没有的,针对小型对象会多占用些内存,来换取更快的处理速度。
在做redis版本升级的过程中,不必专门替换hash-max-listpack-entries与hash-max-listpack-value参数,redis在兼容性上统一了两者,从createSizeTConfig中可以看到hash-max-listpack-entries与hash-max-ziplist-entries都会被解析给server.hash_max_listpackentries统一使用,但还是建议有空替换一下,假如之后对listpack再次进行优化,那么t_hash中对于ziplist相关的的兼容将会失效。

createSizeTConfig("hash-max-listpack-entries", "hash-max-ziplist-entries", MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_listpack_entries, 512, INTEGER_CONFIG, NULL, NULL),
createSizeTConfig("hash-max-listpack-value", "hash-max-ziplist-value", MODIFIABLE_CONFIG, 0, LONG_MAX, server.hash_max_listpack_value, 64, MEMORY_CONFIG, NULL, NULL),