redis源码之字典dict

未完待续…

字典dict

1.简介:

它支持插入、删除、替换、查找和获取随机元素等操作。
哈希表会自动在表的大小的二次方之间进行调整。
键的冲突通过链表来解决。
rehash

2.定义

/*
 * 1.哈希表节点
 */
typedef struct dictEntry {

    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

/*
 * 2.哈希表
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
 */
typedef struct dictht {

    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;

    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

/*
 * 3.字典
 */
typedef struct dict {

    // 类型特定函数
    dictType *type;

    // 私有数据,保存了需要传给那些类型特定画数的可选参数。
    void *privdata;

    // 哈希表,一般情况下,字典只使用ht[OJ 晴希表, ht[1]晗希表只会在对ht[0]哈希表进行rehash 时使用。
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */

    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */

} dict;

/*
 * 4.字典类型特定函数
 */
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);

    // 销毁值的函数
    void (*valDestructor)(void *privdata, void *obj);

} dictType;

这里写图片描述
这里写图片描述

3.hash算法,得到索引值

Murmurhash2和DJBhash
Redis 计算晗希值和索引值的方法如下:
#使用字典设置的哈希函数,计算键key 的哈希值
hash= dict->type->hashFunction(key);
#使用哈希衰的sizemask 属性和哈希值,计算出索引值
#根据情况不同, ht[x]可以是ht[0l 或者ht[1]
index= hash 品dict->ht[x].sizemask;

解决键冲突
链地址法(因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 所以为了速度考虑,程序总是将新节点添加到链表的表头位置(复杂度为O(1)), 排在其他已有节点的前面。 )

4.rehash

为了让晗希表的负载因子( load factor )维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对晗希表的大小进行相应的扩展或者收缩。
(1)为字典的ht[1]哈希表分配空间:
(1)如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0] .used*2
的2”( 2 的n 次方幕);
(2)如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0] .used的2”;、
(2)将保存在ht[0]中的所有键值对rehash 到ht[1]上面: rehash 指的是重新计算键的晗希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上。
(3)当ht[0]包含的所有键值对都迁移到了ht[1]之后( ht [0]变为空表),释放
ht[0],将ht [1]设置为ht[0] ,并在ht[1]新创建一个空白哈希表,为下一次rehash
做准备。
哈希表的扩展与收缩条件
负载因子 = 哈希表已保存节点数量 / 哈希表大小,load factor= ht[O].used I ht[OJ.size
负载因子大于1是因为,size是table的数量,而每个table还有链表呢 !
当以下条件中的任意一个被满足时,程序会自动开始对晗希表执行扩展操作:
(1)服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于于 1 ;
(2)服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;
当晗希表的负载因子小于0.1 时,程序自动开始对晗希表执行收缩操作。
在执行BGSA阳命令或BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制( copy-onwrite)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,从而尽可能地避免在子进程存在期间进行晗希表扩展操作,这可以避免不必要的内存写入操作,最大限度地节约内存。

5.渐进式 rehash:索引0->sizemask

rehash 动作并不是一次性、集中式地完成的,而是分多次、渐进式地完成的。原因在于,一次性将这些键值对全部rehash到ht[1]的话,庞大的计算量可能会导致服务器在一段时间内停止服务。
以下是哈希表渐进式 rehash 的详细步骤:
(1)为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
(2)在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
(3)在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增1。
(4)随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。

1.因为在进行渐进式 rehash 的过程中, 字典会同时使用 ht[0] 和 ht[1] 两个哈希表, 所以在渐进式 rehash 进行期间, 字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行: 比如说, 要在字典里面查找一个键的话, 程序会先在ht[0] 里面进行查找, 如果没找到的话, 就会继续到 ht[1] 里面进行查找, 诸如此类。
2.另外, 在渐进式 rehash 执行期间, 新添加到字典的键值对一律会被保存到 ht[1] 里面, 而 ht[0] 则不再进行任何添加操作: 这一措施保证了 ht[0] 包含的键值对数量会只减不增, 并随着 rehash 操作的执行而最终变成空表。

typedef struct dictIterator {
    // 被迭代的字典
    dict *d;
    // table :正在被迭代的哈希表号码,值可以是 0 或 1 。
    // index :迭代器当前所指向的哈希表索引位置。
    // safe :标识这个迭代器是否安全
    int table, index, safe;
    // entry :当前迭代到的节点的指针
    // nextEntry :当前迭代节点的下一个节点
    // 因为在安全迭代器运作时, entry 所指向的节点可能会被修改,
    // 所以需要一个额外的指针来保存下一节点的位置,
    // 从而防止指针丢失
    dictEntry *entry, *nextEntry;
    long long fingerprint; // unsafe iterator fingerprint for misuse detection
} dictIterator;
發表評論
所有評論
還沒有人評論,想成為第一個評論的人麼? 請在上方評論欄輸入並且點擊發布.
相關文章