Redis存储原理

news2024/9/20 9:37:39

前言

        我们从redis服务谈起,redis是单reactor,命令在redis-server线程处理。还有若干读写IO线程负责IO操作(redis6.0之后,Redis之pipeline与事务)。此外还有一个内存池线程负责内存管理、一个后台文件线程负责大文件的关闭、一个异步刷盘线程负责持久化。这就使得redis在设计方面对高效的贡献。

        从io密集型的角度来看,redis的磁盘IO是fork子进程异步刷盘,不占用主线程。网络IO如果有多个连接,并且发送了大量的请求,才会考虑IO多线程;从cpu密集型的角度来看,redis有高效的数据结构,加之kv原理,避免了cpu密集。那redis为什么不采用多线程呢?硬伤就是redis的多种数据类型由多种不同的数据结构实现,加锁复杂,锁的粒度不易控制。此外频繁的上下文切换会降低整体性能。那除了单reactor,redis还在什么方面对高效做出来贡献呢?hash表。

存储原理

        redis使用哈希表来组织所有数据,这次我们直接杀到源码。在redisDb中我们可以看到四个dict(散列表)类型的成员(英文这里就不翻译了,原汁原味)。

typedef struct redisDb {
    kvstore *keys;              /* The keyspace for this DB */
    kvstore *expires;           /* Timeout of keys with a timeout set */
    ebuckets hexpires;          /* Hash expiration DS. Single TTL per hash (of next min field to expire) */
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *blocking_keys_unblock_on_nokey;   /* Keys with clients waiting for
                                             * data, and should be unblocked if key is deleted (XREADEDGROUP).
                                             * This is a subset of blocking_keys*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */
    int id;                     /* Database ID */
    long long avg_ttl;          /* Average TTL, just for stats */
    unsigned long expires_cursor; /* Cursor of the active expire cycle. */
    list *defrag_later;         /* List of key names to attempt to defrag one by one, gradually. */
} redisDb;

        可见我们需要对dict下手了,因为数据就存储在dict中。这其实类似c++类的封装,dictType中存放了所有成员函数,其中dictEntry指针数组就是元素存储的地方了。

struct dict {
    dictType *type;

    dictEntry **ht_table[2];//哈希表
    unsigned long ht_used[2];//实际存储的元素个数

    long rehashidx; /* rehashing not in progress if rehashidx == -1 */

    /* Keep small vars at end for optimal (minimal) struct padding */
    unsigned pauserehash : 15; /* If >0 rehashing is paused */

    unsigned useStoredKeyApi : 1; /* See comment of storedHashFunction above */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
    int16_t pauseAutoResize;  /* If >0 automatic resizing is disallowed (<0 indicates coding error) */
    void *metadata[];
};

        我们来看dictEntry,也就是存储元素的结构。

/* -------------------------- types ----------------------------------------- */
struct dictEntry {
    void *key;
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    struct dictEntry *next;     /* Next entry in the same hash bucket. */
};

typedef struct {
    void *key;
    dictEntry *next;
} dictEntryNoValue;

        源码我们找到了,现在就可以来讨论一下哈希表了。哈希表通过hash运算得出index,如果这个地方有元素,就通过链表组织。这就对应了dictEntry **ht_table[2]。

        我们知道,通过链表组织就是一种应对hash冲突的策略,hash冲突会使索引效率降低,然鹅从源码可以看到有一个负载因子,也就是已有元素与数组长度之比(used/size),负载因子越大表示hash冲突就越大,而我们知道hash时间复杂度是O(1),hash冲突到后面会使时间复杂度增加到O(n)。当负载因子大于1的时候就要进行扩容操作,也就是将数组长度翻倍,增大分母减小负载因子。而当负载因子小于0.1(恰好大于used的2^n)的时候就要进行缩容,因为这是占着茅坑不拉屎,浪费空间。不管扩容还是缩容,数组长度都会改变,这时候hash算法就变了,就要对原来的元素重新hash。

        蛋是!redis是个数据库啊,rehash时就有可能有数据操作,所以它就不能像STL中的unordered_map一样直接rehash(CPU密集型的)。redis是采取渐进式rehash策略。Redis的渐进式rehash是一种在数据量增加时对哈希表进行动态扩容的机制,它能够保证数据的均匀分布和快速访问,同时避免了传统rehash操作可能带来的性能下降问题。在Redis中,渐进式rehash的实现原理和步骤如下:

  1. 触发条件:当哈希表中的节点数与哈希表大小的比例超过一定阈值(默认为1:1,或者在执行RDB或AOF操作时为5:1)时,就会触发rehash操作。
  2. 创建新的哈希表:Redis会创建一个新的哈希表,其大小是原哈希表的两倍,以容纳更多的键值对。
  3. 渐进式迁移:Redis不会一次性将所有键值对迁移到新哈希表,而是将迁移过程分散到后续的字典操作中。每次字典操作时,会顺便迁移一部分键值对到新哈希表,这个过程称为渐进式rehash。
  4. 迁移过程:在迁移过程中,Redis会维护一个rehashidx索引,用于记录迁移的进度。每次字典操作时,会迁移rehashidx索引对应的桶中的所有键值对到新哈希表,然后rehashidx递增。
  5. 读写操作在渐进式rehash进行期间,查找操作会在两个哈希表中进行,以确保不会遗漏任何键值对。而新增操作则只会在新的哈希表中进行。
  6. 完成rehash:当所有键值对都迁移到新哈希表后,Redis会将旧哈希表的空间释放,并将rehashidx重置为-1,表示rehash操作完成。
  7. 缩容操作:除了扩容,Redis在数据量减少时也会进行缩容操作,其过程与扩容类似,也是通过渐进式rehash来完成。

        渐进式rehash的优点在于它能够平滑地处理数据迁移,避免了一次性迁移可能导致的性能抖动,确保了Redis在扩容或缩容过程中的稳定性和性能。然而,它也会占用额外的内存空间,因为需要同时维护两个哈希表,并且在迁移过程中可能会导致一定程度的数据不一致。尽管如此,渐进式rehash是Redis中一个非常重要的特性,它使得Redis能够有效地处理动态数据集的变化。rehash源码如下,这里就不逐行解释了,扔给AI便可。

int dictRehash(dict *d, int n) {
    int empty_visits = n*10; /* Max number of empty buckets to visit. */
    unsigned long s0 = DICTHT_SIZE(d->ht_size_exp[0]);
    unsigned long s1 = DICTHT_SIZE(d->ht_size_exp[1]);
    if (dict_can_resize == DICT_RESIZE_FORBID || !dictIsRehashing(d)) return 0;
    /* If dict_can_resize is DICT_RESIZE_AVOID, we want to avoid rehashing. 
     * - If expanding, the threshold is dict_force_resize_ratio which is 4.
     * - If shrinking, the threshold is 1 / (HASHTABLE_MIN_FILL * dict_force_resize_ratio) which is 1/32. */
    if (dict_can_resize == DICT_RESIZE_AVOID && 
        ((s1 > s0 && s1 < dict_force_resize_ratio * s0) ||
         (s1 < s0 && s0 < HASHTABLE_MIN_FILL * dict_force_resize_ratio * s1)))
    {
        return 0;
    }

    while(n-- && d->ht_used[0] != 0) {
        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        assert(DICTHT_SIZE(d->ht_size_exp[0]) > (unsigned long)d->rehashidx);
        while(d->ht_table[0][d->rehashidx] == NULL) {//旧数据为空就找下一个
            d->rehashidx++;
            if (--empty_visits == 0) return 1;
        }
        /* Move all the keys in this bucket from the old to the new hash HT */
        rehashEntriesInBucketAtIndex(d, d->rehashidx);
        d->rehashidx++;
    }

    return !dictCheckRehashingCompleted(d);
}

static void rehashEntriesInBucketAtIndex(dict *d, uint64_t idx) {
    dictEntry *de = d->ht_table[0][idx];
    uint64_t h;
    dictEntry *nextde;
    while (de) {
        nextde = dictGetNext(de);
        void *key = dictGetKey(de);
        /* Get the index in the new hash table */
        if (d->ht_size_exp[1] > d->ht_size_exp[0]) {
            h = dictHashKey(d, key, 1) & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
        } else {
            /* We're shrinking the table. The tables sizes are powers of
             * two, so we simply mask the bucket index in the larger table
             * to get the bucket index in the smaller table. */
            h = idx & DICTHT_SIZE_MASK(d->ht_size_exp[1]);
        }
        if (d->type->no_value) {
            if (d->type->keys_are_odd && !d->ht_table[1][h]) {
                /* Destination bucket is empty and we can store the key
                 * directly without an allocated entry. Free the old entry
                 * if it's an allocated entry.
                 *
                 * TODO: Add a flag 'keys_are_even' and if set, we can use
                 * this optimization for these dicts too. We can set the LSB
                 * bit when stored as a dict entry and clear it again when
                 * we need the key back. */
                assert(entryIsKey(key));
                if (!entryIsKey(de)) zfree(decodeMaskedPtr(de));
                de = key;
            } else if (entryIsKey(de)) {
                /* We don't have an allocated entry but we need one. */
                de = createEntryNoValue(key, d->ht_table[1][h]);
            } else {
                /* Just move the existing entry to the destination table and
                 * update the 'next' field. */
                assert(entryIsNoValue(de));
                dictSetNext(de, d->ht_table[1][h]);
            }
        } else {
            dictSetNext(de, d->ht_table[1][h]);
        }
        d->ht_table[1][h] = de;
        d->ht_used[0]--;
        d->ht_used[1]++;
        de = nextde;
    }
    d->ht_table[0][idx] = NULL;
}

        那我们怎么访问元素?用keys命令吗?非也。当执行 KEYS 命令时,Redis 会锁定数据库,然后检索所有匹配给定模式的键。如果键的数量非常多,这个操作可能会花费很长时间,在此期间,服务器不能响应其他客户端的请求,从而导致服务器阻塞。这是因为 KEYS 命令在执行期间会阻止其他操作,包括但不限于写操作,直到命令执行完成。我们可以使用scan命令。Redis 的 SCAN 命令是一个基于游标的迭代器,用于逐个元素地访问(或迭代)集合类型的元素,如列表(list)、集合(set)、有序集合(sorted set)和哈希(hash)。这个命令非常有用,因为它提供了一种方式来逐步检索大数据集,而不会像使用传统的 KEYS 命令那样对服务器性能造成严重影响。SCAN 命令的基本用法如下:

SCAN cursor [MATCH pattern] [COUNT count]
  • cursor:游标,每次调用 SCAN 命令时,都会返回一个新的游标,用于下一次迭代。
  • MATCH pattern:可选参数,用于过滤返回的元素,只返回符合给定模式的元素。
  • COUNT count:可选参数,建议 Redis 返回的元素数量,实际返回的数量可能会更多或更少。

SCAN 命令的优点包括:

  • 性能:相比于 KEYS 命令,SCAN 命令不会阻塞服务器,因为它是迭代地返回元素,而不是一次性返回所有匹配的元素。
  • 大数据集:对于包含大量元素的集合类型,SCAN 命令可以有效地进行分批处理,这对于处理大数据集非常有用。
  • 模式匹配:通过 MATCH 选项,可以只返回符合特定模式的元素,这在处理具有特定前缀或模式的键时非常有用。
  • 分页:在分页应用中,SCAN 可以用于实现服务器端的分页功能,通过游标来控制返回的数据范围。
  • 灵活性COUNT 参数允许你建议 Redis 返回的元素数量,这有助于控制每次迭代的负载。

        需要注意的是,SCAN 命令返回的元素可能会有重复,因为 Redis 并不保证每次迭代都是从上一次停止的地方开始。因此,客户端需要准备好处理可能的重复数据。此外,SCAN 命令提供的迭代器只能保证在迭代过程中添加的新元素会被返回,但不保证它们的顺序。

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

如若内容造成侵权/违法违规/事实不符,请联系多彩编程网进行投诉反馈,一经查实,立即删除!

相关文章

信息安全数学基础(17)Wilson定理

前言 Wilson定理&#xff08;Wilsons Theorem&#xff09;是数论中的一个基本定理&#xff0c;它揭示了素数与其阶乘之间的一个重要关系。 一、表述 对于任意素数p&#xff0c;有(p−1)!≡−1(modp)&#xff0c;其中(p−1)!表示p−1的阶乘&#xff0c;即123⋯(p−1)。 这个定理…

C++STL~~priority_queue

文章目录 容器适配器一、priority_queue的概念二、priority_queue的使用三、priority_queue的练习四、仿函数五、总结 容器适配器 什么是适配器 适配器是一种设计模式(设计模式是一套被反复使用的、多数人知晓的、经过分类编目的、代码设计经验的总结)&#xff0c;该种模式是将…

python画图|3D bar进阶探索

前述学习过程只能怪&#xff0c;已经探究了3D直方图的基础教程&#xff0c;详见下述链接&#xff1a; python画图|3D直方图基础教程-CSDN博客 实际上&#xff0c;基础文章直接进入了堆叠教程&#xff0c;相对来说基础的程度不够&#xff0c;因此有必要再次探索。 【1】官网教…

spug项目实现代码本地启动步骤

一、spug代码仓库地址: spug: 开源运维平台&#xff1a;面向中小型企业设计的无 Agent的自动化运维平台&#xff0c;整合了主机管理、主机批量执行、主机在线终端、文件在线上传下载、应用发布、任务计划、配置中心、监控、报警等一系列功能。 - Gitee.com 注意&#xff1a;如…

【制作100个unity游戏之32】unity开发属于自己的一个2d/3d桌面宠物,可以实时计算已经获取的工资

最终效果 文章目录 最终效果一、实现Windows消息弹窗二、将窗口扩展到工作区三、穿透能点击到其他区域四、模型交互1、我们可以新增ObjectDrag 代码控制人物拖拖动2、实现模型交互五、最终代码六、其他七、游玩地址使用Live2D实现桌宠七、源码参考完结一、实现Windows消息弹窗 …

jetson nano开发板安装todesk远程乌班图aarch版本

打开todesk官网&#xff0c;并打开Linux系统的一栏&#xff1a;ToDesk远程桌面软件-免费安全流畅的远程连接电脑手机 我这里选择deb安装包&#xff0c;然后安装依赖 sudo apt-get install libappindicator3-1 由于我下载上传到了根目录&#xff0c;所以直接在终端运行安装命令…

Golang | Leetcode Golang题解之第415题字符串相加

题目&#xff1a; 题解&#xff1a; func addStrings(num1 string, num2 string) string {add : 0ans : ""for i, j : len(num1) - 1, len(num2) - 1; i > 0 || j > 0 || add ! 0; i, j i - 1, j - 1 {var x, y intif i > 0 {x int(num1[i] - 0)}if j &g…

nonlocal本质讲解(前篇)——从滤波到Nonlocal均值滤波

线性滤波 → \rightarrow →高斯滤波 → \rightarrow →高斯滤波 → \rightarrow →双边滤波 → \rightarrow →Nonlocal均值滤波 平均 高斯 双边 Nonlocal 目录 线性滤波高斯滤波双边滤波Nonlocal均值滤波 滤波最初是频域的概念&#xff0c;由于频域乘积对应空域卷积&am…

Qwen2-VL环境搭建推理测试

引子 2024年8月30号&#xff0c;阿里推出Qwen2-VL&#xff0c;开源了2B/7B模型&#xff0c;处理任意分辨率图像无需分割成块。之前写了一篇Qwen-VL的博客&#xff0c;感兴趣的童鞋请移步&#xff08;Qwen-VL环境搭建&推理测试-CSDN博客&#xff09;&#xff0c;这么小的模…

Spring Cloud Alibaba-(1)搭建项目环境

1.Spring Cloud Alibaba&#xff08;官网&#xff1a;https://sca.aliyun.com/&#xff09; Spring Cloud Alibaba 是阿里巴巴结合自身丰富的微服务实践而推出的微服务开发的一站式解决方案&#xff0c;是 Spring Cloud 第二代实现的主要组成部分。吸收了 Spring Cloud Netflix…

一键文本提示实现图像对象高质量剪切与透明背景生成

按照提示词裁剪 按照边框裁剪 要实现您描述的功能,即通过一个文本提示就能自动从图片中切割出指定的对象并生成一个带有透明背景的新图像,这需要一个结合了先进的计算机视觉技术和自然语言处理能力的系统。这样的系统可以理解输入的文本指令,并将其转化为对图像内容的精确分…

深入探究HTTP网络协议栈:互联网通信的基石

在我们日常使用互联网的过程中&#xff0c;HTTP&#xff08;HyperText Transfer Protocol&#xff0c;超文本传输协议&#xff09;扮演着至关重要的角色。无论是浏览网页、下载文件&#xff0c;还是进行在线购物&#xff0c;HTTP协议都在背后默默地支持着这些操作。今天&#x…

【隐私计算篇】中国剩余定理解释以及Paillier解密加速应用

1. 背景介绍 本篇主要关注中国剩余定理的原理以及在paillier同态加密系统中的应用。在很多工作中&#xff0c;都可以看到中国剩余定理的影子&#xff0c;特别是同态加密提升计算效率的优化工作中&#xff0c;将paillier与中国剩余定理进行结合&#xff0c;能够实现在加密状态下…

[产品管理-25]:NPDP新产品开发 - 23 - 产品创新中的市场调研 - 定量市场调研的常见工具

目录 前言&#xff1a; 一、问卷调查 二、消费者测评组 三、概念测试与概念分类 概念测试 概念分类 四、感官检验 1、定义与特点 2、基本方法 3、应用领域 4、优势与局限性 五、眼动追踪 1、技术原理 2、应用领域 3、技术优势 4、市场现状与发展趋势 5、结论 …

彩蛋岛 销冠大模型案例

彩蛋岛 销冠大模型案例 任务&#xff1a; https://kkgithub.com/InternLM/Tutorial/tree/camp3/docs/EasterEgg/StreamerSales 视频 https://www.bilibili.com/video/BV1f1421b7Du/?vd_source4ffecd6d839338c9390829e56a43ca8d 项目git地址&#xff1a; https://kkgithu…

设计模式-结构型-11-代理模式

文章目录 1. 基本介绍2. 静态代理2.1 基本介绍UML 类图 2.2 应用实例定义接口目标对象代理对象调用代理 2.3 静态代理优缺点 3. 动态代理3.1 基本介绍3.2 JDK 中生成代理对象的 API参数说明UML类图 3.3 应用实例定义接口目标对象代理工厂调用代理 4. Cglib 代理4.1 基本介绍4.2…

2011-2022年数字金融与企业ESG表现:效应、机制与“漂绿”检验(内含原始数据+处理代码)

2011-2022年数字金融与企业ESG表现&#xff1a;效应、机制与“漂绿”检验&#xff08;内含原始数据处理代码&#xff09; 1、时间&#xff1a;2011-2022年 2、来源&#xff1a;上市公司年报、华证ESG、北大数字普惠金融 3、指标&#xff1a;年份、股票代码、股票简称、行业名…

使用Maven创建一个Java项目并在repository中使用

JDK环境&#xff1a;1.8.0_371 Maven环境 &#xff1a;Apache Maven 3.6.3 配置完成jdk和mvn后&#xff0c;进入到指定文件夹下执行如下语句&#xff1a; mvn archetype:generate -DgroupIdtop.chengrongyu -DartifactIdCyberSpace -DarchetypeArtifactIdmaven-archetype-quic…

Matlab Delany-Bazley和Miki模型预测多孔材料吸声性能

Delany-Bazley模型和Miki模型是常用于预测多孔材料吸声性能的两种模型。Delany-Bazley模型是一种经验模型&#xff0c;用于描述多孔材料的声学特性&#xff0c;特别是复杂多孔材料如泡沫材料。该模型基于材料的几何参数&#xff08;如孔隙率、孔隙形状等&#xff09;来预测材料…

Meta-Learning数学原理

文章目录 什么是元学习元学习的目标元学习的类型数学推导1. 传统机器学习的数学表述2. 元学习的基本思想3. MAML 算法推导3.1 元任务设置3.2 内层优化&#xff1a;任务级别学习3.3 外层优化&#xff1a;元级别学习3.4 元梯度计算3.5 最终更新规则 4. 算法合并5. 理解 MAML 的优…