大粨兔奶糖

  博客园  :: 首页  :: 新随笔  :: 联系 :: 订阅 订阅  :: 管理

redis 字典

前言

借鉴了 黄健宏 的 <<Redis 设计与实现>> 一书, 对 redis 源码进行学习
欢迎大家给予意见, 互相沟通学习

概述

字典是一种用于存储键值对的抽象数据结构

redis 字典使用哈希表作为底层实现

字典结构

定义位置 (src/dict.h)

dict 结构

// 字典
typedef struct dict {
    // 字典类型所使用的操作函数集合
    dictType *type;
    // 私有数据
    void *privdata;
    // 哈希表
    dictht ht[2];
    // rehash 索引
    // 当 rehash 不在进行时, 值为 -1
    int rehashidx; /* rehashing not in progress if rehashidx == -1 */
    // 目前正在运行的安全迭代器的数量
    int iterators; /* number of iterators currently running */
} dict;

// 字典类型所使用的操作函数集合
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;

dictht 结构

/*
 * 哈希表
 * 每个字典都使用两个哈希表,从而实现渐进式 rehash
 */
typedef struct dictht {
    // 哈希表数组
    dictEntry **table;
    // 哈希表大小 (哈希桶的数量)
    unsigned long size;
    // 哈希表大小掩码, 用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量 (键值对数量)
    unsigned long used;
} dictht;
  • dictht 在 dict 结构中存在着2个 (dict 的 ht 属性)
  • ht[0] 是旧表, ht[1] 个是新表
  • ht[1] 新表只在 rehash 的时候使用

dictEntry 结构

// 哈希表节点
typedef struct dictEntry {
    // 键
    void *key;
    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;
    // 指向下个哈希表节点, 形成链表, 链接法解决冲突
    struct dictEntry *next;
} dictEntry;

哈希算法

定义

存储键值对时, 根据键计算出哈希值, 进而计算出索引位置, 将键值对存储到索引位置上
  • hash = dict->type->hashFunction(key)
  • index = hash & dict->ht[x]->sizemask

redis 使用 MurmurHash2 哈希算法

键冲突

不同的 key 用同一哈希算法时, 索引位置可能会相同, 造成键冲突
redis 使用 链接法 解决键冲突, 即索引位置相同的时候, 该位置存储为一个链表, 冲突的节点作为链表节点
注意: 插入冲突节点链表的顺序为, 从链表头部插入

hash seed

为了保证哈希算法计算出的散列值均匀分布, 加入的参数
static uint32_t dict_hash_function_seed = 5381;

字典哈希结构中不存在相同 key 的键值对

dictEntry *dictAddRaw(dict *d, void *key)
{
	// 省略
	// 若指定的 key 在字典中已存在, 在添加操作时直接返回 NULL
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;
	// 省略
}

rehash

定义

哈希表的索引位置是有限的, 随着操作的不断进行, 键冲突的情况会越来越多, 查询效率会逐渐降低, 为了让哈希表的负载因子维持在一个合理的范围内, 当哈希表保存的键值对太多或太少时, 会对哈希表进行扩展和收缩, 这个过程称为 rehash
  • 负载因子
    • 哈希表存储的节点总数 / 哈希桶数量
    • 阈值为5: static unsigned int dict_force_resize_ratio = 5;

rehash 开关

  • // 指示字典是否启用 rehash 的标识
    static int dict_can_resize = 1;
    
  • 字典在 rehash 期间, 不能调整大小

  • 字典在 rehash 开关关闭时, 不能调整大小

    • // 调整字典大小
      int dictResize(dict *d)
      {
          int minimal;
          // 不能在关闭 rehash 或者正在 rehash 的时候调用
          if (!dict_can_resize || dictIsRehashing(d)) return DICT_ERR;
          // 计算让比率接近 1:1 所需要的最少节点数量
          minimal = d->ht[0].used;
          if (minimal < DICT_HT_INITIAL_SIZE)
              minimal = DICT_HT_INITIAL_SIZE;
      
          // 调整字典的大小
          return dictExpand(d, minimal);
      }
      
    • DICT_HT_INITIAL_SIZE: 哈希表初始大小

      • #define DICT_HT_INITIAL_SIZE     4
        

字典扩展 (设置 rehashidx = 0, 可以开始 rehash)

// 字典扩展
int dictExpand(dict *d, unsigned long size)
{
    // 新哈希表
    dictht n;
    // 根据 size 参数, 计算所需调整到的大小
    unsigned long realsize = _dictNextPower(size);
    /*
    * 不能再 rehashing 时对字典调整大小
    * 要调整到的 size 值不能小于目前旧表中已用的大小 d->ht[0].used
    */
    if (dictIsRehashing(d) || d->ht[0].used > size)
        return DICT_ERR;

    // 为新表参数赋初始值
    n.size = realsize;
    n.sizemask = realsize-1;
    n.table = zcalloc(realsize*sizeof(dictEntry*));
    n.used = 0;
	// 若旧表数据为空, 则将新表作为旧表
    if (d->ht[0].table == NULL) {
        d->ht[0] = n;
        return DICT_OK;
    }
	/*
	* 若旧表数据非空
	* 将新创建的表作为新表 ht[1]
	* 设置字典的 rehashidx = 0, 使程序可以开始 rehash
	*/
    d->ht[1] = n;
    d->rehashidx = 0;
    return DICT_OK;
}

计算 rehash 的表的大小

// 计算第一个大于等于 size 的2的n次方的值, 作为哈希表的大小
static unsigned long _dictNextPower(unsigned long size)
{
    unsigned long i = DICT_HT_INITIAL_SIZE;
    if (size >= LONG_MAX) return LONG_MAX;
    while(1) {
        if (i >= size)
            return i;
        i *= 2;
    }
}

字典 rehash 操作

int dictRehash(dict *d, int n) {
    // 只可以在 rehash 进行中时执行
    if (!dictIsRehashing(d)) return 0;
    // 进行 n 步迁移
    while(n--) {
        dictEntry *de, *nextde;
        /*
        * 若旧表节点数为 0
        * 代表数据已经全部迁移完毕
        * 将新表设置为旧表
        * 重置新表参数
        * 关闭 rehash (设置 d->rehashidx = -1)
        */
        if (d->ht[0].used == 0) {
            zfree(d->ht[0].table);
            d->ht[0] = d->ht[1];
            _dictReset(&d->ht[1]);
            d->rehashidx = -1;
            return 0;
        }
        // 断言 rehashidx 没有越界
        assert(d->ht[0].size > (unsigned)d->rehashidx);
        // 遇到空的哈希桶, 跳过, 将 rehash 进度加1, 指向下个哈希桶
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
        // 获取指定位置的哈希桶
        de = d->ht[0].table[d->rehashidx];
        /*
        * 将哈希桶中的数据迁移到新哈希表
        * 哈希桶是个 list 结构
        */
        while(de) {
            unsigned int h;
            // 保存下个节点的指针
            nextde = de->next;
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;
            // 将节点插入到新表的哈希桶表头
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;
            // 更新计数器
            d->ht[0].used--;
            d->ht[1].used++;
            // 继续处理下个节点
            de = nextde;
        }
        // 将刚迁移完的哈希表索引的指针设为空
        d->ht[0].table[d->rehashidx] = NULL;
        // 更新 rehash 索引
        d->rehashidx++;
    }

    return 1;
}

所谓 rehash 操作, 就是将旧表中的数据依据新表的大小重新进行 hash 计算, 放入新表中, 最终全部数据迁移完毕后, 将新表作为旧表, rehash 结束

渐进式 rehash

redis 的 rehash 操作不是集中式的, 一次性完成的, 而是分散到了多个操作当中

之所以用渐进式的方式, 是考虑到若字典中的数据过多, rehash 耗费时间过多, 会造成此期间 redis 不可用

渐进式的操作 (增删改查 均会触发)

dictAddRaw (dictAdd, dictReplace均会调用)
dictGenericDelete
dictFind
dictGetRandomKey

迭代器

redis 的迭代器用于遍历字典, 分为 安全迭代器 与 非安全迭代器

  • 指纹生成

    dictFingerprint 函数用于生成指纹
    
  • 安全迭代器 与 非安全迭代器的区别

    在迭代器释放的时候, 会检测指纹是否发生变化, 若发生变化, 则会程序报错
    这就决定了非安全迭代器只能对哈希表进行查操作, 否则数据发生变化, 指纹就会改变
    

dict api (src/dict.c)

函数 作用 备注
dictIntHashFunction 哈希算法, 计算哈希值 unsigned int dictIntHashFunction(unsigned int key)
dictIdentityHashFunction 直接使用 key 作为哈希值 unsigned int dictIdentityHashFunction(unsigned int key)
dictSetHashFunctionSeed 设置哈希种子 hash seed void dictSetHashFunctionSeed(uint32_t seed)
dictGetHashFunctionSeed 获取哈希种子 hash seed uint32_t dictGetHashFunctionSeed(void)
dictGenHashFunction MurmurHash2 哈希算法 unsigned int dictGenHashFunction(const void *key, int len)
dictGenCaseHashFunction 哈希算法 unsigned int dictGenCaseHashFunction(const unsigned char *buf, int len)
_dictReset 重置哈希表 static void _dictReset(dictht *ht)
dictCreate 创建一个新字典 dict *dictCreate(dictType *type, void *privDataPtr)
_dictInit 初始化字典数据 int _dictInit(dict *d, dictType *type, void *privDataPtr)
dictResize 调整字典大小 int dictResize(dict *d)
dictExpand 根据 size 调整字典大小 int dictExpand(dict *d, unsigned long size)
dictRehash 对指定的字典 d, 进行 n 步 rehash int dictRehash(dict *d, int n)
timeInMilliseconds 返回毫秒为单位的 unix 时间戳 long long timeInMilliseconds(void)
dictRehashMilliseconds 在给定的毫秒内, 以100 步为单位, 进行rehash int dictRehashMilliseconds(dict *d, int ms)
_dictRehashStep 单步 rehash static void _dictRehashStep(dict *d)
dictAdd 将给定的键值对添加到字典中 int dictAdd(dict *d, void *key, void *val)
dictAddRaw 根据指定的 key, 创建新的哈希节点 dictEntry *dictAddRaw(dict *d, void *key)
dictReplace 将给定的键值对存入字典中, 若 key 不存在, 则新增; 若 key 存在, 则更新数据 int dictReplace(dict *d, void *key, void *val)
dictReplaceRaw 创建给定 key 的哈希节点, 若 key 不存在, 则新增; 若 key 存在, 则直接返回 dictEntry *dictReplaceRaw(dict *d, void *key)
dictGenericDelete 删除字典中指定 key 的节点, nofree 参数为0时, 代表同时调用键和值的 free 函数 static int dictGenericDelete(dict *d, const void *key, int nofree)
dictDelete 删除字典中指定 key 的节点, 同时释放键和值 int dictDelete(dict *ht, const void *key)
dictDeleteNoFree 删除字典中指定 key 的节点, 不释放键和值 int dictDeleteNoFree(dict *ht, const void *key)
_dictClear 删除指定字典的指定哈希表 ht 的所有节点, 并重置哈希表属性 int _dictClear(dict *d, dictht *ht, void(callback)(void *))
dictRelease 删除并释放指定字典 void dictRelease(dict *d)
dictFind 返回字典中指定 key 的节点 dictEntry *dictFind(dict *d, const void *key)
dictFetchValue 获取字典中指定 key 的值 void *dictFetchValue(dict *d, const void *key)
dictFingerprint 指纹生成 long long dictFingerprint(dict *d)
dictGetIterator 创建并返回指定字典的非安全迭代器 dictIterator *dictGetIterator(dict *d)
dictGetSafeIterator 创建并返回给定字典的安全迭代器 dictIterator *dictGetSafeIterator(dict *d)
dictNext 返回迭代器指向的当前节点 dictEntry *dictNext(dictIterator *iter)
dictReleaseIterator 释放迭代器 void dictReleaseIterator(dictIterator *iter)
dictGetRandomKey 随机返回字典中的任意节点 dictEntry *dictGetRandomKey(dict *d)
dictGetRandomKeys 随机获取字典中指定 count 个数的节点 int dictGetRandomKeys(dict *d, dictEntry **des, int count)
rev 翻转 bit 位 static unsigned long rev(unsigned long v)
dictScan 字典扫描函数 unsigned long dictScan(dict *d, unsigned long v, dictScanFunction *fn, void *privdata)
_dictExpandIfNeeded 根据需要, 对字典进行扩展 static int _dictExpandIfNeeded(dict *d)
_dictNextPower 计算一个大于等于给定 size 的2的n次方的值, 作为哈希表的大小 static unsigned long _dictNextPower(unsigned long size)
_dictKeyIndex
dictEmpty
dictEnableResize
dictDisableResize
posted on 2017-04-05 08:32  大粨兔奶糖  阅读(170)  评论(0编辑  收藏  举报