Redis字典

本文摘抄自redis阅读笔记

典在Redis中应用十分广泛,它是实现数据库的基础,特别的它是数据库键空间的实现方式,因此非常必要研究透彻字典的构建。

散列方法

也就是hash方法。

  • 思想:根据节点的关键码值确定存储地址。
  • 核心:散列函数。
  • 原理:
    对于任意给定的查找表DL,选定“理想”的散列函数h及相应的散列表HT ,则对于DL中每个元素X,函数值 h(X.key) 为X在HT中的存储位置。
  • 首要问题:
    如何构造使节点“分布均匀”的散列函数
    一旦发生冲突,用什么方法解决(开散列法、闭散列法)

字典的实现

Redis选择高效、实现简单的哈希表作为字典的底层实现
dict.h给出了字典的定义:

/* 
 * 字典 
 * 每个字典使用两个哈希表,用于实现渐进式 rehash 
 */  
typedef struct dict {  
    // 特定于类型的处理函数  
    dictType *type;  
    // 类型处理函数的私有数据  
    void *privdata;  
    // 哈希表(2个)  
    dictht ht[2];         
    // 记录 rehash 进度的标志,值为-1 表示 rehash 未进行  
    int rehashidx;  
    // 当前正在运作的安全迭代器数量  
    int iterators;        
} dict;  

dict 类型使用了两个指向哈希表的指针
哈希表 ht[0] 是字典主要使用的哈希表。
哈希表 ht[1] 是程序对 0 哈希表 rehash 时使用的

哈希表的定义如下:

/* 
 * 哈希表 
 */  
typedef struct dictht {  
    // 哈希表节点指针数组(俗称桶,bucket)  
    dictEntry **table;        
    // 指针数组的大小  
    unsigned long size;       
    // 指针数组的长度掩码,用于计算索引值  
    unsigned long sizemask;   
    // 哈希表现有的节点数量  
    unsigned long used;       
} dictht;  

哈希表的节点定义如下:

/* 
 * 哈希表节点 
 */  
typedef struct dictEntry {  
    // 键  
    void *key;  
    // 值  
    union {  
        void *val;  
        uint64_t u64;  
        int64_t s64;  
    } v;  
    // 链往后继节点  
    struct dictEntry *next;   
} dictEntry;  

如下所示整个字典结构:


创建新字典

dict.c 中给出了创建字典的方法 dictCreate

/* 
 * 创建一个新字典 
 * T = O(1) 
 */  
dict *dictCreate(dictType *type,  
        void *privDataPtr)  
{  
    // 分配空间  
    dict *d = zmalloc(sizeof(*d));  
    // 初始化字典  
    _dictInit(d,type,privDataPtr);  
    return d;  
}  
  
/* 
 * 初始化字典 
 * T = O(1) 
 */  
int _dictInit(dict *d, dictType *type,  
        void *privDataPtr)  
{  
    // 初始化 ht[0]  
    _dictReset(&d->ht[0]);  
    // 初始化 ht[1]  
    _dictReset(&d->ht[1]);  
    // 初始化字典属性  
    d->type = type;  
    d->privdata = privDataPtr;  
    d->rehashidx = -1;  
    d->iterators = 0;  
    return DICT_OK;  
}  
  
/* 
 * 重置哈希表的各项属性 
 * 
 * T = O(1) 
 */  
static void _dictReset(dictht *ht)  
{  
    ht->table = NULL;  
    ht->size = 0;  
    ht->sizemask = 0;  
    ht->used = 0;  
}  
dict *d = dictCreate(&hash_type, NULL);

d 的值表示如下,新创建的两个哈希表没有为 table 属性分配空间
其中 ht[0]->table 空间分配将在第一次往字典添加键值时进行
ht[1]->table 空间分配将在rehash时进行

添加键值对到字典

键值对的添加在 Redis 实现时,是很重要的一部,涉及到效率问题。
情况也比较复杂,需要进行讨论:
字典未初始化,即 0 哈希表的 table 为空,则需要对其初始化
在插入时发生键碰撞,程序需要处理碰撞
插入新元素,使字典满足 rehash 条件,则需要启动相应 rehash 程序。

添加新元素到空白字典
// 所有哈希表的起始大小  
#define DICT_HT_INITIAL_SIZE     4  

第一次往空字典添加键值对时,程序根据DICT_HT_INITIAL_SIZE为 d->ht[0]->table 分配空间。

如下所示是一个空字典:



添加一个键值对以后如下所示:


碰撞处理

在哈希表实现中,当两个不同的键拥有相同的哈希值时,称这两个键发生碰撞(collision),哈希表实现必须想办法对碰撞进行处理。字典哈希表所使用的碰撞解决方法被称之为链地址法:这种方法使用链表将多个哈希值相同的节点串连在一起。假设现在有一个带有三个节点的哈希表,如下图:


对一个新的键值对 key4 和 value4,如果 key4 的哈希值和 key1 的哈希值相同,那么它们将在哈希表的0号索引上发生碰撞。通过将 key4-value4 和 key1-value1两个键值对用链表连接起来, 就可以解决碰撞的问题:

添加新键值对时触发rehash

链地址法实现的碰撞问题,会影响哈希表的性能,而性能主要取决于大小(size属性)与保存节点数量(used 属性)之间的比率:

哈希表的大小与节点数量,比率在 1:1 时,哈希表的性能最好。
如果节点数量比哈希表的大小要大很多的话,那么哈希表就会退化成多个链表,哈希表本身的性能优势便不复存在。

下面这个哈希表, 平均每次失败查找只需要访问 1 个节点(非空节点访问 2 次,空节点访问 1 次):


下面这个哈希表,平均每次失败查询需要访问5个节点:

为了在字典的键值对不断增多的情况下保持良好的性能, 字典需要对所使用的哈希表(ht[0])进行 rehash 操作:
在不修改任何键值对的情况下,对哈希表进行扩容, 尽量将比率维持在 1:1 左右。

dictAdd 在每次向字典添加新键值对之前, 都会对哈希表 ht[0] 进行检查, 对于 ht[0] 的size和used属性, 如果它们之间的比率 ratio = used / size 满足以下任何一个条件的话,rehash 过程就会被激活:

  • 自然 rehash : ratio >= 1 ,且变量 dict_can_resize 为真
  • 强制 rehash : ratio 大于变量 dict_force_resize_ratio
Rehash 执行过程

字典的 rehash 操作实际上就是执行以下任务:

  • 创建一个比 ht[0]->table 更大的 ht[1]->table ;
  • 将 ht[0]->table 中的所有键值对迁移到 ht[1]->table ;
  • 将原有 ht[0] 的数据清空,并将 ht[1] 替换为新的 ht[0] ;

经过以上步骤之后, 程序就在不改变原有键值对数据的基础上, 增大了哈希表的大小。以下展示了一次对哈希表进行 rehash 的完整过程:

开始 rehash

设置字典的rehashidx为0,标识着 rehash 的开始
为 ht[1]->table分配空间,大小至少为 ht[0]->used的两倍;

rehash 进行时

在这个阶段,ht[0]->table的节点会被逐渐迁移到ht[1]->table,因为 rehash是分多次进行的,字典的rehashidx变量会记录rehash进行到 ht[0] 的哪个索引位置上。以下是 rehashidx 值为 2 时,字典的样子:

节点迁移完毕
rehash 完毕

释放 ht[0] 的空间;
用 ht[1] 来代替 ht[0] ,使原来的 ht[1] 成为新的 ht[0] ;
创建一个新的空哈希表,并将它设置为 ht[1] ;
将字典的 rehashidx 属性设置为 -1 ,标识 rehash 已停止;

渐进式 rehash

rehash 程序并不是在激活之后,就马上执行直到完成的,而是分多次、渐进式地完成的。假设这样一个场景:在一个有很多键值对的字典里,某个用户在添加新键值对时触发了 rehash 过程,如果这个 rehash 过程必须将所有键值对迁移完毕之后才将结果返回给用户,这样的处理方式将是非常不友好的。另一方面,要求服务器必须阻塞直到 rehash 完成,这对于 Redis 服务器本身也是不能接受的。
为了解决这个问题, Redis 使用了渐进式(incremental)的 rehash 方式:通过将 rehash 分散到多个步骤中进行,从而避免了集中式的计算
渐进式 rehash 主要由 _dictRehashStep 和 dictRehashMilliseconds 两个函数进行:

  • _dictRehashStep用于对数据库字典、以及哈希键的字典进行被动 rehash
  • dictRehashMilliseconds则由 Redis 服务器常规任务程序(server cron job)执行,用于对数据库字典进行主动 rehash

_dictRehashStep
每次执行 _dictRehashStep,ht[0]->table哈希表第一个不为空的索引上的所有节点就会全部迁移到ht[1]->table。在rehash开始进行之后(d->rehashidx不为-1),每次执行一次添加、查找、删除操作, _dictRehashStep 都会被执行一次:


因为字典会保持哈希表大小和节点数的比率在一个很小的范围内,所以每个索引上的节点数量不会很多,所以在执行操作的同时,对单个索引上的节点进行迁移,几乎不会对响应时间造成影响。

dictRehashMilliseconds
dictRehashMilliseconds 可以在指定的毫秒数内,对字典进行 rehash 。
当 Redis 的服务器常规任务执行时,dictRehashMilliseconds会被执行,在规定的时间内,尽可能地对数据库字典中那些需要rehash的字典进行rehash,从而加速数据库字典的rehash进程。

字典的收缩

当哈希表的可用节点数比已用节点数多很多时,就可以对哈希表进行 rehash 实现收缩字典。
默认情况下,当达到 10% 的时候,就会进行收缩。
redis.c/htNeedResize 函数定义如下:

/* 
* 检查字典的使用率是否低于系统允许的最小比率 
* 是的话返回 1 ,否则返回 0 。 
*/  
int htNeedsResize(dict *dict) {  
   long long size, used;  
   // 哈希表大小  
   size = dictSlots(dict);  
   // 哈希表已用节点数量  
   used = dictSize(dict);  
   // 当哈希表的大小大于 DICT_HT_INITIAL_SIZE   
   // 并且字典的填充率低于 REDIS_HT_MINFILL 时  
   // 返回 1  
   return (size && used && size > DICT_HT_INITIAL_SIZE &&  
           (used*100/size < REDIS_HT_MINFILL));  
}  

字典的迭代

字典带有自己的迭代器实现 —— 对字典进行迭代实际上就是对字典所使用的哈希表进行迭代:
迭代器首先迭代字典的第一个哈希表,然后,如果 rehash 正在进行的话,就继续对第二个哈希表进行迭代。
当迭代哈希表时,找到第一个不为空的索引,然后迭代这个索引上的所有节点。
当这个索引迭代完了,继续查找下一个不为空的索引,如此反覆,直到整个哈希表都迭代完为止。

/* 
 * 字典迭代器 
 * 
 * 如果 safe 属性的值为 1 ,那么表示这个迭代器是一个安全迭代器。 
 * 当安全迭代器正在迭代一个字典时,该字典仍然可以调用 dictAdd 、 dictFind 和其他函数。 
 * 
 * 如果 safe 属性的值为 0 ,那么表示这不是一个安全迭代器。 
 * 如果正在运作的迭代器是不安全迭代器,那么它只可以对字典调用 dictNext 函数。 
 */  
typedef struct dictIterator {  
    // 正在迭代的字典  
    dict *d;                  
    int table,              // 正在迭代的哈希表的号码(0 或者 1)  
        index,              // 正在迭代的哈希表数组的索引  
        safe;               // 是否安全?  
    dictEntry *entry,       // 当前哈希节点  
              *nextEntry;   // 当前哈希节点的后继节点  
} dictIterator;  

最后编辑于
©著作权归作者所有,转载或内容合作请联系作者
  • 序言:七十年代末,一起剥皮案震惊了整个滨河市,随后出现的几起案子,更是在滨河造成了极大的恐慌,老刑警刘岩,带你破解...
    沈念sama阅读 203,362评论 5 477
  • 序言:滨河连续发生了三起死亡事件,死亡现场离奇诡异,居然都是意外死亡,警方通过查阅死者的电脑和手机,发现死者居然都...
    沈念sama阅读 85,330评论 2 381
  • 文/潘晓璐 我一进店门,熙熙楼的掌柜王于贵愁眉苦脸地迎上来,“玉大人,你说我怎么就摊上这事。” “怎么了?”我有些...
    开封第一讲书人阅读 150,247评论 0 337
  • 文/不坏的土叔 我叫张陵,是天一观的道长。 经常有香客问我,道长,这世上最难降的妖魔是什么? 我笑而不...
    开封第一讲书人阅读 54,560评论 1 273
  • 正文 为了忘掉前任,我火速办了婚礼,结果婚礼上,老公的妹妹穿的比我还像新娘。我一直安慰自己,他们只是感情好,可当我...
    茶点故事阅读 63,580评论 5 365
  • 文/花漫 我一把揭开白布。 她就那样静静地躺着,像睡着了一般。 火红的嫁衣衬着肌肤如雪。 梳的纹丝不乱的头发上,一...
    开封第一讲书人阅读 48,569评论 1 281
  • 那天,我揣着相机与录音,去河边找鬼。 笑死,一个胖子当着我的面吹牛,可吹牛的内容都是我干的。 我是一名探鬼主播,决...
    沈念sama阅读 37,929评论 3 395
  • 文/苍兰香墨 我猛地睁开眼,长吁一口气:“原来是场噩梦啊……” “哼!你这毒妇竟也来了?” 一声冷哼从身侧响起,我...
    开封第一讲书人阅读 36,587评论 0 258
  • 序言:老挝万荣一对情侣失踪,失踪者是张志新(化名)和其女友刘颖,没想到半个月后,有当地人在树林里发现了一具尸体,经...
    沈念sama阅读 40,840评论 1 297
  • 正文 独居荒郊野岭守林人离奇死亡,尸身上长有42处带血的脓包…… 初始之章·张勋 以下内容为张勋视角 年9月15日...
    茶点故事阅读 35,596评论 2 321
  • 正文 我和宋清朗相恋三年,在试婚纱的时候发现自己被绿了。 大学时的朋友给我发了我未婚夫和他白月光在一起吃饭的照片。...
    茶点故事阅读 37,678评论 1 329
  • 序言:一个原本活蹦乱跳的男人离奇死亡,死状恐怖,灵堂内的尸体忽然破棺而出,到底是诈尸还是另有隐情,我是刑警宁泽,带...
    沈念sama阅读 33,366评论 4 318
  • 正文 年R本政府宣布,位于F岛的核电站,受9级特大地震影响,放射性物质发生泄漏。R本人自食恶果不足惜,却给世界环境...
    茶点故事阅读 38,945评论 3 307
  • 文/蒙蒙 一、第九天 我趴在偏房一处隐蔽的房顶上张望。 院中可真热闹,春花似锦、人声如沸。这庄子的主人今日做“春日...
    开封第一讲书人阅读 29,929评论 0 19
  • 文/苍兰香墨 我抬头看了看天上的太阳。三九已至,却和暖如春,着一层夹袄步出监牢的瞬间,已是汗流浃背。 一阵脚步声响...
    开封第一讲书人阅读 31,165评论 1 259
  • 我被黑心中介骗来泰国打工, 没想到刚下飞机就差点儿被人妖公主榨干…… 1. 我叫王不留,地道东北人。 一个月前我还...
    沈念sama阅读 43,271评论 2 349
  • 正文 我出身青楼,却偏偏与公主长得像,于是被迫代替她去往敌国和亲。 传闻我的和亲对象是个残疾皇子,可洞房花烛夜当晚...
    茶点故事阅读 42,403评论 2 342

推荐阅读更多精彩内容

  • 字典 Redis 中的字典 由 dict.h/dict 结构表示: type 和 privdata 是针对不同类型...
    jiangmo阅读 529评论 2 0
  • 字典,又称符号表,是保存键值对的抽象数据结构。很多语言都内置字典这种常用的数据结构,但是C语言没有内置,所以red...
    舒小贱阅读 1,316评论 0 2
  • 字典(dictionary), 又名映射(map)或关联数组(associative array),是一种抽象数据...
    待汝豪杰只是凡夫阅读 327评论 0 1
  • Redis字典的底层实现为哈希表, 每个字典使用两个哈希表, 一般情况下只使用 0 号哈希表, 只有在 rehas...
    lmem阅读 581评论 0 0
  • redis所使用的C语言并没有内置丰富的数据结构,因而redis实现了很多数据结构,本文主要介绍字典。 字典又叫映...
    x1wan阅读 412评论 0 1