目录
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数据结构来实现了。
从底层结构来看,只要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;
}