首页 > 数据库 >Redis对象系统

Redis对象系统

时间:2024-03-14 09:23:32浏览次数:19  
标签:对象 Redis 系统 内存 使用 字符串 编码方式

我们之前介绍了Redis的各种基本数据类型.
比如SDS字符串,链表,压缩链表,字典,跳跃表,整数集合等.
但是Redis并不是直接使用它们来构建一个数据库的,而是又包装了一层,使用它们构建了对象系统,然后使用这些对象系统来建立数据库系统.
Redis中主要的对象类型有:
字符串对象
列表对象
哈希对象
集合对象
有序集合对象

相对于上一篇文章写的那些基础数据类型,这些对象类型是不是看起来更亲近了?这些是我们在使用中能够直观感受的数据类型.
文中有编码方式与基础数据类型俩种说法,要注意一个编码方式可能不止使用一个基本数据类型来实现,就比如有序集合对象的skiplist编码方式就使用了dictzskiplist俩种基本数据类型来实现

并且Redis在这个对象系统的基础之上还实现了一些额外的功能:

  • 类型检查和命令多态: 一个对象类型在不同的使用场景下可以使用不同的编码方式,十分灵活,而这对于客户端来说是透明的,无感知的.当执行命令之前会.并且对于客户端的相同命令,服务端可能会因为其对象的使用的编码方式的不同调用不同的函数.
  • 内存自动回收: Redis实现了基于引用计数的对象回收系统,当服务端内一个对象没有人使用之后,会进行内存释放,避免空间的浪费.
  • 共享对象: 我们上面说了有引用计数,那么同样,在某些情况下,Redis中的多个键可以共享一个对象来节约内存
  • 记录访问时间: Redis会对对象的上一次的访问时间进行记录,如果一个对象长时间没有被使用,如果服务端启用了maxmemory,在回收的时候会优先回收这些键.

我们接下来就来介绍一下Redis的对象系统.

对象的类型与编码

我们前面说过,Redis有不同的对象类型,而同一个对象类型在不同的使用场景下能够使用不同的编码方式.
这个时候我们可以来看一下Redis对象的定义:


typedef struct redisObject{
    //类型,对应我们客户端能够直观感受到的对象类型
    unsigned type:4;
    //编码,对应对象所使用的编码方式
    unsigned encoding:4;
    //指向基本数据类型的对象的指针
    void* ptr;
    //引用计数
    int refCount;
    //最后一次被访问的时间
    unsigned lru:22
}

//属性名:整数 (意思是该字段占用的bit位数,该数要小于该字段数据类型占用的位数,就比如int amount:n 这个n的值就需要小于32)

type属性,即一个对象的类型的取值有:

类型常量 描述
REDIS_STRING 字符串对象
REDIS_LIST 列表对象
REDIS_HASH 哈希对象
REDIS_SET 集合对象
REDIS_ZSET 有序集合对象

encoding属性,即一个对象采用的编码方式有:

编码常量 描述
REDIS_ENCODING_INT long类型的整数
REDIS_ENCODING_EMBSTR embstr编码的简单动态字符串
REDIS_ENCODING_RAW 简单动态字符串
REDIS_ENCODING_HT 字典
REDIS_ENCODING_LINKEDLIST 链表
REDIS_ENCODING_ZIPLIST 压缩链表
REDIS_ENCODING_INTSET 整数集合
REDIS_ENCODING_SKIPLIST 跳跃表

一个对象类型至少拥有俩个基础编码方式的实现,其对应关系有:

那么我们如果想要知道某个键的值对应的数据类型该如何获取呢?

  • 可以使用OBJECT ENCODING key命令来获取.

对象类型与对应的编码

我们前面说过一个对象类型至少有俩个编码方式的实现,在不同场景下会使用不同的编码方式.
那么在什么场景下会使用什么样的编码方式呢?在何种情况下对象会进行编码方式的切换呢?这就是我们接下来要讨论的问题

字符串对象

字符串对象的实现有三种: int,embrstr,raw
当存入的字符串值是整数,并且可以使用long来表示,字符串对象就会使用int
如果存入的字符串值长度大于32,并且可以用一个字符串来表示,那么就会使用raw来存储
如果存入的字符串值长度小于32,并且可以用一个字符串来表示,那么就会使用embstr类型来进行存储.

那么这就带来了一个问题,embstr之前我们介绍基本数据类型的时候并没有说过啊?这又是哪来的?

  • embstrraw同样都采用redisObject结构和sdshdr结构来作为字符串的实现,而embstr是专门对于存储短字符串进行的优化,区别就在于:
    当创建存储embstr的对象时,服务端只会调用一次内存分配函数来分配一个连续的内存空间,也就是redisObject结构体对象与sdshdr结构体对象在内存中相邻,这样我们在回收它的内存空间的时候也只需要调用一次内存释放函数
    也因为embstr在内存中的位置是一片连续的内存空间,也就更好能利用缓存的优势.
    而raw会调用俩次内存分配函数来分别分配空间,回收空间的时候也会调用俩次内存释放函数.

在Redis中的浮点数实际上也是采用embstr和raw来存储的,程序会先将浮点数转换成字符串再存入数据库,然后再取出使用的时候再将字符串转换成浮点数.

那么在何种情况下,对象的编码方式会进行转换呢?

  • 如果存储的值可以用long来表示,则会使用int编码方式来存储,如果对该value更新为整数无法表示(需要使用字符串来表示)的操作,又或是只有对字符串才能做的操作,比如append,就会先将该值转换成raw类型,然后再进行操作.
    如果embstr编码方式的值被更改,那么就会创建一个raw编码方式的对象,再进行操作.(因为实际上embstr的函数库是没有编写任何对字符串的修改操作的)

列表对象

列表对象的编码方式有ziplist,linkedlist

前面在介绍压缩链表ziplist的时候可能有人会想:压缩链表的结构不是也挺复杂的,为什么说它能节省了空间呢?

  • 因为之前在讲解linkedlist链表的时候,只说了listNode结构体使用value属性来存储节点的值,并没有说节点是如何存储的,但是实际上,像是一个存储字符串列表的linkedlist链表,其节点的value是指向一个字符串的redisObject的:

    我们可以想象一下:最外层redisObject的ptr指针指向这个adlist.h/list链表,这个链表中又有多个listNode链表节点,每一个节点中又包括一个redisObject,每一个redisObject又指向一个int或是embstr又或是raw字符串,在这其中每一层结构体都有自己的属性需要存储,这样花费空间是不是就多了很多?
  • 但如果是使用ziplist,其结构就会是这样:

    看起来是不是比linkedlist结构节省许多空间了.

那么在何种情况下,对象的编码方式会进行转换呢?

  • 当列表元素存储的字符串长度都小于64字节,并且列表的元素数量小于512,就会采用ziplist,否则采用linkedlist,当然这些数值都是可以进行配置更改的.

哈希对象

哈希对象的编码方式有ziplist,hashtable

当使用ziplist作为实现的时候,哈希的键与值紧紧的挨在一起,当要对该哈希对象添加键值对时,会先将该键值对的键添加到队尾,然后再将该键值对的值也给添加到队尾.
我们可以想像到,当哈希对象的编码方式为ziplist时,其redisObject的ptr指针指向的对象会是这样:

当使用hashtable作为实现的时,使用dict字典来作为哈希对象的实现.
这就和我们印象里的哈希对象使用一样了,当要对hashtable编码方式的哈希对象进行添加键值对时,会将该键值对的键的字符串对象作为键,该键值对的值的字符串对象作为键来存入到dict之中:

那么在何种情况下,对象的编码方式会进行转换呢?

  • 当哈希对象存储的键和值所对应的字符串长度都小于64字节,并且哈希对象中的元素数量小于512,就会采用ziplist,否则采用hashtable,当然这些数值都是可以进行配置更改的.

集合对象

集合对象的编码方式有intset,hashtable

当使用intset作为集合对象的编码方式时,集合对象中的整数都会被存放到整数集合中,就像这样:

当使用hashtable作为集合对象的编码方式时,会将集合中的元素作为hashtable中的键来存储,而其键对应的值则设置为NULL:

那么在何种情况下,对象的编码方式会进行转换呢?

  • 当集合对象中的元素都是整数并且集合中的元素个数小于512的时候就会采用intset,否则采用hashtable

有序集合

有序集合的编码方式有ziplist,skiplist

当使用ziplist作为有序集合对象的编码方式的时候,程序会使用俩个紧挨着的位置存放有序集合的member(成员)和score(分值).当要在有序集合中添加一个新的成员时,会根据这个成员的分值的大小寻找到对应的位置,使用俩个位置来存放这个成员与其分值.

当使用skiplist作为有序集合的编码方式的时候,这个编码方式的实现使用zset结构体:

typeof struct zset{
    zskiplist *zsl;
    dict *dict; 
}

我们可以看到zset结构体是由一个跳跃表和一个字典组成的
其中zsl中存放了以分值排序从小到大的成员.skiplist编码方式使用zskiplist的函数来实现ZRANGE,ZRANK等命令
而dict中存放了键为成员,值为分值的键值对.这样ZSCORE命令根据某个成员查询对应的分值就可以只使用O(1)的时间复杂度了.

看着是不是觉得使用俩个结构体更加复杂了?会不会更浪费内存空间?
但其实zskiplist和dict中指向成员和分值的指针都是指向相同的对象,并不会重复创建对象,只额外消耗了存储指针的内存空间,所以问题不大.
并且通过使用使用zskiplist能够便捷的实现ZRANGE,ZRANK等命令,不需要再花费至少O(NlogN)的时间复杂度和额外的内存空间O(N)来暂时存放排序后的成员,而使用dict又可以让获取成员分值的时间复杂度降到O(1).我们可以肯定的说这是利大于弊的.

那么在何种情况下,对象的编码方式会进行转换呢?

  • 当有序集合对象中的成员长度都小于64字节并且序集合对象中的元素个数小于128的时候就会采用ziplist,否则采用skiplist

类型检查和命令多态

类型检查:Redis有许多命令键对应的值对象类型是有要求的,比如APPEND,SET,STRLEN这些命令就要求值对象的类型是字符串.Redis在执行这些有值对象类型限制的命令时,会先对对应键的值对象type所记录的类型进行检查,如果不符合则向客户端抛出错误.
这是通过redisObject结构体的type字段来实现的.

命令多态:为了实现在不同场景下可以使用不同的编码方式来进行优化,在Redis中对象的类型会有不同编码方式的实现,一个相同的命令,如果对象的编码方式不同,可能会选择使用不同的函数来执行.
比如LLEN命令,获取列表的长度,如果该值对象的编码方式是ziplist,则会返回压缩列表的长度,如果是linkedlist,则会返回链表的节点数量,这也就是多态,会根据实际编码方式选择不同的实现.

内存回收.

内存回收: 我们知道c和c++都是不具备内存回收功能的语言,需要用户来手动进行回收内存空间,但是Redis实现了内存回收.
Redis中基于引用计数方式实现了内存回收,这是通过在redisObject结构体中refCount属性来记录该对象的引用次数来实现的,程序根据一个对象的refCount属性来判断这个对象所占用的内存空间是否应该被回收.

当一个对象刚创建的时候,这个对象的refCount字段的属性为1,每当有一个新的程序引用它的时候,它的refCount字段就会增加1
与此相反,每当有一个程序不再引用它的时候,他的refCount就会减1
当这个对象的refCount为0的时候,redis就会去回收这个对象的内存空间

我们可以通过OBJECT REFCOUNT key命令来查看该值对象被引用的次数

对象共享

我们上面也说了,Redis通过引用计数来进行内存回收,这也就说明一个对象是可以被多个程序来引用的,这样服务端就不需要去重复的创建一个相同的对象,也就能够节省空间.
Redis在启动的时候就会初始化0~9999的字符串对象,用于服务端共享.

要注意的是Redis只对整数值做对象共享优化,这主要是因为要进行共享前,我们肯定要判断新值是否存在于共享对象池中,如果是一个整数字符串对象,比较俩个对象是否相同就只需要O(1)的时间复杂度,但是是字符串就会是O(N),如果是一个列表的对象,就会是O(N²),如果要进行整数字符串以外的对象共享对于CPU时间是不友好的.

记录对象的空转时间

有时候我们进行Redis内存回收的时候,希望那些最近没有用到的对象占用的内存空间优先被回收.
我们就可以通过redisObject中记录的lru字段来判断哪些对象的空转时间最长,来优先回收这些对象.
我们可以使用OBJECT IDLETIME key命令来查看这个值对象的空转时间.

服务器需要打开maxmemory选项,并且设置内存回收策略为volatile-lru或是allkeys-lru,在占用内存超过maxmemory值之后,就会优先回收这些空转时间长的对象了.

标签:对象,Redis,系统,内存,使用,字符串,编码方式
From: https://www.cnblogs.com/youjunhui/p/18065666

相关文章

  • linux系统报错AER PCIe Bus Error
    1、报错信息pcieport0000:00:1c.7:AER:PCIeBusError:severity=Corrected,type=PhysicalLayer,(ReceiverID)device[8086:a33f]errorstatus/mask=00000001/00002000[0[RxErr2、修改grub文件//备份grubsudocp/etc/default/grub/etc/default/grub.bak//随便......
  • 在 Amazon Bedrock 上使用 Anthropic Claude 系统 Prompt
    系统prompt是定义生成式AI模型对用户输入的响应策略的一种好方法。这篇博文将介绍什么是系统prompt,以及如何在基于AnthropicClaude2.x和3的应用中使用系统prompt。亚马逊云科技开发者社区为开发者们提供全球的开发技术资源。这里有技术文档、开发案例、技术专栏、培......
  • 基于SSM的协同过滤算法的电影推荐系统(有报告)。Javaee项目。ssm项目。
    演示视频:基于SSM的协同过滤算法的电影推荐系统(有报告)。Javaee项目。ssm项目。项目介绍:采用M(model)V(view)C(controller)三层体系结构,通过Spring+SpringMvc+Mybatis+Vue+Layui+Elementui+Maven来实现。MySQL数据库作为系统数据储存平台,实现了基于B/S结构的Web系统。报......
  • 一致性哈希算法及其在分布式系统中的应用
    摘要本文将会从实际应用场景出发,介绍一致性哈希算法(ConsistentHashing)及其在分布式系统中的应用。首先本文会描述一个在日常开发中经常会遇到的问题场景,借此介绍一致性哈希算法以及这个算法如何解决此问题;接下来会对这个算法进行相对详细的描述,并讨论一些如虚拟节点等与此算......
  • RedisCluster集群中的插槽为什么是16384个?
    RedisCluster集群中的插槽为什么是16384个?CRC16的算法原理。1.根据CRC16的标准选择初值CRCIn的值2.将数据的第一个字节与CRCIn高8位异或3.判断最高位,若该位为0左移一位,若为1左移一位再与多项式Hex码异或4.重复3至9位全部移位计算结束5.重复将所有输入数据操作完成以上步骤......
  • 【操作系统】执行系统调用后发生了什么?
    执行系统调用后发生了什么?什么是系统调用?系统调用是受控的内核入口,借助于这一机制,进程可以请求内核以自己的名字去执行某些动作。以应用程序编程接口(API)的形式,内核提供了一系列服务供程序访问。包括创建进程、执行I/O,以及为进程间通信创建管道等。执行系统调用后发生的事件......
  • 世界银行使用.NET 7开发的免费电子问卷制作系统Survey Solution
    SurveySolution(下文简称SS)是世界银行数据部开发的一套免费电子问卷制作系统,官网地址为:https://mysurvey.solutions/,github地址:https://github.com/surveysolutions/该系统具有以下几个主要特点:通过内置模版可以轻松地制作一系列传统问卷题型,同时还可以实现层级结构......
  • 数字控制系统Simulink仿真建模(1)(仿真步长和中断触发的设置)
    仿真步长的设置 对于数字控制系统而言,在Simulink仿真环境中,总的来说有三个步长需要考虑。首先由于数字控制系统是离散系统,因此需要在仿真模型的模型设置中将求解器类型设置为固定步长,求解器设置为离散,固定步长大小为整个模型的最小执行步长,即在该模型中的模块将默认按照此步......
  • 基于微信小程序的场地预约系统设计与实现(源码+lw+部署文档+讲解等)
    文章目录前言项目运行截图技术框架后端采用SpringBoot框架前端框架Vue可行性分析系统测试系统测试的目的系统功能测试数据库表设计代码参考数据库脚本为什么选择我?获取源码前言......
  • 大一下计算系统基础笔记
    大一下计算系统基础笔记W21.补码计算溢出的判断:a的补码+b的补码=(a+b)的补码最高位和次高位只有一个进位的时候,才有溢出,其余情况没有溢出,结果都正确最简单的理解方式:正数+正数,负数+负数,如果符号变了就溢出了正+负永远不溢出从原理上来看,计算机用补码表示数字,只有最高位进位没......