SDS 简单动态字符串
每个 sds.h/sdshdr 结构表示一个 SDS 值:
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
为什么不使用c语言简单字符串
原因有如下
获取长度复杂度为O(1)
避免缓冲区溢出
优化策略
redis非常注重性能,于是优化是非常必要的。
减少内存重分配
空间预分配
内存的分配设计到复杂算法或可能的系统调用,比较耗时
于是,每次再字符串增长操作时,不仅仅会扩容到刚好所需的空间,而是会通过一定策略额外分配空间
惰性空间释放
字符串的缩短操作,不会进行内存重分配,而是会通过free记录起来,为未来可能存在的增长提供优化
2.链表
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
还有有一个头节点指向表头和表尾
typedef struct listNode {
listNode *prev;
listNode *next;
void *value;
}listNode;
typedef struct list{
listNode *head;
listNode *tail;
unsigned long len;
}list;
字典
/*
* 字典
** 每个字典使用两个哈希表,用于实现渐进式 rehash
*/
typedef struct dict {
// 特定于类型的处理函数
dictType *type;
// 类型处理函数的私有数据
void *privdata;
// 哈希表(2 个)
dictht ht[2];
// 记录 rehash 进度的标志,值为-1 表示 rehash 未进行
int rehashidx;
// 当前正在运作的安全迭代器数量
int iterators;
} dict;
/*
* 哈希表
*/
typedef struct dictht {
// 哈希表节点指针数组(俗称桶,bucket)
dictEntry **table;
// 指针数组的大小
unsigned long size;
// 指针数组的长度掩码,用于计算索引值
unsigned long sizemask;
// 哈希表现有的节点数量
unsigned long used;
} dictht;
table 属性是一个数组,数组的每个元素都是一个指向 dictEntry 结构的指针。
每个 dictEntry 都保存着一个键值对,以及一个指向另一个 dictEntry 结构的指针:
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
/*
* 哈希表节点
*/
typedef struct dictEntry {
// 键
void *key;
// 值
union {
void *val;
uint64_t u64;
int64_t s64;
} v;
// 链往后继节点
struct dictEntry *next;
} dictEntry;
next 属性指向另一个 dictEntry 结构,多个 dictEntry 可以通过 next 指针串连成链表,从
这里可以看出,dictht 使用链地址法来处理键碰撞:当多个不同的键拥有相同的哈希值时,哈
希表用一个链表将这些键连接起来。
整个字典结构可以表示如下:
1.新创建的两个哈希表都没有为 table 属性分配任何空间:
• ht[0]->table 的空间分配将在第一次往字典添加键值对时进行;
• ht[1]->table 的空间分配将在 rehash 开始时进行;
2.根据字典所处的状态,将一个给定的键值对添加到字典可能会引起一系列复杂的操作:
• 如果字典为未初始化(也即是字典的 0 号哈希表的 table 属性为空),那么程序需要对 0
号哈希表进行初始化;
• 如果在插入时发生了键碰撞,那么程序需要处理碰撞;
• 如果插入新元素使得字典满足了 rehash 条件,那么需要启动相应的 rehash 程序;
跳跃表
表头(head):负责维护跳跃表的节点指针。
• 跳跃表节点:保存着元素值,以及多个层。
• 层:保存着指向其他元素的指针。高层的指针越过的元素数量大于等于低层的指针,为了
提高查找的效率,程序总是从高层先开始访问,然后随着元素值范围的缩小,慢慢降低层
次。
• 表尾:全部由 NULL 组成,表示跳跃表的末尾
typedef struct zskiplist {
// 头节点,尾节点
struct zskiplistNode *header, *tail;
// 节点数量
unsigned long length;
// 目前表内节点的最大层数
int level;
} zskiplist;
typedef struct zskiplistNode {
// member 对象
robj *obj;
// 分值
double score;
// 后退指针
struct zskiplistNode *backward;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 这个层跨越的节点数量
unsigned int span;
} level[];
} zskiplistNode;
整数集合
整数集合(intset)用于有序、无重复地保存多个整数值,它会根据元素的值,自动选择该用什
么长度的整数类型来保存元素。
举个例子,如果在一个 intset 里面,最长的元素可以用 int16_t 类型来保存,那么这个 intset
的所有元素都以 int16_t 类型来保存。
另一方面,如果有一个新元素要加入到这个 intset ,并且这个元素不能用 int16_t 类型来保存
——比如说,新元素的长度为 int32_t ,那么这个 intset 就会自动进行“升级” :先将集合中现
有的所有元素从 int16_t 类型转换为 int32_t 类型,接着再将新元素加入到集合中。
根据需要,intset 可以自动从 int16_t 升级到 int32_t 或 int64_t ,或者从 int32_t 升级到
int64_t 。
压缩列表
Ziplist 是由一系列特殊编码的内存块构成的列表,一个 ziplist 可以包含多个节点(entry),每
个节点可以保存一个长度受限的字符数组(不以 \0 结尾的 char 数组)或者整数,包括:
• 字符数组
– 长度小于等于 63 (26 − 1)字节的字符数组
– 长度小于等于 16383 (2
14 − 1)字节的字符数组
– 长度小于等于 4294967295 (2
32 − 1)字节的字符数组
• 整数
– 4 位长,介于 0 至 12 之间的无符号整数
– 1 字节长,有符号整数
– 3 字节长,有符号整数
– int16_t 类型整数
– int32_t 类型整数
– int64_t 类型整数
因为 ziplist 节约内存的性质,它被哈希键、列表键和有序集合键作为初始化的底层实现来使用
ziplist的构成
pre_entry_length 记录了前一个节点的长度,通过这个值,可以进行指针计算,从而跳转到上一个节点。
encoding 和 length 两部分一起决定了 content 部分所保存的数据的类型(以及长度)。
content 部分保存着节点的内容,它的类型和长度由 encoding 和 length 决定。