redis源码阅读理解,及相关语言细节(哈希编码)---dict.c(上)

dict应该是redis查找速度提升的关键了吧,我们首先要知道什么字典。

字典就是将一个数据通过一系列的变化产生一个哈希值,哈希值与这个数据是一一对应的,但是从哈希代码几乎是不可能回推数据的,所以哈希经常运用在密码学上,也就是说,通过你设置的密码产生一个哈希值,然后数据库会保存这个哈希值,对,数据库保存的是密码的哈希值,然后当你输入密码的时候通过一系列的变化再次产生哈希值,然后通过哈希值对比来判断密码输入的对错,这里面就不得不提哈希变化的几个关键的特点了:

1.不可逆推性

2.不相干性

3.平均性

是的,这是我自己理解总结的,因为我之前也很迷惑,所以我十分努力的研究了这一块,接下来我按照我的理解来解释一下,首先,不可逆推是最简单的,也就是说当你拿到了哈希码之后是无法知道这段哈希代表的实际意义,所以数据库存储的是哈希值,这样,即使数据库泄露了,用户的相关密码也不会出问题,不相干性,这个的意思是,两个十分相近的数据在经过哈希变化之后会变换成完全不同的哈希值也就是说:

1.redissource  2.redisource  这两个数据的哈希值会完全不同,尽管只是改了很小的一部分,但是产生的哈希值任然是千差万别,接下来是平均性,也就是说,任意哈希值的出现都是等概率的,所以在数据库中的可以直接用哈希值映射到对应的内存中,也就是说,只要知道了哈希值,那么我们就能知道这个哈希值所存在的数据地址,这大概就是哈希值的一些基本的理解了,有了这些我们才能明白dict这个文件的代码功能。

另外还有一个概念就是哈希冲突,这一块的意思是说,尽管算法已经尽力去避免数据产生的哈希不同,但是仍然会出现两个不同的数据产生同一个哈希值,当出现这种情况的时候我们就称为出现了哈希冲突,为了解决这种问题,我们需要做很多事情,但是并不是本文的重点,如果想要了解的话可以去参照这个博客点击打开链接

----------------------------------------------------跑题分割线-------------------------------------------------

这一块的内容和本文无关,但是我还是想写一下,可以忽略,哈希可以用到很多地方,主要的运用方向包含以下几个方面

1文件校验

2数字签名

3鉴权协议

为什么我要说这个,因为每一次的笔试招聘都会考到相关的知识,记住他是十分有用的

还有就是非常关键的一点,c++中有一个stl---unorderedmap,即无序的key-value存储,他的底层实现也是运用的哈希编码,而map则是运用的自建红黑树,另外java8一个非常重要的特性更新就是hashmap的性能更新,之前的hashmap底层实现是数组加链表,而新版本的底层实现是,位桶+链表+红黑树实现,在发生hash冲突的时候,并且当链表的大小超过8的时候,会将链表转换为红黑树,这样我们就可以大大的提升查找速度。

-----------------------------------------------------------------------------------------------------------------

好,准备工作做完了,接下来让我们来分析源代码,首先依旧是惯例,放代码:

typedef struct dictEntry {

    // 键
    void *key;

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

    // 链往后继节点
    struct dictEntry *next; 

} dictEntry;

/*
 * 特定于类型的一簇处理函数
 */
typedef struct dictType {
    // 计算键的哈希值函数, 计算key在hash table中的存储位置,不同的dict可以有不同的hash function.
    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;

/*
 * 哈希表
 */
typedef struct dictht {

    // 哈希表节点指针数组(俗称桶,bucket)
    dictEntry **table;      

    // 指针数组的大小
    unsigned long size;     

    // 指针数组的长度掩码,用于计算索引值
    unsigned long sizemask; 

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

} dictht;

首先,第一个结构体是dictEntry ,这个是每个数据的基本存储部分,其中声明了泛型的指针(void*),以及一个联合(union),这里面要说一下联合是个什么东西,因为之前是很少见到这样的结构部分,其实联合和结构体是十分相似的都是可以存储不同数据的包裹,但是不同的是,结构体在内存中申请了一块内存,用于放置成员变量,而联合则不是,在产生一个联合的时候,会申请一个包括各个变量中最长的那个,也就是几个成员公用一块内存,这么说可能你有点乱,这样我们举一个例子:

union
{
int x; //八个字节
double y;  //十六个字节
}

那么在建立这样一个联合的时候,我们会直接申请十六个字节,并且在存储数据的时候,也是覆盖的,也就是联合之中同一时间只会存在一个成员,后面的数值会覆盖之前的,也就是说通过这样的方式,在申请字典的时候可以保证申请的内存的大小是固定的,这样才符合redis对于内存的管理,不得不说整个redis简直就是给我上了一个内存管理的课!之后还有一个变量指向下一个dict entry,也就是我们之前说的如果出现了哈希冲突之后的几种解决方方法之一,接下来的另一个结构就是dictType,这个我们之后在分析,先来看dictht,这个结构体其实也就是dict-hash-table缩写,有一张图片可以有助于我们了解他们的结构(图片的版权属于水印~)


可以看出其实是dictht在管理着dictEntry,我们先大体熟悉一下结构体的内容,之后通过函数来确定结构体中的变量的具体运用,因为如果没有了解函数如何运用这些变量,是无法深入了解这样做的真正用处的。

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

 unsigned long used;    

之后还有一个结构体我放到了最后

typedef struct dict {

    // 特定于类型的处理函数
    dictType *type;

    // 类型处理函数的私有数据
    void *privdata;

    // 哈希表(2个)
    dictht ht[2];       

    // 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
    int rehashidx;

    // 当前正在运作的安全迭代器数量
    int iterators;      

} dict;

为什么呢,因为这个涉及到了rehash的部分,所谓rehash,也就是当前出现了过多的哈希冲突的一种解决方法,也就是说每次新建dict的时候都会建两个dictht用于之后rehash,有了基础的结构之后我们开始了解一下相关的创建,很容易明白,类似于树,这种字典的创建应该从dict开始,来看一下相关的创建函数:

/*
 * 创建一个新字典
 *
 * 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;
}

也就是说,如果我们想创建一个dict要如何做呢,我们只需要

server.commands = dictCreate(&commandTableDictType,NULL); 

沿着这样的顺序,我们发现,在申请完内存之后真正干活的是_dictReset()这样的函数,那么_dictReset()又是怎样的函数呢

static void _dictReset(dictht *ht)
{
    ht->table = NULL;
    ht->size = 0;
    ht->sizemask = 0;
    ht->used = 0;
}

也就是最简单的数据赋值,在执行完之后,我们就获得了一个空的dict,可见空的创建一个dict是比较简单的,但是接下来的插入数据就不会这么简单了,接下来看一下插入键值对的操作代码:

dictEntry *dictAddRaw(dict *d, void *key)
{
    int index;
    dictEntry *entry;
    dictht *ht;

    // 尝试渐进式地 rehash 一个元素
    if (dictIsRehashing(d)) _dictRehashStep(d);

    // 查找可容纳新元素的索引位置
    // 如果元素已存在, index 为 -1
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry */
    // 决定该把新元素放在那个哈希表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 为新元素分配节点空间
    entry = zmalloc(sizeof(*entry));
    // 新节点的后继指针指向旧的表头节点
    entry->next = ht->table[index];
    // 设置新节点为表头
    ht->table[index] = entry;
    // 更新已有节点数量
    ht->used++;

    /* Set the hash entry fields. */
    // 关联起节点和 key
    dictSetKey(d, entry, key);

    // 返回新节点
    return entry;
}

int dictAdd(dict *d, void *key, void *val)
{
    // 添加 key 到哈希表,返回包含该 key 的节点
    dictEntry *entry = dictAddRaw(d,key);

    // 添加失败?
    if (!entry) return DICT_ERR;

    // 设置节点的值
    dictSetVal(d, entry, val);

    return DICT_OK;
}

仔细分析过后可以发现,首先在调用API的时候就会调用dictAddRaw这个函数,在dictAddRaw函数中,首先会判断是否是在rehash的过程中,如果在rehash的过程中的话,那么首先会渐进式的执行rehash,可能你会很好奇,为什么不是一次性rehash完之后在进行操作,这是因为rehash的步骤相对麻烦,如果一直执行rehash的话,那么会极大地干扰redis的运行速度,而redis对实时性要求极高,可以说整个redis的底层都是在为实时性服务,之后会判断是否这个键值对(key-value)是否是已经存在于字典中,这个时候调用的函数是:

static int _dictKeyIndex(dict *d, const void *key)
{
    unsigned int h, idx, table;
    dictEntry *he;

    // 如果有需要,对字典进行扩展
    if (_dictExpandIfNeeded(d) == DICT_ERR)
        return -1;

    // 计算 key 的哈希值
    h = dictHashKey(d, key);

    // 在两个哈希表中进行查找给定 key
    for (table = 0; table <= 1; table++) {

        // 根据哈希值和哈希表的 sizemask 
        // 计算出 key 可能出现在 table 数组中的哪个索引
        idx = h & d->ht[table].sizemask;

        // 在节点链表里查找给定 key
        // 因为链表的元素数量通常为 1 或者是一个很小的比率
        // 所以可以将这个操作看作 O(1) 来处理
        he = d->ht[table].table[idx];
        while(he) {
            // key 已经存在
            if (dictCompareKeys(d, key, he->key))
                return -1;

            he = he->next;
        }

        // 第一次进行运行到这里时,说明已经查找完 d->ht[0] 了
        // 这时如果哈希表不在 rehash 当中,就没有必要查找 d->ht[1]
        if (!dictIsRehashing(d)) break;
    }

    return idx;
}

可以从上面的代码的注释看出,实际的步骤就是先计算哈希值,然后确定地址,如果出现其他的状况那么就直接返回-1,也就意味着插入失败,接下来回到先前的代码,将键值对插入到链表的前端,因为最新放入的数据有很大的可能又被再次使用,因此这样做有助于提升速度,这样插入的过程就完成了,实际上整个插入中同步更新的还有已有节点数量等等,这一节我就先写这么多,之后的rehash,以及一些对应的字典API我放到下一个下一篇讲....

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 0
    评论

“相关推荐”对你有帮助么?

  • 非常没帮助
  • 没帮助
  • 一般
  • 有帮助
  • 非常有帮助
提交
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值