1.2 章节编排
- 数据结构对象
- 单机数据库的实现
- 多机数据库的实现
- 独立功能的实现
2简单动态字符串
2.1 SDS的定义
struct sdshdr{
int len; //记录buf中已经使用字节的数量,等于sds所保存字符串的长度(不包含 )
int free; //记录buf数组中未使用字节的数量
char buf[];
}
2.2 SDS与C字符串的区别
2.2.1 常数复杂度获取字符串长度
2.2.2 杜绝缓冲区溢出
2.2.3 减少修改字符串带来的内存分配次数
通过未使用空间,SDS实现了空间预分配和惰性空间释放这两种优化策略
1.空间预分配
预分配的策略如下:
- 如果修改之后,SDS的len小于1MB,那么分配和len同样大小的free空间。
- 如果修改之后,len大于1MB,那么程序分配1MB的未使用空间
2.惰性空间释放
2.2.4 二进制安全
SDS 可以保存一系列二进制数据,因为它不是用 而是使用len来判断字符的结束
2.2.5 兼容部分C字符串函数
SDS 虽然是二进制安全的,但是总会以c字符串的方式,遵循空字符结尾的方式,以便重用<string.h>定义的含数
2.3 SDS API
函数 | 作用 | 时间复杂度 |
---|---|---|
sdsnew | 创建一个包含给定C字符串的SDS | O(N),N为给定字符串的长度 |
sdsempty | 创建一个不包含任何内容的空SDS | O(1) |
sdsfree | 释放给定的SDS | O(N),N为被释放的长度 |
sdslen | 返回SDS的已使用空间字节数 | O(1) |
sdsavail | 返回SDS的未使用空间字节数 | O(1) |
sdsup | 创建一个给定SDS的副本 | O(N),N为给定SDS的长度 |
sdsclear | 清空SDS保存的字符串内容 | 因为惰性空间释放策略,复杂度为O(1) |
sdscat | 将给定字符串拼接到SDS字符串的末尾 | O(N),N为被拼接C字符串的长度 |
sdscatsds | 将给定SDS字符串拼接到另一个字符串的末尾 | O(N),N为被拼接SDS字符串的长度 |
sdscpy | 将给定的C字符串复制到SDS里面,覆盖原有的SDS字符串 | O(N),N为被复制的C字符串的长度 |
sdsgrowzero | 用空字符串将SDS扩展至给定长度 | O(N),N为扩展新增的字节数 |
sdsrange | 保留SDS给定区间内的数据,不在区间内的数据会被覆盖或清除 | O(N),N为被保留的字节数 |
sdstrim | 接受一个SDS和一个c字符串作为参数,从SDS左右两端分别移除所有在C字符串中出现过的字符 | O(M*N),M为SDS的长度,N为给定C字符串的长度 |
sdscmp | 对比两个SDS字符串是否相同 | O(N),N为两个SDS中较短的那个SDS的长度 |
3 链表
3.1 链表和链表节点的实现
typedef struct listNode{
struct listNode* prev;
struct listNode* next;
void* value;
}listNode;
typedef struct list{
listNode *head;//头节点
listNode *tail;//尾节点
unsigned long len;//链表所包含的节点数量
void *(*dup) (void *ptr);//节点值复制函数
void (*free)(void *ptr);//节点值释放函数
int (*match)(void *ptr,void *key);//节点值对比函数
}
3.2 链表和链表节点的API
4 字典
4.1 字典的实现
4.1.1 哈希表
typedef struct dictht{
dictEntity **table;//哈希表数组
unsigned long size;//哈希表大小
unsigned long sizemask;//哈希表大小掩码,总是等于size-1
unsigned long used;//该哈希表已有节点的数量
}
4.1.2 哈希表节点
typdef struct dictEntity{
void *key;//键
union {
void *val;
uint64_t u64;
int64_t s64;
}
struct dictEntity *next;//指向下个哈希节点,形成链表
}
4.1.3 Redis中的字典由dict.h/dict结构表示
typedef struct dict{
dictType *type;//类型特定函数
void *privdata;//私有数据
dictht ht[2];//哈希表
int trehashidx; //rehash索引,当rehash不再进行时,值为-1
type 和privadata 是针对不同类型的键值对,为创建多态字典而设置的。
- type属性是一个指向dictType的指针,每个dictType结构保存了一簇用于操作特定类型键值对的函数。
- privdata 属性则保存了需要传给那些函数特定函数的可选参数
typedef struct dictType{
unsigned int (*hashFunction)(const void *key);//计算哈希值的函数
void *(*keyDup) (void *privdata,const void *key);//复制键的函数
void *(*valDup)(void *privdata,const void *obj);//复制值的函数
int (*keyCompare) (void *privdata,const void *key1, const void *key2);//对比键的函数
void (*keyDestructor)(void *privdata,void *key);//销毁键的函数
void (*valueDestructor)(void *privdata,void *obj);//销毁值的函数
}
ht[1]哈希表只会在ht[0]哈希表进行rehash的时候使用。
4.2 哈希算法
计算hash索引的方式为:
//使用字典设置的哈希函数,计算键key的哈希值
hash = dict->type->hashFunction(key);
//使用哈希表sizemask属性和哈希值,计算出索引值
//根据情况不同,ht可以是ht[0]或者ht[1]
index = hash&dict->ht[x].sizemask;
当字典被用作数据库的底层实现,或者哈希键的底层实现时,Redis使用MurmurHash2算法来计算键的哈希值。
4.3 解决键冲突
当有两个以上的数量的键被分配到哈希数组的同一个索引上面时,使用拉链法来解决冲突,并使用头插法。
4.4 rehash
redis 对字典的哈希表执行rehash的步骤如下:
- 为字典的ht[1]哈希表分配空间,这个哈希表的空间大小取决于要执行的操作,以及ht[0]当前包含的键值对数量(也即是ht[0].used属性的值):
- 如果执行的是扩展操作,那么ht[1]的大小为第一个大于等于ht[0].used*2的2^n(2的n次方幂)
- 如果执行的是收缩操作,那么ht[1]的大小为第一个大于等于ht[0].used的2^n
2.将保存在ht[0]中的所有键值对rehash到ht[1]上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到ht[1]哈希表的指定位置上
3.将ht[0]包含的所有键值对都迁移到了ht[1]之后(ht[0]表为空表),释放ht[0],将ht[1]设置为ht[0],并在ht[1]新创建一个空白哈希表,为下一次rehash做准备。
哈希表的扩展与收缩
当以下条件中的任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
- 服务器目前没有在执行BGSAVE命令或者BGREWRITEAOF命令,并且哈希表的装载因子大于等于1
- 服务器目前正在执行BGSAVEH或者BGREWRITEAOF命令,并且哈希表的负载因子大于等于5
负载因子=哈希已保存节点数量/哈希表大小
load_factor = ht[0].used/h[0].size
4.5 渐进式rehash
渐进式rehash执行期间的哈希表操作
5 跳跃表
redis只在两个地方使用了跳表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。