Redis数据结构

简单动态字符串[区别于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的链表实现的特点

  1. 双端
  2. 无环
  3. 带表头指针和表尾指针
  4. 带链表长度计数器
  5. 多态

字典

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;

  • 升级

升级条件[不支持降级]
新元素的类型比整数集合现有所有元素的类型都要长时,整数集合需要先进行升级,然后将元素添加到整数集合里面。

流程:

  1.  根据新元素的类型, 扩展整数集合底层数组的空间大小, 并为新元素分配空间。
  2.  将底层数组现有的所有元素都转换成与新元素相同的类型, 并将类型转换后的元素放置到正确的位上, 而且在放置元素的过程中, 需要继续维持底层数组的有序性质不变
  3. 将新元素添加到底层数组里面。

好处:

  1. 提升灵活性
  2. 节约内存

 

压缩列表(ziplist)

Redis使用场景

当一个列表键只包含少量列表项,并且每一个列表项要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做列表键的底层实现.

当一个哈希键只包含少量键值对,而且每一个键值对的键和值要么就是小整数值,要么就是长度比较短的字符串,那么Redis就会使用压缩列表来做哈希键的底层实现.

压缩列表的构成

连锁更新 

连锁更新的复杂度较高,但它真正造成性能问题的几率是很低的。

压缩列表里要恰好有多个连续的、长度介于250字节至253字节之间的节点, 连锁更新才有可能被引发, 在实际中, 这种情况并不多见;即使出现连锁更新, 但只要被更新的节点数量不多, 就不会对性能造成任何影响: 比如说, 对三五个节点进行连锁更新是绝对不会影响性能的。

 

 

《Redis设计与实现》

  • 0
    点赞
  • 0
    收藏
    觉得还不错? 一键收藏
  • 2
    评论
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值