SDS
1.定义
SDS(simple dynamic string)即简单动态字符串,在Redis中并没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组),而使用自己构造的SDS。
2.源码
(1)结构体sds.h/sdshdr的源码如下:
/*
* 保存字符串对象的结构
*/
struct sdshdr {
// buf 中已占用空间的长度
int len;
// buf 中剩余可用空间的长度
int free;
// 数据空间
char buf[];
};
分析:
- len字段是用来保存sds字符串中所包含字符数目的,
- free字段则是用来保存buf数组中空余的部分的长度的,
- buf数组则是实际用来保存字符串的。
(2)图例:
3.实例
将“Hello World!”这个字符串放入SDS中,结构如下图
4.SDS与C字符串的区别
(1)常数复杂度获取字符串长度
通过读取sds对象的len属性的值我们可以使用O(1)获取sds对象保存的字符串长度,
在c字符串中,我们必须对整个数组进行遍历从而获取字符串的长度,其时间复杂度为O(N)。
(2)杜绝缓冲区溢出
在c字符串中,strcat函数将src连接到dest的末尾,但是c字符串假定dest数组中有足够的空余空间来保存src数组,如果dest数组长度不够就会造成缓冲区溢出
在sds对象中也提供了类似的函数 sdscat 和 sdscatsds,这两个函数在调用之前会检查目标sds对象s中free属性是否能够保存要连接的字符串的长度,如果不够,就会对目标sds对象扩容,这就保证了sds对象不会造成缓冲区溢出。
(3)减少修改字符串时内存重分配的次数
在对sds进行修改的时候,redis可以通过“空间预分配”和“惰性空间释放”来保证后续对sds对象的频繁修改而不会造成sds对象的buf数组经常分配空间;
- 空间预分配:对SDS修改后,若len<1MB,则分配free=len,若len>=1MB,则分配free=1MB。
- 惰性空间释放:当需要缩短SDS保存的字符串时,程序并不立即使用内存重分配来缩短多出来的字节,而是使用free将缩短的字节数记录起来,等待将来使用
而c字符串,每次对其进行修改都需要进行一次空间分配和复制操作。
(4)二进制安全
c字符串由于其判断是否结束的标志是从字符串开始到结尾碰到的第一个“\0”字符,这就限制了c字符串不能保存像图片、音频、视频、压缩文件等二进制保存的内容;
sds对象,由于判断其是否结束的标志是其len属性,也就是说无论在len长度内,buf数组中是否包含“\0”都不影响redis判断其是否结束。
5.SDS的API
链表
1.定义
list的内部实现是一个双向链表(无环),由多个listnode组成。
2.源码
- adlist.h/listnode源码:
/*
* 双端链表节点
*/
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
- adlist.h/list源码:
/*
* 双端链表结构
*/
typedef struct list {
// 表头节点
listNode *head;
// 表尾节点
listNode *tail;
// 节点值复制函数
void *(*dup)(void *ptr);
// 节点值释放函数
void (*free)(void *ptr);
// 节点值对比函数
int (*match)(void *ptr, void *key);
// 链表所包含的节点数量
unsigned long len;
} list;
3.图例
- listnode图例
- list图例
4.特点
(1)双端
链表节点带有prev和next指针,获取某个节点的前置节点与后置节点的复杂度都是O(1)
(2)无环
表头节点的prev指针和表尾节点的next指针都指向null,对链表的访问以null为终点
(3)头指针和尾指针
通过list结构的head指针和tail指针,程序获取链表的表头节点和表尾节点的复杂度都为O(1)
(4)链表长度计数器
程序使用list结构的len属性来对list持有的链表节点进行计数,程序获取链表中节点数量的复杂度为O(1)
(5)多态
链表节点使用void*指针来保存节点值,并且可以通过list结构的dup、free、match三个属性为节点值设置类型特定函数,所以链表可用于保存不同类型的值。
5.链表的API
字典
1.定义
字典又称为符号表、关联数组或映射,是用于保存键值对的抽象数据结构,字典中的键是唯一的。
Redis的一个database中所有key到value的映射,就是使用一个dict来维护。
字典采用了一种称为增量式重哈希(incremental rehashing)的方法,在需要扩展内存时避免一次性对所有key进行重哈希,而是将重哈希操作分散到对于dict的各个增删改查的操作中去
dict也是一个基于哈希表的算法。和传统的哈希算法类似,它采用某个哈希函数从key计算得到在哈希表中的位置,采用拉链法解决冲突,并在装载因子(load factor)超过预定值时自动扩展内存,引发重哈希(rehashing)。
2.源码
(1)哈希表dict.h/dictht源码
/*
* 哈希表
*
* 每个字典都使用两个哈希表,从而实现渐进式 rehash 。
*/
typedef struct dictht {
// 哈希表数组
dictEntry **table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计算索引值
// 总是等于 size - 1
unsigned long sizemask;
// 该哈希表已有节点的数量
unsigned long used;
} dictht;
dictht分析:
一个dictEntry指针数组(table)。key的哈希值最终映射到这个数组的某个位置上(对应一个bucket)。如果多个key映射到同一个位置,就发生了冲突,那么就拉出一个dictEntry链表。
size:标识dictEntry指针数组的长度。它总是2的指数。
sizemask:用于将哈希值映射到table的位置索引。它的值等于(size-1),比如7, 15, 31, 63,等等,也就是用二进制表示的各个bit全1的数字。每个key先经过hashFunction计算得到一个哈希值,然后计算(哈希值 & sizemask)得到在table上的位置。相当于计算取余(哈希值 % size)。
used:记录dict中现有的数据个数。它与size的比值就是装载因子(load factor)。这个比值越大,哈希值冲突概率越高。
(2)哈希表节点dict.h/dictEntry的源码
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
(3)字典dict.h/dict的源码
/*
* 字典
*/
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int rehashidx; /* rehashing not in progress if rehashidx == -1 */
// 目前正在运行的安全迭代器的数量
int iterators; /* number of iterators currently running */
} dict;
dict分析:
一个指向dictType结构的指针(type)。它通过自定义的方式使得dict的key和value能够存储任何类型的数据。
一个私有数据指针(privdata)。由调用者在创建dict的时候传进来。
两个哈希表(ht[2])。只有在重哈希的过程中,ht[0]和ht[1]才都有效。而在平常情况下,只有ht[0]有效,ht[1]里面没有任何数据。上图表示的就是重哈希进行到中间某一步时的情况。
当前重哈希索引(rehashidx)。如果rehashidx = -1,表示当前没有在重哈希过程中;否则,表示当前正在进行重哈希,且它的值记录了当前重哈希进行到哪一步了。
当前正在进行遍历的iterator的个数。
3.图例
- 哈希表图例
- 字典图例
4.哈希算法
当要将一个新的键值对添加到字典里面,程序需要先根据键值对的键计算出哈希值和索引值,然后再放入哈希数组指定的索引上面。
计算哈希值:hash=通过hashFunction(key)函数获取,
计算索引值:index=hash&sizemask(哈希值与哈希表的sizemask相与获取)解决键(hash)冲突的方法有链地址法、开放定址法、再哈希法、公共溢出区法,Redis使用的是链地址法。
rehash过程将在下篇的博文中详细介绍
5.字典的API
跳跃表
1.定义
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。
2.源码
- 跳跃表节点redis.h/zskiplistNode源码
/* ZSETs use a specialized version of Skiplists */
/*
* 跳跃表节点
*/
typedef struct zskiplistNode {
// 成员对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
- 跳跃表redis.h/zskiplist的源码
/*
* 跳跃表
*/
typedef struct zskiplist {
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
3.图例
4.跳跃表的API
压缩链表
1.定义
ziplist是一个经过特殊编码的双向链表,它的设计目标就是为了提高存储效率。
ziplist可以用于存储字符串或整数,其中整数是按真正的二进制表示进行编码的,而不是编码成字符串序列。
ziplist能以O(1)的时间复杂度在表的两端提供push和pop操作。
2.源码
压缩表节点ziplist.c/zlentry的源码
/*
* 保存 ziplist 节点信息的结构
*/
typedef struct zlentry {
// prevrawlen :前置节点的长度
// prevrawlensize :编码 prevrawlen 所需的字节大小
unsigned int prevrawlensize, prevrawlen;
// len :当前节点值的长度
// lensize :编码 len 所需的字节大小
unsigned int lensize, len;
// 当前节点 header 的大小
// 等于 prevrawlensize + lensize
unsigned int headersize;
// 当前节点值所使用的编码类型
unsigned char encoding;
// 指向当前节点的指针
unsigned char *p;
} zlentry;
3.图解
(1)压缩列表的组成部分
分析:
(2)压缩列表节点的组成部分
分析:
- previous_entry_length以字节为单位,记录了压缩列表的前一个节点的长度,若前一个节点长度<254字节,它=1字节。否则它=5字节。
- encoding记录了节点所保存数据的类型及长度
- content负责保存节点的值
4.连锁更新
(1)定义
产生连续多次空间扩展操作称为连锁更新
(2)产生条件
添加节点或删除节点
4.ziplist的API
整数集合
1.定义
整数集合是集合键的底层实现之一,它是Redis用于保存整数值的集合抽象数据结构,保证集合中不会出现重复元素。
2.源码
整数集合insert.h/insert的源码
typedef struct intset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
3.图例
4.升级
(1)升级原因
当新的元素加入到整数集合时,若新元素的类型比整数集合现有的元素长时,就需要升级
(2)升级步骤
- 根据新元素的类型,扩展整数集合数组空间的大小,并未新元素分配空间
- 将数组的所有元素转换为新元素类型,维持底层数组的有序性
- 将新元素添加到底层数组。
(3)升级的优点
- 提升整数集合的灵活性
- 节约内存
(4)特点
只能升级不能降级
5.intset的API
quicklist
1.出现原因
quicklist是Redis3.2的新特性,而Redis3.0并没有这个数据结构。
双向链表便于在表的两端进行push和pop操作,但是它的内存开销比较大。首先,它在每个节点上除了要保存数据之外,还要额外保存两个指针;其次,双向链表的各个节点是单独的内存块,地址不连续,节点多了容易产生内存碎片。
ziplist由于是一整块连续内存,所以存储效率很高。但是,它不利于修改操作,每次数据变动都会引发一次内存的realloc。特别是当ziplist长度很长的时候,一次realloc可能会导致大批量的数据拷贝,进一步降低性能。
quicklist结合了双向链表和ziplist的优点,是list列表的底层数据结构。
quicklist本身是一个双向无环链表,它的每一个节点都是一个ziplist。
2.源码
(1)quicklist.h/quicklistNode的源码
/* quicklistNode is a 32 byte struct describing a ziplist for a quicklist.
* We use bit fields keep the quicklistNode at 32 bytes.
* count: 16 bits, max 65536 (max zl bytes is 65k, so max count actually < 32k).
* encoding: 2 bits, RAW=1, LZF=2.
* container: 2 bits, NONE=1, ZIPLIST=2.
* recompress: 1 bit, bool, true if node is temporarry decompressed for usage.
* attempted_compress: 1 bit, boolean, used for verifying during testing.
* extra: 12 bits, free for future use; pads out the remainder of 32 bits */
typedef struct quicklistNode {
struct quicklistNode *prev; // 指向上一个ziplist节点
struct quicklistNode *next; // 指向下一个ziplist节点
unsigned char *zl; // 数据指针,如果没有被压缩,就指向ziplist结构,反之指向quicklistLZF结构
unsigned int sz; // 表示指向ziplist结构的总长度(内存占用长度)
unsigned int count : 16; // 表示ziplist中的数据项个数
unsigned int encoding : 2; // 编码方式,1--ziplist,2--quicklistLZF
unsigned int container : 2; // 预留字段,存放数据的方式,1--NONE,2--ziplist
unsigned int recompress : 1; // 解压标记,当查看一个被压缩的数据时,需要暂时解压,标记此参数为1,之后再重新进行压缩
unsigned int attempted_compress : 1; // 测试相关
unsigned int extra : 10; // 扩展字段,暂时没用
} quicklistNode;
(2)quicklist.h/quicklistLZF的源码
/* quicklistLZF is a 4+N byte struct holding 'sz' followed by 'compressed'.
* 'sz' is byte length of 'compressed' field.
* 'compressed' is LZF data with total (compressed) length 'sz'
* NOTE: uncompressed length is stored in quicklistNode->sz.
* When quicklistNode->zl is compressed, node->zl points to a quicklistLZF */
typedef struct quicklistLZF {
unsigned int sz; // LZF压缩后占用的字节数
char compressed[]; // 柔性数组,指向数据部分
} quicklistLZF;
(3)quicklist.h/quicklist的源码
/* quicklist is a 32 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. */
typedef struct quicklist {
quicklistNode *head; // 指向quicklist的头部
quicklistNode *tail; // 指向quicklist的尾部
unsigned long count; // 列表中所有数据项的个数总和
unsigned int len; // quicklist节点的个数,即ziplist的个数
int fill : 16; // ziplist大小限定,由list-max-ziplist-size给定
unsigned int compress : 16; // 节点压缩深度设置,由list-compress-depth给定
} quicklist;
3.图解
4.quicklist的API
函数 | 作用 |
---|---|
quicklistCreate | 创建一个空的quicklist |
quicklistPush | 首尾插入节点,如果插入节点中的ziplist大小没有超过限制(list-max-ziplist-size),那么直接调用ziplistPush函数压入,否则新建一个quicklist节点 |
quicklistPopCustom | 弹出首尾节点 |
quicklistSetOptions | 设置ziplist大小配置参数(list-max-ziplist-size)和节点压缩深度配置参数(list-compress-depth) |
本人才疏学浅,若有错,请指出,谢谢!
如果你有更好的建议,可以留言我们一起讨论,共同进步!
衷心的感谢您能耐心的读完本篇博文!
参考书籍:《Redis设计与实现(第二版)》—黄健宏
参考链接:快速列表quicklist