Redis-数据结构

news/2024/11/8 20:53:09/

前言

​ 了解Redis,都大概知道Redis有5种基本数据类型:字符串(string)、列表(list)、哈希(hash)、集合(set)、有序集合(zset)、5.0中Stream数据类型。但是这些数据类型的底层都是按照对象结构与对应的编码组合而成。这也就是说有的底层数据结构可以是多个数据类型的原因。

介绍

首先看下图(6.0版本):

img

​ 从图可以清晰的看出,Redis的底层数据结构是由Redis对象的数据类型以及编码类型来确定的。那么可以从两个角度来进行分析:对象设计机制编码类型和底层数据结构

注意:

OBJ_ENCODING_ZIPMAP没有出现在图中,因为在redis2.6开始,它不再是任何数据类型的底层结构(虽然还有zipmap.c的代码); OBJ_ENCODING_LINKEDLIST也不支持了,相关代码也删除了。

redisObject对象

​ **在redis的命令中,用于对键进行处理的命令占了很大一部分,而对于键所保存的值的类型(键的类型),键能执行的命令又各不相同。**如: LPUSHLLEN 只能用于列表键, 而 SADDSRANDMEMBER 只能用于集合键, 等等; 另外一些命令, 比如 DELTTLTYPE, 可以用于任何类型的键。但是要正确实现这些命令, 必须为不同类型的键设置不同的处理方式: 比如说, 删除一个列表键和删除一个字符串键的操作过程就不太一样。

Redis 必须让每个键都带有类型信息, 使得程序可以检查键的类型, 并为它选择合适的处理方式。操作数据类型的命令除了要对键的类型进行检查之外, 还需要根据数据类型的不同编码进行多态处理.

为了解决以上问题, Redis 构建了自己的类型系统, 这个系统的主要功能包括:

  • redisObject 对象.
  • 基于 redisObject 对象的类型检查.
  • 基于 redisObject 对象的显式多态函数.
  • 对 redisObject 进行分配、共享和销毁的机制.

redisObject数据结构

​ redisObject 是 Redis 类型系统的核心, 数据库中的每个键、值, 以及 Redis 本身处理的参数, 都表示为这种数据类型.其中type、encoding和ptr是最重要的三个属性

/** Redis 对象*/
typedef struct redisObject {// 类型unsigned type:4;// 编码方式unsigned encoding:4;// LRU - 24位, 记录最末一次访问时间(相对于lru_clock); 或者 LFU(最少使用的数据:8位频率,16位访问时间)unsigned lru:LRU_BITS; // LRU_BITS: 24// 引用计数int refcount;// 指向底层数据结构实例void *ptr;
} robj;
  • type记录了对象所保存的值的类型

    /*
    * 对象类型
    */
    #define OBJ_STRING 0 // 字符串
    #define OBJ_LIST 1 // 列表
    #define OBJ_SET 2 // 集合
    #define OBJ_ZSET 3 // 有序集
    #define OBJ_HASH 4 // 哈希表
    
  • encoding记录了对象所保存的值的编码,它的值可能是以下常量中的一个:

    /*
    * 对象编码
    */
    #define OBJ_ENCODING_RAW 0     /* Raw representation */
    #define OBJ_ENCODING_INT 1     /* Encoded as integer */
    #define OBJ_ENCODING_HT 2      /* Encoded as hash table */
    #define OBJ_ENCODING_ZIPMAP 3  /* 注意:版本2.6后不再使用. */
    #define OBJ_ENCODING_LINKEDLIST 4 /* 注意:不再使用了,旧版本2.x中String的底层之一. */
    #define OBJ_ENCODING_ZIPLIST 5 /* Encoded as ziplist */
    #define OBJ_ENCODING_INTSET 6  /* Encoded as intset */
    #define OBJ_ENCODING_SKIPLIST 7  /* Encoded as skiplist */
    #define OBJ_ENCODING_EMBSTR 8  /* Embedded sds string encoding */
    #define OBJ_ENCODING_QUICKLIST 9 /* Encoded as linked list of ziplists */
    #define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
    
  • ptr是一个指针,指向实际保存值的数据结构,这个数据结构由type和encoding属性决定。举个例子, 如果一个redisObject 的type 属性为OBJ_LIST , encoding 属性为OBJ_ENCODING_QUICKLIST ,那么这个对象就是一个Redis 列表(List),它的值保存在一个QuickList的数据结构内,而ptr 指针就指向quicklist的对象。这种设计模式类同Java的策略模式一样。

  • lru属性: 记录了对象最后一次被命令程序访问的时间

空转时长:当前时间减去键的值对象的lru时间(对象最后一次被命令程序访问的时间),就是该键的空转时长。Object idletime命令可以打印出给定键的空转时长。

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

命令的类型检查和多态

当执行一个处理数据类型命令的时候,redis执行以下步骤

  • 根据给定的key,在数据库字典中查找和他相对应的redisObject,如果没找到,就返回NULL;
  • 检查redisObject的type属性和执行命令所需的类型是否相符,如果不相符,返回类型错误;
  • 根据redisObject的encoding属性所指定的编码,选择合适的操作函数来处理底层的数据结构;
  • 返回数据结构的操作结果作为命令的返回值。

对象共享

redis一般会把一些常见的值放到一个共享对象中,这样可使程序避免了重复分配的麻烦,也节约了一些CPU时间。

​ **Redis 内部维护[0-9999]的整数对象池。创建大量的整数类型redisObject 存在内存开销,每个redisObject内部结构至少占16字节,甚至超过了整数自身空间消耗。所以Redis内存维护一个[0-9999]的整数对象池,用于节约内存。 除了整数值对象,其他类型如list,hash,set,zset内部元素也可以使用整数对象池。**整数对象池在 Redis 中通过变量 REDIS_SHARED_INTEGERS 定义,不能通过配置修改。

​ Redis 将所有维护的共享对象都放在 sharedObjectsStruct 结构体中,Redis 会把一些常见的给客户端回复的字符串共享起来,以此来节省内存。例如:OK、-ERR、QUEUED等响应字符串。Redis 只对包含整数值的字符串、响应字符串包装成对象进行共享。

引用计数以及对象的消毁

redisObject中有refcount属性,是对象的引用计数,显然计数0那么就是可以回收。

  • 每个redisObject结构都带有一个refcount属性,指示这个对象被引用了多少次;
  • 当新创建一个对象时,它的refcount属性被设置为1;
  • 当对一个对象进行共享时,redis将这个对象的refcount加一;
  • 当使用完一个对象后,或者消除对一个对象的引用之后,程序将对象的refcount减一;
  • 当对象的refcount降至0 时,这个RedisObject结构,以及它引用的数据结构的内存都会被释放。

小结

redis是key-value结构,可以把value看作redisObject对象。它通过使用自己实现的对象机制(redisObject)来实现类型判断、命令多态和基于引用次数的垃圾回收;redis会预分配一些常用的数据对象,并通过共享这些对象来减少内存占用,和避免频繁的为小对象分配内存。

​ 而Redis的垃圾回收也只是针对共享对象。而共享对象是在共享字符串第一次使用的时候进行包装成共享对象,并添加引用次数。后续不再引用的时候,那么就会进行内存回收。

参考

Java全栈知识体系

底层数据结构详解

​ 通过Redis的对象机制可以大概知道Redis的底层数据结构有:

  • 简单动态字符串 - sds
  • 压缩列表 - ZipList
  • 快表 - QuickList
  • 字典/哈希表 - Dict
  • 整数集 - IntSet
  • 跳表 - ZSkipList

一、简单动态字符串 - sds

SDS这是一种用于存储二进制数据的一种结构, 具有动态扩容的特点。其实现位于src/sds.h与src/sds.c中,SDS分为sdshdrbuf、\0。

其中sdshdr是头部, buf是真实存储用户数据的地方,而\0是为了确定所谓的buf。SDS有五种不同的头部. 其中sdshdr5实际并未使用到. 所以实际上有四种不同的头部,

内部属性说明如下:

  • len 保存了SDS保存字符串的长度
  • buf[] 数组用来保存字符串的每个元素
  • alloc分别以uint8, uint16, uint32, uint64表示整个SDS, 除过头部与末尾的\0, 剩余的字节数.
  • flags 始终为一字节, 以低三位标示着头部的类型, 高5位未使用.

为什么要使用SDS?

**1)常数复杂度获取字符串长度。**C语言,只能通过遍历才能获取到字符串的长度,现在只需要获取属性就能知道字符串的长度。

**2)杜绝缓冲池溢出。**对于 SDS 数据类型,在进行字符修改的时候,会首先根据记录的 len 属性检查内存空间是否满足需求,如果不满足,会进行相应的空间扩展,然后在进行修改操作,所以不会出现缓冲区溢出。

3)减少修改字符串时的内存重新分配次数。

​ C语言由于不记录字符串的长度,所以如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,会造成内存泄露。而对于SDS,由于len属性和alloc属性的存在,对于修改字符串SDS实现了空间预分配惰性空间释放两种策略:

1、空间预分配:对字符串进行空间扩展的时候,扩展的内存比实际需要的多,这样可以减少连续执行字符串增长操作所需的内存重分配次数。

2、惰性空间释放:对字符串进行缩短操作时,程序不会立即把缩短之后后多余的字节进行清理,而是使用 alloc 属性将这些字节的数量记录下来,等待后续使用。

**4)二进制安全。**C语言的受限,不能确定准确的结束(比如图片、存储空字符串),可以通过长度来判断字符串是否结束。

5)兼容部分 C 字符串函数。虽然 SDS 是二进制安全的,但是一样遵从每个字符串都是以空字符串结尾的惯例,这样可以重用 C 语言库<string.h> 中的一部分函数。

小结

​ redis的字符串表示为sds, 它是Redis 底层所使用的字符串表示,它被用在几乎所有的Redis 模块中。SDS 除了保存数据库中的字符串值以外,SDS 还可以作为缓冲区(buffer):包括 AOF 模块中的AOF缓冲区以及客户端状态中的输入缓冲区

二、压缩列表 - ZipList

ziplist是为了提高存储效率而设计的一种特殊编码的双向链表。它可以存储字符串或者整数,存储整数时是采用整数的二进制而不是字符串形式存储。它能在O(1)的时间复杂度下完成list两端的push和pop操作。但是因为每次操作都需要重新分配ziplist的内存,所以实际复杂度和ziplist的内存使用量相关。

​ 整个ziplist在内存中的存储格式:zlbytes(4byte)、zltail(4byte)、zllen(2byte)、entry、zlend(1byte)。

  • zlbytes字段的类型是uint32_t, 这个字段中存储的是整个ziplist所占用的内存的字节数
  • zltail字段的类型是uint32_t, 它指的是ziplist中最后一个entry的偏移量. 用于快速定位最后一个entry, 以快速完成pop等操作
  • zllen字段的类型是uint16_t, 它指的是整个ziplit中entry的数量. 这个值只占2bytes(16位): 如果ziplist中entry的数目小于65535(2的16次方), 那么该字段中存储的就是实际entry的值. 若等于或超过65535, 那么该字段的值固定为65535, 但实际数量需要一个个entry的去遍历所有entry才能得到.
  • zlend是一个终止字节, 其值为全F, 即0xff. ziplist保证任何情况下, 一个entry的首字节都不会是255

​ entry的结构有两种结构:

第一种情况:一般结构 <prevlen> <encoding> <entry-data>

prevlen:前一个entry的大小;

encoding:不同的情况下值不同,用于表示当前entry的类型和长度;

entry-data:真是用于存储entry表示的数据;

第二种情况:在entry中存储的是int类型时,encoding和entry-data会合并在encoding中表示,此时没有entry-data字段;

redis中,在存储数据时,会先尝试将string转换成int存储,节省空间;

此时entry结构:`

ziplist是按照实际的内容大小存储,所以增加encoding字段,针对不同的encoding来细化存储大小。但是ziplist中每个data占据的内存不一样,所以为了解决遍历,需要增加记录上一个元素的length,所以增加了prelen字段。

ziplist的缺点

ziplist不预留内存空间, 并且在移除结点后, 也是立即缩容, 这代表每次写操作都会进行内存分配操作。如果扩容, 导致结点占用的内存增长, 并且超过254字节的话, 可能会导致链式反应: 其后一个结点的entry.prevlen需要从一字节扩容至五字节. 最坏情况下, 第一个结点的扩容, 会导致整个ziplist表中的后续所有结点的entry.prevlen字段扩容.

三、快表 - QuickList

quicklist这个结构是Redis在3.2版本后新加的, 之前的版本是list(即linkedlist), 用于String数据类型中。它是一种以ziplist为结点的双端链表结构. 宏观上, quicklist是一个链表, 微观上, 链表中的每个结点都是一个ziplist。

四、字典/哈希表 - Dict

Dict本质上就是哈希表,可以以此维度来进行理解。哈希表是由数组 table 组成,table 中每个元素都是指向 dict.h/dictEntry 结构,key 用来保存键,val 属性用来保存值,值可以是一个指针,也可以是uint64_t整数,也可以是int64_t整数。

而对于哈希冲突,采用的便是链地址法解决冲突,通过next这个指针可以将多个哈希值相同的键值对连接在一起,用来解决哈希冲突。

typedef struct dictht{//哈希表数组dictEntry **table;//哈希表大小unsigned long size;//哈希表大小掩码,用于计算索引值//总是等于 size-1unsigned long sizemask;//该哈希表已有节点的数量unsigned long used;}dicthttypedef struct dictEntry{//键void *key;//值union{void *val;uint64_tu64;int64_ts64;}v;//指向下一个哈希表节点,形成链表struct dictEntry *next;
}dictEntry

​ 当哈希表保存的键值对太多或者太少时,就要通过 rerehash(重新散列)来对哈希表进行相应的扩展或者收缩。

触发扩容的条件

1、服务器目前没有执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于1。

2、服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令,并且负载因子大于等于5。

ps:负载因子 = 哈希表已保存节点数量 / 哈希表大小。

五、整数集 - IntSet

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

typedef struct intset {uint32_t encoding;uint32_t length;int8_t contents[];
} intset;

encoding 表示编码方式,的取值有三个:INTSET_ENC_INT16, INTSET_ENC_INT32, INTSET_ENC_INT64

length 代表其中存储的整数的个数

contents 指向实际存储数值的连续内存区域, 就是一个数组;整数集合的每个元素都是 contents 数组的一个数组项(item),各个项在数组中按值得大小从小到大有序排序,且数组中不包含任何重复项。(虽然 intset 结构将 contents 属性声明为 int8_t 类型的数组,但实际上 contents 数组并不保存任何 int8_t 类型的值,contents 数组的真正类型取决于 encoding 属性的值)

整数集合的升级

当在一个int16类型的整数集合中插入一个int32类型的值,整个集合的所有元素都会转换成32类型,只会升级不会进行降级。 整个过程有三步:

  • 根据新元素的类型(比如int32),扩展整数集合底层数组的空间大小,并为新元素分配空间。
  • 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
  • 最后改变encoding的值,length+1。

六、跳表 - ZSkipList

前言

跳跃表结构在 Redis 中的运用场景只有一个,那就是作为有序列表 (Zset) 的使用。跳跃表的性能可以保证在查找,删除,添加等操作的时候在对数期望时间内完成,这个性能是可以和平衡树来相比较的,而且在实现方面比平衡树要优雅,这就是跳跃表的长处。跳跃表的缺点就是需要的存储空间比较大,属于利用空间来换取时间的数据结构。

结构

zskiplist的核心设计要点

  • 头节点不持有任何数据, 且其level[]的长度为32
  • 每个结点
    • ele字段,持有数据,是sds类型
    • score字段, 其标示着结点的得分, 结点之间凭借得分来判断先后顺序, 跳跃表中的结点按结点的得分升序排列.
    • backward指针, 这是原版跳跃表中所没有的. 该指针指向结点的前一个紧邻结点.
    • level字段, 用以记录所有结点(除过头节点外);每个结点中最多持有32个zskiplistLevel结构. 实际数量在结点创建时, 按幂次定律随机生成(不超过32). 每个zskiplistLevel中有两个字段
      • forward字段指向比自己得分高的某个结点(不一定是紧邻的), 并且, 若当前zskiplistLevel实例在level[]中的索引为X, 则其forward字段指向的结点, 其level[]字段的容量至少是X+1. 这也是上图中, 为什么forward指针总是画的水平的原因.
      • span字段代表forward字段指向的结点, 距离当前结点的距离. 紧邻的两个结点之间的距离定义为1.

小结

​ Redis的数据结构是兼容C语言的语法,毕竟底层都是C写的。所以所有的数据类型都会有长度说明。另外为了尽可能的优化内存使用,加入了编码说明,不同的数据、不同的编码、存储不一样,占用空间不一样。当然越复杂的结构,当然操作复杂度相对更高。

底层数据结构

redis对象与编码(底层结构)对应关系

字符串对象

字符串是Redis最基本的数据类型,不仅所有key都是字符串类型,其它几种数据类型构成的元素也是字符串。注意字符串的长度不能超过512M。字符串对象的编码可以是int,raw或者embstr,int 编码是用来保存整数值,而embstr是用来保存短字符串,raw编码是用来保存长字符串。

  • int 编码:保存的是可以用 long 类型表示的整数值。
  • embstr 编码:保存长度小于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。
  • raw 编码:保存长度大于44字节的字符串(redis3.2版本之前是39字节,之后是44字节)。

列表对象

list 列表,它是简单的字符串列表,按照插入顺序排序,你可以添加一个元素到列表的头部(左边)或者尾部(右边),它的底层实际上是个链表结构。

列表对象的编码是quicklist。 (之前版本中有linked和ziplist这两种编码。进一步的, 目前Redis定义的10个对象编码方式宏名中, 有两个被完全闲置了, 分别是: OBJ_ENCODING_ZIPMAPOBJ_ENCODING_LINKEDLIST。 从Redis的演进历史上来看, 前者是后续可能会得到支持的编码值(代码还在), 后者则应该是被彻底淘汰了)

哈希对象

哈希对象的键是一个字符串类型,值是一个键值对集合。哈希对象的编码可以是 ziplist 或者 hashtable;对应的底层实现有两种, 一种是ziplist, 一种是dict。另外dict作为哈希对象的底层数据结构时, 键与值均是以sds的形式存储的。

压缩列表是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,相对于字典数据结构,压缩列表用于元素个数少、元素长度小的场景。其优势在于集中存储,节省空间。

  • 编码转换

​ 和上面列表对象使用 ziplist 编码一样,当同时满足下面两个条件时,使用ziplist(压缩列表)编码:

1、列表保存元素个数小于512个

2、每个元素长度小于64字节

​ 不能满足这两个条件的时候使用 hashtable 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

集合对象

​ 集合对象 set 是 string 类型(整数也会转换成string类型进行存储)的无序集合。注意集合和列表的区别:集合中的元素是无序的,因此不能通过索引来操作元素;集合中的元素不能有重复。

  • 编码转换

当集合同时满足以下两个条件时,使用 intset 编码:

1、集合对象中所有元素都是整数

2、集合对象所有元素数量不超过512

不能满足这两个条件的就使用 hashtable 编码。第二个条件可以通过配置文件的 set-max-intset-entries 进行配置。

有序集合对象

有序集合对象是有序的。与列表使用索引下标作为排序依据不同,有序集合为每个元素设置一个分数(score)作为排序依据。有序集合的底层实现依然有两种, 一种是使用ziplist作为底层实现, 另外一种比较特殊, 底层使用了两种数据结构: dict与skiplist. 前者对应的编码值宏为ZIPLIST, 后者对应的编码值宏为SKIPLIST。

  • 编码转换

当有序集合对象同时满足以下两个条件时,对象使用 ziplist 编码:

1、保存的元素数量小于128;

2、保存的所有元素长度都小于64字节。

不能满足上面两个条件的使用 skiplist 编码。以上两个条件也可以通过Redis配置文件zset-max-ziplist-entries 选项和 zset-max-ziplist-value 进行修改。

转自


http://www.ppmy.cn/news/83216.html

相关文章

代码随想录补打卡 121买卖股票的最佳时机 122 买卖股票的最佳时机 二123买卖股票的最佳时机 三

代码如下 func maxProfit(prices []int) int { //建立一个二维数组 dp[i][0]表示持有股票的最大金钱 dp[i][1] 表示不持有股票的最大金钱 dp : make([][]int,len(prices)) for i,_ : range dp { dp[i] make([]int,2) } dp[0][0] -prices[0] // 第0天持有股票&#xff0c;…

熟练掌握这5招,让Pandas DataFrame列随你调整

熟练运用Pandas进行数据处理和分析的你&#xff0c;是否遇到过DataFrame列顺序排列不顺的情况? 今天教你5种灵活方法&#xff0c;轻松调整Pandas DataFrame的列顺序&#xff0c;让数据处理更得心应手。 1. 使用loc索引器 可以传入一个列序列表给loc索引器来重新排列列顺序。例…

换个花样玩C++(14) 全方位认识C++的左值,右值,左值引用,右值引用,亡值

早期学习C语言的时候,认为可以被修改的左值是放在左边的,右边的则通常放置右值,后来转C++之后,随着C++不断地完善更新,发现有时候越来越捉摸不透C++了,右值已经与它最初的概念完全不一样了,越来越丰富。 这篇文章我尽可能用一些浅显易懂的文字和简要的代码示例来解释下左…

为何AI无法完全理解人类情感?GPT-4能否理解人类的情绪?

在科幻小说和电影里&#xff0c;我们经常看到超级AI人工智能机器人可以理解、感知甚至模拟人类的情感&#xff0c;但在现实世界中&#xff0c;我们距离这个目标还有一段相当长的距离&#xff0c;即使是强大的GPT-4甚至未来的GPT-5。过高夸大AI的体验和性能&#xff0c;往往并不…

Windows 11 22H2 中文版、英文版 (x64、ARM64) 下载 (updated May 2023)

Windows 11 22H2 中文版、英文版 (x64、ARM64) 下载 (updated May 2023) Windows 11, version 22H2 官方原版&#xff0c;2023 年 5 月 更新 请访问原文链接&#xff1a;https://sysin.org/blog/windows-11/&#xff0c;查看最新版。原创作品&#xff0c;转载请保留出处。 作…

android和flutter的混合项目切换系统语言时app重启启动问题

在 Android 项目中&#xff0c;使用 SharedPreferences 将当前设置的语言保存到本地。 在 Flutter 项目中&#xff0c;使用 flutter_localizations 库实现多语言支持。这个库支持自动检测当前系统的语言&#xff0c;并加载相应的翻译文件。 在 Android 的 Activity 中监听系统语…

设计模式 (四) 行为型设计模式系列

目录 1.职责链模式 2.命令模式 3.解释器模式 5.中介者模式 6.备忘录模式 7.观察者模式 8.状态模式 9.策略模式 10.模板方法模式 1.职责链模式 职责链模式将请求的发送者和接收者解耦&#xff0c;从而允许多个对象都有机会处理请求。这种模式通常用于处理请求的分发、…

吉时利 Keithley 2700数据采集器技术参数

概述&#xff1a; 每个 2700 系列系统均将精密测量、开关和控件集于一个紧凑集成的机箱中&#xff0c;适用于机架安装或台式应用。虽然所有三个系统的核心功能和编程是相同的&#xff0c;但各个主机都具有独特的功能。例如&#xff0c;2701 型具有 10/100BaseTX 以太网接口&am…