概述
redis数据库中每个键值对(key-value)都是由对象(object)组成:
- 数据库的key都是字符串对象(string object)
- 数据库的value可以是字符串对象、列表、字典、集合、有序集合对象
但是这五种类型,在redis底层并不是简单的封装c\c++的基本类型,redis设计了自己的一套底层数据结构。redis底层数据结构:
- 简单动态字符串
- 链表
- 字典
- 跳跃表
- 整数集合
- 压缩列表
下面会分别讨论这几种数据结构,以及redis是怎么使用他们构成value的数据类型
1 简单动态字符串
redis没有直接使用c语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(simple dynamic string, SDS)的抽象类型。在redis中,键值对的键是一个字符串对象,对象的底层实现是一个SDS。
1.1 SDS的定义
struct sdshdr{
int len //buf中已占用空间的长度len,等于字符串长度
int free //buf中剩余空间的长度
char buf[] //数据空间
}
一个SDS例子如下:
- free值为0,代表这个SDS的未使用空间为0
- len为5 表示保存了一个长度为5的字符串
- buf数组char类型数组 用于实际存储
1.2 SDS字符串和c字符串的区别
c语言使用长度为N+1的字符数组表示长度为N的字符传,并且字符数组最后一个元素总是空字符’\0’。例如:
但是c语言的这种方式,不能满足redis对于安全性、性能、功能的要求。
1.2.1 SDS可以常数复杂度获取字符串长度
c语言字符串要想获得字符串长度,需要遍历字符数组,复杂度为O(n)。
SDS字符串获取字符串数组长度,只需要返回struct中len字段 复杂度为O(1)。
1.2.2 杜绝缓冲区溢出
c 字符串 不记录字符串长度,除了获取的时候复杂度高以外,还容易导致缓冲区溢出。
如果我们现在将s1的内容修改为redis cluster,但是又忘了重新为s1分配足够的空间,这时候就会出现以下问题:
s1的数据溢出到s2所在空间中,导致s2保存的内容意外被修改。
如果使用SDS字符串就不会出现这种问题,在调用SDS修改api时,会检查free字段长度,如果不够则会扩展char[] buf ,重新分配空间,避免了缓冲区溢出问题。
1.2.3 减少修改字符串带来的内存重新分配次数
如1.2.1中描述,c语言字符串每次修改,都系要重新分配内存空间,而SDS字符串,因为有预留字符数组buf,可以极大的减少内存重新分配的次数。
1.2.4 二进制安全
C 字符串中的字符必须符合某种编码,并且除了字符串的末尾之外,字符串里面不能包含空字符,否则最先被程序读入的空字符将被误认为是字符串结尾,这些限制使得C字符串只能保存文本数据,而不能保存想图片,音频,视频,压缩文件这样的二进制数据。
但是在Redis中,不是靠空字符来判断字符串的结束的,而是通过len这个属性。那么,即便是中间出现了空字符对于SDS来说,读取该字符仍然是可以的。
2 链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
链表在Redis 中的应用非常广泛,比如列表键的底层实现之一就是链表。当一个列表键包含了数量较多的元素,又或者列表中包含的元素都是比较长的字符串时,Redis 就会使用链表作为列表键的底层实现。
2.1 链表数据结构
每一个链表节点用一个listNode结构表示:
typedef struct listNode{
struct listNode *prev;
struct listNode * next;
void * value;
}
链表的的结构为:
typedef struct list{
//表头节点
listNode * head;
//表尾节点
listNode * tail;
//链表长度
unsigned long len;
//节点值复制函数
void *(*dup) (void *ptr);
//节点值释放函数
void (*free) (void *ptr);
//节点值对比函数
int (*match)(void *ptr, void *key);
}
本质上就是一个双向链表
3 字典
实现类似于java中的hashMap
4 跳跃表
redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另外一个是在集群节点中用作内部数据结构。
4.1 跳跃表定义
我们先来看一下一整个跳跃表的完整结构:
redis 的跳跃表主要由两部分组成:zskiplist(链表)和zskiplistNode (节点)
zskiplistNode数据结构
typedef struct zskiplistNode{
//层
struct zskiplistLevel{
//前进指针
struct zskiplistNode *forward;
//跨度
unsigned int span;
} level[];
//后退指针
struct zskiplistNode *backward;
//分值
double score;
//成员对象
robj *obj;
}
- 层:level 数组可以包含多个元素,每个元素都包含一个指向其他节点的指针。
- 前进指针:用于指向表尾方向的前进指针
- 跨度:用于记录两个节点之间的距离
- 后退指针:用于从表尾向表头方向访问节点
- 分值和成员:跳跃表中的所有节点都按分值从小到大排序。成员对象指向一个字符串,这个字符串对象保存着一个SDS值
zskiplist数据结构:
typedef struct zskiplist {
//表头节点和表尾节点
structz skiplistNode *header,*tail;
//表中节点数量
unsigned long length;
//表中层数最大的节点的层数
int level;
}zskiplist;
4.2 总结
- 跳跃表是有序集合的底层实现之一
- 主要有zskiplist 和zskiplistNode两个结构组成
- 每个跳跃表节点的层高都是1至32之间的随机数
- 在同一个跳跃表中,多个节点可以包含相同的分值,但每个节点的对象必须是唯一的
- 节点按照分值的大小从大到小排序,如果分值相同,则按成员对象大小排序
5 整数集合
《Redis 设计与实现》中这样定义整数集合:“整数集合是集合建的底层实现之一,当一个集合中只包含整数,且这个集合中的元素数量不多时,redis就会使用整数集合intset作为集合的底层实现。”
我们可以这样理解整数集合,他其实就是一个特殊的集合,里面存储的数据只能够是整数,并且数据量不能过大。
5.1 整数集合定义
数据结构:
typedef struct intset{
//编码方式
uint32_t enconding;
// 集合包含的元素数量
uint32_t length;
//保存元素的数组
int8_t contents[];
}
一个整数集合结构示例:
- encoding:用于定义整数集合的编码方式
- length:用于记录整数集合中变量的数量
- contents:用于保存元素的数组,虽然我们在数据结构图中看到,intset将数组定义为int8_t,但实际上数组保存的元素类型取决于encoding
在上述数据结构图中我们可以看到,intset 在默认情况下会帮我们设定整数集合中的编码方式,但是当我们存入的整数不符合整数集合中的编码格式时,就需要使用到Redis 中的升级策略来解决
Intset 中升级整数集合并添加新元素共分为三步进行:
1、根据新元素的类型,扩展整数集合底层数组的空间大小,并为新元素分配空间
2、将底层数组现有的所有元素都转换成新的编码格式,重新分配空间
3、将新元素加入到底层数组中
5.2 总结
- 整数集合是集合建的底层实现之一
- 整数集合的底层实现为数组,这个数组以有序,无重复的范式保存集合元素,在有需要时,程序会根据新添加的元素类型改变这个数组的类型
- 升级操作为整数集合带来了操作上的灵活性,并且尽可能地节约了内存
- 整数集合只支持升级操作,不支持降级操作
6 压缩列表
压缩列表(ziplist)是列表键和哈希键的底层实现之一。当一个列表键只包含少量列表项,并且每个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现。
6.1 压缩列表的构成
一个压缩列表构成如下:
1、zlbytes:用于记录整个压缩列表占用的内存字节数
2、zltail:记录要列表尾节点距离压缩列表的起始地址有多少字节
3、zllen:记录了压缩列表包含的节点数量。
4、entryX:要说列表包含的各个节点
5、zlend:用于标记压缩列表的末端
6.2 总结
- 压缩列表是一种为了节约内存而开发的顺序型数据结构
- 压缩列表被用作列表键和哈希键的底层实现之一
- 压缩列表可以包含多个节点,每个节点可以保存一个字节数组或者整数值
- 添加新节点到压缩列表,可能会引发连锁更新操作。