Redis源码

一、Redis源码

Redis源码官方对于源码说明参考:https://github.com/redis/redis,查看源码,在解压打开压缩包,在src目录下,都是通过C语言实现的:

官网对于上述源码实现功能的说明:

对于上述的源码实现做了分类如下:

1).基本结构说明:

  • Redis对象object.c
  • 字符串t_string.c
  • 列表t_list.c
  • 字典t_hash.c
  • 集合及有序集合t_set.c和t_zset.c
  • 集合及有序集合t_set.c和t_zset.c
  • 数据流t_stream.cStreams的底层实现结构listpack.c和rax.c
  • 简单动态字符串sds.c
  • 整数集合intset.c
  • 压缩列表ziplist.c
  • 快速链表quicklist.c
  • listpack
  • 字典dict.c

2).Redis数据库相关:

  • 数据的底层实现 db.c
  • 持久化rdb.c和aof.c

3).Redis服务端和客户端实现

  • 事件驱动ae.c和ae_epoll.c
  • 网络连接anet.c和networking.c
  • 服务端程序server.c
  • 客户端程序redis-cli.c

4).其他

  • 主从复制replication.c
  • 哨兵sentinel.c
  • 集群cluster.c
  • 数据结构,如hyperloglog.c. geo.c等
  • 功能,如pub/sub.Lua脚本

二、Redis的KV键值对本质

2.1.Redis的KV键值对是如何实现的?

key一般都是String类型的字符串对象,

value类型则为redis对象(redisObject),也可以是集合数据类型的对象,比如List对象.Hash对象、Set对象和Zset对象,但是本质上都是转换为redisObject对象;

2.2.Redis十大数据类型说明

传统的五大数据类型:

  • String
  • List
  • Hash
  • set
  • ZSet

新增加的五种数据类型

  • bitmap:实质String
  • hyperLogLog:实质String
  • GEO:实质Zset
  • Stream:实质Stream
  • BITFIELD:类型看具体key

2.3.Redis中redisObject对象

Redis定义了redisObjec来表示string、hash、list、 set、zset等数据类型,如下:

 redis中每一个键值对,都会有一个dictEntry,在dict.h中查看源码,如下:

上面函数说明如下:

  • 其中dictEntry表示哈希节点的结构,存放了void *key和void *value指针
  • *key执行String对象
  • *value既能指向string对象,也能指向集合类型的对象,例如:List、Hash、Set、Zset对象
  • void *key和void *value指针指向的是内部抽象的Redis对象,Redis 中的每个对象都由redisobject构成

RedisObject 、Redis数据类型和Redis编码方式三者之间的关系如下

三、五大结构底层C语言源码分析

3.1.redis常见数据类型底层结构

redis中数据对象对应的底层结构如下:

  • SDS 动态字符串
  • 双向链表
  • 压缩列表 ziplist
  • 哈希表 hashtable
  • 跳表 skiplist
  • 整数集合 intset
  • 快速列表 quicklist
  • 紧凑列表 listpack

3.2.redis6和redis7数据类型及其对应数据结构说明

redis目前已经到了redis7这个版本,数据类型对应的数据结构也出现了变化,如下:

Redis6对应关系如下:

数据类型数据结构总结
Hash 哈希表    压缩列表ZipList
List 双端链表
Sorted Set 跳表
Set 哈希表 整数数组
String 动态字符串  

Redis7对应关系如下:

数据类型数据结构总结
Hash 哈希表 listpack
List quicklist  
Sorted Set 跳表 listpack
Set 哈希表 整数数组
String 动态字符串  

3.2.简单的命令分析

以set k1 v1 为案例,Redis是KV键值对的数据库,所以每个键值对都会有一个dictEntry(源码位置:dict.h),里面指向了key和value的指针,next 指向下一个 dictEntry。

 

  • key 是字符串,但是 Redis 没有直接使用 C 的字符数组,而是存储在redis自定义的 SDS中。
  • value 既不是直接作为字符串存储,也不是直接存储在 SDS 中,而是存储在redisObject 中。
  • 实际上五种常用的数据类型的任何一种,都是通过 redisObject 来存储的。

案例演示查看编码方式和数据类型:

  • 查看类型 type hello
  • 查看编码 object encoding hello

3.3 redisObject结构的作用

redisObject函数在server.h中,redis中数据类型最终都是转换成了redisObject对象,

对于redisObject函数参数说明如下:

参数 说明
type:4; 当前值对象的数据类型
encoding:4 当前值对象底层存储的编码类型
lru:LRU_BITS; 采用LRU算法清除内存中的对象
int refcount 记录对象引用次数
void *ptr; 指向真正的底层数据结构的指针

3.4.常见五大数据结构解析

3.3.1各个类型的数据结构编码映射定义如下

在object.c下的*strEncoding函数中可以看到数据结构编码映射关系

3.3.2.Redis Debug key

Redis Debug Object命令是—个调试命令,它不应被客户端所使用。当key存在的时候,返回有关信息,key不存在,返回一个错误,语法如下:

但是需要注意,这个命名默认是不运行使用的,要使用,必须开启

开启运行使用Debug命令,只需要将enable-debug-command修改为local,重启服务后才能生效,Debug某个键:

DEBUG object k1

执行后如下:

上面对于某个keyDebug后,内容说明如下:

  • Value at: 内存地址
  • refcount: 引用次数
  • encoding: 物理编码类型
  • serializedlength: 序列化后的长度(注意这里的长度是序列化后的长度,保存为rdb文件时使用了该算法,不是真正存贮在内存的大小),会对字串做一些可能的压缩以便底层优化
  • lru:记录最近使用时间戳
  • lru_seconds_idle:空闲时间

3.3.3.String数据类型

3.3.3.1.三种编码方式

string类型的值查看对应的三种编码方式

编码方式 说明
int

1.保存long型(长整型)的64位(8个字节)有符号整数
2.最小值是-9,223,372,036,854,775,8o8 ( -2^63) ,最大值是9,223,372,036,854,775,807 (2^63 -1),最多19位
3.只有整数才会使用int,如果是浮点数,Redis内部其实先将浮点数转化为字符串值,然后再保存

embstr 代表embstr 格式的 SDS(Simple Dynamic String 简单动态字符串),保存长度小于44字节的字符串
raw 保存长度大于44字节的字符串
  • int编码格式

当字符串键值的内容可以用一个64位有符号整形来表示时,Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT 编码类型。以set k1 123 为例内部的内存结构表示如下:

Redis 启动时会预先建立 10000 个分别存储 0~9999 的 redisObject 变量作为共享对象,这就意味着如果 set字符串的键值在 0~10000 之间的话,则可以 直接指向共享对象 而不需要再建立新对象,此时键值不占空间!,源码在server.h中如下:

  • embstr

对于长度小于 44的字符串,Redis 对键值采用OBJ_ENCODING_EMBSTR 方式,EMBSTR 顾名思义即:embedded string,表示嵌入式的String。从内存结构上来讲 即字符串 sds结构体与其对应的 redisObject 对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。

  • raw编码格式

当字符串的键值为长度大于44的超长字符串时,Redis 则会将键值的内部编码方式改为OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的redisObject的内存不再连续了

但是对于embstr,由于其实现是只读的,因此在对embstr 对象进行修改时,都会先转化为raw再进行修改。因此,只要是修改embstr 对象,修改后的对象—定是ra的,无论是否达到了44个字节

# 创建一个键值对
127.0.0.1:6379> set k2 hello
OK
# 查看编码方式为embstr
127.0.0.1:6379> OBJECT ENCODING k2
embstr
# 给k2添加值world
127.0.0.1:6379> APPEND k2 world
10
# 添加后,值变成了helloworld
127.0.0.1:6379> get k2
helloworld
# 再次检查编码方式,变成了raw
127.0.0.1:6379> OBJECT ENCODING k2
raw
127.0.0.1:6379>

3.3.3.2.案例演示

通过案例代码演示如下:

127.0.0.1:6379>
# 平常的数字类型,查看是int
127.0.0.1:6379> set k1 12345
OK
127.0.0.1:6379> OBJECT ENCODING k1
"int"
# 长度小于20的数字,查看还是int
127.0.0.1:6379> set k1 1234567890123456789
OK
127.0.0.1:6379> OBJECT ENCODING k1
"int"
# 长度大于等于20的数字存储为embstr
127.0.0.1:6379> set k1 12345678901234567890
OK
127.0.0.1:6379> OBJECT ENCODING k1
"embstr"
# 普通字符串,查看类型为embstr
127.0.0.1:6379> set k1 helloword
OK
127.0.0.1:6379> OBJECT ENCODING k1
"embstr"
# 长度小于44的字符串,查看类型embstr
127.0.0.1:6379> set k1 helloworldhelloworldhelloworldhelloworldhel
OK
127.0.0.1:6379> OBJECT ENCODING k1
"embstr"
# 长度大于44的字符串,查看类型raw
127.0.0.1:6379> set k1 helloworldhelloworldhelloworldhelloworldhelloworld
OK
127.0.0.1:6379> OBJECT ENCODING k1
"raw"
127.0.0.1:6379>

3.3.3.3.SDS数据结构

Redis没有直接复用C语言的字符串,而是新建了属于自己的结构SDS用于表示字符串,在Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现,Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)。查看源码在sds.h文件中,如下:

上面截图中,对应函数和参数说明如下(一个字符串最大是512MB):

Redis中字符串的实现,SDS有多种结构(sds.h):
sdshdr5、(2^5=32byte)
sdshdr8、(2 ^ 8=256byte)
sdshdr16、(2 ^ 16=65536byte=64KB)
sdshdr32、 (2 ^ 32byte=4GB)
sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。
 
len 表示 SDS 的长度,使我们在获取字符串长度的时候可以在 O(1)情况下拿到,而不是像 C 那样需要遍历一遍字符串。
alloc 可以用来计算 free 就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。
flage:sds类型
buf 表示字符串数组,真存数据的

3.3.3.4.redis为什么要从新设计一个sds数据结构,而不是直接使用C语言中的sds?

C语言没有Java里面的String类型,只能是靠自己的char[]来实现,字符串在 C 语言中的存储方式,想要获取 「Redis」的长度,需要从头开始遍历,直到遇到 '\0' 为止(\0 是C语言的结束符)。所以,Redis 没有直接使用 C 语言传统的字符串标识,而是自己构建了一种名为简单动态字符串 SDS(simple dynamic string)的抽象类型,并将 SDS 作为 Redis 的默认字符串。

 

C语言

SDS

字符串长度处理

需要从头开始遍历,直到遇到 '\0' 为止,时间复杂度O(N)

 记录当前字符串的长度,直接读取即可,时间复杂度 O(1)

内存重新分配

分配内存空间超过后,会导致数组下标越级或者内存分配溢出

1)空间预分配

SDS 修改后,len 长度小于 1M,那么将会额外分配与 len 相同长度的未使用空间。如果修改后长度大于 1M,那么将分配1M的使用空间。

2)惰性空间释放

有空间分配对应的就有空间释放。SDS 缩短时并不会回收多余的内存空间,而是使用 free 字段将多出来的空间记录下来。如果后续有变更操作,直接使用 free 中记录的空间,减少了内存的分配。

二进制安全

二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如 '\0' 等。前面提到过,C中字符串遇到 '\0' 会结束,那 '\0' 之后的数据就读取不上了

根据 len 长度来判断字符串结束的,二进制安全的问

3.3.3.5.总结

只有整数才会使用 int,如果是浮点数, Redis 内部其实先将浮点数转化为字符串值,然后再保存。embstr 与 raw 类型底层的数据结构其实都是 SDS (简单动态字符串,Redis 内部定义 sdshdr 一种结构)。那这两者的区别见下图:

int
Long类型整数时,RedisObject中的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。
embstr
当保存的是字符串数据且字符串小于等于44字节时,embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject 与 sdshdr 两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片
raw
当字符串大于44字节时,SDS的数据量变多变大了,SDS和RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw 类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr 结构

3.3.4.Hash数据类型

3.3.4.1.编码方式

hash数据结构在redis6和redis7中编码方式是不同的

版本 编码
redis6 ziplist、hashtable
redis7 listpack、hashtable

3.3.4.2.redis6

hash说明和案例演示

hash常见参数说明如下:

  • hash-max-ziplist-entries:使用压缩列表保存时哈希集合中的最大元素个数。
  • hash-max-ziplist-value:使用压缩列表保存时哈希集合中单个元素的最大长度。

Hash类型键的字段个数 小于 hash-max-ziplist-entries 并且每个字段名和字段值的长度 小于 hash-max-ziplist-value 时,Redis才会使用 OBJ_ENCODING_ZIPLIST来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式,案例如下:

# 查看hash相关的熟悉
127.0.0.1:6379> CONFIG GET hash*
1) "hash-max-ziplist-entries"
2) "512"
3) "hash-max-ziplist-value"
4) "64"
# 为了后续方便测试,调整两个属性的值
127.0.0.1:6379> CONFIG SET hash-max-ziplist-entries 3
OK
127.0.0.1:6379> CONFIG SET hash-max-ziplist-value 8
OK
# 创建一个hash
127.0.0.1:6379> HSET user1 name lisi age 23
(integer) 2
# 查看编码类型,由于并未超过设置的3个元素个数的要求,编码为ziplist
127.0.0.1:6379> OBJECT encoding user1
"ziplist"
127.0.0.1:6379> HSET user2 name lisizhangwuji age 23
(integer) 2
# 查看编码类型,由于name对应的值长度超过了8,编码为hashtable
127.0.0.1:6379> OBJECT encoding user2
"hashtable"

127.0.0.1:6379> HSET user3 name zhangsan age 80 sex 1 stuid 003
(integer) 4
# 查看编码类型,由于超过设置的3个元素个数的要求,编码为hashtable
127.0.0.1:6379> OBJECT encoding user3
"hashtable"
127.0.0.1:6379> 

哈希对象保存的键值对数里小于512个,所有的键值对的健和值的字符串长度都小于等于64byte (一个英文字母一个字节)时用ziplist,反之用hashtable),但是需要注意ziplist升级到hashtable使用可以,反过来降级不可以因为,一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。在节省内存空间方面哈希表就没有压缩列表高效了。

ziplist源码解析

Ziplist 压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率,因此只会用于字段数较少的情况,且字段值也较小 的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。当一个 hash对象,只包含少量键值对且每个键值对的键和值要么就是小整数要么就是长度比较短的字符串,那么它用 ziplist 作为底层实现

ziplist是为了节约内存而开发的,它是由连续内存块组成的顺序型数据结构,有点类似于数组ziplist是一个经过特殊编码的双向链表,它不存储指向前一个链表节点prev和指向下一个链表节点的指针next,而是存储上一个节点长度当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面,源码中对于数据结构如下:

对于上面ziplist各个单元说明如下:

那么ziplist对于数据的存取是什么样的呢?

压缩列表zlentry节点结构:每个zlentry由前一个节点的长度也就是prevlen、encoding和entry-data三部分组成,

  • prevlen前节点:(前节点占用的内存字节数)表示前1个zlentry的长度,privious_entry_length有两种取值情况:1字节或5字节。取值1字节时,表示上一个entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时,prev_len取值为1字节,否则,就取值为5字节。记录长度的好处:占用内存小,1或者5个字节
  • enncoding:记录节点的content保存数据的类型和长度。
  • content:保存实际数据内容

添加时案例如下:

为什么entry如此设计?为何要记录前一个节点的长度?

链表在内存中,一般是不连续的,遍历相对比较慢,而ziplist可以很好的解决这个问题。如果知道了当前的起始地址,因为entry是连续的,entry后一定是另一个entry,想知道下一个entry的地址,只要将当前的起始地址加上当前entry总长度。如果还想遍历下一个entry,只要继续同样的操作。

已经有链表了,为什么还要设计压缩链表呢?

原因如下:

  • 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用的内存大,得不偿失。ziplist 是一个特殊的双向链表没有维护双向指针:previous next;而是存储上一个 entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储entry长度更费内存。这是典型的“时间换空间”。
  • 链表在内存中一般是不连续的,遍历相对比较慢而ziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个sizeof(int)就行),但是ziplist的每个节点的长度是可以不一样的,而我们面对不同长度的节点又不可能直接sizeof(entry),所以ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下一个节点。备注:sizeof实际上是获取了数据在内存中所占用的存储空间,以字节为单位来计数。
  • 头节点里有头节点里同时还有一个参数 len,和string类型提到的 SDS 类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历整个链表,直接拿到len值就可以了,这个时间复杂度是 O(1)

ziplist总结如下:

  • ziplist为了节省内存,采用了紧凑的连续存储。
  • ziplist是一个双向链表,可以在时间复杂度为 O(1) 下从头部、尾部进行 pop 或 push。
  • 新增或更新元素可能会出现连锁更新现象(致命缺点导致被listpack替换)。
  • 不能保存过多的元素,否则查询效率就会降低,数量小和内容小的情况下可以使用。
hashtable源码解析

在redis中hashtable被称之为字典,其是由数组加链表的结构组成

3.3.4.2.redis7

listpack说明

Redis7和Redis6在hash中最大的不同,就是将ziplist,替换成了listpack,但是在为了过渡,Redis官方并没有直接剔除了ziplist,为了过渡,进行了保留,下面关于listpack说明如下:

  • hash-max-listpack-entries:使用压缩列表保存时哈希集合中的最大元素个数。
  • hash-max-listpack-value:使用压缩列表保存时哈希集合中单个元素的最大长度。

Hash类型键的字段个数 小于 hash-max-listpack-entries且每个字段名和字段值的长度 小于 hash-max-listpack-value 时,Redis才会使用OBJ_ENCODING_LISTPACK来存储该键,前述条件任意一个不满足则会转换为 OBJ_ENCODING_HT的编码方式

# 查看hast相关的配置,发现listpack和ziplist同时存在
127.0.0.1:6379> config get hash*
hash-max-listpack-entries
512
hash-max-listpack-value
64
hash-max-ziplist-entries
512
hash-max-ziplist-value
64
# 设置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 5
OK
# 再次查看,发现值已经修改
127.0.0.1:6379> config get hash*
hash-max-listpack-entries
3
hash-max-listpack-value
5
hash-max-ziplist-entries
3
hash-max-ziplist-value
5
# 修改ziplist相关的配置
127.0.0.1:6379> CONFIG SET hash-max-ziplist-entries 7
OK
127.0.0.1:6379> CONFIG SET hash-max-ziplist-value 7
OK
# 创建hset
127.0.0.1:6379> hset user11 name zs
1
# 查看编码方式是listpack
127.0.0.1:6379> OBJECT ENCODING user11
listpack
127.0.0.1:6379> hset user11 name zs age 18 sex 男
2
# 这里创建hset,但是只满足了元素个数为3个,但是元素长度为5不满足,所以查看还是listpack
127.0.0.1:6379> hset user12 name zs age 23 sex 1 sutid 12
4
127.0.0.1:6379> OBJECT ENCODING user12
listpack
# 这里创建hset,元素个数为4个,大于了3个的要求,同时元素name长度为超过了5,所以查看编码方式是hashtable
127.0.0.1:6379> hset user12 name zhangsan age 23 sex 1 sutid 12
0
127.0.0.1:6379> OBJECT ENCODING user12
hashtable
127.0.0.1:6379>

上述说明如下:

  • 哈希对象保存的键值对数小于512个;
  • 所有的键值对的健和值的字符串长度都小于等于64byte (一个英文字母一时用listpack,反之用hashtable)
  • listpack升级到hashtable可以,反过来降级不可以
之前已经有了ziplist,为何还要设计listpack呢?

listpack 是 Redis 设计用来取代掉 ziplist 的数据结构,它通过每个节点记录自己的长度且放在节点的尾部,来彻底解决掉了 ziplist 存在的连锁更新的问题,压缩列表新增某个元素或修改某个元素时,如果空间不不够,压缩列表占用的内存空间就需要重新分配。而当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,从而引起「连锁更新」问题,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。

案例说明:压缩列表每个节点正因为需要保存前一个节点的长度字段,就会有连锁更新的隐患

  • 第一步:现在假设一个压缩列表中有多个连续的、长度在 250~253 之间的节点,如下图:

 因为这些节点长度值小于 254 字节,所以 prevlen 属性需要用 1 字节的空间来保存这个长度值,正确使用

  • 第二步:这时,如果将一个长度大于等于 254 字节的新节点加入到压缩列表的表头节点,即新节点将成为entry1的前置节点,如下图:

 因为entry1节点的prevlen属性只有1个字节大小,无法保存新节点的长度,此时就需要对压缩列表的空间重分配操作并将entry1节点的prevlen 属性从原来的 1 字节大小扩展为 5 字节大小。

  • 第三步:连续更新问题出现

 entry1节点原本的长度在250~253之间,因为刚才的扩展空间,此时entry1节点的长度就大于等于254,因此原本entry2节点保存entry1节点的 prevlen属性也必须从1字节扩展至5字节大小。entry1节点影响entry2节点,entry2节点影响entry3节点......一直持续到结尾。这种在特殊情况下产生的连续多次空间扩展操作就叫做「连锁更新」

listpack源码解析

listpack的结构如下:

说明如下:

  • Total Bytes:为整个listpack的空间大小,占用4个字节,每个listpack最多占用4294967295Bytes。
  • num-elements:为listpack中的元素个数,即Entry的个数占用2个字节
  • element-1~element-N:为每个具体的元素
  • listpack-end-byte:为listpack结束标志,占用1个字节,内容为0xFF。

实际使用的时候如下:

entry的结构如下:

  • 当前元素的编码类型(entry-encoding)
  • 元素数据(entry-data)
  • 以及编码类型和元素数据这两部分的长度(entry-len)

listentry在源码中的定义如下:

综述:和ziplist 列表项类似,listpack 列表项也包含了元数据信息和数据本身。不过为了避免ziplist引起的连锁更新问题,listpack 中的每个列表项不再像ziplist列表项那样保存其前一个列表项的长度。

3.3.5.List数据结构

3.3.5.1.Redis6

发展变化说明

redis6版本之前的list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个ziplist,

  • 在Redis3.0之前,list采用的底层数据结构是ziplist压缩列表+linkedList双向链表
  • 在高版本的Redis中底层数据结构是quicklist(替换了ziplist+linkedList),而quicklist也用到了ziplist
  • quicklist就是「双向链表 + 压缩列表」组合,因为一个 quicklist 就是一个链表,而链表中的每个元素又是一个压缩列表

quicklist结构说明

在源码中的定义如下:

 对于上面quicklist的定义中参数说明如下:

quicklistNode结构

上面的方法的参数说明:

注意:quicklistNocle中的*zl指向一个ziplist,一个ziplist可以存放多个元素

3.3.5.2.Redis7

在redis7中,list用quicklist来存储,quicklist存储了一个双向链表,每个节点都是一个listpack,换言之Redis7中的list是listpack和linkedlist的结合体

3.3.6.set数据结构

Redis用intset或hashtable存储set。如果元素都是整数类型,就用intset存储。如果不是整数类型,就用hashtable(数组+链表的存来储结构)。key就是元素的值,value为null。案例如下:

遵循规则:集合元素都是longlong类型并且元素个数<=set-max-intset-entries编码就是intset,反之就是hashtable

# 查看set相关的配置
127.0.0.1:6379> config get set*
set-proc-title
yes
set-max-intset-entries
512
# 修改对于set元素个数的默认值设置
127.0.0.1:6379> CONFIG SET set-max-intset-entries 3
OK
# 再次查看配置已经修改
127.0.0.1:6379> config get set*
set-proc-title
yes
set-max-intset-entries
3
# 集合元素都是longlong类型并且元素个数<=set-max-intset-entries编码就是intset,反之就是hashtable
127.0.0.1:6379> sadd set2 233424
1
127.0.0.1:6379> OBJECT ENCODING set2
intset
127.0.0.1:6379> sadd set3 233424 232 hi world
4
127.0.0.1:6379> OBJECT ENCODING set3
hashtable
127.0.0.1:6379>

3.3.7.zset数据结构

在Redis6中,ZSET的编码格式为ziplist和skiplist,

  • 当有序集合中包含的元素数量超过服务器属性 server.zset_max_ziplist_entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset_max_ziplist_value 的值(默认值为 64 )时,redis会使用跳跃表(skiplist)作为有序集合的底层实现。否则会使用ziplist作为有序集合的底层实现

案例如下:

27.0.0.1:6379> CONFIG GET zset*
1) "zset-max-ziplist-entries"
2) "128"
3) "zset-max-ziplist-value"
4) "64"
#修改默认配置
127.0.0.1:6379> CONFIG SET zset-max-ziplist-entries 3
OK
127.0.0.1:6379> CONFIG SET zset-max-ziplist-value 6
OK
127.0.0.1:6379> CONFIG GET zset*
1) "zset-max-ziplist-entries"
2) "3"
3) "zset-max-ziplist-value"
4) "6"
# 创建zset
127.0.0.1:6379> ZADD zs1 10 a 20 b 30 c
(integer) 3
# 查看编码方式是ziplist
127.0.0.1:6379> OBJECT encoding zs1
"ziplist"
127.0.0.1:6379> ZADD zs2 40 d
(integer) 1
127.0.0.1:6379> OBJECT encoding zs2
"ziplist"
127.0.0.1:6379> ZADD zs3 40 dasdffff
(integer) 1
# 查看编码方式是skiplist
127.0.0.1:6379> OBJECT encoding zs3
"skiplist"
127.0.0.1:6379> 

在Redis7中,ZSET的编码格式为listpack和skiplist,遵循格式如下:

  • 当有序集合中包含的元素数量超过服务器属性 server.zset-max-listpack-entries 的值(默认值为 128 ),或者有序集合中新添加元素的 member 的长度大于服务器属性 server.zset-max-listpack-value 的值(默认值为 64 )时,redis会使用跳跃表(skiplist)作为有序集合的底层实现。否则会使用listpack作为有序集合的底层实现
# 查看zset相关配置
127.0.0.1:6379> CONFIG GET zset*
zset-max-listpack-value
64
zset-max-listpack-entries
128
zset-max-ziplist-value
64
zset-max-ziplist-entries
128
# 将zset元素数量设置为3,不超过3使listpack,超过了使用skiplist
127.0.0.1:6379> CONFIG SET zset-max-listpack-entries 3
OK
# 将zset元素长度设置为6,不超过6使listpack,超过了使用skiplist
127.0.0.1:6379> CONFIG SET zset-max-listpack-value 6
OK
127.0.0.1:6379> CONFIG GET zset*
zset-max-listpack-value
6
zset-max-listpack-entries
3
zset-max-ziplist-value
6
zset-max-ziplist-entries
3
# 不超过3个元素
127.0.0.1:6379> ZADD zs1 10 a 20 b 30 c
3
# 查看编码方式是listpack
127.0.0.1:6379> OBJECT ENCODING zs1
listpack
# 4个元素,超过3个元素
127.0.0.1:6379> ZADD zs2 10 a 20 b 30 c 40 d
4
# 查看编码方式是skiplist
127.0.0.1:6379> OBJECT ENCODING zs2
skiplist
# 元素长度不超过6
127.0.0.1:6379> ZADD zs3 100 123456
1
# 查看编码方式是listpack
127.0.0.1:6379> OBJECT ENCODING zs3
listpack
# 元素长度超过6
127.0.0.1:6379> ZADD zs4 100 1234567
1
# 查看编码方式是skiplist
127.0.0.1:6379> OBJECT ENCODING zs4
skiplist
127.0.0.1:6379>

3.3.8.skiplist数据结构

为什么要引入skiplist(跳表)呢?

对于一个单链表来讲,即便链表中存储的数据是有序的,如果我们要想在其中查找某个数据,也只能从头到尾遍历链表。这样查找效率就会很低,时间复杂度会很高O(N),解决方法:升维,也叫空间换时间。

从下面例子里,我们看出,加来一层索引之后,查找一个结点需要遍历的结点个数减少了,也就是说查找效率提高了。

skiplist是什么?

跳表是可以实现二分查找的有序链表,跳表=链表+多级索引,说明如下:

  • skiplist是一种以空间换取时间的结构。
  • 由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找,提取多层关键节点,就形成了跳跃表
  • 但是skiplist在使用的时候由于索引也要占据一定空间的,所以,索引添加的越多,空间占用的越多

skiplist跳表的时间复杂度说明?

时间复杂度是O(logN),跳表查询的时间复杂度分析,如果链表里有N个结点,会有多少级索引呢?

按照我们前面讲的,两两取首。每两个结点会抽出一个结点作为上一级索引的结点,以此估算:

  • 第一级索引的结点个数大约就是n/2,
  • 第二级索引的结点个数大约就是n/4,
  • 第三级索引的结点个数大约就是n/8,依次类推......

也就是说,第k级索引的结点个数是第k-1级索引的结点个数的1/2,那第k级索引结点的个数就是n/(2^k)

skiplist跳表的空间复杂度说明?

所以空间复杂度是O(N),比起单纯的单链表,跳表需要存储多级索引,肯定要消耗更多的存储空间。那到底需要消耗多少额外的存储空间呢?首先分析一下跳表的空间复杂度。

  • 第一步:首先原始链表长度为n,
  • 第二步:两两取首,每层索引的结点数:n/2, n/4, n/8 ... , 8, 4, 2 每上升一级就减少一半,直到剩下2个结点,以此类推;如果我们把每层索引的结点数写出来,就是一个等比数列。这几级索引的结点总和就是n/2+n/4+n/8…+8+4+2=n-2。所以,跳表的空间复杂度是O(n) 。也就是说,如果将包含n个结点的单链表构造成跳表,我们需要额外再用接近n个结点的存储空间。、
  • 第三步:思考三三取首,每层索引的结点数:n/3, n/9, n/27 ... , 9, 3, 1 以此类推;第一级索引需要大约n/3个结点,第二级索引需要大约n/9个结点。每往上一级,索引结点个数都除以3。为了方便计算,我们假设最高一级的索引结点个数是1。我们把每级索引的结点个数都写下来,也是一个等比数列;
  • 通过等比数列求和公式,总的索引结点大约就是n/3+n/9+n/27+…+9+3+1=n/2。尽管空间复杂度还是O(n) ,但比上面的每两个结点抽一个结点的索引构建方法,要减少了一半的索引结点存储空间。

    所以空间复杂度是O(n);

skiplist跳表的优缺点?

优点:跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的;

缺点: 维护成本相对要高,在单链表中,一旦定位好要插入的位置,插入结点的时间复杂度是很低的,就是O(1);

存在问题:新增或者删除时需要把所有索引都更新一遍,为了保证原始链表中数据的有序性,我们需要先找到要动作的位置,这个查找操作就会比较耗时最后在新增和删除的过程中的更新,时间复杂度也是O(log n);

 

posted @ 2023-04-30 11:56  酒剑仙*  阅读(2190)  评论(1)    收藏  举报