redis存储数据字典 redis 字典实现_hash表


一. 概述

首先,一个字典需要实现什么功能呢 ?

一个键值对来记录数据, 能够插入数据、修改数据、删除数据, 通过键key 能够极可能快速的查找数据。

Redis数据库的底层实现就是字典, 例如, 当我们在redis客户端的命令行上输入一个最简单的命令:redis > SET name "mercury" ,

它就在数据库字典里生成了一个条目(键值对),key 是 值为name的字符串对象, value 是值为mercury 的字符串对象。

除此之外,当hash对象的编码是hashtable 时, 它的实现方式也是字典。

二. 字典结构

hash表查找元素的时间复杂度为O(1), 所以Redis采用hash表来作为字典的底层实现。

hash表解决冲突有两种方法:开放地址法,拉链法。 redis的字典采用拉链法来解决冲突。

为了速度考虑,redis 总是将新节点添加到链表表头位置, 例如, 下图, 先插入k0, 再插入k1, k0和k1被hash 到一个桶中, k1 会被插入到链表的表头。


redis存储数据字典 redis 字典实现_文化程度字典表_02

hash 表结构


让我们先看下节点的代码:


typedef struct dictEntry {
     void *key;
     union{
          void *val;
          uint64_t u64;
          int64_t s64;
     } v;
     struct dictEntry *next;
} dictEntry;


节点由三部分组成: 键key, 值v,指向同一个桶(我们把dictEntry 数组的每个位置叫做一个桶,一个桶存一条拉链)内的后继节点的指针,v 可以是一个指针,或者一个 uint64t整数, 或者是一个int64_t 整数。

hash 表的定义如下:


typedef struct dictht {
      // hash 表数组
      dictEntry ** table;
      // hash表的大小
      unsigned long size;
      unsigned long sizemask;
      // 已有节点的数量
      unsigned long used;
}


注意到dictht 结构中有一个sizemask ,在直觉上 我们一般用 key 的hash 值% hash表的size 来决定key 存放的位置index , 但redis 有个更巧妙的做法,用key计算出来的hash 值同sizemask 做& 运算。CPU做位运算要快于+-*%,工业级数据结构几乎都会用类似的操作来最大程度上提高运行速度。计算索引值的代码如下:


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


上面的dict,type 和 ht 是什么在下面会提到。

字典结构:


typedef struct dict {
    // 类型特定函数
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;


其中 type 和 privdata 是为了创建多态字典而设置的。

dictType 封装了一簇对于key和value 的各种操作的函数指针。 private 保存了传进这些函数的可选参数


typedef struct dictType {
    //哈希计算方法,返回整形变量 
    uint64_t (*hashFunction)(const void *key);
    //复制key函数 
    void *(*keyDup)(void *privdata, const void *key);
    void *(*valDup)(void *privdata, const void *obj);
    //key值比较方法
    int (*keyCompare)(void *privdata, const void *key1, const void *key2);
    //key的析构函数  
    void (*keyDestructor)(void *privdata, void *key);
    void (*valDestructor)(void *privdata, void *obj);
} dictType;


三. 字典扩容

hash 表有一个特性, 随着元素越来越多, 新插入一个元素发生hash冲突的次数就会越来越多, 查找一个元素的速度也会越来越慢。当hash表保存的键值对数量太多或太少,redis 就会对hash 表进行扩容和缩容。 扩容和缩容的操作可以通过执行rehash(重新散列)操作来完成。 rehash的原理很简单, 创建一个新的更大或更小的数组, 重新计算index位置,把元素都拷贝过去, 然后释放原来的数组

dict 中有两个 dictht 结构, 就是用来执行扩容和缩容操作的。假设redis要对ht[0]进行扩容,可以采取如下操作:

  1. 为ht[1] 分配空间,ht[1] 的大小为第一个大于等于 ht[0].used*2 的 2的n次方 。
  2. 将ht[0] 中的元素rehash 到ht[1] 。
  3. 释放ht[0] 。

但这样存在一个问题, 假设当客户端发过来一个add 命令, 刚好触发扩容操作, 如果一次性将h[0] 里面的所有键值对全部rehash 到ht[1]里, 它的耗时会非常长。 所以redis 采取渐进式的rehash 方案,在dict 中,用rehashidx来表示rehash的进度, 其详细步骤如下:

  1. 为ht[1] 分配空间
  2. 将rehashidx 初始化为0 ,代表rehash 工作正式开始。
  3. 每次字典进行删除、查找、更新操作时, 会同时在两个hash表上进行(先查找ht[0], 如果没找到,再去查找ht[1])。 进行添加操作时,会直接添加到ht[1]。
  4. 在进行每次增删改查操作时, 会同时把ht[0] 在rehashidx 索引上的所有键值对都rehash到ht[1]上, 完成后 rehashidx 加1.
  5. 当ht[0] 所有元素都被复制到ht[1], 设置rehashidx 的值为-1 。
  6. 回收 ht[0]。

参考资料:

  • 《Redis设计与实现》
  • Redis 源码注释 :https://github.com/huangz1990/redis-3.0-annotated/tree/unstable/src