Redis源码阅读—数据结构之字典 dict.c/dict.h

dict.c/dict.h


一、 dict的定义

  字典,是一种用于实现键值对(key-value pair)保存的抽象数据结构,通过字典,可以在单个键(key)与单个值(value)之间进行关联(或者说是将键映射成值),而这些关联的键与值即为键值对

  在字典中,每一个键都是独一无二的,所以程序可以在字典中通过键来对值,甚至是键值对进行操作。

  在一些高级编程语言中,字典经常作为一种内置的数据结构出现,但是可惜的是,C语言并不在此列,所以 Redis 自己实现了字典这种数据结构。

  在 Redis 中,字典使用哈希表作为底层实现,而一个哈希表可以有多个哈希结点,每一个哈希节点保存了一组键值对。下图是一个典型的字典:

字典示意图

  下面依次是哈希节点、哈希表、字典、迭代器的实现。

哈希节点(dictEntry )

哈希节点,在 dict.h/dictEntry 中进行了定义:


// 哈希表节点
typedef struct dictEntry {
    
    // 键
    void *key;

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

    // 指向下一个哈希节点,构成单向链表,用以解决键冲突(链地址法)
    struct dictEntry *next;

} dictEntry;

  • key 属性用来保存键值对中的键,联合体(union)v 属性用来保存键值对中的值。由于 v 属性使用 union 关键字进行定义,所以键值对中的值可以是一个指针,一个 uint64_t 类型的整数,或者是一个 int64_t 类型的整数。

  • * next 属性是指向下一个哈希节点的指针,用来将多个哈希值相同的键值对连接起来,构成一个单向链表,用以解决键冲突(collision)即通过链地址法解决键冲突。

哈希表(dictht )

哈希表,在 dict.h/dictht 中进行了定义:


//哈希表
typedef struct dictht {
    
    // 哈希字节数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,总是等于 size - 1
    unsigned long sizemask;

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

} dictht;

  • table 属性是一个数组,数组中的每一个元素都是一个指向 dict.h/dictEntry 结构的指针。

  • size 属性记录了哈希表的大小,也即是 table 数组的大小。

  • used 属性记录了哈希表已有结点,即键值对的数量。

  • sizemask 属性的值总是等于 size-1 ,是用来和哈希值一起决定一个键应该被放在 table 数组的哪一个索引上面的。

字典(dict)

字典,在 dict.h/dict 中进行了定义:


//字典
typedef struct dict {

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

    // 私有数据
    void *privdata;

    // 哈希表
    dictht ht[2];

    // rehash 索引
    // 当 rehash 不在进行时,值为 -1
    int rehashidx;

    // 目前正在运行的迭代器的数量
    int iterators;
    
} dict;

  • ht属性:每一个字典(dict)都有两个哈希表(dictht),用来实现 渐进式 rehash ,保存在 ht 数组当中。并且,在字典当中,一般只会使用 ht[0] 哈希表,只有在对 ht[0] 哈希表进行 rehash 时才会用到 ht[1]。

  • rehashidx 属性用来记录 rehash 进度,当 rehash 没有在进行的时候,rehashidx = -1 。

  • iterators 属性用来记录目前正在运行的迭代器的数量。

  • type 属性和 privdata 属性是用来针对不同类型的键值对,创建多态字典的。

    • type 属性是一个指向 dictType 结构的指针,每个 dictType 结构保存了一组用于操纵特定类型键值对的函数,而 Redis 会为不同用途的字典设置不同的类型特定函数。
    • privdata 属性保存了要传给那些类型特定函数的可选参数。

其中,字典类型特定函数在 dict.h/dictType 中进行了定义:


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


/* 外部变量 Hash table types */
extern dictType dictTypeHeapStringCopyKey;
extern dictType dictTypeHeapStrings;
extern dictType dictTypeHeapStringCopyKeyValue;

字典迭代器(dictIterator )

字典迭代器,在 dict.h/dictIterator 中进行了定义:


// 字典迭代器
typedef struct dictIterator {
        
    // 被迭代的字典
    dict *d;

    // table :正在被迭代的哈希表,值可以是 0 或 1。
    // index :迭代器当前所指向的哈希表的索引位置
    // safe :标识这个迭代器是否安全
    int table, index, safe;

    // entry :当前迭代到的哈希节点
    // nextEntry :当前迭代到的哈希节点的下一个哈希节点
    dictEntry *entry, *nextEntry;

    long long fingerprint; /* unsafe iterator fingerprint for misuse detection */
    
} dictIterator;

  • d 属性是一个指向 dict 结构的指针,保存了当前迭代器正在处理的字典。

  • table 属性用来标识当前正在被迭代的哈希表,即 dict 结构中的 dictht 数组 ht 的下标,值可以为 0 或 1 。

  • index 属性用来标识迭代器当前所指向的哈希表的索引位置。

  • safe 属性用来标识这个迭代器是否是安全的迭代器,这对该迭代器后续能调用的 API 有影响。其中,safe = 1 表示该迭代器可以在迭代时进行增删改等操作,对字典进行修改,即调用 dictDelete、dictFind 等函数,如若不然,则只能使用 dictNext 遍历函数,而不能对字典进行修改。

  • entry 属性是一个指向 dictEntry 结构的指针,指向当前迭代到的哈希节点。

  • nextEntry 属性是一个指向 dictEntry 结构的指针,指向当前迭代到的哈希节点的下一个哈希节点。这个属性存在的原因是,在安全迭代器运行时,entry 指针所指向的节点可能会被修改,所以需要一个指针来保存下一节点的位置,来防止指针丢失。

  • fingerprint 属性是字典的指纹,用于误用检测,即在不安全的迭代器对字典进行操作后,与操作前的指纹值进行对比,若不同,则说明在迭代过程中有对字典进行增删操作,而这些操作在不安全的迭代器中是不被允许的。

宏定义常量

在 dict.h 中,对一些对常量的定义:


/*
 * 字典的操作状态
 */
// 操作成功
#define DICT_OK 0
// 操作失败(或出错)
#define DICT_ERR 1


/*
 * 哈希表的初始大小
 */
#define DICT_HT_INITIAL_SIZE     4

即:

  • DICT_OK:字典操作状态,表示操作成功

  • DICT_ERR:字典操作状态,表示操作失败或出错

  • DICT_HT_INITIAL_SIZE:哈希表的初始大小,值为 4


二、 dict 的 API

dict 的函数实现分为两种,一种是通过宏定义的函数,一种是在 dict.c 内实现的函数。

1. 宏定义函数

函数 作用 时间复杂度
DICT_NOTUSED 用于在字典的私有数据不使用时,让编译器忽略该私有数据,避免编译器错误 O(1)
dictFreeVal 释放字典中给定哈希节点的值 O(1)
dictSetVal 设置字典中给定哈希节点的值 O(1)
dictSetSignedIntegerVal 将一个有符号整数设置为给定哈希节点的值 O(1)
dictSetUnsignedIntegerVal 将一个无符号整数设置为给定哈希节点的值 O(1)
dictFreeKey 释放字典中给定哈希节点的键 O(1)
dictSetKey 设置字典中给定哈希节点的键 O(1)
dictCompareKeys 比较两个键 O(1)
dictHashKey 计算给定字典中给定键的哈希值 O(1)
dictGetKey 获取给定哈希节点的键 O(1)
dictGetVal 获取给定哈希节点的值 O(1)
dictGetSignedIntegerVal 获取给定哈希节点的有符号整数 O(1)
dictGetUnsignedIntegerVal 获取给定哈希节点的无符号整数 O(1)
dictSlots 获取给定字典的大小,即 ht[0].size + ht[1].size O(1)
dictSize 获取给定字典的已有节点数量,即 ht[0].used+ ht[1].used O(1)
dictIsRehashing 判断指定字典是否正在进行 rehash O(1)

2. dict.c 中实现的函数

在 dict.c 中定义了几个静态变量:


// 表示字典是否启用 rehash 
static int dict_can_resize = 1;

// 强制 rehash 的比率
static unsigned int dict_force_resize_ratio = 5;

// 哈希种子,用于 dictGenCaseHashFunction() 函数中
static uint32_t dict_hash_function_seed = 5381;

  • dict_can_resize
      表示字典是否启用 rehash ,程序可以通过后续在API 实现提及的dictEnableResize() 和 dictDisableResize() 函数,来手动地允许或阻止哈希表进行 rehash。但是,即使该值为零,也不是所有的 rehash 都会被阻止,这种情况与下列的 dict_force_resize_ratio 变量有关。

  • dict_force_resize_ratio
      字典强制 rehash 比率。当某个哈希表的已有哈希节点的数量和字典大小之间的比率大于字典强制 rehash 比率,即 size / used > dict_force_resize_ratio 时,即使 dict_can_resize = 0 也会强制执行 rehash。

  • dict_hash_function_seed
      哈希种子,用于哈希函数之一的 dictGenCaseHashFunction() 函数中。

而在 dict.c 中实现的函数,有以下几种类型:

(1)私有函数

函数 作用 时间复杂度
_dictExpandIfNeeded 根据需要,初始化字典的哈希表,或者对字典的现有哈希表进行扩展。
函数内部是通过调用 dictExpand() 函数来实现的
O(N)
_dictNextPower 对字典的哈希表 ht[0] 的大小做幂级扩展,即计算第一个大于等于 ht[0].size 的 2^N ,用作 rehash 操作需要的 ht[1] 的大小 O(1)
_dictKeyIndex 返回哈希表中可以将给定的 key 插入的索引位置。
如果哈希表正在进行 rehash ,那么返回的索引总是新的哈希表的索引,即 ht[1] 的索引,因为在 rehash 进行的过程中,插入的新节点总是插入到 ht[1] 当中的
O(N)
_dictInit 初始化字典 O(1)

(2)哈希函数

函数 作用 备注
dictIdentityHashFunction 直接使用 key 作为哈希值
dictIntHashFunction 对一个整数进行哈希 Thomas Wang's 32 bit Mix Function
dictGenHashFunction 对字符串进行哈希 MurmurHash2, by Austin Appleby
dictGenCaseHashFunction 对大小写不敏感的字符串进行哈希 基于 DJB 哈希
dictSetHashFunctionSeed 设置哈希种子 dict_hash_function_seed
dictGetHashFunctionSeed 获取哈希种子 dict_hash_function_seed

具体实现与分析日后单开一篇进行学习与分析。

(3)其余 API 实现

函数 作用 时间复杂度
_dictReset 对给定哈希表的各项属性值进行重置或初始化 O(1)
dictCreate 创建一个新的字典,内部调用了 _dictInit() 函数用以实现字典的初始化 O(1)
dictResize 缩小给定的字典的负载因子,即 ht[0].used / ht[0].size,让其比例接近于 1 : 1。
内部通过调用dictExpand() 函数实现
O(N)
dictExpand 扩展或创建一个新的哈希表
具体来说,先创建一个新的哈希表,之后:
1. 如果给定的字典的 ht[0] 不存在,那么将新的哈希表设置为 ht[0] ,即意味着创建一个新的哈希表
2. 如果给定的字典的 ht[0] 存在,那么将新的哈希表设置为 ht[1] ,并且将字典的 rehashidx 属性设置为 0,使程序可以开始对字典进行 rehash,即对已有哈希表进行扩展
O(N)
dictRehash 执行 N 步渐进式 rehash,将给定字典的哈希表 ht[0] 当中的哈希节点分 N 步 rehash 到哈希表 ht[1] 当中。
返回 0,表示所有键都已经从 ht[0] 迁移到了 ht[1] 当中。
返回 1,表示仍有键需要从 ht[0] 迁移到 ht[1] 当中。
需要注意的是,每步 rehash 都是以一个哈希表索引(桶)作为单位的,即每一步 rehash 之后,被 rehash 的桶里的所有节点都会被移动到新哈希表
O(N)
timeInMilliseconds 获取以毫秒为单位的 UNIX 时间戳 O(1)
dictRehashMilliseconds 在给定毫秒数内,以 100 步为单位,对字典进行 rehash (调用 dictRehash() 函数),返回该函数调用期间的 rehash 步数 O(N)
_dictRehashStep 在字典不存在安全迭代器的情况下,对字典进行单步 rehash。字典在有安全迭代器的情况下,是不能进行 rehash 的。
这个函数被多个查找、更新操作调用,使得字典在被使用的同时进行 rehash
O(1)
dictAdd 尝试将给定键值对添加到字典中。
内部调用了 dictAddRaw() 函数,通过对调用该函数返回的哈希节点的 val 属性赋值完成操作
O(N)
dictAddRaw 尝试将键插入到字典中,是比 dictAdd() 函数更加底层的函数。
如果键在字典中已经存在,返回 NULL;否则,程序会创建新的哈希结点,插入到字典相应的索引位置,为该哈希节点的 key 属性赋值,并返回该哈希节点,交由用户为该哈希节点的 val 属性赋值(即在 dictAdd() 函数中对该哈希节点的 val 属性赋值)
O(N)
dictReplace 将给定的键值对添加到字典中,如果键已经存在,那么对旧有的哈希节点的值进行更新。
如果键值对是全新添加的,返回 1;如果键值对是通过对原有的键值对更新得来的,返回 0
O(N)
dictReplaceRaw 查找函数,根据给定的 key 查找哈希节点并返回。
如果给定的 key 已经存在,返回包含该 key 的哈希节点;如果给定的 key 不存在,那么调用 dictAddRaw() 函数将 key 添加到字典当中,再返回包含该 key 的哈希节点。
总之,该函数总是会返回包含给定 key 的哈希节点
O(N)
dictGenericDelete 从字典中删除包含给定键的哈希节点,通过函数的 nofree 参数可以选择是否调用键和值的释放函数来删除键值 O(1)
dictDelete 从字典中删除包含给定键的哈希节点,并且调用键值的释放函数来删除键值 O(1)
dictDeleteNoFree 从字典中删除包含给定键的哈希节点,并且不调用键值的释放函数来删除键值 O(1)
_dictClear 删除哈希表上的所有节点,并重置哈希表的各项属性 O(N)
dictRelease 删除并释放整个字典 O(N)
dictFind 获取字典中包含键 key 的哈希节点 O(1)
dictFetchValue 获取包含给定键 key 的哈希节点的值 O(1)
dictFingerprint 获取给定字典的指纹。
指纹是一个 64 位的数字,用于表示字典在某个时间的状态。指纹在创建和释放一个不安全迭代器时获取,并且两次获取的指纹会进行比对,如果指纹不一致,那么说明在迭代过程中执行了被禁止的操作
O(1)
dictGetIterator 创建并返回给定字典的不安全迭代器 O(1)
dictGetSafeIterator 创建并返回给定字典的安全迭代器 O(1)
dictNext 获取迭代器指向的当前节点 O(1)
dictReleaseIterator 释放给定字典迭代器 O(1)
dictGetRandomKey 随机返回字典中任意一个节点 O(N)
dictGetRandomKeys 随机返回字典中多个节点 O(N)
rev 反转位,工具函数 O(1)
dictScan 对字典中的哈希节点进行迭代。
具体源码与算法日后分析
O(N)
dictEmpty 清空字典中的两个哈希表上的所有哈希节点,并重置字典属性 O(N)
dictEnableResize 开启自动 rehash,即将 dict_can_resize 设为 1 O(1)
dictDisableResize 关闭自动 rehash,即将 dict_can_resize 设为 0 O(1)

三、 dict 的特性

  1. 每个字典带有两个哈希表 ht[0] 与 ht[1]
      由于随着操作的进行,哈希表中保存的键值对数量会随之不断地变化,为了使哈希表的负载因子保持在一个合理的范围,会对哈希表进行相应的扩展或者收缩,所以需要进行 rehash 操作。
      所以每个字典都会带有两个哈希表 ht[0] 与 ht[1],其中,ht[0] 用于在平时使用,而 ht[1] 用于在 rehash 进行时使用:将 ht[0] 上的所有结点 rehash 到 ht[1] 上。
      rehash,指的是对键的哈希值与索引值重新计算,然后将键值对放到 ht[1] 的指定位置。
      在 rehash 进行时,新添加的哈希节点会被添加到哈希表 ht[1] 当中,确保哈希表 ht[0] 当中的结点只减不增。当 ht[0] 表中的结点全部迁移到 ht[1] 表中后,会释放 ht[0] 表,将 ht[1] 设置为 ht[0],并重置 ht[1]。

  2. 渐进式 Rehash
      对于上述提到的 rehash 操作,并不是一次性全部迁移完成,而是分多次、渐进式地完成的,因为在实际环境中,哈希表中保存的节点实际上是非常多的,如果一次性全部迁移的话,会对服务器的性能造成很大影响,甚至会导致服务器服务停止。
      具体来说,就是在对字典进行添加、删除、查找和更新操作时,会在需要 rehash 操作时进行单步 rehash,从而将庞大的工作量分摊开来。

  3. 键冲突
      哈希表使用链地址法来解决键冲突,即被分配到同一索引的多个键值会拉成一个单向链表,正因为如此,在查找操作时难免会用到循环结构,而新的哈希节点插入时也是采用头插法来实现的。

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

推荐阅读更多精彩内容