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

redis 字典的更多相关文章

  1. Redis 字典的实现

    [Redis 字典的实现] 注意 dict 类型使用了两个指针,分别指向两个哈希表. 其中, 0 号哈希表(ht[0])是字典主要使用的哈希表, 而 1 号哈希表(ht[1])则只有在程序对 0 号哈 ...

  2. 阿里面试官:HashMap 熟悉吧?好的,那就来聊聊 Redis 字典吧!

    最近,小黑哥的一个朋友出去面试,回来跟小黑哥抱怨,面试官不按套路出牌,直接打乱了他的节奏. 事情是这样的,前面面试问了几个 Java 的相关问题,我朋友回答还不错,接下来面试官就问了一句:看来 Jav ...

  3. Redis 字典结构细谈

    Redis 字典底层基于哈希表实现. 一.哈希表结构 1.dictht: typedef struct dictht { dictEntry **table; //哈希表数组,存储具体的键值对元素,对 ...

  4. REDIS 字典数据结构

    对于REDIS来讲  其实就是一个字典结构,key ---->value  就是一个典型的字典结构 [当然  对于vaule来讲的话,有不同的内存组织结构 这是后话] 试想一个这样的存储场景: ...

  5. redis字典的底层实现hashTable

    Redis的字典使用哈希表作为底层实现.一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对 哈希表的数据结构为 table属性是一个数组,数组中的每个元素都是指向dictE ...

  6. 《闲扯Redis七》Redis字典结构的底层实现

    一.前言 上节<闲扯Redis六>Redis五种数据类型之Hash型 中说到 Hash(哈希对象)的底层实现有: 1.ziplist 编码的哈希对象使用压缩列表作为底层实现 2.hasht ...

  7. 《闲扯Redis八》Redis字典的哈希表执行Rehash过程分析

    一.前言 随着操作的不断执行, 哈希表保存的键值对会逐渐地增多或者减少, 为了让哈希表的负载因子(load factor)维持在一个合理的范围之内, 当哈希表保存的键值对数量太多或者太少时, 程序需要 ...

  8. redis字典

    字典作为一种保存键值对的数据结构,在redis中使用十分广泛,redis作为数据库本身底层就是通过字典实现的,对redis的增删改查实际上也是构建在字典之上. 一.字典的结构

  9. redis字典快速映射+hash釜底抽薪+渐进式rehash | redis为什么那么快

    前言 相信你一定使用过新华字典吧!小时候不会读的字都是通过字典去查找的.在Redis中也存在相同功能叫做字典又称为符号表!是一种保存键值对的抽象数据结构 本篇仍然定位在[redis前传]系列中,因为本 ...

随机推荐

  1. Javaweb之Servlet入门

    1. 什么是Servlet? Java Servlet 是运行在 Web 服务器或应用服务器上的程序:他是浏览器(HTTP客户端)请求和HTTP服务器上资源(访问数据库)之间的中间层. 2. 什么是S ...

  2. 1635: [Usaco2007 Jan]Tallest Cow 最高的牛

    1635: [Usaco2007 Jan]Tallest Cow 最高的牛 Time Limit: 5 Sec  Memory Limit: 64 MBSubmit: 383  Solved: 211 ...

  3. python 语法笔记(一)

    #python3里面input默认接收到的事str类型,而python2里面却认为是int类型n=int(input('请输入想要第几个数')) #如果将int省去,python3中该程序将会报错a, ...

  4. 去除IOS浏览器下面的工具栏

    在head标签里添加下面的元素 即可 <meta id="viewport" name="viewport" content="width=de ...

  5. 前端学PHP之自定义模板引擎

    前面的话 在大多数的项目组中,开发一个Web程序都会出现这样的流程:计划文档提交之后,前端工程师制作了网站的外观模型,然后把它交给后端工程师,它们使用后端代码实现程序逻辑,同时使用外观模型做成基本架构 ...

  6. [cookie篇]从cookie-parser中间件说起

    当我们在写web的时候,难免会要使用到cookie,由于node.js有了express这个web框架,我们就可以方便地去建站.在使用express时,经常会使用到cookie-parser这个插件. ...

  7. python遍历一个目录,输出所有文件名

    python遍历一个目录,输出所有文件名 python os模块 os import os  def GetFileList(dir, fileList):  newDir = dir  if os. ...

  8. For循环及例题

    For循环    (1)循环操作某一个功能(执行某段代码)    (2)四要素                  循环初始值                  循环条件                 ...

  9. 高可用系列之Nginx

    1.1Keepalived高可用软件 Keepalived起初是专为LVS设计的,专门用来监控LVS集群系统中各个服务节点的状态,后来又加入了VRRP的功能,因此除了配合LVS服务外,也可以作为其他服 ...

  10. XJOI1689相连的城市

    相连的城市 n个城市中,某些城市间有道路互相连接.给出与每个城市相邻的城市有多少个,请输出城市间的邻接矩阵. 输入格式: 第一行输入一个正整数n,表示城市的个数. 第二行输入n个用空格隔开的非负整数, ...