博客
关于我
Redis 数据结构 :SDS、链表、字典、跳表、整数集合、压缩列表
阅读量:553 次
发布时间:2019-03-09

本文共 8774 字,大约阅读时间需要 29 分钟。

文章目录


SDS

结构分析

由于C字符串存在大量问题,所以在Redis中,并没有使用C风格字符串,而是自己构建了一个简单动态字符串即SDS(simple dynamic string)

struct sdshdr {           // buf 中已占用空间的长度    int len;    // buf 中剩余可用空间的长度    int free;    // 数据空间    char buf[];};

为解决C字符串缓冲区溢出问题以及长度计算问题,SDS中引入了len来统计当前已使用空间长度,free来计算剩余的空间长度

C字符串的主要缺陷就是因为它没有记录自己的长度,而如果在需要了解长度时,就只能通过O(N)的效率进行一次遍历

在这里插入图片描述

在这里插入图片描述

并且因为C字符串没有统计剩余空间的字段,也没有容量字段,所以很容易就会因为strcat等函数造成缓冲区的溢出,为弥补这一缺陷,redis在sds中增加了free字段
在这里插入图片描述
通过标记剩余空间,当对SDS进行插入操作时,就会提前判断当前剩余空间是否足够,如果不足则会先进行空间的拓展,再进行插入,这样就解决了缓冲区溢出的问题


内存策略

由于Redis作为一个高效的内存数据库,用于速度要求严苛,插入删除频繁

的场景,为了提高内存分配的效率,防止大量使用内存重分配而调用系统函数导致的性能损失问题(用户态和内核态的切换),Redis主要依靠空间预分配和惰性空间释放来解决这个问题


空间预分配

为减少空间分配的次数,当需要进行空间拓展时,不仅仅会为SDS分配修改所必须要的空间,并且会为SDS预分配额外的未使用空间。

预分配未使用空间的策略如下

  • 当SDS修改后的长度小于1MB时,将会预分配大小和当前len一样的空间(free = len),也就是使空间增长一倍,来减少因为初始时申请大空间导致的连续分配问题
  • 当SDS修改后的长度大于等于1MB时,每次分配都会分配1MB的空间,防止空间的浪费。

惰性空间释放

当我们对SDS进行删除操作时,并不会立即回收删除后空余的空间,而是将空余空间以free字段记录下来,以备后面使用。

这样做的目的在于防止因为空间缩短后因为再度插入导致的空间拓展问题
并且如果有需求需要真正释放空间,Redis也提供了对应的API,所以不必担心会因为惰性的空间释放而导致的内存浪费问题。


总结

比起 C 字符串, SDS 具有以下优点:

  1. 常数复杂度获取字符串长度。(len字段)
  2. 杜绝缓冲区溢出。(free字段)
  3. 减少修改字符串长度时所需的内存重分配次数。(空间预分配,惰性空间释放)
  4. 二进制安全。(以二进制形式处理)
  5. 兼容部分 C 字符串函数。(底层基于C字符串,以空字符结尾)

链表

结构分析

typedef struct listNode {       // 前置节点    struct listNode *prev;        // 后置节点    struct listNode *next;        // 节点的值    void *value;} listNode;/* * 双端链表迭代器 */typedef struct listIter {       // 当前迭代到的节点    listNode *next;        // 迭代的方向    int direction;} listIter;/* * 双端链表结构 */typedef struct list {       // 表头节点    listNode *head;    // 表尾节点    listNode *tail;    // 节点值复制函数    void *(*dup)(void *ptr);    // 节点值释放函数    void (*free)(void *ptr);    // 节点值对比函数    int (*match)(void *ptr, void *key);    // 链表所包含的节点数量    unsigned long len;} list;

在这里插入图片描述

从上面的结构可以看出,Redis的链表是一个带头尾的双端无环链表,并且通过len字段记录了链表节点的长度
在这里插入图片描述

同时为了实现多态与泛型,链表中还提供了dup,free,match属性来设置相关的函数,使得链表支持不同类型的值的存储


总结

  • 链表被广泛用于实现 Redis 的各种功能, 比如列表键, 发布与订阅, 慢查询, 监视器, 等等。
  • 每个链表节点由一个 listNode 结构来表示, 每个节点都有一个指向前置节点和后置节点的指针, 所以 Redis 的链表实现是双端链表。
  • 每个链表使用一个 list 结构来表示, 这个结构带有表头节点指针、表尾节点指针、以及链表长度等信息。
  • 因为链表表头节点的前置节点和表尾节点的后置节点都指向 NULL , 所以 Redis 的链表实现是无环链表。
  • 通过为链表设置不同的类型特定函数, Redis 的链表可以用于保存各种不同类型的值。

字典

结构分析

Redis的字典底层采用了哈希表来进行实现。

首先看看字典底层哈希表的结构

typedef struct dictht {       // 哈希表数组    dictEntry **table;    // 哈希表大小    unsigned long size;        // 哈希表大小掩码,用于计算索引值    // 总是等于 size - 1    unsigned long sizemask;    // 该哈希表已有节点的数量    unsigned long used;} dictht;

在这里插入图片描述

哈希表中记录了当前的总长度,已有节点,以及当前索引大小(用于哈希函数来计算节点位置)

为解决哈希冲突,Redis字典采用了链地址法来构造了哈希桶的结构,也就是哈希数组中的每个元素都是一个链表。

在这里插入图片描述

下面来看看哈希节点的结构

typedef struct dictEntry {       // 键    void *key;        // 值    union {           void *val;        uint64_t u64;        int64_t s64;    } v;    // 指向下个哈希表节点,形成链表    struct dictEntry *next;} dictEntry;

可以看到,为保证键值对适用于多重类型,key值使用的时void的形式,而value使用了64位有符号整型和64位无符号整型,void指针的一个联合体,每个节点使用next来链接成一个链表

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 (*valDestructor)(void *privdata, void *obj);} dictType;

为保证字典具有多态及泛型,dictType中提供了如哈希函数以及K-V的各种操作函数,使得字典适用于多重情景


rehash

/* * 字典 */typedef struct dict {       // 类型特定函数    dictType *type;    // 私有数据    void *privdata;    // 哈希表    dictht ht[2];    // rehash 索引    // 当 rehash 不在进行时,值为 -1    int rehashidx; /* rehashing not in progress if rehashidx == -1 */    // 目前正在运行的安全迭代器的数量    int iterators; /* number of iterators currently running */} dict;

从字典的结构中,我们可以看到里面同时存放了两个哈希表,以及一个rehashidx属性。

这就牵扯到了字典的核心之一,rehash。

Redis作为一个插入频繁且对效率要求高的数据库,当插入的数据过多时,就会因为哈希表中的负载因子过高而导致查询或者插入的效率降低,此时就需要通过rehash来进行重新扩容并重新映射。

但是如果只是用一个哈希表,映射时就会导致数据库暂时不可用,作为一个使用频繁的数据库,短期的停机几乎是不可容许的问题,所以Redis设计时采用了双哈希的结构,并采用了渐进式rehash的方法来解决这个问题。
在这里插入图片描述

rehash的步骤如下

  • 为ht[1]的哈希表分配空间
  • 将ht[0]中的键值对重新映射到ht[1]上
  • 当ht[0]的数据迁移完成,此时ht[0]为一个空表,此时释放ht[0],并让ht[1]成为新的ht[0],再为ht[1]创建一个新的空白哈希表,为下一次的rehash做准备
    在这里插入图片描述

在这里插入图片描述

在这里插入图片描述


渐进式rehash

由于数据库中可能存在大量的数据,而rehash的时候又过长,为了避免因为rehash造成的服务器停机,rehash的过程并不是一次完成的,而是一个多次的,渐进式的过程。