1、5大数据结构底层C语言源码分析
1.1、从set hello world说起
set hello world为例,因为 Redis是KV键值对的数据库,每个键值对都会有一个 dictEntry(源码位置:dict.h),里面指向了key和 value的指针,next指向下一个 dictEntry
key是字符串,但是 Redis没有直接使用C的字符数组, 而是存储在redis自定义的SDS中。
value既不是直接作为字符串存储,也不是直接存储在SDS中,而是存储在 redisObject中。
实际上五种常用的数据类型的任何一种,都是通过 redisObject来存储的
1.1.1、每个键值对都会有一个dictEntry
1.1.2、看看类型
127.0.0.1:6379> set hello world
OK
127.0.0.1:6379> type hello
string
1.1.3、看看编码
127.0.0.1:6379> object encoding hello
"embstr"
1.1.4、debug结构
127.0.0.1:6379> debug object hello
Value at:0x7f93eb80e3e0 refcount:1 encoding:embstr serializedlength:6 lru:5700013 lru_seconds_idle:141
127.0.0.1:6379> get hello
"world"
127.0.0.1:6379> debug object hello
Value at:0x7f93eb80e3e0 refcount:1 encoding:embstr serializedlength:6 lru:5700219 lru_seconds_idle:2
在get 操作后再次debug,lru发生变化
1.2、redisObject结构的作用
typedef struct redisObject {
unsigned type:4; //对象的类型,包括:OBJ_STRING,OBJ_LIST,OBJ_HASH,OBJ_SET,OBJ_ZSET
unsigned encoding:4; //具体的数据结构
unsigned lru:LRU_BITS; //24位,对象最后一次被命令程序访问的时间,与内存回收有关
int refcount; //引用计数,当refcount为0的时候,表示该对象已经不被任何对象引用,则可以进行垃圾回收了
void *ptr; //指向对象实际的数据结构
} robj;
为了便于操作, Redis采用redisObject结构来统一五种不同的数据类型,这样所有的数据类型就都可以以相同的形式在函数间传递而不用使用特定的类型结构。同时,为了识别不同的数据类型, redisObjec中定义了type和 encoding字段对不同的数据类型加以区别。
简单地说, redisObjec就是string、hash、list、set、zset的父类,可以在函数间传递时隐藏具体的类型信息,所以作者抽象了redisObjec结构来到达同样的目的。
源码位置:src/server.h
① RedisObject各字段的含义
[1] 4位的type表示具体的数据类型
[2] 4位的encoding表示该类型的物理编码方式见下表,同一种数据类型可能有不同的编码方式。
(比如 String就提供了3种 int embstr raw)
[3] lru字段表示当内存超限时采用LRU算法清除内存中的对象。
[4] refcount表示对象的引用计数。
[5] ptr指针指向真正的底层数据结构的指针。
源码位置:src/server.h
② 案例:set age 17
127.0.0.1:6379> set age 17
OK
127.0.0.1:6379> type age
string
127.0.0.1:6379> object encoding age
"int"
type | 类型 |
encoding | 编码,此处是数字类型 |
Iru | 最近被访问的时间 |
refcount | 等于1,表示当前对象被引用的次数 |
ptr | value值是多少,当前就是17 |
1.3、数据类型以及数据结构的关系
程序员写代码时脑子底层思维
1.3.1、String数据结构介绍
1.3.1.1、3大编码格式
① int
保存long型(长整型)的64位(8个字节)有符号整数
9223372036854775807 long的最小值~最大值,多一点会变成embstr 如:9223372036854775808
上面数字最多19位
补充:
只有整数才会使用int,如果是浮点数, Redis内部其实先将浮点数转化为字符串值,然后再保存。
② embstr
代表 embstr格式的SDS(Simple Dynamic String简单动态字符串),保存长度小于44字节的字符串
EMBSTR顾名思义即: embedded string,表示嵌入式的 String
③ raw
保存长度大于44字节的字符串
1.3.1.2、3大编码案例
案例测试:
127.0.0.1:6379> set k1 123 #普通字符串
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> set k1 9223372036854775807 #长度小于20的数
OK
127.0.0.1:6379> object encoding k1
"int"
127.0.0.1:6379> set k1 9223372036854775808 #长度大于等于20的数
OK
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> set k1 abc #普通字符串
OK
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> set k1 aaaaaaaaaaaaaaaaaa #长度小于44的字符串
OK
127.0.0.1:6379> object encoding k1
"embstr"
127.0.0.1:6379> set k1 aaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaaa #长度大于44的字符串
OK
127.0.0.1:6379> object encoding k1
"raw"
C语言中字符串的展现
Redis没有直接复用C语言的字符串,而是新建了属于自己的结构---SDS
在 Redis数据库里,包含字符串值的键值对都是由SDS实现的(Redis中所有的键都是由字符串对象实现的即底层是由SDS实现, Redis中所有的值对象中包含的字符串对象底层也是由SDS实现)
SDS简单动态字符串
// sds.h
struct __attribute__ ((__packed__)) sdshdr8 {
uint8_t len; /* 当前字符数组的长度 */
uint8_t alloc; /* 当前字符数组总共分配的内存大小 */
unsigned char flags; /* 当前字符数组的属性,用来标识到底是sdsdhr8 还是sdshdr16等 */
char buf[]; /* 字符串真正的值*/
};
Redis中字符串的实现,SDS有多种结构(sds.h):
sdshdr5、(2^5=32byte) never used
sdshdr8、(2^8=256byte)
sdshdr16、(2^16=65536byte=64KB)
sdshdr32、(2^32byte=4GB)
sdshdr64,2的64次方byte=17179869184G用于存储不同的长度的字符串。
len表示SDS的长度,使我们在获取字符串长度的时候可以在(1)情况下拿到,而不是像C那样需要遍历一遍字符串。
alloc可以用来计算free就是字符串已经分配的未使用的空间,有了这个值就可以引入预分配空间的算法了,而不用去考虑内存分配的问题。
buf表示字符串数组,真存数据的。
源码位置src/sds.h
Redis为什么重新设计一个SDS数据结构?
C语言没有Java里面 String的类型,只能是靠自己的char[]来实现,字符串在C语言中的存储方式,想要获取「 Redis」的长度,需要从头开始遍历,直到遇到'\0'为止。所以, Redis没有直接使用C语言传统的字符串标识,而是自己构建了一种名为简单动态字符串SDS(simple dynamic string)的抽象类型,并将SDS作为 Redis的默认字符串。
C语言 | SDS | |
字符串长度处理 | 需要从头开始遍历,直到遇到'\0'为止,时间复杂度O(N) | 记录当前字符串的长度,直接读取即可,时间复杂度O(1) |
内存重新分配 | 分配内存空间超过后,会导致数组下标越级或者内存配溢出 | 空间预分配 SDS修改后,len长度小于1M,那么将会额外分配与len相同长度的未使用空间。 如果修改后长度大于1M,那么将分配1M的使用空间。 惰性空间释放 有空间分配对应的就有空间释放。SDS缩短时并不会回收多余的内存空间,而是使用free字段将多出来的空间记录下来。如果后续有变更操作,直接使用free中记录的空间,减少了内存的分配。 |
二进制安全 | 二进制数据并不是规则的字符串格式,可能会包含一些特殊的字符,比如'\0'等。前面提到过,C中字符串遇到'\0'会结束,那'\0'之后的数据就读取不上了 | 根据len长度来判断字符串结束的,二进制安全的问题就解决了 |
源码分析
[1] 用户API set k1 v1 底层发生了什么?调用关系
[2]三大编码
① INT编码格式 setk1 123
命令示例:setk1 123
当字符串键值的内容可以用一个64位有符号整形来表示时, Redis会将键值转化为long型来进行存储,此时即对应 OBJ_ENCODING_INT编码类型。内部的内存结构表示如下:
Redis启动时会预先建立10000个分别存储0~9999的 redisObject变量作为共享对象,这就意味着如果set字符串的键值在0~10000之间的话,则可以直接指向共享对象而不需要再建立新对象,此时键值不占空间
set k1 123
set k2 123
源码解读 src/object.c
② EMBSTR编码格式 set k1 abc
源码解读 src/object.c
对于长度小于44的字符串, Redis对键值采用 OBJ_ENCODING_EMBSTR方式, EMBSTR顾名思义即: embedded string,表示嵌入式的 String.从内存结构上来讲即字符串sds结构体与其对应的 redisObject对象分配在同一块连续的内存空间,字符串sds嵌入在redisObject对象之中一样。
图解:
③ RAW编码格式 set k1 大于44长度的一个字符串,随便写
当字符串的键值为长度大于44的超长字符串时, Redis则会将键值的内部编码方式改为 OBJ_ENCODING_RAW格式,这与OBJ_ENCODING_EMBSTR编码方式的不同之处在于,此时动态字符串sds的内存与其依赖的 redisObject的内存不再连续了
明明没有超过阈值,为什么变成了raw
[3]转变逻辑图
案例结论
只有整数才会使用int,如果是浮点数, Redis内部其实先将浮点数转化为字符串值,然后再保存。
embstr与raw类型底层的数据结构其实都是SDS(简单动态字符串, Redis内部定义 sdshdr一种结构)
那这两者的区别见下图:
1 int | Long类型整数时, RedisObject的ptr指针直接赋值为整数数据,不再额外的指针再指向整数了,节省了指针的空间开销。 |
2 embstr | 当保存的是字符串数据且字符串小于等于44字节时, embstr类型将会调用内存分配函数,只分配一块连续的内存空间,空间中依次包含 redisObject与 sdshdr两个数据结构,让元数据、指针和SDS是一块连续的内存区域,这样就可以避免内存碎片 |
3 raw | 当字符串大于44字节时,SDS的数据量变多变大了,SDS和 RedisObject布局分家各自过,会给SDS分配多的空间并用指针指向SDS结构,raw类型将会调用两次内存分配函数,分配两块内存空间,一块用于包含 redisObject结构,而另一块用于包含 sdshdr结构 |
1.3.1.3、总结
Redis内部会根据用户给的不同键值而使用不同的编码格式,自适应地选择较优化的内部编码格式,而这一切对用户完全透明!
1.3.2、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的编码方式
127.0.0.1:6379> config get hash*
1) "hash-max-ziplist-entries" //使用压缩列表保存时哈希集合中的最大元素个数 512
2) "512"
3) "hash-max-ziplist-value" //使用压缩列表保存时哈希集合中单个元素的最大长度 64
4) "64"
127.0.0.1:6379> hmset person id 11 age 12 name z3 birth 2021-0101 sex f
OK
127.0.0.1:6379> hgetall person
1) "id"
2) "11"
3) "age"
4) "12"
5) "name"
6) "z3"
7) "birth"
8) "2021-0101"
9) "sex"
10) "f"
127.0.0.1:6379> type person
hash
127.0.0.1:6379> object encoding person //编码格式为 ziplist
"ziplist"
127.0.0.1:6379> del person
(integer) 1
127.0.0.1:6379> config set hash-max-ziplist-entries 4 //设置使用压缩列表保存时哈希集合中的最大元素个数 为4
OK
127.0.0.1:6379> config set hash-max-ziplist-value 4 //设置使用压缩列表保存时哈希集合中单个元素的最大长度 为4
OK
127.0.0.1:6379> hmset person id 11 age 12 name z3 birth 2021-0101 sex f //person 的元素个数 超过4
OK
127.0.0.1:6379> type person
hash
127.0.0.1:6379> object encoding person //编码格式成了hashtable
"hashtable"
127.0.0.1:6379>
1.3.2.1、案例
[1] 结构
hash-max-ziplist-entries: 使用压缩列表保存时哈希集合中的最大元素个数。
hash-max-ziplist-value: 使用压缩列表保存时哈希集合中单个元素的最大长度。
[2] 结论
1.哈希对象保存的键值对数量小于512个
2.所有的键值对的健和值的字符长度都于等于64byte(一个英文字母一个字节)时用 ziplist,反之用 hashtable
3.ziplist升级到hashtable可以,反过来降级不可以
一旦从压缩列表转为了哈希表,Hash类型就会一直用哈希表进行保存而不会再转回压缩列表了。
在节省内存空间方面哈希表就没有压缩列表高效了。
[3] 流程
1.3.2.2、hash的两种编码格式
[1] ziplist
[2] hashtable
1.3.2.3、源码分析
[1] 源码位置 src/ziplist.c
Ziplist压缩列表是一种紧凑编码格式,总体思想是多花时间来换取节约空间,即以部分读写性能为代价,来换取极高的内存空间利用率, 因此只会用于字段个数少,且字段值也较小的场景。压缩列表内存利用率极高的原因与其连续内存的特性是分不开的。
想想我们学过的一种GC垃圾回收机制:标记--压缩算法,当一个hash对象只包含少量键值对且每个键值对的键和值要么就是小整数要么就是长度比较短的字符串,那么它用 ziplist作为底层实现
① ziplist什么样
ziplist是一个经过特殊编码的双向链表,它不存储指向上一个链表节点和指向下一个链表节点的指针,而是存储上一个节点长度和当前节点长度,通过牺牲部分读写性能,来换取高效的内存空间利用率,节约内存,是一种时间换空间的思想。只用在字段个数少,字段值小的场景里面
typedef struct zlentry {
unsigned int prevrawlensize; /* 存储上一个链表节点的长度数值所需要的字节数*/
unsigned int prevrawlen; /* 上一个链表节点占用的长度 */
unsigned int lensize; /* 存储当前链表节点长度数值所需要的字节数 */
unsigned int len; /* 当前链表节点占用的长度 */
unsigned int headersize; /* 当前链表节点的头部大小(prevrawlensize+lensize),即非数据域的大小 */
unsigned char encoding; /* 编码方式 */
unsigned char *p; /* 压缩链表以字符串的形式保存,该指针指向当前节点起始位置 */
} zlentry;
② ziplist各个组成单元什么意思
属性 | 类型 | 长度 | 用途 |
zlbytes | uint32_t | 4字节 | 记录整个压缩列表占用的内存字节数:在对压缩列表进行内存重分配,或者计算 zlend的位置时使用 |
zitail | uint32_t | 4字节 | 记录压缩列表表尾节点距离压缩列表的起始地址有多少字节:通过这个偏移量,程序无须遍历整个压缩列表就可以确定表尾节点的地址 |
zllen | uint16_t | 2字节 | 记录了压缩列表包含的节点数量:当这个属性的值小于UNINT16_MAX(65535)时,这个属性的值就是压缩列表包含节点的数量;当这个值等于UNINT16_MAX时,节点的真实数量需要遍历整个压缩列表才能计算得出 |
entryX | 列表节点 | 不定 | 压缩列表包含的各个节点,节点的长度由节点保存的内容决定 |
zlend | uint8_t | 1字节 | 特殊值0xFF(十进制255),用于标记压缩列表的末端 |
③ 明明有链表了,为什么出来一个压缩链表?
(1) 普通的双向链表会有两个指针,在存储数据很小的情况下,我们存储的实际数据的大小可能还没有指针占用内存大,得不偿失。 ziplist是
一个特殊的双向链表没有维护双向指针prev next;而是存储上一个entry的长度和当前entry的长度,通过长度推算下一个元素在什么地方。牺
牲读取的性能,获得高效的存储空间,因为(简短字符串的情况)存储指针比存储 entry长度更费内存。这是典型的“时间换空间”。
(2)链表在内存中一般是不连续的,遍历相对比较慢,而iziplist可以很好的解决这个问题,普通数组的遍历是根据数组里存储的数据类型找到下
一个元素的(例如int类型的数组访问下一个元素时每次只需要移动一个 sizeof(int)就行),但是 ziplist的每个节点的长度是可以不一样的,而我们
面对不同长度的节点又不可能直接 sizeof(entry),所以 ziplist只好将一些必要的偏移量信息记录在了每一个节点里,使之能跳到上一个节点或下
一个节点。
(3)头节点里有头节点里同时还有一个参数len,和 string类型提到的SDS类似,这里是用来记录链表长度的。因此获取链表长度时不用再遍历
整个链表,直接拿到len值就可以了,这个时间复杂度是O(1)
简单版:保持内存连续,去掉前指针,后指针,指针反而会浪费空间,只记录前一个节点的偏移量
④ 压缩列表节点的构成
压缩列表是 Redis为节约空间而实现的一系列特殊编码的连续内存块组成的顺序型数据结构,本质上是字节数组
在模型上将这些连续的数组分为3大部分,分别是 header+entry集合+end
其中 header由zlbytes+zltail+zllen组成,
entry是节点,
zlend是一个单字节255(1111 1111),用做 ZipList的结尾标识符。见下:压缩列表结构:由 zlbytes、 zltail、 zllen、 entry、 zlend这五部分组成
zlbytes 4字节,记录整个压缩列表占用的内存字节数
zltail 4字节,记录压缩列表表尾节点的位置。
zllen 2字节,记录压缩列表节点个数。
zlentry 列表节点,长度不定,由内容决定。
zlend 1字节,0xFF标记压缩的结束。
源码见ziplist.c
⑤ zlentry实体结构解析
压缩列表 zlentry节点结构:每个 zlentry由前一个节点的长度、 encoding和 entry-data三部分组成
typedef struct zlentry {
//上一个节点的长度所占的字节数
unsigned int prevrawlensize;
//上一个节点的长度
unsigned int prevrawlen;
//编码当前节点长度len所需要的字节数
unsigned int lensize;
//当前节点长度
unsigned int len;
//当前节点的header大小,header=prevrawlensize+lensize
unsigned int headersize;
//当前节点的编码格式
unsigned char encoding;
//当前节点指针
unsigned char *p;
} zlentry;
前节点:(前节点占用的内存字节数)表示前1个 zentry的长度,prev_len有两种取值情况:1字节或5字节。取值1字节时,表示上一个 entry的长度小于254字节。虽然1字节的值能表示的数值范围是0到255,但是压缩列表中 zlend的取值默认是255,因此,就默认用255表示整个压缩列表的结束,其他表示长度的地方就不能再用255这个值了。所以,当上一个entry长度小于254字节时, prev_len取值为1字节,否则,就取值为5字节。
enncoding:记录节点的 content保存数据的类型和长度。
content:保存实际数据内容
typedef struct zlentry {
//prevrawlensize是prevrawlen的大小,有1字节和5字节两种
unsigned int prevrawlensize;
//prevrawlen是上一个节点的长度
unsigned int prevrawlen;
//编码当前节点长度len所需要的字节数
unsigned int lensize;
//len为当前节点长度
unsigned int len;
//当前节点的header大小,header=prevrawlensize+lensize
unsigned int headersize;
//当前节点的编码格式
unsigned char encoding;
//当前节点指针
unsigned char *p;
} zlentry;
压缩列表的遍历:
通过指向表尾节点的位置指针p1,减去节点的 previous_entry_length,得到前一个节点起始地址的指针。如此循环,从表尾遍历到表头节点。从表尾向表头遍历操作就是使用这一原理实现的,只要我们拥有了一个指向某个节点起始地址的指针,那么通过这个指针以及这个节点的previous_entry_length属性程序就可以一直向前一个节点回溯,最终到达压缩列表的表头节点。
⑥ ziplist存取情况
[2] 源码位置 src/t_hash.c
在 Redis中, hashtable被称为字典(dictionary),它是一个数组+链表的结构
[3] OBJ_ENCONDING_HT 编码分析
每个键值对都会有一个 dictEntry
OBJ_ENCODING_HT这种编码方式内部才是真正的哈希表结构,或称为字典结构,其可以实现O(1)复杂度的读写操作,因此效率很高。
在 Redis内部,从 OBJ_ENCODING_HT类型到底层真正的散列表数据结构是一层层嵌套下去的,组织关系见下图:
源码位置 src/dict.h
.3.3、List数据结构介绍
1.3.3.1、案例
127.0.0.1:6379> config get list* //默认配置
1) "list-max-ziplist-size"
2) "-2"
3) "list-compress-depth"
4) "0"
(1) ziplist压缩配置 : list-compress-depth 0
表示一个 quicklist两端不被压缩的节点个数。这里的节点是指 quicklist双向链表的节点,而不是指 ziplist里面的数据项个数
参数 list-compress-depth 的取值含义如下:
0:是个特殊值,表示都不压缩。这是Redis的默认值。
1:表示 quicklist两端各有1个节点不压缩,中间的节点压缩。
2:表示 quicklist两端各有2个节点不压缩,中间的节点压缩。
3:表示 quicklist两端各有3个节点不压缩,中间的节点压缩。
依此类推
(2)ziplist中entry配置: list-max-ziplist-size -2
当取正值的时候,表示按照数据项个数来限定每个 quicklist节点上的ziplist长度。比如,当这个参数配置成5的时候,表示每个 quicklist节点的ziplist最多包含5个数据项。当取负值的时候,表示按照占用字节数来限定每个 quicklist节点上的ziplist长度。这时,它只能取-1到-5这五个值,
每个值含义如下:
-5:每个 quicklist节点上的ziplist大小不能超过64Kb.(注:1kb=>1024 bytes)
-4:每个 quicklist节点上的 ziplist大小不能超过32Kb
-3:每个 quicklist节点上的 ziplist大小不能超过16Kb.
-2:每个 quicklist节点上的 ziplist大小不能超过8Kb。(-2是 Redis给出的默认值)
-1:每个 quicklist节点上的 ziplist大小不能超过4Kb
1.3.3.2、List的一种编码格式
list用 quicklist来存储, quicklist存储了一个双向链表,每个节点都是一个 ziplist
qucklist 是ziplist和linkedlist的结合体
1.3.3.3、源码解析
源码位置 src/qucklist.h, head和tail指向双向列表的表头和表尾
/* quicklist is a 40 byte struct (on 64-bit systems) describing a quicklist.
* 'count' is the number of total entries.
* 'len' is the number of quicklist nodes.
* 'compress' is: -1 if compression disabled, otherwise it's the number
* of quicklistNodes to leave uncompressed at ends of quicklist.
* 'fill' is the user-requested (or default) fill factor.
* 'bookmakrs are an optional feature that is used by realloc this struct,
* so that they don't consume memory when not used. */
typedef struct quicklist {
quicklistNode *head; /* 指向双向列表的表头 */
quicklistNode *tail; /* 指向双向列表的表尾 */
unsigned long count; /* 所有的ziplist中一共存了多少个元素 */
unsigned long len; /* 双向链表的长度,node的数量 */
int fill : QL_FILL_BITS; /* fill factor for individual nodes */
unsigned int compress : QL_COMP_BITS; /* QL_COMP_BITS=16,压缩深度 0:不压缩 */
unsigned int bookmark_count: QL_BM_BITS;
quicklistBookmark bookmarks[];
} quicklist;
quicklistNode中的*zl指向一个ziplist,一个ziplist可以存放多个元素
typedef struct quicklistNode {
struct quicklistNode *prev; /* 前一个节点 */
struct quicklistNode *next; /* 后一个节点 */
unsigned char *zl; /* 指向实际的ziplist */
unsigned int sz; /* 当前ziplist占用多少字节 */
unsigned int count : 16; /* 当前ziplist中存储了多少个元素,占16bit(下同),最大65536个 */
unsigned int encoding : 2; /* 是否采用LZF压缩算法压缩节点,1:RAW 2:LZF */
unsigned int container : 2; /* 2:ziplist,未来可能支持其他结构存储 */
unsigned int recompress : 1; /* 当前ziplist是不是已经被解压出来临时使用 */
unsigned int attempted_compress : 1; /* 测试用 */
unsigned int extra : 10; /* 预留给未来使用 */
} quicklistNode;
1.3.4、Set数据结构介绍
1.3.4.1、案例
Redis用intset或hashtable存储set。如果元素都是整数类型,就用 intset存储。
如果不是整数类型,就用 hashtable(数组+链表来存储结构)。key就是元素的值, value为null
127.0.0.1:6379> config get set*
1) "set-max-intset-entries"
2) "512"
127.0.0.1:6379> config set set-max-intset-entries 3
OK
127.0.0.1:6379> sadd set1 123
(integer) 1
127.0.0.1:6379> object encoding set1
"intset"
127.0.0.1:6379> sadd set2 a b c d
(integer) 4
127.0.0.1:6379> object encoding set2
"hashtable"
127.0.0.1:6379> sadd set3 9223372036854775807
(integer) 1
127.0.0.1:6379> object encoding set3
"intset"
127.0.0.1:6379> sadd set3 9223372036854775808
(integer) 1
127.0.0.1:6379> object encoding set3
"hashtable"
127.0.0.1:6379>
集合元素都是long类型,并且元素个数<=set-max-intset-entries编码就是intset,反之就是hashtable
1.3.4.2、Set的两种编码格式
intset
hashtable
1.3.4.3、源码解析
源码位置 src/t_set.c
1.3.5、ZSet数据结构介绍
1.3.5.1、案例
当有序集合中包含的元素数量超过服务器属性 zset-max-ziplist-entries 的值(默认值为128),
或者有序集合中新添加元素的 member的长度大于服务器属性 zset-max-ziplist-value 的值(默认值为64)时,
redis会使用跳跃表作为有序集合的底层实现。
否则会使用 ziplist作为有序集合的底层实现
127.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"
127.0.0.1:6379> zadd zset1 80 a 70 b 90 c
(integer) 3
127.0.0.1:6379> object encoding zset1
"ziplist"
127.0.0.1:6379> zadd zset1 100 d
(integer) 1
127.0.0.1:6379> object encoding zset1
"skiplist"
127.0.0.1:6379> zadd zset2 80 aaaaa
(integer) 1
127.0.0.1:6379> object encoding zset2
"ziplist"
127.0.0.1:6379> zadd zset2 80 aaaaaxxxx
(integer) 1
127.0.0.1:6379> object encoding zset2
"skiplist"
127.0.0.1:6379>
1.3.5.1、ZSet的两种编码格式
ziplist
skiplist
1.3.5.2、源码分析
源码位置 src/t_zset.c
1.4、小总结
类型 | 编码 | 对象 |
REDIS_STRING | REDIS_ENCODING_INT | 使用整数值实现的字符对象 |
REDIS_STRING | REDIS_ENCODING_EMBSTR | 使用 embstr编码的简单动态字符串实现的字符串对象 |
REDIS_STRING | REDIS_ENCODING_RAW | 使用简单动态字符串实现的字符串对象 |
REDIS_LIST | REDISENCODING_ZIPLIST | 使用压缩列表实现的列表对象 |
REDIS_LIST | REDIS_ENCODING_LINKEDLIST | 使用双端链表实现的列表对象 |
REDIS_HASH | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的哈希对象 |
REDIS_HASH | REOIS_ENCODING_HT | 使用字典实现的哈希对象 |
REDIS_SET | REDIS_ENCODING_INTSET | 使用整数集合实现的集合对象 |
REDIS_ZSET | REDIS_ENCODING_ZIPLIST | 使用压缩列表实现的有序集合对象 |
REDIS_ZSET | REDIS_ENCODING_SKIPLIST | 使用跳跃表和字典实现的有序集合对象 |
redis数据类型以及数据结构的关系
不同数据类型对应的底层数据结构
1.字符串
int:8个字节的长整型。
embstr:小于等于44个字节的字符串。
raw:大于44个字节的字符串。
Redis会根据当前值的类型和长度决定使用哪种内部编码实现。
2.哈希
ziplist(压缩列表):当哈希类型元素个数小于hash-max-ziplist-entries配置(默认512个)、同时所有值都小于hash-max-ziplist-value配置(默认64字节)时,
Redis会使用ziplist作为哈希的内部实现, ziplist使用更加紧凑的结构实现多个元素的连续存储,所以在节省内存方面比 hashtable更加优秀。
hashtable(哈希表):当哈希类型无法满足ziplist的条件时, Redis会使用 hashtable作为哈希的内部实现,因为此时 ziplist的读写效率会下降,而 hashtable的读写时间复杂度为O(1)
3.列表
ziplist(压缩列表):当列表的元素个数小于list-max-ziplist-entries配置(默认512个),同时列表中每个元素的值都小于 list-max-ziplist-value配置时(默认64字节),
Redis会选用ziplist来作为列表的内部实现来减少内存的使用。
linkedlist(链表):当列表类型无法满足ziplist条件时, Redis会使用 linkedlist作为列表的内部实现。 quicklist ziplist和linkedlist的结合以ziplit的为节点的链表(linkedlist)
4.集合
intset(整数集合):当集合中的元素都是整数且元素个数小于set-max-intset-entries配置(默认512个)时, Redis会选用intset来作为集合的内部实现,从而减少内存的使用。
hashtable(哈希表):当集合类型无法满足intset的条件时 Redis会使用hashtable作为集合的内部实现。
5.有序集合
ziplist(压缩列表):当有序集合的元素个数小于 zset-max-ziplist entries配置(默认128个),同时每个元素的值都小于zset-max-ziplist-value配置(默认64字节)时,
Redis会用ziplist来作为有序集合的内部实现, ziplist可以有效减少内存的使用。
skiplist(跳跃表):当ziplist条件不满足时,有序集合会使用 skiplist作为内部实现,因为此时 ziplist的读写效率会下降。
redis数据类型以及数据结构的时间复杂度
名称 | 时间复杂度 |
哈希表 | O(1) |
跳表 | O(logN) |
双向链表 | O(N) |
压缩列表 | O(N) |
整数数组 | O(N) |
2、skiplist跳表面试题
2.1、是什么
跳表是可以实现二分查找的有序链表
skiplist是一种以空间换取时间的结构。由于链表,无法进行二分查找,因此借鉴数据库索引的思想,提取出链表中关键节点(索引),先在关键节点上查找,再进入下层链表查找。提取多层关键节点,就形成了跳跃表
总结来讲 跳表=链表+多级索引
2.2、说说链表和数组的优缺点?为什么引出跳表
2.2.1、痛点
优化
2.2.2、跳表的时间复杂度
跳表查询的时间复杂度分析
首先每一级索引我们提升了2倍的跨度,那就是减少了2倍的步数,所以是n/2、n/4、n/8以此类推;
第k级索引结点的个数就是n/(2^k);
假设索引有h级,最高的索引有2个结点:n/(2^h)=2,从这个公式我们可以求得h=log2(N)-1
所以最后得出跳表的时间复杂度是O(logN)
时间复杂度是 O(log N)
2.2.3、跳表的空间复杂度
跳表查询的空间复杂度分析
首先原始链表长度为n
如果索引是每2个结点有一个索引结点,每层索引的结点数:n/2,n/4,n/8 ...8,4,2以此类推;
或者索引是每3个结点有一个索引结点,每层索引的结点数:n/3,n/9,n/27 ... 9,3,1以此类推;
所以空间复杂度是O(n)
所以空间复杂度是 O(N)
2.3、优缺点
跳表是一个最典型的空间换时间解决方案,而且只有在数据量较大的情况下才能体现出来优势。而且应该是读多写少的情况下才能使用,所以它的适用范围应该还是比较有限的
维护成本相对要高-新增或者删除时需要把所有索引都更新一遍;
最后在新增和删除的过程中的更新,时间复杂度也是O(logn)
3、参考资料和官网
redis官网
http://redisdoc.com/