所有原理实现基于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,拉链冲突采用头插法