哈希表的实现

哈希表的实现

  • 哈希表可以认为是redis最为重要的一个数据结构
  • 实现了key,value的查找,保存所有的key等

1.基本数据结构

typedef struct dictEntry {
    void *key;// 键
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v; // 值(可以有几种不同类型)
    struct dictEntry *next;// 指向下一个哈希节点(形成链表)
} dictEntry;
  • 这个数据结构可以认为是hash表的一个元素,包含键值对。用于链接法散列。是链接法散列中的链表部分。具体的第一个key,value 可以是联合体里面的几种类型
    最后一个是指向具有相同hash值的元素。
typedef struct dictType {
        unsigned int (*hashFunction)(const void *key);//哈希计算方法,返回整形变量
        void *(*keyDup)(void *privdata, const void *key);
        void *(*valDup)(void *privdata, const void *obj);
        int (*keyCompare)(void *privdata, const void *key1, const void *key2);
        void (*keyDestructor)(void *privdata, void *key);//key的析构函数
        void (*valDestructor)(void *privdata, void *obj);//val的析构函数
} dictType;
  • 这个数据结构定义了一些在hash表上的一系列操作,结构体里面都是函数指针,每个hash表里面都会有这个数据结构,对每个hash表进行操作。
typedef struct dictht {
        dictEntry **table;//字典实体,使用的是数组形式,方便查找key
        unsigned long size;//字典大小
        unsigned long sizemask;//字典掩码,为size-1
        unsigned long used;//这个字典表中(数组中),被使用的个数
} dictht;
  • 定义了hash表的形式,使用一个保存hash表元素地址的一个数组,还有存储hash表的大小,大小掩码,使用个数。可以认为是一个数组的扩展。方便管理的数组。
typedef struct dict {
        dictType *type;//定义了对于hash表操作的一些列函数
        void *privdata;//私有的数据
        dictht ht[2];//两个hash表,一个新的一个旧的,可以在必要的时候进行hash表的扩张,和hash表的rehash。
        long rehashidx; /* rehashing not in progress if rehashidx == -1 *///rehash的标志-1表示没在rehash,可以根据这个值来取得两张hash表哪个可以使用。
        int iterators; /* number of iterators currently running *///这个hash表上迭代器的个数
} dict;
  • 对于两个hash表的管理数据结构,所有的操作都是通过这个数据结构来进行的。包含hash元素的添加,删除,查找,扩张,rehash。
typedef struct dictIterator {
        dict *d;//管理hash表的数据机构
        long index;//索引
        int table, safe;//表和安全性标志
        dictEntry *entry, *nextEntry;//当前元素和下一个元素
        /* unsafe iterator fingerprint for misuse detection. */
        long long fingerprint;//指纹
} dictIterator;
  • hash表上面的迭代器,可以通过迭代器来访问hash表中的元素。当safe=1时候是安全的,可以对于hash表进行删除插入操作。否则只能读取hash表
static void _dictReset(dictht *ht)
  • 对于hash表进行空的初始化,把hash表中的组数,大小都设置为默认值。
dict *dictCreate(dictType *type, void *privDataPtr)
  • 创建一个管理hash表的数据结构,首先分配内存,设置管理hash表数据结构的hash类型,私有数据,迭代器,最为主要的是初始化两张hash表。
    这样两张hash表就创建成功了,但是现在里面并没有任何元素,也可以说这两张表(两个数组)内容是hash元素指针,两个数组里面并没有开始分配内存,只有数组名。
  • 正真进行分配内存的时候(给dictht的table分配内存)是在dict的扩张时候。见下面的函数int dictExpand(dict *d, unsigned long size)
int _dictInit(dict *d, dictType *type,void *privDataPtr)

-根据参数对于管理两个hash表的数据结构进行初始化,设置

int dictExpand(dict *d, unsigned long size)
  • 创建或者扩张hash表,其中size为hash表的大小,也就是dictht中table的大小,动态分配内存。
  • 创建hash表:table存储的是hash元素值的指针,在这个时候会给table分配内存,分配内存的大小是大于size的2的指数倍。给的size大小需要检查,不能超过long_max值。同时这个值还不能和当前hash表的大小一致,这样没什么意义。
  • 分配好table的大小之后,就可以把它赋值给管理hash表的数据结构了。管理数据结构dict通过ht[2]来对这两个hash表进行管理。
  • 这样就给两个hash表分配的内存,其实这两个hash表的指针是指向同一快内存的。这只是在创建的时候两张hash表是同一块内存。ht[0]=ht[1] if (ht[0].table=null)
  • Expend过程:上面的创建过程给两张表指针指向同一块内存,其实如果两个hash表已经初始化了之后,就可以达到扩展的过程。ht[0].table=ht[1].table if (ht[0].table=null)
  • 这语句的意思就是在扩张的时候一个hash表已经分配内存了,就会给另一张表分配内存。并不会覆盖原来有值的hash表。
int dictRehash(dict *d, int n)
  • 重新hash,就是把hashtable[0](ht[0])里面的所有的键重新hash一下,然后把键值存储在hashtable1里面,并且hashtable[0]还指向重新hash后的hash表。

  • 整个表都rehash完成后返回0,部分rehash返回1。

  • 具体的过程是这样的:

  • 首先在hashtable[0]里面找到所有非空的键值对(数组中查找到链表头节点(头指针)),然后对这个查找到的链表进行遍历,链表中的每一个元素都是键值对。根据链表节点中的键key,找到在hashtable[1]数组中的索引位置,然后插入到这个索引所在的链表的表头。

  • 这样就可以完成一个字典实体的转换,一只把整个链表遍历完毕。然后在把hashtable[0]数组中的所有非空元素都遍历完毕。
    最后实现了,hashtable[0]里面的所有元素,转换为hashtable[1]中的所有元素。- 实现rehash的过程。注意每次最多遍历n*10个元素。

  • 结束了上面的两重遍历之后,就可以检查是否完成整个rehash的过程是否完成。会进行一些列的释放资源的操作,比如释放hashtable[0]的内存,使得hashtable[1]和hashtable[0]指针互换。重置hashtable[1]

  • 从这里也可以看出来,主要的hash表还是hashtable[0],hashtable[1]只是起到了中间作用。还有一点就是可能会存在部分rehash的过程。可以把部分rehash的内容,存放在另一个临时的hash表中。

  • 回过头来看看,rehash的过程是把一个hash表中所有元素拿出来之后,按着某种方式对key进行重新hash然后存放在其他的表中。

long long timeInMilliseconds(void)
  • 获取当前时间的毫秒数,使用到了gettimeofday函数
int dictRehashMilliseconds(dict *d, int ms)
  • 在ms毫秒之间进行hash,也就是说控制rehash的时间超过ms毫秒。只要rehash时间没有超过ms毫秒,那么增加rehash的参数n大小,每次增加100,知道所有rehash时间超过ms毫秒.
  • 这是一种怎样的控制啊,强制要求rehash超过一定的时间,我觉着可能是为了控制rehash的大小。移动元素的个数。考虑到rehash执行的时间对于其他请求的影响。
static void _dictRehashStep(dict *d)
  • 当没有迭代器的时候会每次rehash10个元素(调用dictRehash(dict, 1) ),逐渐的把hashtable[0]里面的元素迁移到hashtable[1]中。什么时候能够知道完全rehash完毕呢? 根据dictRehash函数的返回值。
static int _dictKeyIndex(dict *d, const void *key)
  • 查找key所在hashtable([0],[1])中的index,key存在返回所在数组的index,否则返回-1。
  • 先根据key和整个hash表的hash计算方法,计算出key的hash码,也就是在hashtable中的索引,其实是这个key应该在哪个位置上。肯定会有一个index,其次如果这个index指向的链表中
  • 如果链表为空,那么返回hash码,否则需要遍历这个链表查看是否存在与key对应的元素。最后如果正在rehash,或者rehash 并没有完成(部分rehash),那么就需要,把两个表都遍历,否则只查找一个表(hashtable[0])
dictEntry *dictAddRaw(dict *d, void *key)
  • 添加只有key的字典元素到hashtable中,并返回可以设置value的字典元素指针。后来需要调用setvalue函数来进行value的设置。
    这个函数会首先调用_dictKeyIndex查找到应该在的index,当然也可能会key已经存在了。然后根据是否部分rehash,来决定index是在哪一个表中。hashtable[0] or hashtable[1]
  • 然后创建字典元素实体,并设置key值,放在hashtable中。
int dictAdd(dict *d, void *key, void *val)
  • 添加对key,value对到hashtable中,向字典中添加元素。
  • 会调用dictAddRaw添加元素的key
  • 然后调用dictSetVal设置key在hashtable中的实体的value值。
dictEntry *dictFind(dict *d, const void *key)
  • 查找key所在的字典实体,返回字典实体的指针
  • 这个查找的过程和_dictKeyIndex类似,只是这里面要查找到具体的字典实体所在的内存地址
  • 首先遍历两个hashtable(数组),找到头节点,接着遍历链表,找到和key相同的字典实体,并返回地址。找不到就会返回NULL
int dictReplace(dict *d, void *key, void *val)
  • 调用dictFind找到字典实体,调用dictSetVal设置字典的新值,然后释放原来字典元素的内容。
dictEntry *dictReplaceRaw(dict *d, void *key)
  • 查找key,如果存在则添加key,不存在怎返回NULL
static int dictGenericDelete(dict *d, const void *key, int nofree)
  • 查找并删除一个key元素,
  • 根据两张hashtable来进行查找,找到一个头节点之后,进行遍历,知道找到key的值所在的字典,如果没有找到返回error,找到了之后进行删除并释放内存,返回ok
int dictDeleteNoFree(dict *ht, const void *key)
  • 调用dictGenericDelete(ht,key,1)删除值,但是并不释放内存。
int dictDelete(dict *ht, const void *key)
  • 调用dictGenericDelete(ht,key,0)删除值,并释放内存。
int _dictClear(dict *d, dictht *ht, void(callback)(void *))
  • 清除指定的ht整个字典内容并,释放内存。
void dictRelease(dict *d)
  • 调用_dictClear(d,&d->ht[0],NULL)调用_dictClear(d,&d->ht[1],NULL),分别释放两个hashtable
void *dictFetchValue(dict *d, const void *key)
  • 首先调用dictFind,然后获得这个键对于的字典实体,进而获得字典值。
dictIterator *dictGetIterator(dict *d)
  • 创建dictIterator实体,并把内容迭代的指针指向dict d。获得的是不安全的迭代器,只能通过这个迭代器读取字典,不能写操作。
dictIterator *dictGetSafeIterator(dict *d)
  • 获得安全的迭代器版本,可以对整个字典进行读写操作。
dictEntry *dictNext(dictIterator *iter)
  • 获得下一个字典元素指针。
  • 具体的操作:
  • 1.next不为空的时候直接返回next的内容,这个时候迭代器里面保存了当前hashtable的索引值,所以可以,等到把这个链表遍历完了,就可增加索引index的值,按着往数组后面开始遍历另一个链表
  • 2.next为空的时候,这个时候需要判断,是否为安全的迭代器,如果是增加指纹信息(我猜测可能是为了后面进行通过迭代器进行修改字典进行的验证),并且需要增加迭代器的引用个数。
  • 判断一个表是否遍历完了。如果这个表遍历完了(通过index和桶的大小),并且正在进行rehash那么就需要切换到另一个hashtable中进行依次从数组的开始0进行遍历。
void dictReleaseIterator(dictIterator *iter)
  • 释放迭代器
  • 如果为安全版本的迭代器,需要减少字典迭代器上面的引用,否则需要验证是否为当前字典的指纹信息。
dictEntry *dictGetRandomKey(dict *d)
  • 随机获得字典中的一个键值对。
  • 随机找到一个索引,并且随机获得这个索引指向的链表中的一个元素。
最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 159,716评论 4 364
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 67,558评论 1 294
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 109,431评论 0 244
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 44,127评论 0 209
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 52,511评论 3 287
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 40,692评论 1 222
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 31,915评论 2 313
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 30,664评论 0 202
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 34,412评论 1 246
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 30,616评论 2 245
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 32,105评论 1 260
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 28,424评论 2 254
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 33,098评论 3 238
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 26,096评论 0 8
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 26,869评论 0 197
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 35,748评论 2 276
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 35,641评论 2 271

推荐阅读更多精彩内容