Redis源码(1)基本数据结构(中)

本文涉及的产品
云数据库 Tair(兼容Redis),内存型 2GB
Redis 开源版,标准版 2GB
推荐场景:
搭建游戏排行榜
简介: Redis源码(1)基本数据结构

Redis源码(1)基本数据结构1:https://developer.aliyun.com/article/1508210

字典Dict

源码文件dict.h 和 dict.c。

Redis中字典使用哈希表实现。同时,为了实现渐进式Rehash的操作,每一个字典都有两个hash表(新/旧)。

结构定义

Redis中字典使用哈希表实现。同时,为了实现渐进式Rehash的操作,每一个字典都有两个hash表(新/旧)。

// 字典定义
typedef struct dict {
    // 类型特定函数
    // 这一指针指向的结构体中存储了hash表常用的函数指针
    dictType *type;
    // 私有数据:保存了需要传给那些类型特定函数的可选参数(见后文)
    void *privdata;
    // 哈希表
    dictht ht[2];
    // 该值表示rehash进行到的下标索引位置
    // 当 rehash 不在进行时,值为 -1
    // 开始时值为0
    // 正在进行中时,值处于0到size之间
    int rehashidx;     
    int iterators; // 目前正在运行的安全迭代器的数量
} dict;

其中的hash表定义如下:

//每个字典都使用两个哈希表,从而实现渐进式 rehash 。
typedef struct dictht {
    // 哈希表数组(存储的是指向节点指针数组的指针)
    // 都使用指针的方式可以节省空间
    dictEntry **table;
    // 哈希表大小
    unsigned long size;
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;
    // 该哈希表已有节点的数量
    unsigned long used;
} dictht;

hash表的节点(Entry)定义如下,Redis是采用开链法来处理hash冲突的:

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

一个hash表的实例见下:

PS:在redis的hash表结构中,如果出现hash冲突,新的key-value是在旧的key-value前面的,这么做也很好理解:如果插入到链表最后,那么还需要一个遍历链表的操作,O(N)的复杂度

一个普通状态下的字典实例见下

接下来我们看看其中的重要API,比如计算hash值、处理hash冲突或者是rehash。

接口API

总览

函数 作用 时间复杂度
dictCreate 创建一个新的字典。
dictAdd 将给定的键值对添加到字典里面。
dictReplace 将给定的键值对添加到字典里面, 如果键已经存在于字典,那么用新值取代原有的值。


dictFetchValue 返回给定键的值。
dictGetRandomKey 从字典中随机返回一个键值对。
dictDelete 从字典中删除给定键所对应的键值对。
dictRelease 释放给定字典,以及字典中包含的所有键值对。 , N 为字典包含的键值对数量。

**dictAddRaw:**新增键值对

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

    // 如果条件允许的话,进行单步 rehash
    // T = O(1)
    if (dictIsRehashing(d)) _dictRehashStep(d); 
    
    // 计算键在哈希表中的索引值
    // 如果值为 -1 ,那么表示键已经存在
    if ((index = _dictKeyIndex(d, key)) == -1)
        return NULL;
    // 如果字典正在 rehash ,那么将新键添加到 1 号哈希表
    // 否则,将新键添加到 0 号哈希表
    ht = dictIsRehashing(d) ? &d->ht[1] : &d->ht[0];
    // 为新节点分配空间
    entry = zmalloc(sizeof(*entry));
    // 将新节点插入到链表表头
    entry->next = ht->table[index];
    ht->table[index] = entry;
    // 更新哈希表已使用节点数量
    ht->used++;
    // 设置新节点的键
    // T = O(1)
    dictSetKey(d, entry, key);
    return entry;
}

在其中有个函数:dictkeyindex()——计算该字典中可以插入该键值对的index,如果标志符号dict_can_resize为正,那么会在hash表的size和used比率大于1(即负载因子)时(没有执行BGSAVE)进行rehash或者大于5(执行BGSAVE)时进行强制rehash。

计算hash值时是这样的:

index = hash & dict->ht[0].sizemask

即是利用计算出的hash值跟sizemask相与,这个hash算法被称为MurmurHash2(目前有3了,但是Redis没用),这种算法的优点在于,即使输入的键是有规律的,算法仍能给出一个很好的随机分布性,并且算法的计算速度也非常快。

  • 为什么负载因子一个是1一个是5?

根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行,服务器执行扩展操作所需的负载因子并不相同,这是因为在执行BGSAVE命令或BGREWRITEAOF 命令的过程中,Redis 需要创建当前服务器进程的子进程,而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率,所以在子进程存在期间,服务器会提高执行扩展操作所需的负载因子,**从而尽可能地避免在子进程存在期间进行哈希表扩展操作,**这可以避免不必要的内存写入操作,最大限度地节约内存。

// 指示字典是否启用 rehash 的标识
static int dict_can_resize = 1;
// 强制 rehash 的比率(强制不可被上面的标志所阻止)
static unsigned int dict_force_resize_ratio = 5;

rehash的具体操作见接下来的这个API。

Rehash

int dictRehash(dict *d, int n) {
    // 只可以在 rehash 进行中时执行
    if (!dictIsRehashing(d)) return 0;
    // 进行 N 步迁移
    // T = O(N)
    while(n--) {
        dictEntry *de, *nextde;
        
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
        // 释放 0 号哈希表
        zfree(d->ht[0].table);
        // 将原来的 1 号哈希表设置为新的 0 号哈希表
        d->ht[0] = d->ht[1];
        // 重置旧的 1 号哈希表
        _dictReset(&d->ht[1]);
        // 关闭 rehash 标识
        d->rehashidx = -1;
        // 返回 0 ,向调用者表示 rehash 已经完成
        return 0;
        }
    // 确保 rehashidx 没有越界
    assert(d->ht[0].size > (unsigned)d->rehashidx);
    
    // 略过数组中为空的索引,找到下一个非空索引
    // hash表的entry中第一个就是void* key,所以可以直接访问其是否为空
    // 来判断该出是否存在键值对
    while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;
    // 指向该索引的链表表头节点
    de = d->ht[0].table[d->rehashidx];
    // 将链表中的所有节点迁移到新哈希表
    // T = O(1)
    while(de) {
            unsigned int h;    
            // 保存下个节点的指针
            nextde = de->next;           
            /* Get the index in the new hash table */
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            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;
}

通过上述源码我们可以得出:


Redis的hash表采用渐进式分步hash的方法

rehash的过程中字典的**两个hash表同时存在,并且在迭代、更新、删除键的时候都需要考虑这两个hash表,**值得注意的是:字典的删除(delete)、查找(find)、更新(update)等操作会在两个哈希表上进行,但插入只会在第二个表中,rehash完毕之后会重置第一个表。

在rehash时,如果扩展操作,那么ht[1]的大小为第一个大于等于 ht[0].used*2的2n**;如果收缩操作(**负载因子小于0.1**),**那么ht[1]的大小为第一个大于等于ht[0].used的2n。

rehsh时,相关API会经常进行单步rehash,数据库操作主要是调用int dictRehashMilliseconds(dict *d, int ms) **,**即在指定时间内执行rehash,时间到了就返回已迭代到的index。

迭代器

跟链表一样,Redis字典中也存在迭代器,主要是为了实现遍历一个字典。

其定义如下:

安全迭代器

dictIterator *dictGetSafeIterator(dict *d) {
    dictIterator *i = dictGetIterator(d);
    // 设置安全迭代器标识
    i->safe = 1;
    return i;
}

非安全迭代器

dictIterator *dictGetIterator(dict *d)
{
    dictIterator *iter = zmalloc(sizeof(*iter));

    iter->d = d;
    iter->table = 0;
    iter->index = -1;
    iter->safe = 0;
    iter->entry = NULL;
    iter->nextEntry = NULL;
    return iter;
}

两种迭代器都共用同一个函数接口:


dictEntry *dictNext(dictIterator *iter)


该接口返回迭代器指向的当前节点。在如果是安全迭代器调用该函数,会更新字典的iterator计数器(安全迭代器);如果是非安全迭代器调用该函数,会计算此时字典的fingerprint,以确定用户没有违规操作。


有关迭代器的重点函数是:****dictScan()。详见源码注释。


跳跃表skiplist

源码文件:t_zset.c 中所有以 zsl 开头的函数。


跳跃表是一种可以对有序链表进行近似二分查找的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。


跳跃表支持平均O(logN) 、最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。


Redis在两个地方用到了跳跃表,一个是实现有序集合,另一个是在集群节点中用作内部数据结构(后文会将,见Redis多机数据库)。


在大部分情况下,跳跃表的效率可以和平衡树相媲美,并且因为跳跃表的实现比平衡树要来得更为简单,所以有不少程序都使用跳跃表来代替平衡树。

通过下面这个图,来看看跳表这个数据结构:

跳表其实是空间换时间的结构,因为每隔一定的点都需要建立一个链表索引。

它的查询过程如下:


即先同层查找,而后往下一层找,直到最后的找到节点,类似一个二分的过程。


另外,我们想要为跳表插入或者删除数据,我们首先需要找到插入或者删除的位置,然后执行插入或删除操作,前边我们已经知道了,跳表的查询的时间复杂度为 O(logn),因为找到位置之后插入和删除的时间复杂度很低,为 O(1),所以最终插入和删除的时间复杂度也为 O(longn)。


想进一步了解可参考:


https://zhuanlan.zhihu.com/p/68516038

Redis源码(1)基本数据结构3:https://developer.aliyun.com/article/1508223

相关实践学习
基于Redis实现在线游戏积分排行榜
本场景将介绍如何基于Redis数据库实现在线游戏中的游戏玩家积分排行榜功能。
云数据库 Redis 版使用教程
云数据库Redis版是兼容Redis协议标准的、提供持久化的内存数据库服务,基于高可靠双机热备架构及可无缝扩展的集群架构,满足高读写性能场景及容量需弹性变配的业务需求。 产品详情:https://www.aliyun.com/product/kvstore     ------------------------------------------------------------------------- 阿里云数据库体验:数据库上云实战 开发者云会免费提供一台带自建MySQL的源数据库 ECS 实例和一台目标数据库 RDS实例。跟着指引,您可以一步步实现将ECS自建数据库迁移到目标数据库RDS。 点击下方链接,领取免费ECS&RDS资源,30分钟完成数据库上云实战!https://developer.aliyun.com/adc/scenario/51eefbd1894e42f6bb9acacadd3f9121?spm=a2c6h.13788135.J_3257954370.9.4ba85f24utseFl
相关文章
|
18天前
|
C语言
【数据结构】栈和队列(c语言实现)(附源码)
本文介绍了栈和队列两种数据结构。栈是一种只能在一端进行插入和删除操作的线性表,遵循“先进后出”原则;队列则在一端插入、另一端删除,遵循“先进先出”原则。文章详细讲解了栈和队列的结构定义、方法声明及实现,并提供了完整的代码示例。栈和队列在实际应用中非常广泛,如二叉树的层序遍历和快速排序的非递归实现等。
94 9
|
17天前
|
存储 搜索推荐 算法
【数据结构】树型结构详解 + 堆的实现(c语言)(附源码)
本文介绍了树和二叉树的基本概念及结构,重点讲解了堆这一重要的数据结构。堆是一种特殊的完全二叉树,常用于实现优先队列和高效的排序算法(如堆排序)。文章详细描述了堆的性质、存储方式及其实现方法,包括插入、删除和取堆顶数据等操作的具体实现。通过这些内容,读者可以全面了解堆的原理和应用。
59 16
|
17天前
|
C语言
【数据结构】二叉树(c语言)(附源码)
本文介绍了如何使用链式结构实现二叉树的基本功能,包括前序、中序、后序和层序遍历,统计节点个数和树的高度,查找节点,判断是否为完全二叉树,以及销毁二叉树。通过手动创建一棵二叉树,详细讲解了每个功能的实现方法和代码示例,帮助读者深入理解递归和数据结构的应用。
65 8
|
19天前
|
存储 C语言
【数据结构】手把手教你单链表(c语言)(附源码)
本文介绍了单链表的基本概念、结构定义及其实现方法。单链表是一种内存地址不连续但逻辑顺序连续的数据结构,每个节点包含数据域和指针域。文章详细讲解了单链表的常见操作,如头插、尾插、头删、尾删、查找、指定位置插入和删除等,并提供了完整的C语言代码示例。通过学习单链表,可以更好地理解数据结构的底层逻辑,提高编程能力。
48 4
|
22天前
|
存储 消息中间件 NoSQL
Redis数据结构:List类型全面解析
Redis数据结构——List类型全面解析:存储多个有序的字符串,列表中每个字符串成为元素 Eelement,最多可以存储 2^32-1 个元素。可对列表两端插入(push)和弹出(pop)、获取指定范围的元素列表等,常见命令。 底层数据结构:3.2版本之前,底层采用**压缩链表ZipList**和**双向链表LinkedList**;3.2版本之后,底层数据结构为**快速链表QuickList** 列表是一种比较灵活的数据结构,可以充当栈、队列、阻塞队列,在实际开发中有很多应用场景。
|
21天前
|
存储 C语言
【数据结构】顺序表(c语言实现)(附源码)
本文介绍了线性表和顺序表的基本概念及其实现。线性表是一种有限序列,常见的线性表有顺序表、链表、栈、队列等。顺序表是一种基于连续内存地址存储数据的数据结构,其底层逻辑是数组。文章详细讲解了静态顺序表和动态顺序表的区别,并重点介绍了动态顺序表的实现,包括初始化、销毁、打印、增删查改等操作。最后,文章总结了顺序表的时间复杂度和局限性,并预告了后续关于链表的内容。
53 3
|
26天前
|
存储 消息中间件 NoSQL
Redis 数据结构与对象
【10月更文挑战第15天】在实际应用中,需要根据具体的业务需求和数据特点来选择合适的数据结构,并合理地设计数据模型,以充分发挥 Redis 的优势。
55 8
|
26天前
|
存储 NoSQL Java
介绍下Redis 的基础数据结构
本文介绍了Redis的基础数据结构,包括动态字符串(SDS)、链表和字典。SDS是Redis自实现的动态字符串,避免了C语言字符串的不足;链表实现了双向链表,提供了高效的操作;字典则类似于Java的HashMap,采用数组加链表的方式存储数据,并支持渐进式rehash,确保高并发下的性能。
介绍下Redis 的基础数据结构
|
19天前
|
C语言
【数据结构】双向带头循环链表(c语言)(附源码)
本文介绍了双向带头循环链表的概念和实现。双向带头循环链表具有三个关键点:双向、带头和循环。与单链表相比,它的头插、尾插、头删、尾删等操作的时间复杂度均为O(1),提高了运行效率。文章详细讲解了链表的结构定义、方法声明和实现,包括创建新节点、初始化、打印、判断是否为空、插入和删除节点等操作。最后提供了完整的代码示例。
39 0
|
22天前
|
存储 NoSQL 关系型数据库
Redis的ZSet底层数据结构,ZSet类型全面解析
Redis的ZSet底层数据结构,ZSet类型全面解析;应用场景、底层结构、常用命令;压缩列表ZipList、跳表SkipList;B+树与跳表对比,MySQL为什么使用B+树;ZSet为什么用跳表,而不是B+树、红黑树、二叉树