Loading

Redis中的跳表

date: 2020-10-15 14:58:00
updated: 2020-10-19 17:58:00

Redis中的跳表

参考网址1
参考网址2

redis 数据类型 zset 实现有序集合,底层使用的数据结构是跳表。

源码在 src/t_zset.c 文件中,相关数据结构的定义在 src/server.h 文件中。(4.0版本)

元素有序的时候,如果是数组,可以通过二分查找来提速;如果是链表,如何提速? => 跳表,插入/删除/搜索 都是O(logn)

第一层索引 n/2 个节点,第二层 n/4 个节点,第三层 n/8,第K层 n/(2^k)
假设第K层有2个节点,即 n/(2^k) = 2 => k = log2(n) - 1

typedef struct zskiplist {
    // 头节点,尾节点
    struct zskiplistNode *header, *tail;
    // 节点数量
    unsigned long length;
    // 目前表内节点的最大层数
    int level;
} zskiplist;

typedef struct zskiplistNode {
    // member 对象
    robj *obj;
    // 分值
    double score;
    // 后退指针
    struct zskiplistNode *backward;
    // 层
    struct zskiplistLevel {
        // 前进指针
        struct zskiplistNode *forward;
        // 这个层跨越的节点数量
        unsigned int span;
    } level[];
} zskiplistNode;

1. 跳表 SkipList

java.util.concurrent.ConcurrentSkipListMap/ConcurrentSkipListSet 类中也有实现

查询链表时会从头到尾的遍历链表,最坏的时间复杂度是O(N),这是一次比较一个值,如果跳着1个元素来进行比较(比较下标为2n+1的元素),那么就相当于一次性比较2个元素,效率就会提高 => 跳表

查询

跳表是牺牲空间来换取时间,除了最底层是最原始的数据外,其他的每一层,其实都相当于是一个索引,最理想的是按照第一层1级跳,第二层2级跳,第三层4级跳,第四层8级跳。。。但是考虑到有插入,如果插入的时候还要保证这个递增关系,那么就要调整当前的数据结构,时间太长,所以是否插入会有一个25%概率比较

插入

插入有一个地方需要注意,最底层肯定是要插入数据的,然后产生一个随机数,根据幂次定律,越大的值生成的几率越小。

2. 如何确定层数

int zslRandomLevel(void){
    int level = 1;
    while((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))
        level += 1;
    retrun (level < ZSKIPLST_MAXLAVEL) ? level : ZSKIPLST_MAXLAVEL;
}
// 0xFFFF 65535
// ZSKIPLIST_P 默认为 0.25
// define ZSKIPLST_MAXLAVEL 64 常量值为64

3. 为什么使用跳跃表,而不是平衡树等用来做有序元素的查找

  1. 跳跃表的时间复杂度和红黑树是一样的,而且实现简单
  2. 在并发的情况下,红黑树在插入删除的时候可能需要做rebalance的操作,这样的操作可能会涉及到整个树的其他部分;而链表的操作就会相对局部,只需要关注插入删除的位置即可,只要多个线程操作的地方不一样,就不会产生冲突

开发者的解释:
There are a few reasons:

  1. They are not very memory intensive. It’s up to you basically. Changing parameters about the probability of a node to have a given number of levels will make then less memory intensive than btrees. => 并不是非常耗费内存。控制好ZSKIPLIST_P的值,内存消耗和平衡树差不多
  2. A sorted set is often target of many ZRANGE or ZREVRANGE operations, that is, traversing the skip list as a linked list. With this operation the cache locality of skip lists is at least as good as with other kind of balanced trees. => 有序集合经常会进行 zrange 或 zrevrange 这样的范围查找,跳表里的双向链表可以十分方便的进行这操作
  3. They are simpler to implement, debug, and so forth. For instance thanks to the skip list simplicity I received a patch (already in Redis master) with augmented skip lists implementing ZRANK in O(log(N)). It required little changes to the code.
    About the Append Only durability & speed, I don’t think it is a good idea to optimize Redis at cost of more code and more complexity for a use case that IMHO should be rare for the Redis target (fsync() at every command). Almost no one is using this feature even with ACID SQL databases, as the performance hint is big anyway. => 实现简单,zrank 还能达到O(logn)的时间复杂度

About threads: our experience shows that Redis is mostly I/O bound. I’m using threads to serve things from Virtual Memory. The long term solution to exploit all the cores, assuming your link is so fast that you can saturate a single core, is running multiple instances of Redis (no locks, almost fully scalable linearly with number of cores), and using the “Redis Cluster” solution that I plan to develop in the future.

在 src/t_zset.c 文件中,主要有两个结构,zskiplist 和 zskiplistNode,前者保存跳跃表信息(如表头节点、表尾节点、长度),而 zskiplistNode 用于保存节点
另外,跳跃表中的节点按照分值大小进行排序, 当分值相同时, 节点按照成员对象的大小进行排序。这一点也是redis针对跳表这个结构做出的优化之一。具体的优化点为:

  • 允许重复分值,即多个节点允许相同的分值,但是每个节点的成员对象必须是唯一的
  • 比较的时候不仅仅是分值,还有整个对象,即分值相同,按照成员对象大小排序
  • 在第一层有一个back指针,适用于 ZREVRANGE 方法,允许从尾部到头部来遍历列表
posted @ 2020-10-22 12:09  猫熊小才天  阅读(1855)  评论(0编辑  收藏  举报