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

相关文章
|
29天前
|
消息中间件 缓存 NoSQL
Redis各类数据结构详细介绍及其在Go语言Gin框架下实践应用
这只是利用Go语言和Gin框架与Redis交互最基础部分展示;根据具体业务需求可能需要更复杂查询、事务处理或订阅发布功能实现更多高级特性应用场景。
168 86
|
2月前
|
存储 缓存 NoSQL
【📕分布式锁通关指南 12】源码剖析redisson如何利用Redis数据结构实现Semaphore和CountDownLatch
本文解析 Redisson 如何通过 Redis 实现分布式信号量(RSemaphore)与倒数闩(RCountDownLatch),利用 Lua 脚本与原子操作保障分布式环境下的同步控制,帮助开发者更好地理解其原理与应用。
119 6
|
3月前
|
存储 缓存 NoSQL
Redis核心数据结构与分布式锁实现详解
Redis 是高性能键值数据库,支持多种数据结构,如字符串、列表、集合、哈希、有序集合等,广泛用于缓存、消息队列和实时数据处理。本文详解其核心数据结构及分布式锁实现,帮助开发者提升系统性能与并发控制能力。
|
14天前
|
存储 消息中间件 NoSQL
Redis数据结构:别小看这5把“瑞士军刀”,用好了性能飙升!
Redis提供5种基础数据结构及多种高级结构,如String、Hash、List、Set、ZSet,底层通过SDS、跳表等实现高效操作。灵活运用可解决缓存、计数、消息队列、排行榜等问题,结合Bitmap、HyperLogLog、GEO更可应对签到、UV统计、地理位置等场景,是高性能应用的核心利器。
|
29天前
|
存储 缓存 NoSQL
Redis基础命令与数据结构概览
Redis是一个功能强大的键值存储系统,提供了丰富的数据结构以及相应的操作命令来满足现代应用程序对于高速读写和灵活数据处理的需求。通过掌握这些基础命令,开发者能够高效地对Redis进行操作,实现数据存储和管理的高性能方案。
65 12
|
27天前
|
存储 消息中间件 NoSQL
【Redis】常用数据结构之List篇:从常用命令到典型使用场景
本文将系统探讨 Redis List 的核心特性、完整命令体系、底层存储实现以及典型实践场景,为读者构建从理论到应用的完整认知框架,助力开发者在实际业务中高效运用这一数据结构解决问题。
|
1月前
|
存储 缓存 NoSQL
【Redis】 常用数据结构之String篇:从SET/GET到INCR的超全教程
无论是需要快速缓存用户信息,还是实现高并发场景下的精准计数,深入理解String的特性与最佳实践,都是提升Redis使用效率的关键。接下来,让我们从基础命令开始,逐步揭开String数据结构的神秘面纱。
|
NoSQL Redis
[redis设计与实现][7]基本数据结构——对象
Redis对基础数据类型进行了封装,构建出上层的对象系统,这个系统包含:字符串对象、列表对象、哈希对象、集合对象和有序集合对象。 Redis对象结构: [cce lang=”c”] typedef struct redisObject { //类型 unsigned type:4; //
1879 0
|
5月前
|
缓存 NoSQL 关系型数据库
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
美团面试:MySQL有1000w数据,redis只存20w的数据,如何做 缓存 设计?
|
21天前
|
存储 缓存 NoSQL
Redis专题-实战篇二-商户查询缓存
本文介绍了缓存的基本概念、应用场景及实现方式,涵盖Redis缓存设计、缓存更新策略、缓存穿透问题及其解决方案。重点讲解了缓存空对象与布隆过滤器的使用,并通过代码示例演示了商铺查询的缓存优化实践。
111 1
Redis专题-实战篇二-商户查询缓存