简单动态字符串[区别于c语言中的字符串]
-
SDS的定义
Redis没有直接使用C语言传统的字符串表示(以空字符结尾的字符数组,以下简称C字符串),而是自己构建了一种名为简单动态字符串(simple dynamic string,SDS)的抽象类型,并将SDS用作Redis的默认字符串表示。
struct sdshdr{
// 记录buf数组中已使用字节的数量
// 等于SDS所保存字符串的长度
int len;
// 记录buf数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
int buf[];
}
-
空间预分配
如果对 SDS 进行修改之后, SDS 的长度(也即是 len 属性的值)将小于 1 MB , 那么程序分配和 len 属性同样大小的未使用空间, 这时 SDS len 属性的值将和 free 属性的值相同。
如果对 SDS 进行修改之后, SDS 的长度将大于等于 1 MB , 那么程序会分配 1 MB 的未使用空间。
-
惰性空间释放-用于优化SDS的字符串缩短操作
当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。
-
二进制安全[c语言中字符串以\0结尾,而sds用len判断字符串结尾]
在c语言中会输出"Redis"
总结
链表
链表提供了高效的节点重排能力,以及顺序性的节点访问方式,并且可以通过增删节点来灵活地调整链表的长度。
-
使用场景
当一个列表键包含了数量较多的元素,又或者列表中包含的元素是比较长的字符串时,Redis就会使用链表作为列表键的底层实现。
-
链表和链表节点的实现
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);
}list;
多个listNode可以通过prev和next指针组成双端链表。
-
Redis的链表实现的特点
- 双端
- 无环
- 带表头指针和表尾指针
- 带链表长度计数器
- 多态
字典
Redis的字典使用哈希表作为底层实现,一个哈希表里面可以有多个哈希表节点,而每个哈希表节点就保存了字典中的一个键值对。
-
使用场景
Redis数据库就是使用字典作为其底层实现的。当一个哈希键包含的键值对比较多,又或者键值对中的元素都是比较长的字符串时,Redis就会使用字典作为哈希键的底层实现。
-
字典的实现
typdef struct dictEntry{
// 键
void* key;
// 值
union{
void* val;
uint64_tu64;
int64_ts64;
} v;
// 指向下一个哈希表节点,形成链表
struct dictEntry* next;
} dictEntry;
typedief struct dictht{
// 哈希表数组
dictEntry** table;
// 哈希表大小
unsigned long size;
// 哈希表大小掩码,用于计数索引值
// 总是等于size - 1
unsigned long sizemask;
// 该哈希表已有节点的数目
unsigned long used;
} dictht;
下面是一个大小为4的空哈希表(没有任何键值对).
-
哈希表的扩展与收缩
扩展和搜索哈希表的工作通过执行rehash(重新散列)操作来完成。
扩展条件:
1. 服务器目前没有在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 1 ;
2. 服务器目前正在执行 BGSAVE 命令或者 BGREWRITEAOF 命令, 并且哈希表的负载因子大于等于 5 ;收缩条件:
1. 当哈希表的负载因子小于0.1时,会自动对哈希表进行收缩操作。
*负载因子 = 哈希表已保存节点数量 / 哈希表大小[load_factor = ht[0].used / ht[0].size]
根据 BGSAVE 命令或 BGREWRITEAOF 命令是否正在执行, 服务器执行扩展操作所需的负载因子并不相同, 这是因为在执行 BGSAVE 命令或 BGREWRITEAOF 命令的过程中, Redis 需要创建当前服务器进程的子进程, 而大多数操作系统都采用写时复制(copy-on-write)技术来优化子进程的使用效率, 所以在子进程存在期间, 服务器会提高执行扩展操作所需的负载因子, 从而尽可能地避免在子进程存在期间进行哈希表扩展操作, 这可以避免不必要的内存写入操作, 最大限度地节约内存。
-
渐进式rehash
原因:
一次性海量的计算量可能会导致服务器在一段时间内停止服务。好处:
采取分而治之的方式,将rehash键值对所需的计算工作均摊到对字典的每个添加、删除、查找和更新操作上,从而避免了集中式rehash而带来的庞大计算量。
过程:
1. 为 ht[1] 分配空间, 让字典同时持有 ht[0] 和 ht[1] 两个哈希表。
2. 在字典中维持一个索引计数器变量 rehashidx , 并将它的值设置为 0 , 表示 rehash 工作正式开始。
3. 在 rehash 进行期间, 每次对字典执行添加、删除、查找或者更新操作时, 程序除了执行指定的操作以外, 还会顺带将 ht[0] 哈希表在 rehashidx 索引上的所有键值对 rehash 到 ht[1] , 当 rehash 工作完成之后, 程序将 rehashidx 属性的值增一。
4. 随着字典操作的不断执行, 最终在某个时间点上, ht[0] 的所有键值对都会被 rehash 至 ht[1] , 这时程序将 rehashidx 属性的值设为 -1 , 表示 rehash 操作已完成。
-
解决键冲突:
Redis的哈希表使用链地址法(separate chaining)来解决键冲突,每个哈希表节点都有一个next指针,多个哈希表节点可以用next指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来,这就解决了键冲突的问题.
跳跃表
跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。平均O(logN) 最坏 O(N)。
-
使用场景
一个是实现有序集合键,另一个是在集群节点中用作内部数据结构,除此之外,跳跃表在Redis里面没有其他用途。
-
跳跃表实现
typedef struct zskiplist{
// 表头节点和表尾节点
struct zskiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
typedef struct zskiplistNode{
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel{
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
-
总结
* 跳跃表是有序集合的底层实现之一, 除此之外它在 Redis 中没有其他应用。
* Redis 的跳跃表实现由 zskiplist 和 zskiplistNode 两个结构组成, 其中 zskiplist 用于保存跳跃表信息(比如表头节点、表尾节点、长度), 而 zskiplistNode 则用于表示跳跃表节点。
* 每个跳跃表节点的层高都是 1 至 32 之间的随机数。
* 在同一个跳跃表中, 多个节点可以包含相同的分值, 但每个节点的成员对象必须是唯一的。
* 跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。
整数集合
-
使用场景
当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现.
-
整数集合实现
typedef struct intset{
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数组
int8_t contents[];
} intset;
-
升级
升级条件[不支持降级]:
新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后将元素添加到整数集合里面。流程:
- 根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
- 将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变。
- 将新元素添加到底层数组里面。
好处:
- 提升灵活性
- 节约内存
压缩列表(ziplist)
Redis使用场景
当一个列表键只包含少量列表项,并且每一个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现.
当一个哈希键只包含少量键值对,而且每一个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现.
压缩列表的构成
连锁更新
连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的。
压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点, 连锁更新才有可能被引发, 在实际中, 这种情况并不多见;即使出现连锁更新, 但只要被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是绝对不会影响性能的。
《Redis设计与实现》