12-Redis底层数据结构

文章目录

Redis底层数据结构

  • Redis底层数据结构是Redis基本数据类型和诸多功能实现的基础,字典和跳跃表是最重要的两种数据结构。

一、字典

  • Redis中的字典是一种以Hash表为基础的数据结构,在Redis的很多功能实现中都使用到了字典。我们来看看Hash表和字典在Redis源码中的定义等信息。下面代码在Redis源码的src/dict.h文件中,可阅读参考文章[2]。

1.1 Hash表节点Entry

  • dictEntry是hash表存储的单个元素,类似于Java的HashMap中的Entry
/*
 * 哈希表节点
 */
typedef struct dictEntry {
    
    // 键
    void *key;

    // 值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
    } v;

    // 指向下个哈希表节点,形成链表
    struct dictEntry *next;

} dictEntry;

1.2 Hash表dictht

/* This is our hash table structure. Every dictionary has two of this as we
 * implement incremental rehashing, for the old to the new table. */
/*
 * 哈希表,使用拉链法解决哈希冲突(每个字典都使用两个哈希表,从而实现渐进式 rehash )
 */
typedef struct dictht {
    
    // 哈希表数组
    dictEntry **table;

    // 哈希表大小
    unsigned long size;
    
    // 哈希表大小掩码,用于计算索引值
    // 总是等于 size - 1
    unsigned long sizemask;

    // 该哈希表已有节点的数量
    unsigned long used;

} dictht;

1.3 字典dict

  • dict是Redis的字典,其中包含两个哈希表dictht,这是为了方便进行rehash操作。在扩容时,将其中一个dictht上的键值对rehash到另一个dictht上面,完成之后释放空间并交换两个dictht 的角色。其实字典本身的数据结构特点和Hash表是一样的,内部保存2个
    Hash表以及rehashindex等辅助信息是便于实现渐进式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;
  • 字典的rehash操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的rehash操作给服务器带来过大的负担。渐进式rehash通过记录dict的rehashidx完成,它从0开始每执行一次rehash都会递增。例如在一次rehash中,要把dict[0] rehash到 dict[1],会把dict[0]上table[rehashidx]的键值对rehash到dict[1]上,dict[0]的table[rehashidx]指向null,并令rehashidx++。在rehash期间,每次对字典执行添加、删除、查找或者更新操作时,都会执行一次渐进式rehash。采用渐进式rehash 会导致字典中的数据分散在两个dictht 上,因此对字典的查找操作也需要到对应的dictht去执行。

1.4 字典reHash

  • 下面是字典结构的rehash操作函数。我对比了源码,这个是Redis2.8及一下的版本源码,3.0及以上版本源码略有不同。
/* Performs N steps of incremental rehashing. Returns 1 if there are still
 * keys to move from the old to the new hash table, otherwise 0 is returned.
 *
 * 执行 N 步渐进式 rehash 。
 *
 * 返回 1 表示仍有键需要从 0 号哈希表移动到 1 号哈希表,
 * 返回 0 则表示所有键都已经迁移完毕。
 *
 * Note that a rehashing step consists in moving a bucket (that may have more
 * than one key as we use chaining) from the old to the new hash table.
 *
 * 注意,每步 rehash 都是以一个哈希表索引(桶)作为单位的,
 * 一个桶里可能会有多个节点,
 * 被 rehash 的桶里的所有节点都会被移动到新哈希表。
 *
 * T = O(N)
 */
int dictRehash(dict *d, int n) {

    // 只可以在 rehash 进行中时执行
    if (!dictIsRehashing(d)) return 0;

    // 进行 N 步迁移
    // T = O(N)
    while(n--) {
        dictEntry *de, *nextde;

        /* Check if we already rehashed the whole table... */
        // 如果 0 号哈希表为空,那么表示 rehash 执行完毕
        // T = O(1)
        if (d->ht[0].used == 0) {
            // 释放 0 号哈希表
            zfree(d->ht[0].table);
            // 将原来的 1 号哈希表设置为新的 0 号哈希表
            d->ht[0] = d->ht[1];
            // 重置旧的 1 号哈希表
            _dictReset(&d->ht[1]);
            // 关闭 rehash 标识
            d->rehashidx = -1;
            // 返回 0 ,向调用者表示 rehash 已经完成
            return 0;
        }

        /* Note that rehashidx can't overflow as we are sure there are more
         * elements because ht[0].used != 0 */
        // 确保 rehashidx 没有越界
        assert(d->ht[0].size > (unsigned)d->rehashidx);

        // 略过数组中为空的索引,找到下一个非空索引
        while(d->ht[0].table[d->rehashidx] == NULL) d->rehashidx++;

        // 指向该索引的链表表头节点
        de = d->ht[0].table[d->rehashidx];
        /* Move all the keys in this bucket from the old to the new hash HT */
        // 将链表中的所有节点迁移到新哈希表
        // T = O(1)
        while(de) {
            unsigned int h;

            // 保存下个节点的指针
            nextde = de->next;

            /* Get the index in the new hash table */
            // 计算新哈希表的哈希值,以及节点插入的索引位置
            h = dictHashKey(d, de->key) & d->ht[1].sizemask;

            // 插入节点到新哈希表
            de->next = d->ht[1].table[h];
            d->ht[1].table[h] = de;

            // 更新计数器
            d->ht[0].used--;
            d->ht[1].used++;

            // 继续处理下个节点
            de = nextde;
        }
        // 将刚迁移完的哈希表索引的指针设为空
        d->ht[0].table[d->rehashidx] = NULL;
        // 更新 rehash 索引
        d->rehashidx++;
    }

    return 1;
}

1.5 小结

  • 字典也是和Hash表一样的k-v存储集合,但是是基于Hash表来实现的。
  • 在字典的内部维护了2个Hash表,来实现渐进式rehash。rehash的时候不是一次性完成,而是渐进式的将元素从一个Hash表迁移到另一个Hash表,注意一次迁移的不是一个k-v对结构dictEntry,而是按照桶为单位进行迁移的。
  • 在Redis的本身源码中,字典是很多基础功能实现的基础数据结构,比如在Redis集群中使用字典来保存集群的所有服务器节点信息,在Redis中默认包含16个数据库,每一个库对应一个redisDb的数据结构,在redisDb内部就是通过一个dict字典来保存该库下的全部键值对
  • 字典可以理解为一个优化后的Hash表,其功能和Hash几乎一样,但是做rehash的时候性能更好,以空间换取了时间。

二、跳跃表

  • 跳跃表是一种数据结构,它基于多指针有序链表实现的,可以看成多个有序链表。在Redis中它是有序集合的底层实现之一。另外在Redis中一些涉及到排序的场景也会使用到跳跃表,比如Redis分片集群中使用跳跃表保存了槽和key之间的关系,以槽为分值键为成员对槽进行排序,便于进行区间操作,或者处理槽和键之间的关系和对槽进行rehash。

  • Redis中的有序集合是一种以跳跃表为基础的数据结构,我们来看看跳跃表和有序集合在Redis源码中的定义等信息。下面代码在Redis源码的src/redis.h文件中,可阅读参考文章[2]。

2.1 跳跃表节点zskiplistNode

  • zskiplistNode是对应跳跃表节点的数据结构。节点除了包含目标的key之外,还包含分值、前后指针、跨度等信息。
/* ZSETs use a specialized version of Skiplists */
/*
 * 跳跃表节点
 */
typedef struct zskiplistNode {

    // 成员对象
    robj *obj;

    // 分值
    double score;

    // 后退指针
    struct zskiplistNode *backward;

    // 层
    struct zskiplistLevel {

        // 前进指针
        struct zskiplistNode *forward;

        // 跨度
        unsigned int span;

    } level[];

} zskiplistNode;

2.2 跳跃表zskiplist

  • zskiplist是对应跳跃表的数据结构。包含一个跳跃表的头尾节点指针、节点数量和最大层数。
/*
 * 跳跃表
 */
typedef struct zskiplist {

    // 表头节点和表尾节点
    struct zskiplistNode *header, *tail;

    // 表中节点的数量
    unsigned long length;

    // 表中层数最大的节点的层数
    int level;

} zskiplist;

2.3 有序集合zset

  • zset是对应有序集合的数据结构。zset内部维护一个字典来保证按照成员取出分值,提示维护一个跳跃表来支持按照分值操作成员,比如排序等。
/*
 * 有序集合
 */
typedef struct zset {

    // 字典,键为成员,值为分值
    // 用于支持 O(1) 复杂度的按成员取分值操作
    dict *dict;

    // 跳跃表,按分值排序成员
    // 用于支持平均复杂度为 O(log N) 的按分值定位成员操作
    // 以及范围操作
    zskiplist *zsl;

} zset;

2.4 跳跃表操作

  • 跳表的查找,插入等操作,可以阅读参考文章[4]

2.5 小结

  • 跳跃表也是一种加速查找的数据结构,内部通过多层指针和双向指针来实现加速查找,在排序和范围操作时性能较好,并基于它实现了Redis中的有序集合。相比于红黑树这样的查找结构,有下面优点,为什么Redis中不使用红黑树呢?后面给了一个表格
A.插入快。因为不需要进行旋转等操作来维护平衡性;
B.更容易实现。实现比红黑树简单许多;
C.支持无锁操作。
对比项 红黑树复杂度 跳表复杂度
插入元素 O(log n) O(log n)
删除元素 O(log n) O(log n)
查找元素 O(log n) O(log n)
有序输出元素 O(log n) O(log n)
区间操作元素 不如跳表 性能很好
  • 区间操作时,红黑树定位到端点后,再从首位置开始每次都要查找后继节点,相对来说是比较耗时的。跳表的区间操作,定位到2端的元素之后,顺序遍历即可,显然性能更好

三、参考

上一篇:html书写行内元素时-tab和换行会在行内元素间引入间距


下一篇:一本通 字符串 图书管理