Redis-数据结构-字典

来源:互联网 发布:电影《美国黑帮》知乎 编辑:程序博客网 时间:2024/06/04 20:51

字典在Redis中的应用相当广泛,比如Redis的数据库就是使用字典来作为底层实现的,对数据库的增删改查操作也是构建在字典的操作之上的。


Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

1、哈希表dict.h/dictht

typedef struct dictht  {       //哈希表数组      dictEntry  **table;         //哈希表大小      unsigned long size;     //哈希表大小掩码,用于计算索引值     //总是等于size-1      unsigned long sizemask;     //该哈希表已有节点的数量      unsigned long used;}dictht;
table属性是一个数组,数组中的每个元素都指向一个哈希表节点dict.h/dictEntry结构(键值对)的指针;

2、哈希表节点dict.h/dictEntry

typedef struct dictEntry {       //键       void *key;       //值       union{           void *val;           uint64_t u64;           int64_t s64;       }v;       //指向下个哈希表节点,形成链表       struct dictEntry *next;} dictEntry;
3、字典dict.h/dict

typedef struct dict {      //类型特定函数      dictType *type;      //私有数据      void *privdata;      //哈希表      dictht ht[2];      //rehash索引      //当rehash不再进行时,值为-1      int trehashidx;/*rehashing not in progress if rehashidx == -1*/}dict;typedef struct dicType {      //计算哈希表的函数      unsigned int (*hashFunction)(const void *key);      //复制键的函数      void *(*keyDup)(void *privdata,const void *key);      //复制值的函数      void *(*valDup)(void *privdata,const void *obj);      //对比键的函数      int (*keyCompare)(void *privada,const void *key1, const void *key2);      //销毁键的函数      void (*keyDestructor)(void *privdata, void *key);      //销毁值的函数      void (*valDestructor)(void *privdata, void *obj);}



哈希表算法

当要将一个新的键值对添加到字典里面时,程序会根据键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上.
Redis 计算哈希值和索引的方法如下:

hash = dict->type->hashFunction(k);index = hash & dict->ht[0].sizemask 

假设,要将上图中键值对 k1和v1添加到字典中,使用 hashFunction 计算出 k1 的哈希值为9,那么

index = 9 & 3 = 1;

Redis 使用 MurmurHash2 算法 来计算键的哈希值.

解决键冲突

当有两个或两个以上的键被分配到了哈希表数组的同一索引上时,称这些键发生了冲突( collision)

Redis 的哈希表使用链地址法来解决冲突,每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单项链表,被分配到同一个索引上的多个节点可以用这个对单向链表连接起来,这就解决了键冲突的问题.

Rehash

随着操作的不断进行, 哈希表保存的键值对会逐渐地增多或减少,为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时, 程序需要对哈希表的大小进行相应的扩展或者收缩.这个过程叫做rehash.

Redis 对字典的哈希表执行 rehash 的步骤如下:

  1. 为字典的 ht[1] 哈希表分配空间,空间的大小取决于要执行的操作,以及 ht0]当前包含的键值对数量( used 属性值):

    • 如果执行的是扩展操作,那么 ht[1] 的大小为第一个大于等于 ht0].used*2的 $2^n$ .

    • 如果执行的是收缩操作,那么ht[1]的大小为打一个大于等于 ht[0].used 的$2^n$.

  2. 将保存在 ht[0] 中所有键值对 rehash ht[1] 上面: 任何事指的是重新计算键的哈希值和索引值,然后键键值对放到ht[1] 哈希表的指定位置.

  3. ht[0] 包含的所有键值对都迁移到了 ht[1] 之后, 释放 ht[0], 再将ht[1]设置为 ht[0],并在 ht[1] 后面创建一个空白的哈希表.

举个例子,假设程序要对下图的 `ht[0] 进行扩展操作

  • ht[0].used 当前值为4 , $2^3$ 恰好是第一个大于等于 4*2 的值,所以 ht[1] 哈希表的大小设置为8,下图展示了 ht[1] 分配了空间之后字典的样子.

  • 将 ht[0] 包含的四个键值对 rehash 到 ht[1], 图下图所示:

  • 释放 ht[0], 将 ht[1] 设置为 ht[0]. 再分配一个空哈希表. 哈希表的大小由原来的4 扩展至8.


渐进式 rehash

上一节说过, 扩展或收缩哈希表需要将 ht[0] 里的所有键值对 rehash 到ht[1] 中,但是这个 rehash
动作并不是一次性,集中式完成的,而是分多次,渐进式完成的.

这么做的原因是,当哈希表里保存的键值对多至百万甚至亿级别时,一次性地全部 rehash 的话,庞大的计算量会对服务器性能造成严重影响.

以下是渐进式 rehash 的步骤:

  1. ht[1] 分配空间

  2. 在字典中维持一个索引计数器变量 rehashidx, 将它的值设置为0,表示 rehash 正式开始

  3. 在 rehash 进行期间,每次对字典进行增删改查时,顺带将 ht[0] 在 rehashidx 索引上的所有键值对 rehash 到 ht[1] 中,同时将rehashidx 加 1.

  4. 随着操作不断进行,最终在某个时间点上, ht[0] 所有的键值对全部 rehash 到ht[1] 上,这时将 rehashidx 属性置为 -1,表示 rehash操作完成.

在渐进式 rehash 执行期间,新添加到字典的键值对一律保存到 ht[1] 里,不会对ht[0] 做添加操作,这一措施保证了 ht[0]只减不增,并随着 rehash 进行, 最终编程空表.

渐进式的 rehash 避免了集中式 rehash 带来 的庞大计算量和内存操作.