Redis:数据结构

news2024/11/16 12:56:28

简单动态字符串SDS

Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构

建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型,并将SDS用作Redis的默认字符

串表示。

SDS 的实现

struct sdshdr{
  // 记录buf中已使用字节数量等于sds保存字符串的长度
  int len;
  // 记录buf数组中未使用字节的数量
  int free;
  // 字节数组,保存字符串
  char buf[];
}

SDS 与 C字符串的区别

  1. 常数复杂度获取字符串的长度。sds中记录了字符串长度可以直接获取
  2. 杜绝缓存区溢出。sds的空间分配完全杜绝了发生缓冲区溢出的可能性,当sds API需要对SDS进行修改的时候,API会先检查SDS的空间是否满足修改的需要,如果不满足,会自动将SDS的空间扩展至执行修改所需的大小。
  3. 减少修改字符串时带来的内存冲分配次数。
    1. 空间预分配。优化SDS字符串增长操作,当SDS需要进行空间扩展的时候,程序不仅会为SDS分配修改锁必须要的空间,还会为SDS分配额外的未使用空间。分配公式
      1. 如果对SDS进行修改之后,SDS的长度小于1MB,那么程序分配和len属性同样大小的未使用空间,这时SDS的len属性的值和free相同
      2. 如果对SDS进行修改之后,SDS的长度大于1MB,那么程序会分配1MB的未使用空间。
    2. 惰性空间的释放。用于优化SDS的字符串缩短操作,当SDS缩短的时候,程序不立即使用内存重分配来回收缩短后多出来的字节,而是使用free属性将这些字节的数量记录起来。等待将来使用。SDS API提供的有真正释放空间的方法。
  4. 二进制安全。C字符串必须符合某种编码,并且除字符串末尾外不能包含空字符,只能保存文本数据。SDS通过len判断字符串末尾。SDS不是保存字符而是保存二进制,所以SDS不仅可以保存文本数据还可以保存二进制。

链表

链表的实现

链表节点的结构:

typedef struct listNode{
  struct listNode *prev;
  struct listNode *next;
  void *value;
}listNode;

持有链表的结构:

typedef struct list{
  listNode *head;
  listNode *tail;
  unsigned long len;
  // 节点值复制函数
  void *(*dup)(void *ptr);
  // 节点值释放函数
  void (*free)(void *ptr);
  // 节点值对比函数
  int (*match)(void *ptr, void *key);
}list;

Redis 链表的特点

  • 双向:双向列表。
  • 无环:表头的prev指针和表尾的next指针都执行null。
  • 长度计数:获取节点数量的复杂度为O(1)
  • 多态:链表节点使用void*指针来保存节点值,可以通过list结构的dup,free,match三个属性为节点值设置类型特定函数,所以链表可以用于保存各种不同类型的值。

字典(map)

字典的实现

Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。

哈希表的结构定义:

typedef struct dictht{
  //哈希表数组
  dictEntry **table;
  // 哈希表的大小
  unsigned long size;
  // 哈希表大小掩码,用于计算索引,总是等于size- 1
  unsigned long sizemask;
  // 哈希表已有节点的数量
  unsigned long used;
}

table属性是一个数组,数组中的每一个元素都是一个指向dict.h/dictEntry结构,每个dictEntry结构保存一个键值对。

哈希表节点结构定义:

typedef struct dictEntry{
  void *key;
  union{
    void *val;
    unit64_tu64;
    int64_ts64;
  }v;
  // 指向下一个哈希表节点,形成链表 
  struct dictEntry *next;
}

字典结构的定义:

typedef struct dict{
  // 类型特定函数
  dictType *type;
  // 私有数据
  void *privadata;
  // 哈希表
  dictht ht[2];
  // rehash 索引 当rehash不再进行时,值为-1
  int trehashidx;
}
  • type 是一个指向dictType结构的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数,Redis会为用途不同的字典设置不同的类型特定函数。
  • privdata保存了需要传给那些类型特定函数的可选参数。
  • ht[2] 保存了两个hash表,一般情况下只使用h[0], 在进行rehash的时候会使用到h[1]
  • trehashidx 记录了rehash的进度。

解决hash冲突

使用链地址法解决hash冲突。

rehash

哈希表保存的键值对数量太多或者太少的时候将要执行rehash(重新散列)

rehash的步骤:

  1. 为字典的ht[1] 哈希表分配空间,这个哈希表的大小决定于要执行的操作以及ht[0] 当前包含的键值对数量(即ht[0].used属性的值)、
    1. 如果执行的扩展操作,ht[1].size = 第一个大于等于ht[0].used * 2的2的n次幂
    2. 收缩操作,ht[1] = 第一个大于等于ht[0].used * 2的2的n次幂
  2. 保存在ht[0]中的键值对重新rehash到ht[1]上。
  3. 迁移完成之后,释放ht[0] 将 ht[1] 设置为ht[0],并在ht[1]新创建一个空白哈希表。

什么时候进行扩展与收缩呢?

程序自动对哈希表扩展的条件(满足一条即可):

  1. 服务器目前没有在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表负载因子大于等于1
  2. 服务器在执行BGSAVE或者BGREWRITEAOF命令,并且哈希表负载因子大于等于5

负载因子(factor) = 哈希表以保存节点数量 / 哈希表大小。(ht[0].used / ht[0].size)

自动收缩的条件:负载因子小于0.1

渐进式rehash

在字典中维持一个索引计数器变量rehashidx,通过这个变量进行rehash。

首先将rehashidx设置为0,表示rehash开始。在rehash期间,每次对字典进行添加,删除,查找或者更新操作时,顺带进行rehash 即将ht[0]哈希表在rehashidx索引上的键值对rehash到ht[1]上 ,rehashidx逐渐增加,所有的rehash之后将rehashidx设置为-1表示结束。

这种方式避免了集中式的rehash带来的庞大工作量。在进行使用中逐渐的rehash完成。

在渐进式rehash过程中,字典会同时使用ht[0]和ht[1]两个哈希表。例如查找回去两个哈希表中进行。新添加的只保存到ht[1]中。最终ht[0]变为空表。

跳跃表

跳跃表支持平均O(logN),最坏O(N)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。在Redis中用于实现有序集合。

跳跃表的实现

跳跃表由zskiplistNode和zskiplist两个结构定义。

在这里插入图片描述

header指向的节点是表头节点,表头节点只有层,其它属性不会使用。

跳跃表节点的实现:

typedef struct zskiplistNode{
  // 层
  struct zskiplistLevel{
    //前进指针
    struct zskiplistNode *forward;
    //跨度
    unsigned int span;
  } level[];
  // 后退指针
  struct zskiplistNode *backward;
  //分值
  double score;
  //成员对象
  robj *obj;
}zskiplistNode;
  1. 层 level[]。通过层加快访问其它节点的速度。数组中的每个元素包含一个指向其它节点的指针和与指向节点的跨度。每次创建一个新跳跃表节点的时候,程序根据幂次定律(power law,越大的数出现的概率越小)随机生成一个介于1和32之间的值作为level数组的大小,这个大小就是层的高度。
    1. **前进指针。**每个层都有一个指向表尾方向的前进指针。
    2. **层的跨度:**用于记录两个节点之间的距离。
  2. **后退指针。**用于表尾向表头方向访问节点。每次只能回退至前一个节点。
  3. **分值和成员。**节点的分值是一个double类型的浮点数,跳跃表中的所有节点都按分值从小到大来排序。节点的成员对象是一个指针,它指向一个字符串对象保存着一个SDS值。成员对象必须是唯一的,分值是可以相同的。

跳跃表结构定义:

typedef struct zskiplist{
  //表头节点和表尾节点
  struct zskiplistNode *header, *tail;
  //表中节点的数量
  unsigned long length;
  //表中层数最大的节点层数(不包括表头节点)
  int level;
}zskiplist;

通过这个结构持有跳跃表节点。

整数集合

整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。

整数集合的实现

整数集合是Redis用于保存整数数值的集合抽象数据结构,它可以保存int16_t,int32_t或者int64_t的整数值,并且保证集合中不会出现重复元素。

intset结构表示一个整数集合:

typedef struct intset{
  //编码方式
  uint32_t encoding;
  //集合包含的元素数量
  uint32_t length;
  //保存元素的数组
  int8_t contents[];
}intset;

contents 数组是整数集合的底层实现,整数集合的每个元素都是contents数组的一个数组项(item)各个项在数组中按值的大小从小到大有序地排列,并且数组中不会包括任何重复项。contents属性声明为int8_t类型的数组,但实际上contents数组不保存任何int8_t类型的数组,contents数组的真正类型取决于encoding属性的值。

升级

每当将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现在所有元素的类型都要长时,整数集合需要先进行升级(upgrade)然后才能将新元素添加到整数集合里面。

升级整数集合并添加新元素分为三步进行:

  1. 根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间。
  2. 将底层数组现有的所有元素都转换成与新元素相同的类型,并将类型转换后的元素放置到正确位置上。
  3. 将新元素加到底层数组里面。

升级的好处:(1)提升整数集合的灵活性(使用者不用考虑整数的类型,底层会自动升级类型,不用担心类型错误)。(2)尽可能的节约内存。

整数集合不支持降级,升级后,编码就会一直保持升级后的状态。

压缩列表

压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。

压缩列表的实现

压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构。

在这里插入图片描述

压缩列表节点的构成:

[外链图片转存失败,源站可能有防盗链机制,建议将图片保存下来直接上传在这里插入图片描述

节点的previous_entry_length属性以字节为单位,记录了压缩列表中前一个节点的长度。所以程序可以通过指针运算,根据当前节点的起始地址来计算出前一个节点的起始地址。

压缩列表从表尾向表头遍历操作就是通过这一原理实现的。

encoding属性记录了节点的content属性所保存数据的类型以及长度。

在这里插入图片描述

content复制保存节点的值,可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。

对象

上面介绍了Redis用到的所有主要数据结构,Redis并没有字节使用这些数据结构来实现键值对数据库,而是基于这些数据结构创建了一个对象系统,这个系统包含字符串,列表,哈希,集合,有序集合这五种类型的对象,每一种对象都至少用到了一种前面介绍的数据结构

Redis对象系统还是先了**基于引用计数技术的内存回收机制。**对象的引用计数属性还带有对象共享的作用。

集合对象底层实现可以是intset(整数集合)或者hashtable

有序集合对象底层实现可以是ziplist或者skiplist。还包含一个用字典为有序集合创建了一个从成员到分值的映射。

哈希对象底层实现可以是ziplist或者hashtable

列表对象底层实现可以是ziplist或者linkedlist

对象的空转时长,对象结构包含一个lru属性记录对象最后一次被命令程序访问的时间。

服务器打开了maxmemory选项时,并且服务器用于回收内存的算法为volatile-lru或者allkeys-lru,那么当服务器占用的内存数超过了maxmemory选项所设置的上限值时,空转时长较高的那部分键会优先被释放,从而回收内存。

参考《Redis设计与实现》

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

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

相关文章

aws lambda rust的sdk和自定义运行时

rust的aws sdk 参考资料 https://docs.aws.amazon.com/sdk-for-rust/latest/dg/getting-started.htmlhttps://awslabs.github.io/aws-sdk-rust/https://github.com/awslabs/aws-sdk-rusthttps://github.com/awsdocs/aws-doc-sdk-examples/tree/main/rust_dev_preview rus sd…

XlsReadWriteII EXCEL Cell Font 单元字体设置

XlsReadWriteII EXCEL Cell Font 单元字体设置 通过XLSReadWriteII5在写EXCEL时,由于XLSReadWriteII5中使用的是个性化的TFont,因而通过参数带入TFont,不能完整地将TFont带入Cell的Font,问题解决如下: 一、问题说明 1、…

Python学习-----模块2.0(常用模块之时间模块-->time)

目录 前言: time简介 导入模块 1.时间戳 2.时间元组 (1)把时间戳转换为元组形式 (2)元组转换为时间戳输出 (3)把元组转换为格式化时间 (4)把时间戳转换为格式化时间…

【深度学习】模型评估

上一章——多分类问题和多标签分类问题 文章目录算法诊断模型评估交叉验证测试算法诊断 如果你为问题拟合了一个假设函数,我们应当如何判断假设函数是否适当拟合了?我们可以通过观察代价函数的图像,当代价函数达到最低点的时候,此…

手机/移动端的UI框架-Vant和NutUI

下面推荐2款手机/移动端的UI框架。 其实还有很多的框架,各个大厂都有UI框架。目前,找来找去,只有腾讯的移动端是setup语法写的TDesign,其他大厂,虽然都是VUE3写的,但是都还未改成setup的语法,而…

张晨光-JAVA零基础保姆式JDBC技术教程

JDBC文档 JDBC概述 JDBC概述 Java DataBase Connectivity Java 数据库连接技术 JDBC的作用 通过Java语言操作数据库,操作表中的数据 SUN公司为**了简化、**统一对数据库的操作,定义了一套Java操作数据库的规范,称之为JDBC JDBC的本质 是官方…

【JavaSE专栏10】Java的顺序结构、选择结构和循环结构

作者主页:Designer 小郑 作者简介:Java全栈软件工程师一枚,来自浙江宁波,负责开发管理公司OA项目,专注软件前后端开发(Vue、SpringBoot和微信小程序)、系统定制、远程技术指导。CSDN学院、蓝桥云…

金融错配程度/信贷错配程度/资本错配程度/资本资源错配程度(1998-2021年)

数据来源:根据沪深A股上市公司数据进行测算 时间跨度:1998-2021年 区域范围:沪深A股上市公司 指标说明: 参考邵挺(2010)、周煜皓和张胜勇(2014)的研究,运用金融错配负担水平来衡量信贷错配(Fd&#xff…

Redis-哨兵模式以及集群

在开始这部分内容之前需要先说一下复制功能,因为这是Redis实现主从数据同步的实现方式。复制功能如果存在两台服务器的话,我们可以使用redis的复制功能,让一台服务器去同步另一台服务器的数据。现在我启动了两台redis服务器,一个端…

【大数据】HADOOP-Yarn集群界面UI指标项详解(建议收藏哦)

目录首页(Cluster)节点信息Scheduler Metrics:集群调度信息节点信息详解(Nodes)应用列表信息(applications)队列详情页(Scheduler)指标详细说明(非常重要&…

斯坦福:chatGPT可能有了人类心智,相当于9岁儿童!

chatGPT已经具备了人类独心智,这话不是我说的,是一位来自斯坦福大学计算机科学家说的。很多玩过chatGPT的人都见识过他的”无所不知”,但是,我觉得最让我吃惊的是,他比以前我们用过那些对话机器人最大的差别就是你甚至…

「数据科学」数据科学家为什么应该学习PostgreSQL?

SQL是成为数据科学家的必要条件吗?答案是肯定的。数据科学已经发展了,虽然许多数据科学家仍然使用CSV文件(值以逗号分隔的文本文件),但它们不是最好的选择。Python Panda库允许从CSV文件加载数据,但是这些文件有许多约束。例如,它…

各种常用C/C++集成开发环境的安装与配置

新学期又开始了,帮助又一堆菜鸟安装和配置C/C集成开发环境是一件恼人的工作。 本文引用自作者编写的下述图书; 本文允许以个人学习、教学等目的引用、讲授或转载,但需要注明原作者"海洋饼干叔 叔";本文不允许以纸质及电子出版为目的…

【k8s】如何搭建搭建k8s服务器集群(Kubernetes)

搭建k8s服务器集群 服务器搭建环境随手记 文章目录搭建k8s服务器集群前言:一、前期准备(所有节点)1.1所有节点,关闭防火墙规则,关闭selinux,关闭swap交换,打通所有服务器网络,进行p…

Bash Shell 通过ls命令筛选文件

Bash Shell 通过ls命令及其管道根据大小名称筛选文件 最近参与的项目当中有需要用pyarmor加密项目的要求,听网上吹的pyarmor都那么神,用了一下感觉也一般,试用版普通模式下文件加密居然还有大小32KB的限制,加密到一半就失败了&am…

Linux常用命令汇总

1、tcpdump抓包 tcpdump这个命令是用来抓包的,默认情况下这个命令是没有的,需要安装一下: yum install -y tcpdump 使用这个命令的时候最好是加上你网卡的名称,不然可能使用不了: tcpdump -nn -i {网卡名称} 网卡名称…

iOS开发笔记之九十六——本地Data Persistence总结笔记

本质上来说,不管是哪种缓存方式最终都会以文件的形式存储在磁盘上,只不过上层进行了某种“封装”或“抽象”,所以还是做了分类,目前iOS本地持久化缓存(Storage/Persistence)有以下几种形式:User…

vTESTstudio - VT System CAPL Functions - VT2004(续1)

成熟,就是某一个突如其来的时刻,把你的骄傲狠狠的踩到地上,任其开成花或者烂成泥。vtsStartStimulation - 启动激励输出功能:自动激励输出注意:在启动激励输出之前,一定要设置好输出模式Target:目标通道变量空间名称,例…

TLB内存页表 - LoongArch

TLB内存页表 - LoongArch 文章目录TLB内存页表 - LoongArch页表操作指令TLB相关寄存器页表格式CpuSetAttributesUEFI Memory attribute页表操作指令 LDDIR: 用于软件页表遍历过程中目录项的访问. LDPTE: 用于在软件页表遍历过程中页表项的访问. INVTLB: 用于无效TLB中的内容. …

Mybatis源码分析:Mybatis的数据存储对象

前言:SQLSession是对JDBC的封装 一:SQLSession和JDBC的对照说明 左边是我们的客户端程序,右边是我们的MySQL数据仓,或者叫MySQL实例 Mybatis是对JDBC的封装,将JDBC封装成了一个核心的SQLSession对象 JDBC当中的核心对…