标签:列表 level 正在执行 fun byte 命令 progress 释放 分组
Redis没有直接使用C语言传统的字符串表示,而是自己构建了一种名为简单动态字符串(SDS)的抽象类型作为Redis的默认字符串表示。
struct sdshdr {
// 记录 buf 数组中已使用字节的数量
// 等于 SDS 所保存字符串的长度
int len;
// 记录 buf 数组中未使用字节的数量
int free;
// 字节数组,用于保存字符串
char buf[];
};
SDS与C字符串的区别:
1、常数复杂度获取字符串长度
设置和更新 SDS 长度的工作是由 SDS 的 API 在执行时自动完成的, 使用 SDS 无须进行任何手动修改长度的工作。
2、杜绝缓冲区溢出
3、减少修改字符串时带来的内存重分配次数
因为 C 字符串并不记录自身的长度,所以每次增长或者缩短一个 C 字符串, 程序都总要对保存这个 C 字符串的数组进行一次内存重分配操作:
为了避免 C 字符串的这种缺陷, SDS 通过未使用空间解除了字符串长度和底层数组长度之间的关联,由 SDS 的 free 属性记录。
通过未使用空间, SDS 实现了空间预分配和惰性空间释放两种优化策略。
(1)空间预分配:
空间预分配用于优化 SDS 的字符串增长操作,当 SDS 的 API 对一个 SDS 进行修改, 并且需要对 SDS 进行空间扩展的时候, 程序不仅会为 SDS 分配修改所必须要的空间, 还会为 SDS 分配额外的未使用空间。
通过空间预分配策略, Redis 可以减少连续执行字符串增长操作所需的内存重分配次数。在扩展 SDS 空间之前, SDS API 会先检查未使用空间是否足够, 如果足够的话, API 就会直接使用未使用空间, 而无须执行内存重分配。
(2)惰性空间释放:
惰性空间释放用于优化 SDS 的字符串缩短操作: 当 SDS 的 API 需要缩短 SDS 保存的字符串时, 程序并不立即使用内存重分配来回收缩短后多出来的字节, 而是使用 free 属性将这些字节的数量记录起来, 并等待将来使用。SDS 也提供了相应的 API , 让我们可以在有需要时, 真正地释放 SDS 里面的未使用空间。
4、二进制安全
5、兼容部分 C 字符串函数
虽然 SDS 的 API 都是二进制安全的, 但它们一样遵循 C 字符串以空字符结尾的惯例,通过遵循 C 字符串以空字符结尾的惯例,SDS 可以在有需要时重用 <string.h> 函数库, 从而避免了不必要的代码重复。
压缩列表(ziplist)是列表键和哈希键的底层实现之一。使用场景:
压缩列表是Redis为了节约内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点,每个节点可以保存一个字节数组或者一个整数值。
1、压缩列表整体结构
2、压缩列表节点结构
每个压缩列表节点可以保存一个字节数组或者一个整数值,每个压缩列表节点都由previous_entry_length、encoding、content三部分组成。
(1)previous_entry_length
(2)encoding
记录了节点的 content 属性所保存数据的类型及长度。
(3)content
保存节点的值,节点值可以是一个字节数组或者整数,值的类型和长度由节点的encoding属性决定。
(4)连锁更新
假如压缩列表里恰好有多个连续的、长度介于 250 到 253 字节之间的节点,如果这时在这些节点前新加一个长度大于254字节的节点,后一个节点的 previous_entry_length 长度就需要从1字节增加到5字节,很可能会带来一系列空间重分配操作(视连续的长度介于 250 到 253 字节的节点个数所决定),但出现的机率一般不高,ziplistPush等命令的平均复杂度为O(N),最坏为O(N*N)。
typedef struct listNode{
//前置节点
struct listNode *prev;
//后置节点
struct listNode *next;
//节点的值
void *value;
}listNode;
虽然仅仅使用多个 listNode 结构就可以组成链表, 但使用 adlist.h/ list 来持有链表的话, 操作起来会更方便:
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;
dup、free 和 match 成员是用于实现多态链表所需的类型特定函数:
Redis链表特性:
字典是一种用于保存键值对的抽象数据结构。字典中的每个键都是独一无二的,Redis的数据库就是使用字典来作为底层实现的,字典还是哈希对象的底层实现之一。
字典使用哈希表作为底层实现,一个哈希表里可以有多个哈希表节点,每个哈希表节点就保存了字典中的一个键值对。
1、哈希表结构
typedef struct dictht{
//哈希表数组
dictEntry **table;
//哈希表大小
unsigned long size;
//哈希表大小掩码,用于计算索引值
//总是等于 size-1
unsigned long sizemask;
//该哈希表已有节点的数量
unsigned long used;
} dictht;
2、哈希表节点结构
typedef struct dictEntry{
//键
void *key;
//值
union{
void *val;
uint64_t u64;
int64_t s64;
}v;
//指向下个哈希表节点,形成链表
struct dictEntry *next;
} dictEntry;
3、字典结构
typedef struct dict {
// 类型特定函数
dictType *type;
// 私有数据
void *privdata;
// 哈希表
dictht ht[2];
// rehash 索引
// 当 rehash 不在进行时,值为 -1
int trehashidx; /* rehashing not in progress if rehashidx == -1 */
} dict;
4、哈希算法
将一个新的键值对添加到字典里面时,程序需要先根据键值对的键计算出哈希值和索引值,然后再根据索引值,将包含新键值对的哈希表节点放到哈希表数组的指定索引上面。
Redis计算哈希值和索引值的方法如下:
//使用字典设置的哈希函数,计算键 key 的哈希值
hash = dict->type->hashFunction(key);
//使用哈希表的 sizemask 属性和哈希值,计算出索引值
//根据情况不同, ht[x] 可以是 ht[0] 或者 ht[1]
index = hash & dict->ht[x].sizemask;
5、解决键冲突
当有两个或以上数量的键被分配到了哈希表数组的同一个索引上面时, 我们称这些键发生了冲突。
Redis 的哈希表使用链地址法来解决键冲突: 每个哈希表节点都有一个 next 指针,多个哈希表节点可以用 next 指针构成一个单向链表,被分配到同一个索引上的多个节点可以用这个单向链表连接起来。因为 dictEntry 节点组成的链表没有指向链表表尾的指针, 所以为了速度考虑, 程序总是将新节点添加到链表的表头位置(复杂度为 O(1)),排在其他已有节点的前面。
6、rehash
随着操作的不断执行,哈希表保存的键值会逐渐地增多或者减少,为了让哈希表的负载因子维持在一个合理的范围之内,当哈希表保存的键值对数量太多或者太少时,程序需要对哈希表的大小进行相应的扩展或者收缩。Redis对字典的哈希表执行rehash的步骤如下:
(1)为字典的 ht[1] 哈希表分配空间,这个哈希表的空间大小取决于要执行的操作以及 ht[0] 当前包含键值对数量(即 ht[0].used 属性的值):
(2)将保存在 ht[0] 中的所有键值对rehash到 ht[1] 上面:rehash指的是重新计算键的哈希值和索引值,然后将键值对放置到 ht[1] 哈希表的指定位置上。
(3)当 ht[0] 包含的所有键值对都迁移到了 ht[1] 之后,释放 ht[0],将 ht[1] 设置为 ht[0],并在 ht[1] 新创建一个空白哈希表,为下一次rehash做准备。
哈希表的负载因子=哈希表已保存节点数量 / 哈希表大小
load_factor = ht[0].used / ht[0].size
当以下条件中任意一个被满足时,程序会自动开始对哈希表执行扩展操作:
当哈希表负载因子小于0.1时,程序自动开始对哈希表执行收缩操作。
7、渐进式rehash
服务器不是一次性将ht[0]里面的所有键值对全部rehash到ht[1],而是分多次、渐进式地将ht[0]里面的键值对慢慢地rehash到ht[1]。详细步骤:
因为在渐进式rehash的过程中,字典会同时使用ht[0]和ht[1]两个哈希表,所以在渐进式rehash进行期间,字典的删除、查找、更新等操作都会在两个表上进行。例如,要在字典里面查找一个键的话,程序会先在 ht[0] 里面进行查找,如果没找到的话,就会继续到 ht[1] 里面进行查找。
在渐进式 rehash 执行期间,新添加到字典的键值对一律会被保存到 ht[1] 里面,而 ht[0] 则不在进行任何添加操作,这一措施保证了ht[0]包含的键值对数量会只减不增。
整数集合是集合键的底层实现之一,当一个集合只包含整数值元素,并且这个集合的元素数量不多时,Redis就会使用整数集合作为集合键的底层实现。可以保存类型为int16_t、int32_t或者int64_t的整数值,并且会保证集合中不会出现重复元素。
typedef struct inset {
// 编码方式
uint32_t encoding;
// 集合包含的元素数量
uint32_t length;
// 保存元素的数值
int8_t contents[];
} intset;
升级:
每当我们要将一个新元素添加到整数集合里面,并且新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后才能将新元素添加到整数集合里面。
因为每次向整数集合添加新元素都可能会引起升级,而每次升级都需要对底层数组中已有的所有元素进行类型转换,所以向整数集合添加新元素的时间复杂度为O(N)。
因为引发升级的新元素的长度总是比整数集合现有所有元素的长度都大,所以这个新元素的值要么就大于所有现有元素(放数组最开头,索引 0),要么就小于所有现有元素(放数组最末尾,索引为 length-1)。
升级的好处:
降级:整数集合不支持降级操作,一旦对数组进行了升级,编码就会一直保持升级后的状态。
跳跃表是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问节点的目的。跳跃表支持平均 O(logN)、最坏 O(N) 复杂度的节点查找,还可以通过顺序性操作来批量处理节点。Redis 只在两个地方用到了跳跃表,一个是实现有序集合键,另一个是在集群节点中用作内部数据结构。
1、跳跃表节点结构
typedef struct zskiplistNode {
// 后退指针
struct zskiplistNode *backward;
// 分值
double score;
// 成员对象
robj *obj;
// 层
struct zskiplistLevel {
// 前进指针
struct zskiplistNode *forward;
// 跨度
unsigned int span;
} level[];
} zskiplistNode;
(1)层
(2)后退指针
用于从表尾向表头方向访问节点:跟可以一次跳过多个节点的前进指针不同,因为每个节点只有一个后退指针,所以每次只能后退至前一个节点。
(3)分值和成员
2、跳跃表结构
仅靠多个跳跃表节点就可以组成一个跳跃表,但通过使用一个 zskiplist 结构来持有这些节点,程序可以更方便地对整个跳跃表进行处理,比如快速访问跳跃表的表头节点和表尾节点,或者快速地获取跳跃表节点的数量(也即是跳跃表的长度)等信息。
typedef struct zskiplist {
// 表头节点和表尾节点
structz skiplistNode *header, *tail;
// 表中节点的数量
unsigned long length;
// 表中层数最大的节点的层数
int level;
} zskiplist;
标签:列表 level 正在执行 fun byte 命令 progress 释放 分组
原文地址:https://www.cnblogs.com/zjxiang/p/12151880.html