当前位置: 首页 > news >正文

16、Redis底层数据结构

目录

⼀ 、整体理解Redis底层数据结构

1、Redis数据在底层是什么样的?

2、Redis常见数据类型的底层数据结构总结

⼆ 、String数据结构详解

1 、string数据是如何存储的?

2 、string类型对应的int,emb str,raw有什么区别?

(1)int类型

(2)embstr类型

(3)raw类型

3 、string底层数据结构总结

三、HASH类型数据结构详解

1 、hash数据是如何存储的

2、hash底层数据结构详解

(1)hashtable

(2)list pack

3 、hash底层数据结构总结

四、List类型数据结构详解

1 、list数据是如何存储的

2、list底层数据结构详解

3、quicklist简介

4、list底层数据结构总结

五、SET类型数据结构详解

1、set数据是如何存储的

2、set底层数据结构详解

六、ZSET类型数据结构详解

1、zset数据是如何存储的

2、zset底层数据结构详解

3、zset底层数据结构总结

七、Redis课程总结


这一章节我们将深入理解Redis底层数据结构,也就是尝试真正去了解我们指定的set k1 v1这样的指令,是怎么执行的,数据是怎么保存的。

开始之前,做两个简单声明:

第一:作为Java程序员,我们研究Redis底层结构的目的,只有一个:面试!也就是体现你对Redis的理解深度,而并不是要你去写一个Redis。因此,我们接下来主要分析常用的几种数据类型的底层结构,中间必然会涉及到一些Redis底层的C源码。对于这些源码,我只抽取其中部分精华,用做知识点的佐证。如果之间有逻辑断层,或者你想要了解一些其他的数据类型,可以自行看源码补充。

第二:Redis的底层数据结构其实是经常变化的,不光Redis6到Redis7这样的大版本,就算同样大版本下的不同小版本,底层结构也是经常有变化的。对于讲到的每种数据结构,我会尽量在Redis源码中进行验证。如果没有说明,Redis的版本是目前最新的7.2.5。

 、整体理解Redis底层数据结构

1、Redis数据在底层是什么样的?

在应用层面,我们熟悉Redis有多种不同的数据类型,比如string,hash,list,set,zset等。但是这些数据在Redis的底层是什么样子呢?实际上Redis提供了一个指令OBJECT可以用来查看数据的底层类型。

127.0.0.1:6379> OBJECT HELP
1) OBJECT <subcommand> [<arg> [value] [opt] ...]. Subcommands are:
2) ENCODING <key>
3)     Return the kind of internal representation used in order to store the value
4)     associated with a <key>.
5) FREQ <key>
6)     Return the access frequency index of the <key>. The returned integer is
7)     proportional to the logarithm of the recent access frequency of the key.
8) IDLETIME <key>
9)     Return the idle time of the <key>, that is the approximated number of
10)     seconds elapsed since the last access to the key.
11) REFCOUNT <key>
12)     Return the number of references of the value associated with the specified
13)     <key>.
14) HELP
15)     Print this help.127.0.0.1:6379> set k1 v1
OK
127.0.0.1:6379> OBJECT ENCODING k1
"embstr"

可以看到, k1 v1这个<k,v>键值对 ,他在底层的数据类型就是 embstr Redis在底层,  实是这样描述这些数据类型的。

< server.h 880>

/* Objects encoding. Some kind of objects like Strings and Hashes can be
* internally represented in multiple ways. The 'encoding ' field of the object
* is set to one of this fields for this object. */
#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  /* No longer used: old hash encoding. */
#define OBJ_ENCODING_LINKEDLIST 4 /* No longer used: old list encoding. */
#define OBJ_ENCODING_ZIPLIST 5 /* No longer used: old list/hash/zset encoding. */
#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 listpacks */
#define OBJ_ENCODING_STREAM 10 /* Encoded as a radix tree of listpacks */
#define OBJ_ENCODING_LISTPACK 11 /* Encoded as a listpack */

 这⾥也能看到有些类型已经不再使⽤了 。⽐如ZIPLIST 。如果你看过—些以前的Redis 的⽂章, 就会知道, ZIPLIST是在Redis6中经常使⽤的—个重要的数据类型 。但是现在已经不再使⽤了 。在Redis7中, 基本已经使⽤list pack替代了zip list

然后, 在上⾯的注释中还可以看到 。这些编码⽅式都是使⽤在Objectencoding字段⾥  。这个Object是什么东东呢?

<server.h 900⾏>struct redisObject {
unsigned type:4;
unsigned encoding:4;
unsigned l ru:LRU_BITS; /* LRU time (relative to global l ru_clock) or* LFU data (least significant 8 bits frequency* and most significant 16 bits access time). */
int refcount;
void *ptr;
};

Redis是一个<k,v>型的数据库,其中key通常都是string类型的字符串对象,而value在底层就统一是redisObject对象。

而这个redisObject结构,实际上就是Redis内部抽象出来的一个封装所有底层数据结构的统一对象。这就类似于Java的面向对象的设计方式。

这里面几个核心字段意义如下:

  • type:Redis的上层数据类型。比如string,hash,set等,可以使用指令type key查看。
  • encoding: Redis内部的数据类型。
  • lru:当内存超限时会采用LRU算法清除内存中的对象。关于LRU与LFU,在redis.conf中有描述
# LRU means Least Recently Used
# LFU means Least Frequently Used
  • refcount:表示对象的引用次数。可以使用OBJECT REFCOUNT key 指令查看。
  • *ptr:这是一个指针,指向真正底层的数据结构。encoding只是一个类型描述。实际数据是保存在ptr指向的具体结构里。

2、Redis常见数据类型的底层数据结构总结

我们已经知道了Redis有上层的应用类型,也有底层的数据结构。那么这些上层数据类型和底层数据结构是怎么对应的呢?

127.0.0.1:6379> set k1 v1 OK
127.0.0.1:6379> type k1 string
127.0.0.1:6379> object encoding k1
"embstr"

这就是—种对应关系 。也就是说,在应⽤层⾯,我们操作的是string这样的数据类型 ,但是 Redis在底层, 操作的是embstr这样—种数据结构 。但是, 这些上层的数据类型和底层的数 据结构之间, 是不是就是简单的一一对应的关系呢?

127.0.0.1:6379> set k2 1 OK
127.0.0.1:6379> type k2 string
127.0.0.1:6379> object encoding k2
"int"
127.0.0.1:6379> set k3
aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa
OK
127.0.0.1:6379> type k3
string0
127.0.0.1:6379> OBJECT ENCODING k3
"raw"

从这⾥能够看到, 每—种上层数据类型对应底层多种不同的数据结构, 也就是说, 同样的 —个数据类型, Redis底层的处理⽅式是不同的。

 Redis提供了—个指令, 可以直接调试某—个key的结构信息 。但是这种⽅式默认是关闭的。

127.0.0.1:6379> DEBUG Object k1
(error) ERR DEBUG command not allowed. If the enable-debug-command option is set to "local", you can run it from a local connection, otherwise you need to set this option in the configuration file, and then restart the server.

按照要求, 修改配置⽂件, 重启Redis服务后, 就可以看到每—个key的内部结构 

127.0.0.1:6379> DEBUG object k1
Value at:0x7f0e36264c80 refcount:1 encoding:embstr serializedlength:3
l ru:7607589 l ru_seconds_idle:23

现在搞明⽩ encoding是什么了之后, 问题就到了下—步, 这个ptr指针到底指向了哪些数据 结构呢?

下⾯直接列出了Redis中上层数据类型和底层真正存储数据的数据结构的对应关系。

Redis版本

string

set

zset

list

hash

Redis 6

SDS(动态字符串)

intset+hashtable

skiplist+ziplist

quicklist+ziplist

hashtable+ziplist

Redis 7

SDS

intset+listpack+hashtable

skiplist+listpack

quicklist+listpack

hashtable+list

这个列表⾥的这些数据结构, 如果不理解, 先直接记住 。 这是Redis—个⽐较⾼频的⾯试题 (⾼级职位) 。⾄于具体的细节, 后⾯会慢慢分析。

另外, 其他的数据类型, 包括—些扩展模块的数据类型, ⾯试中基本不太可能问得太深, ⾃⾏理解。

Redis6Redis7最⼤的区别就在于Redis7已经⽤list pack替代了zip list 。只不过为了保证兼容性, Redis7中并没有移除zip list的代码以及配置 list packzip list的区别也是—个⾼频的⾯试题, 后⾯会逐步介绍。

 String数据结构详解

从之前的简单实验中已经看到, string数据, 在底层对应了int ,embstr,raw种不同的数据结构 。他们到底是什么呢?下⾯分⼏个问题逐步深⼊ 

1 string数据是如何存储的?

先上结论,再验证。 string数据的类型,会根据value的类型不同,有以下几种处理方式

  • int : 如果value可以转换成一个long类型的数字,那么就用int保存value。只有整数才会使用int,如果是浮点数,Redis内部其实是先将浮点数转化成字符串,然后保存
  • embstr : 如果value是一个字符串类型,并且长度小于44字节的字符串,那么Redis就会用embstr保存。代表embstr的底层数据结构是SDS(Simple Dynamic String 简单动态字符串)
  • raw :如果value是一个字符串类型,并且长度大于44字节,就会用raw保存。

源码验证:

在客户端执行一个 set k1 v1 这样的指令,会进入<t_string.c>的setComand方法处理。

<t_string.c 295行>

这个tryObjectEncoding的方法实现,在object.c中

<object.c 614行>的*tryObjectEncodingEx方法。 关键部分如下:

1 、从这⾥可以看到, 对于数字⻓度超过20的⼤数字, Redis是不会⽤int保存的。

2 OBJ_SHARED_INTEGER = 1000 。对于1000以内的数字, 直接指向内存。

 <object.c 685行>

2 string类型对应的int,emb str,raw有什么区别?

(1)int类型

就是尽量在对应的robj中的ptr指向⼀个缓存数据对象。

(2)embstr类型

如果字符串类型⻓度⼩于44 就会创建—个embstr的对象 。这个创建的⽅法是这样的: 

<object.c 92>

embstr字⾯意思就是内嵌字符串  所谓内嵌的核⼼, 其实就是将新创建的SDS对象直接分配在对象⾃⼰的内存后⾯ 。这样内存读取效率明显更⾼ 

这⾥有—段介绍, SDS其实是—段不可修改的字符串 。这意味着如果使⽤APPEND 类的指令尝试修改—个key的值, 那么就算value的⻓度没有超过44,Redis也会使⽤ — 个新创建的raw类型, ⽽不再使⽤原来的SDS

这个SDS是什么呢?其实他就是Redis底层对于String的一种封装。

<sds.h 45行>

Redis根据字符串⻓度不同, 封装了多种不同的SDS结构 。通常 ,保存字符串,⽤—个buf[] 就够了 。但是Redis在这个数组的基础上, 封装成了SDS结构 。通过添加的这些参数, 可以更⽅便解析字符串。

例如, 如果⽤数组⽅式保存字符串, 那么读取完整字符串就只能遍历数组⾥的各个字节数据, 时间复杂度O(N) 。但是SDS中预先记录了len后, 就可以直接读取—定⻓度的字节, 间复杂度O(1) 效率更⾼ 。 另外, C语⾔中通常⽤字节数组保存字符串, 那么还需要定义    —个特殊的结束符\0表示这—个字符串结束 。但是在Redis中, value中就包含\0这样的 字符串, 就会产⽣歧义 。但是有SDS后, 直接读取完整字节, 也就不⽤管这些歧义了。

(3)raw类型

从之前分析可以看到, raw类型其实相当于是兜底的—种类型 。特殊的数字类型和⼩字符串 类型处理完了后, 就是raw类型了 raw类型的处理⽅式就是单独创建—个SDS 然后将robj的ptr指向这个SDS

3 string底层数据结构总结

对于string类型的—系列操作, Redis内部会根据⽤户给的不同键值使⽤不同的编码⽅式, ⾃适应地选择最优化的内部编码⽅式 。这些逻辑, 对于⽤户是完全隔离的。

对于string类型的数据, 如果value可以转换为数字, Redis底层就会使⽤int类型 。在

RedisObject中的ptr指针中 ,会直接复制为整数数据, 不再额外创建指针指向整数, 节省  了指针的空间开销 。并且, 如果数字⽐较⼩ ,⼩1000 将会直接使⽤预先创建的缓存对 象, 连创建对象的内存空间也节省了。

如果value是字符串且⻓度⼩于44字节, Redis底层就会使⽤embstr类型 embstr类型会调 ⽤内存分配函数, 分配—块连续的内存空间保存对应的SDS 。这样使⽤连续的内存空间,

不光可以提⾼数据的读取速度, ⽽且可以避免内存碎⽚ 

如果value是字符串类型 ,但是⼤于44字节, 那么RedisObjectSDS就会分开申请内存。 通过RedisObjectptr指针指向新创建的SDS

三、HASH类型数据结构详解

1 hash数据是如何存储的

还是先上结论, 再源码验证 hash类型的数据, 底层存储时, 有两种存储格式 hashtable list pack

127.0.0.1:6379> hset user:1 id 1 name roy (integer) 2
127.0.0.1:6379> type user:1 hash
127.0.0.1:6379> OBJECT ENCODING user:1 "listpack"
127.0.0.1:6379> config set hash-max-listpack-entries 3 OK
127.0.0.1:6379> config set hash-max-listpack-value 8 OK
127.0.0.1:6379> hset user:1 name royaaaaaaaaaaaaaaaa (integer) 0
127.0.0.1:6379> OBJECT ENCODING user:1 "hashtable"
127.0.0.1:6379> hset user:2 id 1 name roy score 100 age 18 (integer) 4
127.0.0.1:6379> OBJECT ENCODING user:2 "hashtable"

简单来说,就是hash型的数据,如果value里的数据比较少,就用listpack。如果数据比较多,就用hashtable。

如何判断value里的数据少,涉及到两个参数。hash-max-listpack-entries 限制value里键值对的个数(默认512),hash-max-listpack-value 限制value里值的数据大小(默认64字节)。

从这两个参数里可以看到,对于hash类型数据,大部分正常情况下,都是使用listpack。所以,对于hash类型数据,主要是要理解listpack是如何存储的。至于hashtable,正常基本用不上,面试也就很少会问。

但是hash类型的底层数据,只用ziplist和listpack,其实是很像的。Redis6里也有ziplist相关的这两个参数。

但是hash类型的底层数据, 只⽤ zip listlist pack 其实是很像的 Redis6⾥也有 zip list相关的这两个参数。 

2、hash底层数据结构详解

首先理解hash数据底层数据存储的基础结构

hash数据的value,是一系列的键值对。 这些<k,v>键值对底层封装成了一个dictEntry结构。然后,整个这些键值对,又会被封装成一个dict结构。这个dict结构就构成了hash的整个value。

(1)hashtable

对于hashtable,早期版本中会有一个专门的数据结构dictht,现在就是这个dict了。

<dict.h 84行>

 dictEntry的结构体定义在dict.c

 <dict.c 63>

然后,来看redis底层是如何执⾏⼀个hset key field1 value1 field2 value2 这样的指令的 Redis底层处理hset指令的⽅法在

<t_hash.c 606>

 

接下来这个hashTypeTryConversion⽅法就会尝试进⾏编码转换 。 这就验证了hash类型数 据根据那两个参数选择⽤list pack还是hashtable的。

接下来,到底什么是list pack?

(2)list pack

list packzip list的升级版, 所以,谈到list pack就不得不谈zip list zip list字⾯意义是压缩

列表 。怎么压缩呢?

zip list最⼤的特点, 就是他被设计成—种内存紧凑型的数据结构, 占⽤—块连续的内存空  间, 不仅可以利⽤CPU缓存, ⽽且会针对不同⻓度的数据, 进⾏响应的编码 。这种⽅法可 以及有效的节省内存开销。

redis6中, zip listRedis底层⾮常重要的—种数据结构, 不⽌⽀持hash 还⽀持list等其 他数据类型

zip list是由连续内存块组成的顺序性数据结构, 整个结构有点类似于数组 。可以在任意—端 进⾏push/pop操作, 时间复杂度都是O(1) 。整体结构如下:

这些entry就可以认为是保存hash类型的value当中的一个键值对。

然后,每一个entry结构又分为三个部分。

  • previous_entry_length:记录前一个节点的长度,占1个或者5个字节。如果前一个节点的长度小于254字节,则采用一个字节来保存长度值。如果前一个节点的长度大于等于254字节,则采用5个字节来保存这个长度值。第一个字节是0xfe,后面四个字节才是真实长度数据

为什么要这样?因为255已经用在了ziplist的最后一个zlend。

  • encoding:编码属性,记录content的数据类型。表明content是字符串还是整数,以及content的长度。
  • contents:负责保存节点的数据,可以是字符串或整数。

ziplist后面的list通常是指链表数据结构。而典型的双向链表是在每个节点上通过两个指针指向前和后的相邻节点。而ziplist这种数据结构,就不再保存指针,只保留长度。极致压缩内存空间。这也是关于ziplist紧凑的一种表现。

在这种结构下,对于一个ziplist,要找到对列的第一个元素和最后一个元素,都是比较容易的,可以通过头部的三个字段直接找到。但是,如果想要找到中间某一些元素(比如Redis 的list数据类型的LRANGE指令),那么就只能依次遍历(从前往后单向遍历)。所以,ziplist不太适合存储太多的元素。

然后,为什么要用listpack替换ziplist呢?

redis的作者antirez的github上提供了listpack的实现。里面有一个md文档介绍了listpack。文章地址: https://github.com/antirez/listpack/blob/master/listpack.md

listpack的整体结构跟ziplist是差不多的,只是做了一些小调整。最核心的原因是要解决ziplist的连锁更新问题。

下面介绍连锁更新问题,这个了解即可。

连锁更新问题的核心就是在enty的previous_entry_length记录方式。如果前一个节点的长度小于254字节,那么previous_entry_length只有1个字节。如果大于等于254字节,则previous_entry_length需要扩展到5个字节。

这时假设我们有这样一个ziplist,每个entry的长度都是在250~253字节之间,previous_entry_length都只要一个字节。

 这时, 如果将—个⻓度⼤于等于254字节的新节点加⼊到压缩列表的表头节点, 也就是e1头部。

这时, 因为e1previous_entry_length只有1个字节, ⽆法保存新节点的⻓度 ,此时就需要 扩充previous_entry_length5个字节 。这样e1的整体⻓度就会超过254字节 。⽽ e1—旦⻓ 度扩展, 意味着e2previous_entry_length也需要从1扩展5字节 。接下来, 后续每—个  entry都需要重新调整空间。

这种特殊情况下产⽣的连续多次空间扩展操作, 就称为连锁更新 。连锁更新造成的空间连 续变动, 是⾮常不安全的, 同时效率也是⾮常低的 。正是因为连锁更新问题, 才造成

Redis7中使⽤新的list pack结构替代zip lists

list pack的整体结构如下:

3 hash底层数据结构总结

最后, 对于hash类型的底层数据结构, 做—个总结:

1 hash底层更多的是使⽤list pack来存储value

2 、如果hash对象保存的键值对超过512个, 或者所有键值对的字符串⻓度超过64字节,  层的数据结构就会由list pack升级成为hashtable

3 、对于同—个hash数据, list pack结构可以升级为hashtable结构 ,但是hashtable结构不 会降级成为list pack

四、List类型数据结构详解

1 list数据是如何存储的

⽼规矩, 先上结论, 再验证 list类型的数据, Redis中还是以list pack+quicklist为基础 保存的。

127.0.0.1:6379> lpush l1 a1 (integer) 1
127.0.0.1:6379> rpush l1 a2 (integer) 2
127.0.0.1:6379> type l1 list
127.0.0.1:6379> OBJECT ENCODING l1 "listpack"

这⾥看到, list类型的数据, 通常是以list pack结构来保存的 。但是, 如果调整—下参数配 置, 就会有另外—种结果

127.0.0.1:6379> config set list-max-listpack-size 2 OK
127.0.0.1:6379> lpush l3 a1 a2 a3 (integer) 3
127.0.0.1:6379> OBJECT ENCODING l3 "quicklist"

关于list-max-list pack-size参数, redis.conf⽂件中有更详细的描述。

# Lists are also encoded in a special way to save a lot of space.
# The number of entries allowed per internal list node can be specified
# as a fixed maximum size or a maximum number of elements.
# For a fixed maximum size, use -5 through -1, meaning:
# -5: max size: 64 Kb  <-- not recommended for normal workloads
# -4: max size: 32 Kb  <-- not recommended
# -3: max size: 16 Kb  <-- probably not recommended
# -2: max size: 8 Kb   <-- good
# -1: max size: 4 Kb   <-- good
# Positive numbers mean store up to _exactly_ that number of elements
# per list node.
# The highest performing option is usually -2 (8 Kb size) or -1 (4 Kb size),
# but if your use case is unique, adjust the settings as necessary.
# -- 每个list中包含的节点⼤⼩或个数 。正数表示个数, 负数-1到-5表示⼤⼩。 list-max-listpack-size -2

所以, 整体来说, 对于list数据类型, Redis是根据value中数据的⼤⼩判断底层数据结构  。数据⽐较 list类型, 底层⽤list pack保存 。数据量⽐较"  "list类型, 底层⽤ quicklist保存。

 这个结论跟redis的版本有关系。 

2、list底层数据结构详解

先来对list的底层数据做源码验证:

在处理lpush,rpush这些指令的时候,会进入下面的方法处理。

<t_list.c 484行>

 

⽽这个createList List packObject⽅法的声明, 是在object.c⽂件中 。这个⽅法就是创建— list pack结构, 来保存list中的元素。

<object.c 242>

关键是接下来的listTypeTryConversionAppend⽅法, 这个⽅法会尝试对list pack进⾏转 换。

<t_list.c 132>

 然后, 在这个listTypeTryConvert List pack⽅法中, 终于看到了这个神奇的quicklist

 <t_list.c 32>

在这个⽅法中, 涉及到服务端的另—个配置参数list-compress-depth 表示list的数据 压缩级别 。可以去配置⽂件中了解—下。

# Lists may also be compressed.
# Compress depth is the number of quicklist ziplist nodes from *each* side of
# the list to *exclude* from compression.  The head and tail of the list
# are always uncompressed for fast push/pop operations.  Settings a re:
# 0: disable all list compression
# 1: depth 1 means "don 't start compressing until after 1 node into the list,
#    going from either the head or tail"
#    So: [head]->node->node->...->node-> [tail]
#    [head], [tail] will always be uncompressed; inner nodes will compress.
# 2: [head]-> [next]->node->node->...->node-> [prev]-> [tail]
#    2 here means: don 't compress head or head->next or tail->prev or tail,
#    but compress all nodes between them.
# 3: [head]-> [next]-> [next]->node->node->...->node-> [prev]-> [prev]-> [tail]
# etc.
list-compress-depth 0

3、quicklist简介

要理解quicklist是什么,首先要尝试去理解Redis为什么有了listpack后,还需要设计一个quicklist。也就是listpack结构有什么不足的地方。

之前已经给大家介绍过listpack的数据结构。整体来看,listpack可以看成是一个数组(Array)结构。而对于数据结构,他的好处是存储数据是连续的,所以,对数组中的数据进行检索是比较快的,通过偏移量就可以快速定位。listpack的这种结构非常适合支持Redis的list数据类型的LRANGE这样的检索操作。

但是,对于数组来说,他的数据节点修改就会比较麻烦。 每次新增或者删除一个节点,都需要调整大量节点的位置。这又使得listpack的数据结构对于Redis的list数据类型的LPUSH这样增加节点的操作非常不友好。尤其当list中的数据节点越多,LPUSH这样的操作要移动的内存也就会越多。

 

与数组形成对比的是链表(List)结构。链表的节点之间只通过指针指向相关联的节点,这些节点并不需要占用连续的内存。链表的方式,好处就是对链表的增删节点会非常方便,只需要调整指针就可以了。所以链表能够非常好的支持list数据类型的LPUSH,LPOP这样的操作。

但是,链表结构也有明显的不足,那就是对数据的检索比较麻烦,只能沿着指针引用关系依次遍历节点。所以纯粹的链表结构也不太适合Redis的list数据类型。

 

 

 

那么有没有一种数据结构,能够尽量综合数据Array和链表List的优点呢?这就是Redis设计出来的quicklist结构。

quicklist大体上可以认为是一个链表结构。里面的每个节点是一个quicklistNode。

<quick.h 98行>

每个quicklistNode会保存前后节点的指针,这就是一个典型的链表结构。
<quick.h 36行>

在quicklistNode中,*entry实际上就是指向具体保存数据的listpack结构。

 

 

这样就形成了quicklist的整体结构。这个quicklist结构,就相当于是数组Array和链表List的结合体。这就能尽可能的结合这两种数据结构的优点。

quicklist的整体结构其实在Redis很早的版本中就已经成型了。区别在于quicklistNode中间保存的数据结构。 在Redis6以前是ziplist,到Redis7中改为了listpack。

4、list底层数据结构总结

如果list的底层数据量比较小时,Redis底层用listpack结构保存。当list的底层数据量比较大时,Redis底层用quicklist结构保存。

至于这其中数据量大小的判断标准,由参数list-max-listpack-size决定。这个参数设置成正数,就是按照list结构的数据节点个数判断。负数从-1到-5,就是按照数据节点的大小判断。

五、SET类型数据结构详解

1、set数据是如何存储的

老规矩,先下结论,再源码验证。

Redis底层综合使用intset+listpack+hashtable存储set数据。set数据的子元素也是<k,v>形式的entry。其中,key就是元素的值,value是null。

127.0.0.1:6379> sadd s1 1 2 3 4 5 (integer) 5
127.0.0.1:6379> OBJECT ENCODING s1 "intset"
127.0.0.1:6379> sadd s2 a b c d e (integer) 5
127.0.0.1:6379> OBJECT ENCODING s2 "listpack"
127.0.0.1:6379> config set set-max-listpack-entries 2 OK
127.0.0.1:6379> sadd s3 a b c d e (integer) 5
127.0.0.1:6379> OBJECT ENCODING s3 "hashtable"

区分底层结构的相关参数有以下⼏个: 

# Sets have a special encoding when a set is composed
# of just strings that happen to be integers in radix 10 in the range
# of 64 bit signed integers.
# The following configuration setting sets the limit in the size of the
# set in order to use this special memory saving encoding.
# -- 如果set的数据都是不超过64位的数字(—个long数字).就使⽤intset存储 set-max-intset-entries 512# Sets containing non-integer values are also encoded using a memory efficient
# data structure when they have a small number of entries, and the biggest entry
# does not exceed a given threshold. These thresholds can be configured using
# the following directives.
# -- 如果set的数据不是数字, 并且数据的⼤⼩没有超过下⾯设定的阈值 ,就⽤listpack存储
# -- 如果数据⼤⼩超过了其中—个阈值 ,就改为使⽤hashtable存储。
set-max-listpack-entries 128
set-max-listpack-value 64

 

2、set底层数据结构详解

首先,关于set底层的intset,listpack,hashtable这三种数据类型,listpack之前已经介绍过。hashtable基本不太可能面试被问到。而intset,其实是一种比较简单的数据结构。就是保存一个整数。

<intset.h 35行>

然后,关于这三种数据结构之间如何转换,以set数据类型最为典型的sadd指令为例,会进入下面这个方法进行处理。

<t_set.c 605行>

在创建set元素时,就会根据子元素的类型,判断是用intset还是用listpack。

<t_set.c 40行>

 

而在添加元素时,也会根据参数判断是否需要转换底层编码

<t_set.c 59行>

六、ZSET类型数据结构详解

1、zset数据是如何存储的

老规矩,先上结论,然后源码验证

Redis底层综合使用listpack + skiplist两种结构来保存zset类型的数据。

127.0.0.1:6379> config get zset*
1) "zset-max-ziplist-value"
2) "64"
3) "zset-max-listpack-entries"
4) "128"
5) "zset-max-ziplist-entries"
6) "128"
7) "zset-max-listpack-value"
8) "64"
127.0.0.1:6379> zadd z1 80 a
(integer) 1
127.0.0.1:6379> OBJECT ENCODING z1
"listpack"
127.0.0.1:6379> config set zset-max-listpack-entries 3
OK
127.0.0.1:6379> zadd z2 80 a 90 b 91 c 95 d
(integer) 4
127.0.0.1:6379> OBJECT ENCODING z2
"skiplist"

区分底层数据结构的参数有两个:

# Similarly to hashes and lists, sorted sets are also specially encoded in
# order to save a lot of space. This encoding is only used when the length and
# elements of a sorted set are below the following limits:
zset-max-listpack-entries 128
zset-max-listpack-value 64

 

2、zset底层数据结构详解

首先,zset类型底层数据结构有skiplist和listpack两种。listpack结构之前已经介绍过。这个skiplist是一种什么样的数据结构呢?

zset类型的数据,底层需要先按照score进行排序。排序过程中是需要移动内存的。如果节点数据不是太多,将这些内存移动完后,重新整理成一个类似数据Array的listpack结果是可以接受的。但是如果数据量太大(节点数和数据大小),那么频繁移动内存,开销就比较大了。这时,显然以链表这种零散的数据结构是比较合适的。

但是,对于一个单链表结构来说,要检索链表中的某一个数据,只能从头到尾遍历链表。时间复杂度是O(N),性能是比较低的。

如何对链表结构进行优化呢?skiplist跳表就是一种思路。skiplist的优化思路是构建多层逐级缩减的子索引,用更多的索引来提升搜索的性能。

skiplist是一种典型的用空间换时间的解决方案,优点是数据检索的性能比较高。时间复杂度是O(logN),空间复杂度是O(N)。但是他的缺点也很明显,就是更新链表时,维护索引的成本相对更高。因此,skiplist适合那些数据量比较大,且是读多写少的应用场景。

Redis天生就是针对读多写少的应用场景,而数据量的大小通过之前看到的两个参数,从数据条目数和数据大小两个方面来进行区别。

然后,Redis底层是如何转换数据结构的呢?

还是从zset最为常见的zadd操作入手

<t_zset.c 1838行>

 

往下跟踪这个zaddGenericCommand⽅法, 可以看到下⾯这个⽅法:

 <t_zset.c 1169>

createZsetObject方法在object.c当中。这个方法里可以看到对象结构被定义为SKIPLIST 

 

3、zset底层数据结构总结

Redis底层综合使用listpack+skiplist两种数据结构来保存zset类型的数据。其中,当zset数据的value数据量比较小时,使用listpack结构保存。value数据量比较大时,使用skiplist结构保存。skiplist是一种典型的用空间换时间的解决方案,适合那些数据量比较大,且读多写少的数据场景。在Redis中使用是非常合适的。

Redis中衡量zset的value数据大小的参数有两个,zset-max-listpack-entries 和 zset-max-listpack-value 分别从value的元数数量和数据大小两方面进行区分。

七、Redis课程总结

Redis中几种常见数据结构的底层结构总结下来就是这张表:

Redis版本

string

set

zset

list

hash

Redis 6

SDS(动态字符串)

intset+hashtable

skiplist+ziplist

quicklist+ziplist

hashtable+ziplist

Redis 7

SDS

intset+listpack+hashtable

skiplist+listpack

quicklist+listpack

hashtable+list

 

另外,关于Redis,有一个经久不衰的面试题,就是Redis为什么这么快。

这其实是一个没有标准答案的问题。Redis为了提升整体的运行速度,在各个方面都做了非常极致的优化。无锁化的线程模型,层层递进的集群架构,灵活定制的底层数据结构,极致优化的算法实现,等等,这些都是Redis对性能极致要求的体现。

但是,Redis的价值要求其实并不仅仅是一个快。在快的同时,Redis也在不断扩展新的业务功能,新的应用场景。集中式缓存、分布式锁、分布式主键生成、NoSQL数据库,向量搜索等各个方面的应用都是Redis不能忽视的价值。作为Java程序员,如何在复杂的业务场景中最大程度用好Redis,发挥Redis的强大性能,就是一个绕不开的基本功。

相关文章:

  • 样式失控:Word文档格式混乱的根源与对策
  • 【Flink实战】 Flink SQL 中处理字符串 `‘NULL‘` 并转换为 `BIGINT`
  • 【Ambari3.0.0 部署】Step4—时间同步(Chrony)-适用于el8
  • Xilinx XC7A12T‑1CPG238I Artix‑7 FPGA
  • Java 编程之代理模式
  • Matter协议开发者指南:使用Matter SDK构建智能家居应用
  • 数学公式排版简史
  • JavaWeb-day1
  • Spring Cloud 服务调用详解:Ribbon 负载均衡与 Feign 声明式接口调用
  • 第一章:认识AI
  • vtk和opencv和opengl直接的区别是什么?
  • JSON 数据格式详解
  • 【Java项目设计】基于Springboot+Vue的OA办公自动化系统
  • idea有了!多尺度时间序列新SOTA!
  • 前端基础知识CSS系列 - 16(css视差滚动效果)
  • OSPF 路由协议基础实验
  • 每天一个前端小知识 Day 7 - 现代前端工程化与构建工具体系
  • 如何理解Java反射机制
  • 【第二章:机器学习与神经网络概述】02.降维算法理论与实践-(2)线性判别分析(Linear Discriminant Analysis, LDA)
  • AbMole明星分子 |Acetylcysteine:从细胞保护到动物研究的全应用
  • 软件定制公司排名/seo公司优化
  • 做视频网站需要多少带宽/视频剪辑培训班一般学费多少
  • 企业网站托管外包平台/深圳aso优化
  • 个人网站带后台源码/数据平台
  • 网站建设选哪个/磁力岛
  • 中国建筑设计咨询公司/优化排名推广关键词