【Redis】数据结构:哈希表、整数集合、跳表

64 阅读3分钟

哈希表 hash

typedef struct dict {
    …
    //两个Hash表,交替使用,用于rehash操作
    dictht ht[2]; 
    …
} dict;

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

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

  • 链式哈希:相当于数组+链表

  • 哈希冲突:单项链表连接起来

  • rehash:防止链表长度过大查找效率低

    • 给「哈希表 2」 分配空间,比「哈希表 1」 大 2 倍;
    • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
    • 把「哈希表 2」 设置为「哈希表 1」,「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。
    • 问题:哈希表大,导致大量数据拷贝
  • 渐进式rehash:

    • 给「哈希表 2」 分配空间;
    • 新增、删除、查找或者更新操作时,操作哈希表1,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上;
    • 查找,先「哈希表 1」后哈希表 2
    • 当把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。
  • rehash 触发条件:负载因子=节点数/哈希表大小

    • =1:没有RDB、AOF,rehash

    • =5:强制rehash

整数集合 inset

typedef struct intset {
    //编码方式,int16_t 、int32_t 、int64_t 
    uint32_t encoding;
    //集合包含的元素数量
    uint32_t length;
    //保存元素的数组
    int8_t contents[];
} intset;
  • 整数集合的升级:新加入元素超过encoding设置大小

    • 不会重新分配一个新类型的数组,在原数组扩展,然将每个元素按间隔类型大小分割
  • 不支持降级

好处:

  • 节省内存资源,需要更大内存空间类型才进行升级,默认使用int8_t类型;

跳表 zskiplist

优势:平均 O(logN) 复杂度的节点查找,高效范围查找(比红黑树好)。

typedef struct zset {
    // 哈希表
    dict *dict;
    // 跳表
    zskiplist *zsl;
} zset;
  • 插入或更新,依次在跳表和哈希表中操作,保证信息一致。

    • 跳表:负责范围查找
    • hash:获取元素权重
typedef struct zskiplist {
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zskiplistNode {
    //Zset 对象的元素值
    sds ele;
    //元素权重值
    double score;
    //后向指针
    struct zskiplistNode *backward;
    //节点的level数组,保存每层上的前向指针和跨度
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;
  • 排位:查找元素经过的跨度之和

跳表节点查询过程

  • 从头节点的最高层开始,逐一遍历每一层。根据权重判断:

    • 当前节点的权重 < 要查找的权重:访问该层上的下一个节点。
    • = 要查找的权重并且当前节点的 SDS 类型数据「小于」要查找的数据:访问该层上的下一个节点。
    • 都不满足,或者下一个节点为空:使用下一层指针,相当于跳到了下一层接着查找。
  • 相邻两层的节点数量最理想的比例是 2:1,查找复杂度可以降低到 O(logN)。

  • 跳表在创建节点的时候,根据概率随机生成每个节点的层数

    • 随机数<0.5,增加一层,以此类推
    • ZSKIPLIST_MAXLEVEL 最高的层数,Redis 7.0 为 32,Redis 5.0 为 64,Redis 3.0 为 32。