Redis五种数据类型的底层结构
Redis中有一个核心对象叫做redisObject ,用来表示所有的键值对,用redisObject结构体来表示string、hash、list、set、zset这五种基本数据类型。
string 字符串
redis字符串的存储方式有两种:SDS(简单动态字符串)、直接存储(存储对象为整数时使用)
SDS特点:可动态扩容、二进制安全、快速遍历字符串 、兼容传统的C字符串。
string的编码:int、raw、embstr
-
直接存储,使用int编码
- int:存储对象为整数时使用
-
SDS存储,使用raw或embstr编码
-
raw:存储对象为长度大于32位的字符串时使用(创建/释放对象时共有两次内存分配:1、创建/释放redisObject对象,2、创建/释放sdshdr结构。此时redisObject和sdshdr并不在一个连续空间内)
-
embstr:存储对象为长度小于等于32位的字符串时使用(创建/释放对象时只有一次内存分配,redisObject和sdshdr都分配在同一个连续的内存空间里)
-
SDS类似于Java的ArrayList,采用预分配冗余空间的方式来减少内存的频繁分配,可动态扩容。
传统的C字符串与以 ‘\0’ 字符作为结尾,会忽略 ‘\0’ 结尾以后的所有字符,即无法存储带有 ‘\0’ 的字符串数据(比如图片、视频等二进制文件)。Redis的SDS在传统的C字符串基础上增加了一个字符串头(sdshdr),字符串头中的len成员记载了该字符串的长度,并根据len成员来判断该字符串的结尾位置,这意味着它可以存放任何二进制的数据和文本数据,包括 ‘\0’ ,所以SDS是二进制安全的。因为有len成员的存在,获取字符串长度的时间复杂度为O(1)。
list 列表(队列)
在redis 3.2之前,list底层采用ziplist(压缩列表)与linkedlist(双向链表)存储
-
linkedlist:本质上是一个双向链表,每一个节点都是一个存储对象。因为链表存储空间不连续,链表节点在内存中过于分散,因此会产生过多的空间碎片。linkedlist一般用于存储数据较多较大的列表对象。
-
ziplist:类似于字节数组,ziplist的节点在内存中是连续存储的,但是不同于数组,为了节省内存,ziplist的每个节点所占的内存大小可以不同。每个节点可以是一个字节数组或一个整数。只有在满足以下两个条件时,redis才会用ziplist存储列表对象:
(1)列表对象保存的所有字符串元素的长度均小于64字节。
(2)列表对象保存的元素数量小于512个。
在redis 3.2,添加了新的数据结构quicklist(快速列表),统一使用quicklist来存储列表对象。
- quicklist:快速列表也是一个双向链表,quicklist是由ziplist和linkedlist组合而成的,它的每一个节点都是一个ziplist。quicklist既弥补了ziplist占用连续空间过大的缺点,又避免了像linkedlist那样空间占用过于碎片化。但quicklist并非所有节点都为ziplist,quicklist在存储对象时会对列表中间的一批节点进行压缩操作,列表中间被压缩的节点数据结构为quicklistZF(被压缩过的ziplist),列表两端节点数据结构为ziplist。因为两端节点会被频繁操作,所以quicklist不会压缩两端节点,既优化了空间又保证了性能。
set 集合
无序集合,集合成员是唯一的,集合内不能出现重复的数据。
set的编码:intset(整数集合)、hashtable(哈希表)。
-
intset:一个整数集合,所有元素都保存在此。能存储三种类型的数据,分别是int16_t、int32_t、int64_t。intset类似于SDS和数组,内存连续。
集合对象使用intset编码需要满足以下两个条件:
(1)所有元素都是整数值。
(2)元素个数小于等于512个。 -
hashtable:底层由字典实现,用字典的key键保存集合对象,字典的value值为null。
sorted set 有序集合(zset)
sorted set的编码:ziplist、skiplist(跳跃表)。
-
ziplist:sorted set使用ziplist存储对象需要满足以下两个条件:
(1)所有元素长度小于64字节。
(2)元素个数小于128个。 -
skiplist:skiplist本质上也是一个链表,但它的每个节点都会随机生成不同的层数,是一种概率型数据结构。和平衡树相比,跳跃表的实现更为简单,在做范围查找操作及插入/删除操作时,平衡树都要比跳跃表复杂(进行范围查找时平衡树需要进行中序遍历,较难实现,而跳跃表只需要在找到小值之后,对第一层链表进行遍历就可以实现。进行插入/删除操作时平衡树的子树有可能需要进行一定的调整,而跳跃表只需要简单地修改一下相邻节点的指针。)。
跳跃表节点层数生成代码:
int randomizeLevel(double p, int lmax) {
//初始层数为1
int level = 1;
//随机数函数
Random random = new Random();
//如果生成的随机数符合条件,则层数+1,进入下一层循环
while (random.nextDouble() < p && level < lmax) {
//层数越高,概率越低
level++;
}
//如果生成的随机数不符合条件,跳出循环,返回当前层数
return level;
}
如果生成一层的概率为1/2,则生成两层的概率就为1/4,三层为1/8,层数越高,生成的概率越低。
hash 哈希
hash的编码:ziplist、hashtable(哈希表)。
-
ziplist:在ziplist中,键值对是以紧密相连的方式放入的,先放入key,再放入value,键值对总是向表尾添加。hash对象需要满足以下两个条件才能使用ziplist存储:
(1)所有键值对的键和值的字符串长度都小于64字节。
(2)键值对数量小于512个。 -
hashtable:底层是字典,hash对象的每一个键值对都直接存入字典中。