Redis中的zset原理以及用Java实现跳跃表

准备工作

先在Redis官网下载最新的稳定版本6.2。按照官网给出的安装指南到Linux服务器上安装。

zadd调用过程

redis/src/server.c 文件中定义了所有命令运行要调用的方法。zadd命令运行会调用t_zset.c文件的zaddCommand方法。

struct redisCommand redisCommandTable[] = {
    
    
{
    
    "zadd",zaddCommand,-4,
     "write use-memory fast @sortedset",
     0,NULL,1,1,1,0,0,0},
}

redis/src/t_zset.c 的zaddGenericCommand方法会调用一个关键的方法,就是zsetAdd,这个方法会判断元素存储数据结构是ziplist还是skiplist。如果zset集合的元素个数大于redis.config文件设置的zset_max_ziplist_entries大小或者zset集合元素的member的长度大于redis.config设置的zset_max_ziplist_value大小,就会使用skiplist数据结构存储。

void zaddCommand(client *c) {
    
    
    zaddGenericCommand(c,ZADD_NONE);
}

int zsetAdd(robj *zobj, double score, sds ele, int *flags, double *newscore) {
    
    
    //......省略
    if (zobj->encoding == OBJ_ENCODING_ZIPLIST) {
    
    
        
        if ((eptr = zzlFind(zobj->ptr,ele,&curscore)) != NULL) {
    
    
           //......省略
        } else if (!xx) {
    
    
            zobj->ptr = zzlInsert(zobj->ptr,ele,score);
            if (zzlLength(zobj->ptr) > server.zset_max_ziplist_entries ||
                sdslen(ele) > server.zset_max_ziplist_value)
                zsetConvert(zobj,OBJ_ENCODING_SKIPLIST);
            //......省略
        } else {
    
    
           //......省略
        }
    } else if (zobj->encoding == OBJ_ENCODING_SKIPLIST) {
    
    
        //......省略
    } else {
    
    
        //......省略
    }
     //......省略
}

redis/conf文件可以设置这两个参数的大小。
为了节省空间,当zset集合的元素个数少于128个并且zadd的member的长度小于64字节,使用ziplist数据结构存储元素,否则使用skiplist数据结构存储元素

zset-max-ziplist-entries 128
zset-max-ziplist-value 64

尝试修改zset-max-ziplist-value的值为3,修改完后重启redis-server。向命名为test的zset集合中依次添加元素A、BB、CCC、DDDD,使用OBJECT ENCODING test(集合名称)查看当前使用的数据结构,数据结构在插入第四个节点后修改为skiplist,因为字符串DDDD的长度大于3。
在这里插入图片描述

zskiplist

redis/src/server.c 文件中定义了zskiplistNode和zskiplist的结构。

(1)zskiplistNode包括字符串ele、浮点类型的分值score、后退指针backward和层级数组lelel。每一个层级包括前进指针和跨度。在查找某个节点的过程中, 将沿途访问过的所有层的跨度累计起来, 得到的结果就是目标节点在跳跃表中的排位。

(2)zskiplist包括头结点、尾结点、包含结点个数和结点(非头结点)最大层级数。

typedef struct zskiplistNode {
    
    
    sds ele;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
    
    
        struct zskiplistNode *forward;
        unsigned long span;
    } level[];
} zskiplistNode;

typedef struct zskiplist {
    
    
    struct zskiplistNode *header, *tail;
    unsigned long length;
    int level;
} zskiplist;

typedef struct zset {
    
    
    dict *dict;
    zskiplist *zsl;
} zset;

借用《Redis设计与实现》描述跳跃表的一张图。
(1)头结点包含32个层级,但不存储实际元素。
(2)包含o1、o2、o3三个元素。
(3)查找o3可以直接通过第5级索引查找。
在这里插入图片描述

用Java实现跳跃表

自己尝试去实现了一个跳跃表,只实现了一个插入和查找的方法,没有考虑重复元素的情况。

import lombok.Builder;
import lombok.Data;

import java.util.Map;
import java.util.Random;
import java.util.concurrent.ConcurrentHashMap;

@Data
@Builder
class SkipListNode {
    
    
    private String ele;
    private double score;
    private SkipListNode backward;
    private SkipListLevel level[];
}

@Data
@Builder
class SkipList {
    
    
    private SkipListNode header;
    private SkipListNode tail;
    private long length;
    private int level;
}

@Data
@Builder
class SkipListLevel {
    
    
    private SkipListNode forward;
    private long span;
}

@Data
@Builder
class ZSet {
    
    
    private Map<String, SkipList> data;
    //跳跃表头结点的最大层级是32.
    public static final int SIZE = 3;

    //添加元素,先判断name这个集合是否存在,如果不存在就初始化
    public SkipListNode add(String name, String ele, double score) {
    
    
        SkipList skipList = data.get(name);
        if(null == skipList) {
    
    
            //初始化
            skipList = initSkipList();
            data.put(name, skipList);
        }
        //随机生成一个层级
        Random random = new Random();
        int level = random.nextInt(SIZE);
        //从头结点的层级开始遍历
        SkipListNode skipListNode = SkipListNode.builder().ele(ele).score(score).level(initSkipListLevel(level + 1)).build();
        for (int i = level; i >= 0; i--) {
    
    
            //计算排位
            double rank = 0;
            SkipListLevel prevLevel = skipList.getHeader().getLevel()[i];
            if(null == prevLevel.getForward()) {
    
    
                prevLevel.setForward(skipListNode);
                prevLevel.setSpan((long) score);
                continue;
            }

            SkipListNode prevNode = prevLevel.getForward();
            while(null != prevLevel.getForward() && score >= (rank + prevLevel.getSpan())) {
    
    
                rank += prevLevel.getSpan();
                prevNode =  prevLevel.getForward();
                prevLevel = prevLevel.getForward().getLevel()[i];
            }

            //插入结点
            if(null != prevLevel.getForward()) {
    
    
                skipListNode.getLevel()[i].setForward(prevLevel.getForward());
                skipListNode.getLevel()[i].setSpan((long) (prevLevel.getForward().getScore() - skipListNode.getScore()));
            }
            prevNode.getLevel()[i].setForward(skipListNode);
            prevNode.getLevel()[i].setSpan((long) (skipListNode.getScore() - prevNode.getScore()));

        }
        return skipListNode;
    }

    public SkipListNode find(String name, double score) {
    
    
        SkipList skipList = data.get(name);
        if(null == skipList) {
    
    
            return null;
        }
        int fromIndex = SIZE - 1;
        SkipListNode prevNode = skipList.getHeader();
        while(null != prevNode) {
    
    
            SkipListLevel skipListLevel = prevNode.getLevel()[fromIndex];
            SkipListNode forwardNode = skipListLevel.getForward();
            if(null == forwardNode || forwardNode.getScore() > score) {
    
    
                fromIndex--;
            } else if(forwardNode.getScore() == score) {
    
    
                System.out.println("在第" + (fromIndex + 1) + "级索引找到:(" + forwardNode.getEle() + "," + forwardNode.getScore() + ")==>");
                return forwardNode;
            } else {
    
    
                System.out.println("在第" + (fromIndex + 1) + "级索引找到:(" + forwardNode.getEle() + "," + forwardNode.getScore() + ")==>");
                prevNode  = forwardNode;
            }
        }
        return null;
    }

    private SkipList initSkipList() {
    
    
        //设置头结点
        SkipListNode header = SkipListNode.builder().level(initSkipListLevel(SIZE)).build();
        SkipList skipList =  SkipList.builder().header(header).build();
        return skipList;
    }

    private SkipListLevel[] initSkipListLevel(int level) {
    
    
        SkipListLevel levels[] = new SkipListLevel[level];
        for (int i = 0; i < level; i++) {
    
    
            levels[i] = SkipListLevel.builder().build();
        }
        return levels;
    }

}

public class SkipListTest {
    
    

    public static void main(String[] args) {
    
    
        ZSet zSet = ZSet.builder().data(new ConcurrentHashMap<>()).build();
        zSet.add("test", "A", 10);
        zSet.add("test", "B", 20);
        zSet.add("test", "C", 30);
        zSet.add("test", "D", 40);
        zSet.add("test", "E", 50);

        SkipList skipList = zSet.getData().get("test");
        SkipListNode header = skipList.getHeader();
        SkipListLevel[] levels = header.getLevel();
        for (int i = levels.length - 1; i >= 0 ; i--) {
    
    
            SkipListLevel level = levels[i];
            SkipListNode node = level.getForward();
            while(null != node) {
    
    
                System.out.print("(" + node.getEle() + "," + node.getScore() + ") ");
                node = node.getLevel()[i].getForward();
            }
            System.out.println();
        }

        zSet.find("test", 50);
    }
}

运行结果:
在这里插入图片描述
为了避免生成太多层级索引,我将生成的最大层级限制改为了3。运行一次,用随机数生成结点的最大层级索引。这里结点A有三个层级,结点B有3级索引,结点C有1级索引,结点D有1级索引,结点E有2级索引。查找结点E,会经过3级索引(A,10)、(B,20)、2级索引(E,50)。比只有1级索引需要经过(A,10)、(B,20)、(C,30)、(D,40)、(E,50)快。

参考

Redis 跳跃表的实现
后台开发第七十八讲|redis,有序集合(orderedset),跳表,面试,源码学习一节课搞定1. 跳表的演变 2. 跳表的实现 3. redis中zset实现

猜你喜欢

转载自blog.csdn.net/u012734723/article/details/115035533