上一篇我们已经介绍完了Redis的内存模型,本篇我们一起来看一下Redis数据结构。Redis常用的数据结构有以下几种:简单动态字符串
链表
字典
跳表
整数集合
压缩列表
对象
Redis 没有直接使用 C 字符串(即以空字符’\0’结尾的字符数组)作为默认的字符串表示,而是使用了SDS。SDS 是简单动态字符串(Simple Dynamic String)的缩写。它是自己构建了一种名为 简单动态字符串(simple dynamic string,SDS)的抽象类型,并将 SDS 作为Redis的默认字符串表示。struct sdshdr{
//记录buf数组中已使用字节的数量
//等于 SDS 保存字符串的长度
int len;
//记录 buf 数组中未使用字节的数量
int free;
//字节数组,用于保存字符串
char buf[];
}
其中,len 保存了SDS保存字符串的长度,buf[] 数组用来保存字符串的每个元素,free记录了 buf 数组中未使用的字节数量。
buf数组的长度=free+len+1。SDS在c字符串的基础上加入了free和len字段,带来了很多好处:获取字符串长度时间复杂度降低
避免了缓冲区溢出
可以使用空间预分配策略
存取二进制数据
获取字符串长度SDS是O(1)的时间复杂度,而 c字符串是O(n)的时间复杂度。使用 C 字符串的 API 时,如果字符串长度增加(如 strcat 操作)而忘记重新分配内存,很容易造成缓冲区的溢出。而 SDS 由于记录了长度,相应的 API 在可能造成缓冲区溢出时会自动重新分配内存,杜绝了缓冲区溢出。修改字符串时内存的重分配:对于 C 字符串,如果要修改字符串,必须要重新分配内存(先释放再申请),因为如果没有重新分配,字符串长度增大时会造成内存缓冲区溢出,字符串长度减小时会造成内存泄露。而对于 SDS,由于可以记录 len 和 free,因此解除了字符串长度和空间数组长度之间的关联,可以在此 基础上进行优化。空间预分配策略(即分配内存时比实际需要的多)使得字符串长度增大时重新分配内存的概率大大减小。惰性空间释放策略使得字符串长度减小时重新分配内存的概率大大减小。SDS 可以存取二进制数据,C 字符串不可以。因为 C 字符串以空字符作为字符串结束的标,而对于 一些二进制文件(如图片等)。内容可能包括空字符串,因此 C 字符串无法正确存取,而 SDS 以字符串长度 len 来作为字符串结束标识,因此没有这个问题。此外,由于 SDS 中的 buf 仍然使用了 C 字符串(即以’\0’结尾),因此 SDS 可以使用 C 字符串库中的 部分函数。但是需要注意的是,只有当 SDS 用来存储文本数据时才可以这样使用,在存储二进制数据时则不行 (’\0’不一定是结尾)。接下来我们说一下链表,链表在Redis中的应用非常广泛,列表(List)的底层实现之一就是双向链表。此外发布与订阅、慢查询、 监视器等功能也用到了链表。其数据结构定义如下:typedef struct listNode {
//前置节点 struct listNode *prev;
//后置节点 struct listNode *next;
//节点的值 void *value;}listNode
typedef struct list {
//表头节点 listNode.head; 表尾节点 listNode.tail;
//链表所包含的节点数量
unsigned long len;
//节点值复制函数 void *(*dup)(void *ptr);
//节点值释放函数 void *(*free)(void *ptr);
//节点值对比函数 int (*match)(void *ptr,void *key);} list;

双端
无环
带链表长度计数器
多态
链表具有前置节点和后置节点的引用,获取这两个节点时间复杂度都为O(1)。表头节点的 prev 指针和表尾节点的 next 指针都指向 NULL,对链表的访问都是以 NULL 结束。通过 len 属性获取链表长度的时间复杂度为 O(1)。链表节点使用 void* 指针来保存节点值,可以保存各种不同类型的值。