Redis数据结构之字典

字典经常作为一种数据结构内置在很多高级编程语言中,但是Redis使用C语言实现,没有内置这种数据结构,因此Redis自己构建了字典的实现。
Redis数据库就是使用字典的数据结构来作为底层实现。另外Redis的哈希键对象也是使用了字典的数据结构。

字典的实现

Redis的字典使用了哈希表作为底层实现,其中一个哈希表包含了多个哈希节点,而每个哈希节点又包含多个键值对
字典结构

字典

typedef struct dict {
    dictType *type;
    void *privdata;
    dictht ht[2];
    long rehashidx; /* rehashing not in progress if rehashidx == -1 */
    unsigned long iterators; /* number of iterators currently running */
} dict;

哈希表

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
typedef struct dictht { // 哈希表
    dictEntry **table; // 哈希表的数组,数组中每个元素都是指针,指向 dictEntry 结构
    unsigned long size; // 哈希表的大小,table 数组的大小
    unsigned long sizemask; // 哈希表掩码,用于计算索引值,等于 size-1
    unsigned long used; // 哈希表已有的节点(键值对)数量
} dictht;

哈希节点

typedef struct dictEntry {
    void *key;  // 键 key
    union { // 值 val
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next; // 指向下一个哈希表节点,链表法解决hash冲突
} dictEntry;

哈希节点维护一个链表用于解决哈希冲突。

哈希冲突

什么是哈希冲突

哈希的过程就是给定一个输入,然后经过指定的哈希函数,计算出一个输出。输入的值范围大于输出的值范围,所以一定存在某多个输入,经过哈希函数计算的输出是相等的。那么这些不同的输入就是发生了哈希碰撞。
哈希冲突

如何解决哈希冲突

常见的哈希冲突解决办法有链地址法、开放寻址法、再哈希法等。redis哈希表使用链地址法解决冲突的问题。
Redis哈希表哈希节点维护一个next指针,将冲突的哈希节点通过单链表的方式串起来。

  if (d->type->no_value) {
  		/* Allocate an entry without value. */
        entry = createEntryNoValue(key, *bucket);
    } else { // 如果哈希节点已经存在元素,则将新的节点添加到哈希节点链表的头部,已冲突的节点链接到新节点的next处
        entry->key = key;
        entry->next = *bucket;
    }
    *bucket = entry;
    d->ht_used[htidx]++

字典扩容

当哈希表存储节点达到一定数量后,会对字典进行扩容,以提升字典效率。

扩容时机

字典的扩容时机主要关注两个指标。

  • 当前can_resize标识
  • 负载因子
    首先查看判定是否需要扩容的源码段。
    if ((dict_can_resize == DICT_RESIZE_ENABLE &&
         d->ht_used[0] >= DICTHT_SIZE(d->ht_size_exp[0])) ||
        (dict_can_resize != DICT_RESIZE_FORBID &&
         d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0]) > dict_force_resize_ratio))
    {
        return dictExpand(d, d->ht_used[0] + 1);
    }

当在两种场景下,会对字典进行翻倍的扩容。

  • 当前resize为DICT_RESIZE_ENABLE,且负载因子大于1
  • 当前resize为非DICT_RESIZE_FORBID(resize为DICT_RESIZE_ENABLE),且负载因子大于5

can_resize标识

can_resize有三个枚举

typedef enum {
    DICT_RESIZE_ENABLE,
    DICT_RESIZE_AVOID,
    DICT_RESIZE_FORBID,
} dictResizeEnable;

redis会根据当前进程情况对resize进行更新

/* This function is called once a background process of some kind terminates,
 - as we want to avoid resizing the hash tables when there is a child in order
 - to play well with copy-on-write (otherwise when a resize happens lots of
 - memory pages are copied). The goal of this function is to update the ability
 - for dict.c to resize or rehash the tables accordingly to the fact we have an
 - active fork child running. */
void updateDictResizePolicy(void) {
    if (server.in_fork_child != CHILD_TYPE_NONE)
        dictSetResizeEnabled(DICT_RESIZE_FORBID);
    else if (hasActiveChildProcess())
        dictSetResizeEnabled(DICT_RESIZE_AVOID);
    else
        dictSetResizeEnabled(DICT_RESIZE_ENABLE);
}
  • 默认resize=DICT_RESIZE_ENABLE
  • 当前处于子进程中resize=DICT_RESIZE_FORBID
  • 存在子进程时,resize=DICT_RESIZE_AVOID

负载因子

负载因子 = d->ht_used[0] / DICTHT_SIZE(d->ht_size_exp[0])

  • 当负载因子大于等于 1 ,且 Redis 没有在执行 bgsave 命令、 bgrewiteaof 命令, RDB 快照 或 AOF 重写 时,进行 rehash 操作
  • 当负载因子大于等于 5 时,不管有没有有在执行 RDB 快照 或AOF 重写,都会强制进行 rehash 操作

渐进式rehash

redis在进行rehash时,会将ht[0]中的全部键rehash到ht[1]中。但是redis在rehash动作并不是一次性的完成的。而是通过分治的思想,分多次、渐进式的完成的。
因为redis是单线程模型,如果一个大key在进行一次性rehash时,会有较大的计算量可能导致服务器在一段时间内停止服务,从而引起客户端长时间阻塞不可用。

/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *  * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table, however
 * since part of the hash table may be composed of empty spaces, it is not
 * guaranteed that this function will rehash even a single bucket, since it
 * will visit at max N*10 empty buckets in total, otherwise the amount of
 * work it does would be unbound and the function may block for a long time. */
int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    if (!dictIsRehashing(d)) return 0;

    while(n-- && d->ht[0].used != 0) {
        dictEntry *de, *nextde;

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(d->ht[0].size > (unsigned long)d->rehashidx);
        while(d->ht[0].table[d->rehashidx] == NULL) {
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        while(de) {
            uint64_t 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;
        d->rehashidx++;
    }

    /* Check if we already rehashed the whole table... */
    if (d->ht[0].used == 0) {
        zfree(d->ht[0].table);
        d->ht[0] = d->ht[1];
        _dictReset(&d->ht[1]);
        d->rehashidx = -1;
        return 0;
    }

    /* More to rehash... */
    return 1;
}
  • 存在rehash的过程中,不允许再次发起rehash
  • 每次最大遍历10个桶,防止遍历过多空桶,导致阻塞线程
  • 每次将键值从ht[0]迁移到ht[1]后,d->ht[0].used-- d->ht[1].used++ d->rehashidx++;
  • rehash完毕后,rehashidx置为-1

哈希键操作

新增

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

    if (dictIsRehashing(d)) _dictRehashStep(d);

    /* Get the index of the new element, or -1 if
     * the element already exists. */
    if ((index = _dictKeyIndex(d, key, dictHashKey(d,key), existing)) == -1)
        return NULL;

    /* Allocate the memory and store the new entry.
     * Insert the element in top, with the assumption that in a database
     * system it is more likely that recently added entries are accessed
     * more frequently. */
    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. */
    dictSetKey(d, entry, key);
    return entry;
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果key已经存在,不执行set操作
  • 新增时只写入ht[1],保证ht[0]的元素只会越来越少,不会新增。

更新

/* Add or Overwrite:
 * Add an element, discarding the old value if the key already exists.
 * Return 1 if the key was added from scratch, 0 if there was already an
 * element with such key and dictReplace() just performed a value update
 * operation. */
int dictReplace(dict *d, void *key, void *val)
{
    dictEntry *entry, *existing, auxentry;

    /* Try to add the element. If the key
     * does not exists dictAdd will succeed. */
    entry = dictAddRaw(d,key,&existing);
    if (entry) {
        dictSetVal(d, entry, val);
        return 1;
    }

    /* Set the new value and free the old one. Note that it is important
     * to do that in this order, as the value may just be exactly the same
     * as the previous one. In this context, think to reference counting,
     * you want to increment (set), and then decrement (free), and not the
     * reverse. */
    auxentry = *existing;
    dictSetVal(d, existing, val);
    dictFreeVal(d, &auxentry);
    return 0;
}

读取

dictEntry *dictFind(dict *d, const void *key)
{
    dictEntry *he;
    uint64_t h, idx, table;

    if (d->ht[0].used + d->ht[1].used == 0) return NULL; /* dict is empty */
    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);
    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key))
                return he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) return NULL;
    }
    return NULL;
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果当前在reahsh,遍历ht[0]和ht[1],否则只遍历ht[0]
  • 通过哈希函数找到对应的哈希桶,对桶内的元素进行键值对比,如果键值不相等,会进行对next后续键再次对比

删除

/* Search and remove an element. This is an helper function for
 * dictDelete() and dictUnlink(), please check the top comment
 * of those functions. */
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree) {
    uint64_t h, idx;
    dictEntry *he, *prevHe;
    int table;

    if (d->ht[0].used == 0 && d->ht[1].used == 0) return NULL;

    if (dictIsRehashing(d)) _dictRehashStep(d);
    h = dictHashKey(d, key);

    for (table = 0; table <= 1; table++) {
        idx = h & d->ht[table].sizemask;
        he = d->ht[table].table[idx];
        prevHe = NULL;
        while(he) {
            if (key==he->key || dictCompareKeys(d, key, he->key)) {
                /* Unlink the element from the list */
                if (prevHe)
                    prevHe->next = he->next;
                else
                    d->ht[table].table[idx] = he->next;
                if (!nofree) {
                    dictFreeKey(d, he);
                    dictFreeVal(d, he);
                    zfree(he);
                }
                d->ht[table].used--;
                return he;
            }
            prevHe = he;
            he = he->next;
        }
        if (!dictIsRehashing(d)) break;
    }
    return NULL; /* not found */
}
  • 如果在rehash进行中,首先执行一步rehash
  • 如果当前在reahsh,遍历ht[0]和ht[1],否则只遍历ht[0]
  • 通过判断key值,如果是需要删除的key,则会调整哈希节点的next指针,达到删除效果
  • 更新哈希表元素占用指标used参数

本文来自互联网用户投稿,该文观点仅代表作者本人,不代表本站立场。本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如若转载,请注明出处:/a/155449.html

如若内容造成侵权/违法违规/事实不符,请联系我们进行投诉反馈qq邮箱809451989@qq.com,一经查实,立即删除!

相关文章

Flutter 中在单个屏幕上实现多个列表

今天&#xff0c;我将提供一个实际的示例&#xff0c;演示如何在单个页面上实现多个列表&#xff0c;这些列表可以水平排列、网格格式、垂直排列&#xff0c;甚至是这些常用布局的组合。 下面是要做的&#xff1a; 实现 让我们从创建一个包含产品所有属性的产品模型开始。 …

云ES使用集群限流插件(aliyun-qos)

aliyun-qos插件是阿里云Elasticsearch团队自研的插件,能够提高集群的稳定性。该插件能够实现集群级别的读写限流,在关键时刻对指定索引降级,将流量控制在合适范围内。例如当上游业务无法进行流量控制时,尤其对于读请求业务,可根据aliyun-qos插件设置的规则,按照业务的优先…

矿区安全检查VR模拟仿真培训系统更全面、生动有效

矿山企业岗位基数大&#xff0c;生产过程中会持续有新入矿的施工人员及不定期接待的参观人员&#xff0c;下井安全须知培训需求量大。传统实景拍摄的视频剪辑表达方式有限&#xff0c;拍摄机位受限&#xff0c;难以生动表达安全须知的内容&#xff0c;且井下现场拍摄光线不理想…

【算法】复习搜索与图论

&#x1f34e; 博客主页&#xff1a;&#x1f319;披星戴月的贾维斯 &#x1f34e; 欢迎关注&#xff1a;&#x1f44d;点赞&#x1f343;收藏&#x1f525;留言 &#x1f347;系列专栏&#xff1a;&#x1f319; 蓝桥杯 &#x1f319;请不要相信胜利就像山坡上的蒲公英一样唾手…

Mysql分组查询每组最新的一条数据

在工作中遇到一个问题&#xff0c;需要查出每个公司最新的那条数据。 所以需根据公司进行分组&#xff1a; 未进行分组时&#xff1a; select a.id, b.name companyName, result_asset ,result_liability ,result_net_asset, a.create_time ,a.is_deleted from bus_proper…

【Linux】环境变量--PATH环境变量/环境变量的操作/命令行参数

文章目录 一、PATH环境变量1.什么是PATH环境变量2.如何添加PATH环境变量3.系统中的其他环境变量4.环境变量的来源 二、环境变量的操作1.设置环境变量2.通过getenv获取环境变量3.环境变量的意义 三、命令行参数 一、PATH环境变量 1.什么是PATH环境变量 这里我们先提出一个问题…

一起学docker系列之三docker的详细安装步骤

目录 前言1. 准备环境2. 卸载已有的Docker3. 安装编译工具4. 安装必需的软件5. 配置镜像仓库6. 更新YUM软件包索引7. 安装Docker CE8. 启动Docker9. 测试Docker10. 卸载Docker结语 前言 安装Docker是一项重要的任务&#xff0c;因为它为应用程序提供了容器化的环境&#xff0c…

uni-app开发微信小程序 vue3写法添加pinia

说明 使用uni-app开发&#xff0c;选择vue3语法&#xff0c;开发工具是HBliuderX。虽然内置有vuex&#xff0c;但是个人还是喜欢用Pinia&#xff0c;所以就添加进去了。 Pinia官网连接 添加步骤 第一步&#xff1a; 在项目根目录下执行命令&#xff1a; npm install pinia …

【无标题】chapter6卷积

此例以说明全连接层处理图片的时候会遇到参数过多 模型过大的问题 参数比要研究的物体总数还多 卷积&#xff0c;特殊的全联接层 平移不变形&#xff0c;局部性 原本权重为二维&#xff08;输入和输出全联接&#xff0c;想想下表组合&#xff0c;就是个二维的矩阵&#xff09;…

Scrapy----Scrapy简介

文章目录 概述与应用背景架构和组件功能和特点社区生态概述与应用背景 Scrapy,一个高效、灵活、且强大的Web爬取框架,被广泛应用于数据抓取和网页内容的结构化提取。它是用Python编写的,支持多平台运行,适用于数据挖掘、在线零售信息收集、历史数据存档等多种场景。Scrapy…

idea运行项目之后一直卡在Writing classes… 解决方案

最近遇到idea里直接运行一个Spring boot项目后&#xff0c;idea一直慢悠悠的parsing java&#xff0c;然后就writing classes&#xff0c;然后就一直卡着不动了&#xff0c;运气好10几分钟能把项目启动起来。 多年的摸鱼经验告诉我&#xff0c;事出反常必有妖&#xff0c;赶紧…

【Java】详解多线程通信

&#x1f33a;个人主页&#xff1a;Dawn黎明开始 &#x1f380;系列专栏&#xff1a;Java ⭐每日一句&#xff1a;什么都不做&#xff0c;才会来不及 &#x1f4e2;欢迎大家&#xff1a;关注&#x1f50d;点赞&#x1f44d;评论&#x1f4dd;收藏⭐️ 文章目录 &#x1f510;多…

求10的阶乘之和

这个问题很简单&#xff0c;我们用for循环就可以做到&#xff01; 目录 1.用两个for循环求值 2.用一个for循环求值 1.用两个for循环求值 int main() {int i 1;int ret 1;int sum 0;int n 0;for (n 1; n < 10; n){ret 1;for (i 1; i < n; i){ret ret * i;}sum …

文件传输客户端 SecureFX mac中文版支持多种协议

SecureFX mac是一款功能强大的文件传输客户端&#xff0c;可在 Mac 操作系统上使用。它由 VanDyke Software 公司开发&#xff0c;旨在为用户提供安全、可靠、高效的文件传输服务。 SecureFX 支持多种协议&#xff0c;包括 SFTP、SCP、FTP、FTP over SSL/TLS 和 HTTP/S。它使用…

Django 配置 Email Admin 详细指南

概要 Django 是一个高级的 Python Web 框架&#xff0c;它鼓励快速开发和清洁、实用的设计。当你正在开发一个 Django 项目时&#xff0c;监控网站的运行情况是非常必要的。Django 提供了一个功能强大的 admin 界面&#xff0c;但同时也可以通过配置 email admin 来获取网站的…

第9章 K8s进阶篇-持久化存储入门

9.1 k8s存储Volumes介绍 Container&#xff08;容器&#xff09;中的磁盘文件是短暂的&#xff0c;当容器崩溃时&#xff0c;kubelet会重新启动容器&#xff0c;但最初的文件将丢失&#xff0c;Container会以最干净的状态启动。另外&#xff0c;当一个Pod运行多个Container时&…

设计模式解码:软件工程架构的航标

引言 软件工程领域的设计模式&#xff0c;就像是建筑师手中的设计蓝图&#xff0c;它们是经验的总结&#xff0c;指导开发者如何在面对层出不穷的编程难题时&#xff0c;构建出既稳固又灵活的软件结构。就像一座经过精心设计的大厦能够经受住风雨的考验一样&#xff0c;一个利用…

MAC地址注册的网络安全影响和措施分析

MAC地址注册对网络安全具有重要影响&#xff0c;同时也需要采取相应的措施来应对潜在的安全风险。以下是有关MAC地址注册的网络安全影响和应对措施的分析&#xff1a; 影响&#xff1a; 1. 身份验证&#xff1a;MAC地址注册可用于设备的身份验证&#xff0c;但MAC地址本身并不…

MATLAB 机械臂逆运动学进行轨迹控制建模

系列文章目录 文章目录 系列文章目录前言一、模型概览1.1 Target Pose Generation 目标姿势生成1.2 Inverse Kinematics 逆运动学1.3 Manipulator Dynamics 机械手动力学1.4 Pose Measurement 姿势测量 二、机械手定义三、生成航点四、模型设置五、模拟机械手运动六、将结果可视…

how to find gcc openbug

https://gcc.gnu.org/bugzilla/query.cgi?formatadvanced