1.简单动态字符串(SDS)
数据结构
SDS结构定义
struct sdshdr{
//SDS中保存的字符串的长度
int len;
//SDS中未使用的空间长度
int free;
//保存字符串的字节数组
char buf[];
}
例如在SDS中存入Redis这个单词,结构如图所示(本文图均来自《Redis设计和实现(第二版)》):
Redis的底层源码是基于C语言实现的,所以字符串存储时和C一样,最后一位需要加空字符’/0’。
既然基于C语言实现,那为什么不直接使用C语言中定义的字符串呢,这是因为相较于C字符串,SDS有很多优点。
SDS的优点
- 常数复杂度获得字符串的长度。在SDS的结构定义中,很明显可以看到自带字符串长度的属性,因此我们可以复杂度O(1)来获得长度,而C很显然需要遍历,复杂度为O(N)。
- 缓冲区不会溢出。同样是相对于C字符串而言。C字符串在修改时,字符串的长度如果大于旧字符串的话,会导致当前的存储空间向后扩大,那么很可能之前紧邻的另一个字符串的部分字符会被意外的覆盖修改,也就是说字符串A(当前修改的)的数据溢出到紧邻的字符串B。SDS分配内存的策略不同,在修改或者拼接字符串前,先检查空间是否够,不如果不够那么先扩展,拷贝到新空间中,释放原空间,在新空间中进行修改或者拼接的操作。
- 减少频繁的内存重分配。在重分配内存空间时,如果需要分配13个字节,那么SDS在分配13个字节供数据使用外,还会分配同样长度的13字节的未使用空间,以便下次使用。例如,
重分配前:
分配后:
可以看到free值为13。 - 二进制安全。C字符串遇到空字符’/0’时会认为结束,因此不能保存二进制文件。SDS虽然也是以空字符作为字符串的结尾,但SDS用字符串长度len来判断是否是字符串的结尾。
- 兼容部分C字符串函数。正因为SDS字符串结尾处和C字符串一样以空字符’/0’作为结尾,所以可以兼容C字符串的额部分函数。
2.链表
链表结构定义
节点的定义:
typedef struct listNode{
//前驱
struct listNode *prev;
//后继
struct listNode *next;
//值
void *value;
}listNode;
很显然redis中的链表是个双向链表,如下图(图来自《Redis设计和实现(第二版)》)。
链表的定义:
typedef struct list{
//表头节点
listNode *head;
//表尾节点
listNode *tail;
//链表中节点的数量
unsigned long len;
//节点值复制函数
void *(*dup)(void *ptr);
//节点值释放函数
void (*free)(void *ptr);
//节点值对比函数,判断两个值是否相等。
int (*match)(*ptr,*key);
}list;
图示如下:
可以看出,redis的链表具有以下性质(和其他语言的链表相比较):
- 双向链表。包含前驱和后继,同时有头节点和尾节点。
- 有链表节点长度的属性。
- 无环。因为头节点和尾节点的下一个节点均为null。
- 多态。因为节点的定义中值的类型使用void*,因此链表可以保存不同数据类型的值。
3.字典
字典的结构定义
字典的底层实现为哈希表,一个哈希表中有多个哈希表节点,每个哈希节点中保存着一个键值对。
哈希表节点结构定义:
typedef struct dictEntry{
//键,各种数据类型都可以作为键
void *key;
//值,可以是一个指针*val,或者是下面的两种整型数据。
union{
void *val;
unit64_tu64;
int64_ts64;
}v;
//指向下一个哈希节点
struct dictEntry *next;
}dictEntry;
哈希表结构定义
typedef struct dictht{
//哈希表数组,哈希节点类型,数组中存放的是哈希节点
dictEntry **table;
//哈希表的大小,理解为容量更合适。
unsigned long size;
//哈希掩码,计算索引值n%哈希掩码
unsigned long sizemark;
//节点的数量(小于等于哈希表的大小)
unsigned long used;
}dictht;
下图为空哈希表的结构:
字典的结构定义:
typedef struct dict{
//哈希表。字典只使用ht[0],ht[1]只有在rehash时会使用。
dictht ht[2];
//类型特定函数。dictType包括计算hash,复制键值,对比键,销毁键值等函数。
dictType *type;
//私有数据
void *privdata;
//hash索引,rehash没有进行时,默认为-1
int trehashidx;
}
字典的图示如下:
字典解决hash冲突同样采用拉链法(链地址法)。
字典的rehash操作
当字典中保存的键值对太多或者太少时,为了保证字典的负载因子在一个合理的范围内,字典会进行扩展或者收缩,需要执行rehash来完成,但字典的rehash是个渐进式的,避免数据量大时影响redis的正常运行。