引言
详细、系统地学习Redis,这本书无疑是最佳选择。刷再多的文章比不上看书哈哈,个人观点~
《Redis设计与实现》写得比较通俗易懂,零基础的也易上手,从实现底层出发介绍但又没有那么多让人看者犯晕的源码,涉及到稍微复杂一点的逻辑都用伪代码和流程图实现。唯一的缺点就是内容有一点点冗余。
很早就买了,可惜看的太慢了,只有四分之一吧,也就是第一部分,下面把冗余的部分提炼一下,总结一下我看到的精华。
本文主要介绍Redis的数据结构与对象部分。Redis以5大对象来满足键的数据结构:字符串对对象,列表对象,哈希对象,集合对象,有序集合对象。这些对象的底层实现依赖特定的编码类型,而某种编码类型又对应1种或多种数据结构。下面分别介绍简单动态字符串、链表、字典、跳跃表、整数集合、压缩列表这6种基本数据结构在Redis中的实现。
先上文章思维导图:
文章导读
- 简单动态字符串(数据结构,区别,内存分配策略,复杂度)
- 链表(数据结构,复杂度)
- 字典(数据结构,哈希算法,渐进式rehash,复杂度)
- 跳跃表(数据结构,插入,查找,删除,复杂度)
- 整数集合(数据结构,升降级,复杂度)
- 压缩列表(数据结构,连锁更新,复杂度)
- 对象(存储结构,5大对象及编码结构,命令类型,内存回收,对象共享)
- 总结
一、简单动态字符串
Redis中,涉及可以被修改的字符串值时,都用简单动态字符串(simple dynamic string,SDS)来实现。比如包含字符串值的键值对在底层的实现。C字符串(C语言中传统字符串,以空字符串结尾的字符数组)则用于无须对字符串进行修改的地方,比如日志打印。
SDS还被用作缓冲区,比如AOF模块中的AOF缓冲区,客户端状态中的输入缓冲区。
1.1 SDS定义
struct sdshdr{
//buf已使用的字节数
int len;
//buf未使用的字节数
int free;
//字节数组,用于保存字符串
char buf[];
}
buf遵循C字符串以空字符串结尾的惯例,保存空字符串的1字节空间不计算在SDS的len属性里面,并为空字符分配额外1字节空间,对用户来说是透明的。
如中展示了SDS的数据结构,5字节未使用空间,已使用5字节,buf存储了字符串值,最后一个字节保存了空字符'\0'
。这里要注意的是,free和len的计算不涉及空字符。
1.2 SDS与C字符串的区别
- SDS有常数级的时间复杂度获取字符串长度。
由于C字符串不会记录自身长度,因此只能遍历,直到遇到结尾的空字符为止,时间复杂度为O(N)。而SDS对于字符串长度的记录都是在其API中执行的,所以时间复杂度为O(1)。 - SDS杜绝缓冲区溢出。
由于C字符串未记录自身长度,容易导致缓冲区溢出。在执行字符串拼接时,如果没有足够的空间,并且相邻内存地址被其他字符串占用时,字符串的数据将溢出,且容易意外修改相邻的字符串内容。相比而言,SDS会将这种情况扼杀在摇篮之中,SDS API先判断空间是否满足,如果不满足则将空间扩展至执行修改所需的大小。 - SDS拥有内存分配策略,详见1.3。
- SDS API都是二进制安全的。
C字符串的字符必须符合某种编码,并且中间不能有空字符,否则读取时会被误以为是字符串结尾。种种局限使得C字符串只能存文本,不能存图片,音频,视频,压缩文件等二进制数据。 为确保Redis对不同使用场景的支持,SDS API都是二进制安全的,也就是所有SDS API都会以二进制的方式存取buf中的数据,数据的写入和读出都是一个样的。由于SDS读取时并不是依靠空字符来判断结束的,而是len属性,所以是二进制安全的。 - 兼容部分C字符串函数。
SDS虽然都是二进制安全的,但也遵循以空字符结尾的习惯。SDS API总会在buf数组分配空间时多分配一个字节用于容纳空字符,这是为了保存文本的SDS重用一部分<string.h>库函数,避免代码重复。
1.3 内存分配策略
由于C字符并不记录自身长度,并且需要一个字符空间保存空字符串,因此每次增长或缩短字符串时,就要对其进行一次内存重分配操作。增长字符串时要看空间是否够用,否则会有缓冲区溢出;缩短字符串要释放不用的空间,否则会有内存泄漏。
Redis经常被用于速度要求严苛,数据被频繁修改的场合,每次修改字符串都要重新分配内存,就会占用很多时间。为避免这个问题,redis采用了空间预分配和惰性空间释放两种策略。
空间预分配
空间预分配用于优化SDS字符串增长操作。在扩展SDS空间前,SDS API会先检查未使用空间够不够,如果不够,则进行空间预分配。此时,程序不仅会为SDS分配修改所必须要的空间,还为其分配额外未使用的空间。
- 修改后的SDS<1MB,程序分配和len属性同样大小的未使用空间,此时SDS的len与free大小相等。比如修改后实际存储字符串的空间变为13字节,那么len=13,free=13,buf数组整体的长度=13+13+1(额外1字节保存空字符)。
- 修改后SDS>=1MB。程序会分配1MB的未使用空间。比如修改后实际存储字符串的空间变为2MB,那么len=2M,free=1MB,buf数组整体的长度=2MB+1MB+1byte。
通过空间的预分配,将连续增长N次字符串需要的内存分配次数从一定需要N次变为最多N次。因而可以减少连续执行字符串增长操作所需的内存重分配的次数。
惰性空间释放
惰性空间的释放用于优化SDS字符串缩短操作。当SDS API需要缩短保存的字符串时,程序并不立即回收这部分内存,而是使用free属性将字节的数量记录,等待使用。与此同时,SDS提供了相关API,在有需要时,真正释放未使用空间,不需要担心惰性空间造成的内存浪费。
C字符串与SDS的区别简单来说:
1.4 SDS时间复杂度
SDS相关操作及时间复杂度:
二、链表
当一个列表键包含了数量比较多的元素,或者列表中包含的元素都是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
2.1 链表和链表节点的实现
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
节点由前驱后继组成,多个节点组成的链表为双端链表。
使用adlist.h/list
来持有,操作链表:
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;
整个链表串起来后,如下图:
Redis的链表特性可以总结如下: