redis源码学习-t_hash篇
0 条评论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
解释来说
- listpack编码条件:哈希对象保存的所有键值对的键和值的字符串长度都小于 hash-max-listpack-value字节.且哈希对象保存的键值对数量小于 hash-max-listpack-entries 个
- hashtable编码条件:哈希对象保存的所有键值对的键和值的字符串长度都大于等于 hash-max-listpack-value字节或哈希对象保存的键值对数量大于等于 hash-max-listpack-entries 个
需要注意的是,编码方式一旦变化那么不会根据t_hash的缩小再变回来。
t_hash中负责处理的具体命令
- hdel:删除一个或多个哈希表字段
- hexists:查看哈希表 key 中指定的字段是否存在
- hget:获取存储在哈希表中指定字段的值
- hgetall:获取在哈希表中指定 key 的所有字段和值
- hincrby:为哈希表 key 中的指定字段的整数加减
- hincrbyloat:为哈希表 key 中的指定字段的浮点数加减
- hkeys:获取所有哈希表中的字段
- hlen:获取哈希表中字段的数量
- hmget:获取所有给定字段的值
- hmset:同时将多个 field-value 对设置到 key 中
- hset:: 将一个field-value 对设置到 key 中
- hsetnx:只有在字段 field 不存在时,将一个field-value 对设置到 key 中
- hvals:获取哈希表中所有值
- 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),