redis存储原理与数据模型学习笔记

news2025/1/12 1:50:58

目录

  • 1 redis线程模型
    • 1.1 线程组成
    • 1.2 redis命令处理是单线程
  • 2 redis db 存储分析
    • 2.1 先了解代码
      • server.h
      • dict.h
    • 2.2 从kv存储分析
    • 2.3 负载因子
    • 2.4 渐进式rehash机制
      • 数据访问scan
  • 3 数据模型分析
    • 以zset为例
    • 跳表

1 redis线程模型

1.1 线程组成

在这里插入图片描述
redis-server
命令处理
网络事件的监听
bio close file 异步关闭大文件
bio aof fsync 异步 aof 刷盘
bio lazy free 异步清理大块内存
io thd * io 多线程
emalloc bg thd jemalloc 后台线程

1.2 redis命令处理是单线程

在这里插入图片描述

单线程为什么快?

在这里插入图片描述

2 redis db 存储分析

2.1 先了解代码

server.h

/* Redis database representation. There are multiple databases identified
 * by integers from 0 (the default database) up to the max configured
 * database. The database number is the 'id' field in the structure. */
typedef struct redisDb {
    dict *dict;                 /* The keyspace for this DB */
    dict *expires;              /* Timeout of keys with a timeout set */          ---->  自动过期的key
    dict *blocking_keys;        /* Keys with clients waiting for data (BLPOP)*/
    dict *ready_keys;           /* Blocked keys that received a PUSH */
    dict *watched_keys;         /* WATCHED keys for MULTI/EXEC CAS */   ---->  事物操作时有用到的  watch
    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. */
    clusterSlotToKeyMapping *slots_to_keys; /* Array of slots to keys. Only used in cluster mode (db 0). */
} redisDb;

dict *dict:指向字典对象的指针,用于存储键值对数据。字典是Redis中的核心数据结构,用于快速查找和访问键值对。
dict *expires:指向字典对象的指针,用于存储键的过期时间。当键设置了过期时间时,会将键和其对应的过期时间存储在这个字典中。
dict *blocking_keys:指向字典对象的指针,用于存储被阻塞的键。当某个客户端在执行阻塞操作时,将被阻塞的键存储在这个字典中。
dict *ready_keys:指向字典对象的指针,用于存储状态已改变的键。当某个键的值在被其他操作修改后,将把该键存储在这个字典中。
dict *watched_keys:指向字典对象的指针,用于存储被监视的键。当某个事务中的键被监视时,将被监视的键存储在这个字典中。
int id:表示数据库的ID。
long long avg_ttl:表示数据库中所有键的平均过期时间。
unsigned long expires_cursor:用于过期键的迭代游标。
list *defrag_later:指向链表对象的指针,用于存储需要碎片整理的键。
clusterSlotToKeyMapping *slots_to_keys:用于集群模式下的槽位和键的映射。

dict.h

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 */
    int16_t pauserehash; /* If >0 rehashing is paused (<0 indicates coding error) */
    signed char ht_size_exp[2]; /* exponent of size. (size = 1<<exp) */
};

注意 dictEntry **ht_table[2];

2.2 从kv存储分析

怎么从key定位到value?
哈希原理:
数组 + hash(key) % 数组长度 ===>确定value存储位置 参考https://blog.csdn.net/qq43645149/article/details/131242533 的3.3节

  1. 字符串key经过 hash 函数运算得到 64 位整数;
  2. 相同字符串key多次通过 hash 函数得到相同的 64 位整数;
  3. 整数对 取余可以转化为位运算;
  4. 抽屉原理 n+1个苹果放在 n 个抽屉中,苹果最多的那个抽屉至少有 2 个苹果;
    64位整数远大于数组的长度,比如数组长度为 4,那么 1、5、9、1+4n 都是映射到1号位数组;所以大概率会发生冲突;

ht_table二维指针,这里可以理解为指针数组,对应哈希存储数组 ht 的每一个槽位ht[i] 挂的是一个链表
ht_table[2]中的2,怎么理解?ht的槽位成对出现,ht[1]是为扩容备用的。
在这里插入图片描述
避免冲突,使用强随机函数siphash, 然后就是 扩容
在这里插入图片描述
unsigned long ht_used[2]; 用来保存ht[0], ht[1]的长度,用2的n次幂表示size,
这样hash(key) % size 取余运算就可以优化为位运算:hash(key) & (2^-1) 。

怎么判断扩容呢?

2.3 负载因子

负载因子 = used / size;used 是数组存储元素的个数,
size 是数组的长度;
负载因子越小,冲突越小;负载因子越大,冲突越大;
redis 的负载因子是 1;

如果负载因子 > 1,则会发生扩容;扩容的规则是翻倍;
如果正在 fork (在 rdb、aof 复写以及 rdb-aof 混用情况下)时,会阻止扩容;但是此时若负载因子 > 5,
索引效率大大降低, 则马上扩容;这里涉及到写时复制原理;

如果负载因子 < 0.1,则会发生缩容;缩容的规则是恰好包含used 的 ;
恰好的理解:假如此时数组存储元素个数为 9,恰好包含该元素的就是 ,也就是 16;

这里有一个问题,当执行scan命令的时候,突然发生扩容或者缩容怎么办?

127.0.0.1:6379> SET key1 "value1"
OK
127.0.0.1:6379> SET key2 "value2"
OK
127.0.0.1:6379> SET key3 "value3"
OK
127.0.0.1:6379> SCAN 0 MATCH key* COUNT 100       
1) "13"    // 游标值
2) 
   1) "key3"
   2) "key2"
   3) "key1"    // 匹配模式为key*,返回所有以key开头的键

2.4 渐进式rehash机制

源码位置:
./redis-main/src/dict.c:211:int dictRehash(dict *d, int n)

当 hashtable 中的元素过多的时候,不能一次性 rehash 到ht[1];这样会长期占用 redis,其他命令得不到响应;所以需
要使用渐进式 rehash;
rehash步骤:
将 ht[0] 中的元素重新经过 hash 函数生成 64 位整数,再对ht[1] 长度进行取余,从而映射到 ht[1];
渐进式规则:

  1. 分治的思想,将 rehash 分步到之后的每步“增、删、查”的操作当中;
  2. 在定时器中,最大执行一毫秒 rehash ;每次步长 100 个数组槽位; (避免命令单线程阻塞)
    处于渐进式rehash阶段不会发生扩容缩容。
    对应源码:
dictEntry *dictAddRaw(dict *d, void *key, dictEntry **existing)
static dictEntry *dictGenericDelete(dict *d, const void *key, int nofree)
dictEntry *dictFind(dict *d, const void *key)
dictEntry *dictGetRandomKey(dict *d)

/* Rehash in ms+"delta" milliseconds. The value of "delta" is larger 
 * than 0, and is smaller than 1 in most cases. The exact upper bound 
 * depends on the running time of dictRehash(d,100).*/
int dictRehashMilliseconds(dict *d, int ms) {
    if (d->pauserehash > 0) return 0;

    long long start = timeInMilliseconds();
    int rehashes = 0;

    while(dictRehash(d,100)) {
        rehashes += 100;
        if (timeInMilliseconds()-start > ms) break;
    }
    return rehashes;
}

数据访问scan

通过往高位加1,往低位进1的方式遍历,如下图
在这里插入图片描述
为什么2对应2和6?因为6%4=2, 6%8=6, 当key时2,6的时候,size=4, key%size = 2。
为什么要用这种访问方式呢?==> 可以确保“扩容”或"缩容的时候“ 可以访问正确遍历完所有的数据(两次缩容或扩容处理不了)。

27.0.0.1:6379> set aa 1
OK
127.0.0.1:6379> set bbb 2
OK
127.0.0.1:6379> set ccccc 3
OK
127.0.0.1:6379> set ddddddddddddd 4
OK
127.0.0.1:6379> keys *
1) "ddddddddddddd"
2) "ccccc"
3) "aa"
4) "bbb"
127.0.0.1:6379> scan 0 match * count 1
1) "2"  # 这里提示下一个是2
2) 1) "ddddddddddddd"
# 这个时候,在另外一个终端 执行:127.0.0.1:6379> set ffffffffffffffffff 5
# OK
127.0.0.1:6379> scan 2 match * count 1
1) "6"  # 这里提示下一个是6,假装扩容了
2) 1) "ffffffffffffffffff"
127.0.0.1:6379> scan 6 match * count 1
1) "1"
2) 1) "bbb"
127.0.0.1:6379> scan 1 match * count 1
1) "5"  # ? aa ccccc 一起出来了?
2) 1) "aa"
   2) "ccccc"
127.0.0.1:6379> scan 5 match * count 1
1) "0"
2) (empty array)

aa 与 ccccc 一起出来,aa, ccccc这里是什么存储结构呢?

127.0.0.1:6379> keys *
1) "aa"
2) "ccccc"
3) "ffffffffffffffffff"
4) "ddddddddddddd"
5) "bbb"

3 数据模型分析

数据量小或少的时候,用简单的数据结构,反之用较为复杂的数据结构。
在这里插入图片描述

以zset为例

在这里插入图片描述
在 redis.conf 可以查找到相关的定义:
zset-max-listpack-entries 128
zset-max-listpack-value 64
然后在t_zset.c中找到 createZsetObject 与 createZsetListpackObject,可加断点调试。

 /* Lookup the key and create the sorted set if does not exist. */
    zobj = lookupKeyWrite(c->db,key);
    if (checkType(c,zobj,OBJ_ZSET)) goto cleanup;
    if (zobj == NULL) {
        if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
        if (server.zset_max_listpack_entries == 0 ||
            server.zset_max_listpack_value < sdslen(c->argv[scoreidx+1]->ptr))
        {
            zobj = createZsetObject();           // OBJ_ENCODING_SKIPLIST
        } else {
            zobj = createZsetListpackObject();  // OBJ_ENCODING_LISTPACK
        }
        dbAdd(c->db,key,zobj);
    }

当元素过大,或者list过长,转换为跳表。

 /* check if the element is too large or the list
             * becomes too long *before* executing zzlInsert. */
            if (zzlLength(zobj->ptr)+1 > server.zset_max_listpack_entries ||
                sdslen(ele) > server.zset_max_listpack_value ||
                !lpSafeToAdd(zobj->ptr, sdslen(ele)))
            {
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);  // 转换为跳表
            } else {
                zobj->ptr = zzlInsert(zobj->ptr,ele,score);
                if (newscore) *newscore = score;
                *out_flags |= ZADD_OUT_ADDED;
                return 1;
            }

对应1.2节中的:
在这里插入图片描述

跳表

方便范围查询,多层级有序链表,让其实现二分查找的效率,缺点占用的内存空间会多50%。

这里用一个比较特殊的例子(完美跳表,第二层是从第一层每隔一个元素取一个,第三层也是从第二层每隔一个元素取一个。。。)理解一下,如下图,要访问节点12,第一层常规的遍历方法用的次数明显比蓝色箭头用的次数要多。
但是优势不明显,但如果是找节点10,跳表只需比较一次。平均下来接算法复杂度接近于 O ( log ⁡ 2 n ) O(\log_2n) O(log2n)
注意第一层是有序链表,跳表是从最高层往底层跳,从而找到目标。
在这里插入图片描述
上面的跳表在增加或删除元素时,需要重新构建跳表,效率比较低。
非完美跳表,插入节点会用随机层数的方式,这样第二层对于第三层就可能不会是“每隔一个节点提取一个元素了”,相邻的元素可能会被直接提取到上一层,更高层也是。
另外一个例子:插入17
在这里插入图片描述
另外,为了快速的索引到某个节点,zset还引入了字典功能,帮助快速的索引到节点。结构图如下:
在这里插入图片描述

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

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

相关文章

Nerf论文前后介绍以及今后方向(2020年各个方向工作论文分析) NEURAL VOLUME RENDERING:NERF AND BEYOND

你好&#xff01; 这里是“出门吃三碗饭”本人&#xff0c; 本文章接下来将介绍2020年对Nerf工作的一篇总结论文NEURAL VOLUME RENDERING:NERF AND BEYOND&#xff0c;论文作者是佐治亚理工学院的Frank Dellaert同学和 MIT的Lin Yen-Chen同学&#xff0c;非常感谢两位大佬的总结…

序列化和反序列化的认识【protobuf、json、xml】

1. 什么是序列化与反序列化&#xff1f; 程序员在编写应用程序的时候往往需要将程序的某些数据存储在连续的内存中&#xff0c;然后将其写入文件或是将其传输到网络中的另一台计算机上以实现通讯。这个将程序数据转换成能被存储并传输的格式的过程被称为序列化&#xff08;seri…

C++ array (STL array) 序列容器

文章目录 1 类模板2 begin()/end() 和 cbegin()/cend()3 cbegin() 和 cend()4 rbegin()/rend() 和 crbegin()/crend()5 访问array容器中单个元素参考 1 类模板 array 容器以类模板的形式定义在 头文件&#xff0c;并位于命名空间 std 中&#xff0c;如下所示&#xff1a; nam…

一、约束编程简介(Constraints Programming)

文章目录 1、约束编程求解器2、值域传播器THE END 1、约束编程求解器 \qquad 约束编程是一种编程范式&#xff0c;旨在解决那些需要满足一系列约束条件的问题。在约束编程中&#xff0c;问题被定义为一组变量和约束条件&#xff0c;而不是指定解决问题的算法步骤。通过定义变量…

XXE漏洞复现步骤

0X00XXE注入定义 XXE注入&#xff0c;即XML External Entity&#xff0c;XML外部实体注入。通过 XML 实体&#xff0c;”SYSTEM”关键词导致 XML 解析器可以从本地文件或者远程 URI 中读取数据。所以攻击者可以通过 XML 实体传递自己构造的恶意值&#xff0c;是处理程序解析它…

Nginx-负载均衡详解

本文已收录于专栏 《中间件合集》 目录 概念说明什么是Nginx什么是负载均衡 功能介绍配置过程1.修改nginx配置文件添加服务组修改HTTP模块 2.保存配置文件3.重启配置文件4.查看配置文件是否重启成功5.还可以配置其他策略轮询权重最少连接数 常用命令总结提升 概念说明 了解ngi…

MySQL:环境安装和数据库基础

环境安装 说明&#xff1a; • 安装与卸载中&#xff0c;用户全部切换成为root&#xff0c;⼀旦 安装&#xff0c;普通用户能使⽤的 • 初期练习&#xff0c;mysql不进行用户管理&#xff0c;全部使⽤root进行&#xff0c;尽快适应mysql语句&#xff0c;后⾯学了用户管 理&a…

实例讲解,一文弄懂workqueue和waitqueue

本期主题&#xff1a; 讲清workqueue和waitqueu&#xff1a; 从中断讲起waitqueue是什么workqueue总结 往期链接&#xff1a; linux设备驱动中的并发linux设备驱动中的编译乱序和执行乱序linux设备驱动之内核模块linux字符驱动linux字符驱动之ioctl部分linux字符驱动之read、…

数据结构07:查找[C++][朴素二叉排序树BST]

图源&#xff1a;文心一言 考研笔记整理8k字&#xff0c;小白友好、代码可跑&#xff0c;请小伙伴放心食用~~&#x1f95d;&#x1f95d; 第1版&#xff1a;查资料、写BUG、画导图、画配图~&#x1f9e9;&#x1f9e9; 参考用书&#xff1a;王道考研《2024年 数据结构考研复习…

【MATLAB第45期】基于MATLAB的深度学习SqueezeNet卷积神经网络混凝土裂纹图像识别预测模型

【MATLAB第45期】基于MATLAB的深度学习SqueezeNet卷积神经网络混凝土裂纹图像识别预测模型 引言 该文章展示如何微调名为SqueezeNet的预训练深度卷积网络&#xff0c;以执行裂纹图像分类预测。并使用一种称为Grad-CAM的技术来解释和分析分类输出。文章使用L.Zhang介绍的混凝土…

C++初阶之C++入门最全详解

C入门 1. C关键字&#xff08;C98&#xff09;2. 命名空间2.1 命名空间定义2.2 命名空间使用 3. C输入&输出4. 缺省参数4.1 缺省参数概念4.2 缺省参数分类 5. 函数重载5.1 函数重载概念5.1.1 参数类型不同5.1.2 参数个数不同5.1.3 参数类型顺序不同 5.2 C支持函数重载的原理…

Spring Cloud Alibaba Seata(二)

目录 一、Seata 1、Seata-AT模式 1.1、具体案例 1.2、通过Seata的AT模式解决分布式事务 2、Seata-XA模式 3、Seata-TCC模式 4、Seata-SAGA模式 一、Seata 1、Seata-AT模式 概念&#xff1a;AT模式是一种无侵入的分布式事务解决方案&#xff0c;在 AT 模式下&#xff0c…

git修改默认主分支main为master和设置git默认创建的项目默认分支都为master

文章目录 前言一、设置新建仓库默认分支为master1.点击GitHub右上角的头像2. 选中settings&#xff08;设置&#xff09;3.点击Repositories&#xff08;存储库&#xff09;4.更改main为master后点击update 二、设置已建仓库的默认分支为master1.找到你要改的项目点击settings&…

STL序列式容器的概念

文章目录 1 迭代器2 什么是序列式容器3 序列式容器容器中常见的函数成员参考 1 迭代器 迭代器和C指针非常类似&#xff0c;它可以是需要的任意类型&#xff0c;通过迭代器可以指向容器中的某个元素&#xff0c;如果需要&#xff0c;还可以对该元素进行读写操作。 迭代器类别 …

ThreeJS案例一——在场景中添加视频,使用人物动作以及用键盘控制在场景中行走的动画

准备 首先我们需要两个模型&#xff0c;一个是场景模型&#xff0c;另一个是人物模型。 人物模型我这里用的Threejs官网中的给的模型&#xff0c;名称是Xbot.glb。 当然人物模型也可以自己去这个网站下载sketchfab&#xff0c;下载后给模型添加动画mixamo 下载模型动画 先让…

C++ STL vector容器用法

文章目录 1 vector初始化方法2 vector容器迭代器3 data()函数4 emplace_back()和push_back()的区别5 insert()函数6 vector删除元素参考 1 vector初始化方法 方式1&#xff1a; std::vector<double> values;//创建空的vcetor values.reserve(20); //设置容器的内存分配…

【实战】 JWT、用户认证与异步请求(1) —— React17+React Hook+TS4 最佳实践,仿 Jira 企业级项目(四)

文章目录 一、项目起航&#xff1a;项目初始化与配置二、React 与 Hook 应用&#xff1a;实现项目列表三、TS 应用&#xff1a;JS神助攻 - 强类型四、JWT、用户认证与异步请求1.login2.middleware of json-server3.jira-dev-tool&#xff08;imooc-jira-tool&#xff09;安装问…

《流浪地球 2》的硬核黑科技

电影中&#xff0c;由刘德华饰演的量子计算机工程师图恒宇有一个惊心动魄的情节。为了同步启动全球地球发动机&#xff0c;需要重启互联网&#xff0c;避免地壳破碎和地质灾害。而重启互联网的关键则是要启动“根服务器”。电影中没有具体交代是什么根服务器&#xff0c;但是当…

Ubuntu18.04屏幕分辨率问题

本篇博客最早发布于实验室公共博客&#xff0c;但已无人维护&#xff0c;现迁移至个人博客 起因 本来昨天还好好的&#xff0c;过了一夜&#xff0c;就变了&#xff0c;像极了咳咳(自行脑补) redwallbot-2小车上固定的屏幕&#xff0c;屏幕分辨率本来应该是1920x1080的&#…

DevOps系列文章之 linux安装ftp

第一步 1、用root 进入系统 2、使用命令 rpm -qa|grep vsftpd 查看系统是否安装了ftp&#xff0c;若安装了vsftp&#xff0c;使用这个命令会在屏幕上显示vsftpd的版本 3、使用命令rpm -e vsftpd 即可卸载ftp 4、再使用rpm -qa|grep vsftpd 查看系统是否已删除ftp&#xff0…