字符串
Redis里使用的字符串又叫做简易动态字符串,其实这里它这里的设计与 C++ 的 string 类有异曲同工之妙。
SDS
先看看 SDS 的定义。
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
这里要说的是,SDS 遵循 C 字符串的定义模式,末尾以空字符结尾,保存空字符的 1 字节长度并不计算在 len 属性里。
扩容与回收
当进行一次对 SDS 的操作之后,SDS 变长之后的SDS的长度,即 len 的属性值小于 1 MB时,程序将分配大小等于 len 的长度的额外空间给 SDS,此时 free 的长度等于 len。这时候 buf 的实际长度为 len + free + 1。
如果操作之后变长的 SDS 长度大于 1 MB,程序则分配大小为 1 MB 的额外空间给 SDS。比如,修改后的 len 长度为 20 MB,那么此刻分配给 free 长度 1 MB,此时的 buf 数组长度为 20 MB + 1 MB + 1 byte。
如果对 SDS 操作后,SDS 变短,此刻不会立即回收 buf 数组的长度,而是将空余长度保存到 free 属性里,留给后面可能的使用。
通过空间预分配的策略,可以减少连续操作字符串后,导致空间重分配的次数,进而提升速率,本质上是空间换时间的方案。
二进制安全
为了确保 Redis 可以适用于不同的场景, SDS 的 API 都是二进制安全的,程序对 SDS 处理都是以处理二进制的方式处理存在 buf 数组里的数据,输入写入什么样读出来就什么样,它使用 len 来判断字符串是否读到了结束,这样避免了 C 里以空字符为结束,导致某些字符串截断的问题。
与 C 字符串相比
获取字符串长度的时间复杂度更低,达到了O(1),SDS 的 API 都是安全的,不会造成缓冲器溢出等问题。修改字符串 N 次,最多执行 N 次空间重分配。兼容二进制数据。
链表
定义
链表定义如下:
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;
链表节点定义如下:
typedef struct listNode {
// 前置节点
struct listNode *prev;
// 后置节点
struct listNode *next;
// 节点的值
void *value;
} listNode;
可以看到,本质上 reids 里的链表是一个双端链表,并且结构内维护了链表的长度,以及对链表内节点进行复制、释放、比较的函数指针。