redis 有序集合内部实现(5)

本篇是redis系列的第四篇。本文主要围绕redis 有序集合 结构展开讨论

Redis中的sorted set,是在skiplist, dict和ziplist基础上构建起来的:

  1. 当数据较少时,sorted set是由一个ziplist(另起一篇文章单独介绍)来实现的。
  2. 当数据多的时候,sorted set是由一个叫zset的数据结构来实现的,这个zset包含一个dict + 一个skiplist。dict用来查询数据到分数(score)的对应关系,而skiplist用来根据分数查询数据(可能是范围查找,skiplist根据分数查找数据)。

sorted set ziplist 实现

在这里我们先来讨论一下前一种情况——基于ziplist实现的sorted set。ziplist就是由很多数据项组成的一大块连续内存。由于sorted set的每一项元素都由数据和score组成,因此,当使用zadd命令插入一个(数据,score)对的时候,底层在相应的ziplist上就插入两个数据项:数据在前,score在后。

ziplist的主要优点是节省内存,但它上面的查找操作只能按顺序查找(可以正序也可以倒序)。因此,sorted set 的各个查询操作,就是在ziplist上从前向后(或从后向前)一步步查找,每一步前进两个数据项,跨域一个(数据, score)对。

随着数据的插入,sorted set底层的这个ziplist就可能会转成zset的实现。那么到底插入多少才会转呢?

zset-max-ziplist-entries 128

zset-max-ziplist-value 64
  1. 当sorted set中的元素个数,即(数据, score)对的数目超过128的时候,也就是ziplist数据项超过256的时候。
  2. 当sorted set中插入的任意一个数据的长度超过了64的时候。

sorted set ziplist实现

Redis 的 zset 是一个复合结构,一方面它需要一个 hash 结构来存储 value 和 score 的对应关系,另一方面需要提供按照 score 来排序的功能,还需要能够指定 score 的范围来获取 value 列表的功能,这就需要另外一个结构「跳跃列表」。

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

zset 的内部实现是一个 hash 字典加一个跳跃列表 (skiplist)。hash 结构在讲字典结构时已经详细分析过了(见字典),它很类似于 Java 语言中的 HashMap 结构

在读下面内容之前不懂跳表的可以先行阅读    什么是跳表   这篇文章,以对跳表有个大概的了解。

上图就是跳跃列表的示意图,图中只画了四层,Redis 的跳跃表共有 32 层。每一个 kv 块对应的结构如下面的代码中的zslnode结构,header 也是这个结构,只不过 value 字段是 null 值——无效的,score 是 Double.MIN_VALUE,用来垫底的。节点 之间使用指针串起来形成了双向链表结构,它们是 有序 排列的,从小到大。不同的 节点层高可能不一样,层数越高的 节点越少。同一层的 节点会使用指针串起来。每一个层元素的遍历都是从 header 出发。

typedef struct zskiplistNode {
    robj *obj;
    double score;
    struct zskiplistNode *backward;
    struct zskiplistLevel {
        struct zskiplistNode *forward;
        unsigned int span;
    } level[];
} zskiplistNode;

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

设想如果跳跃列表只有一层会怎样?插入删除操作需要定位到相应的位置节点 (定位到最后一个比「我」小的元素,也就是第一个比「我」大的元素的前一个),定位的效率肯定比较差,复杂度将会是 O(n),因为需要挨个遍历。也许你会想到二分查找,但是二分查找的结构只能是有序数组。跳跃列表有了多层结构之后,这个定位的算法复杂度将会降到 O(lg(n))。

如图所示,我们要定位到那个紫色的 kv,需要从 header 的最高层开始遍历找到第一个节点 (最后一个比「我」小的元素),然后从这个节点开始降一层再遍历找到第二个节点 (最后一个比「我」小的元素),然后一直降到最底层进行遍历就找到了期望的节点 (最底层的最后一个比我「小」的元素)。

我们将中间经过的一系列节点称之为「搜索路径」,它是从最高层一直到最底层的每一层最后一个比「我」小的元素节点列表。

有了这个搜索路径,我们就可以插入这个新节点了。不过这个插入过程也不是特别简单。因为新插入的节点到底有多少层,得有个算法来分配一下,跳跃列表使用的是随机算法。

 

随机层数

对于每一个新插入的节点,都需要调用一个随机算法给它分配一个合理的层数。我们知道传统的跳跃表每层晋升的概率是50%,所以第一层的概率是: 50% 的 Level1,25% 的 Level2,12.5% 的 Level3,一直到最顶层。。。。 ,

不过 Redis 标准源码中的晋升概率只有 25%。所以官方的跳跃列表更加的扁平化,层高相对较低,在单个层上需要遍历的节点数量会稍多一点。节点最大的层数不允许超过一个最大值,记为MaxLevel(redis里为32)。

 

直接上代码,这里我是用java实现了下插入的过程。

/**
 * 链表从小到大排序
 * @param <E>
 */
public class SkipList <E extends Comparable<? super E>> {
	//跳表层数32层: 定义成32层理论上对于2^32-1个元素的查询最优。
	transient final int MAX_LEVEL = 32;
	//层级晋升概率
	transient final double p =0.25;
	//当前跳表的有效层
	transient int validLevel = 0;

	transient int size;
	//跳表的头部节点
	transient ZskiplistNode<E> header ;
	//跳表的尾节点
	transient ZskiplistNode<E> last ;

	public SkipList(){
		header = new ZskiplistNode<E>(MAX_LEVEL, null,0);
		validLevel=1;
	}

	public ZskiplistNode<E> add(E val,double score) {
		//从头结点开始搜索,遍历结束后  cur节点为要插入节点位置左边最近的一个节点
		ZskiplistNode<E> cur = header;
		// 记录搜索的路径节点
		ZskiplistNode<E>[] update = new ZskiplistNode[MAX_LEVEL];
		// 存储搜索路径节点的  排名
		int rank [] = new int[MAX_LEVEL];

		/**
		 * 从所有节点中的最高层级开始搜索,效率最好
		 *  逐步降级寻找目标节点,得到「搜索路径」
		 * 从header节点开始遍历,横向遍历 找到下一个节点比较,
		 * 		1、若大于则把node.next[i].forward赋给cur当前遍历节点,然后开始遍历cur节点
		 * 		2、若等于则判断node.next[i].forward节点值是否大于val,值大于执行1
		 * 	 	3、若分值相等  val小于, 则继续向下遍历header(redis里需要先在字典里判断val是否存在,所以redis里跳表里 val不会重复,score可以相等)
		 * 	把搜索的路径存起来,方便后续维护新节点的next数组的指针
		 *
		 * 	假设插入的val值不在当前链表中
		 */
		for (int i = validLevel-1; i >= 0; i--){
			//rank[i]用来记录 update[i]距离header的距离,搜索路径节点的排名
			// rank[i]初始化为上一层所跨越的节点总数,因为上一层已经加过
			rank[i] = i == validLevel ? 0 : rank[i+1];
			//遍历链表,一直遍历到 要插入的结点位置的最近的一个节点
			while (cur.next[i].forward!=null
					 && (cur.next[i].forward.score<score ||
							//	这里允许score相等,但是val不会重复
					 		(cur.next[i].forward.score == score && cur.next[i].forward.val.compareTo(val)< 0))
			){
				//横着搜索的时候累加
				rank[i] +=cur.next[i].span;
				//记录跨越了多少个节点
				cur = cur.next[i].forward;
			}
			update[i]=cur;
		}
		int level = randomLevel();
		//如果随机出的层数 大于当前最高层,更新多出来的那几层
		if (level > validLevel){
			for (int i = validLevel; i < level; i++){
				// 跨度都为0
				rank[i] = 0;
				update[i] = header;
				update[i].next[i].span = size;
			}
			//更新最高层
			validLevel = level;
		}
		//创建新节点
		cur = new ZskiplistNode(level,val,score);
		//为新节点更新前置指针(搜索路径执行新节点)和后置指针(搜索路径的forward赋值给新节点的forward)
		for (int i = 0; i < level; i++) {
			//值得注意的一点  update[i] 到cur.next[i]  之间肯定没有节点了
			//跳跃表新节点的每一层,改变其forward指针的指向(搜索路径节点的每一层指针 赋给新节点)
			cur.next[i].forward = update[i].next[i].forward;
			//搜索路径节点的  指向新节点
			update[i].next[i].forward = cur;
			//更新跨度
			cur.next[i].span = update[i].next[i].span - (rank[0] - rank[i]);
			update[i].next[i].span = (rank[0] - rank[i])+1;
		}
		// 更新高层的span值
		for (int i = level;i<validLevel;i++){

			update[i].next[i].span++;
		}
		// 重排一下后向指针
		cur.backward =(update[0] == header)?null:update[0];
		//如果插入节点的0层存前向节点则前向节点的后向指针为插入节点
		if (cur.next[0].forward != null){
			cur.next[0].forward.backward =cur;
		}else{
			//否则该节点为跳跃表的尾节点
			last=cur;
		}
		size++;
		return cur;
	}

	static class ZskiplistNode <E extends Comparable<? super E>> {
		/*
		 * 节点存储的值Val
		 */
		E val;

		double score;
		/*
		 * 节点指向第i层的节点next[i]
		 */
		ZskiplistLevel [] next;

		ZskiplistNode <E> backward;

		ZskiplistNode (int MAX_LEVEL, E val,double score) {
			this.next = new ZskiplistLevel [MAX_LEVEL];
			this.val = val;
			this.score =score;
			for (int i = 0;i<MAX_LEVEL;i++){
				next[i]=new ZskiplistLevel();
			}
		}
	}
	@Override
	public String toString() {
		StringBuilder sb = new StringBuilder();
		ZskiplistNode<E> cur = header,next =null;
		for (int i = validLevel-1; i >= 0; i--) {
			sb.append("第["+i+"]层");
			next =cur.next[i].forward;
			while (next != null) {
				sb.append(next.val + ","+next.score+" ");
				next = next.next[i].forward;
			}
			sb.append("\n");
		}
		return sb.toString();
	}
	static class ZskiplistLevel{

		 ZskiplistNode forward;

		/**
		 * 跨度,用来计算 排名,next[i]到前一节点的距离  不包括开始节点(包括结束节点)的距离
		 */
		int span;
	}
	public int size() {
		return size;
	}

	private int randomLevel(){
		int level = 1;
		while (Math.random()< p && level < this.MAX_LEVEL){
			level =level+1;
		}
		return level;
	}

	public static void main(String [] args){
		SkipList skipList =new SkipList();
		Random random = new Random();

		for (int i = 0; i< 100 ;i++){

			int a = random.nextInt(100);
			skipList.add("str"+i,a);
		}
		System.out.println(skipList.toString());
	}
}

首先我们在搜索合适插入点的过程中将「搜索路径」摸出来了,然后就可以开始创建新节点了,创建的时候需要给这个节点随机分配一个层数,再将搜索路径上的节点和这个新节点通过前向后向指针串起来。如果分配的新节点的高度高于当前跳跃列表的最大高度,就需要更新一下跳跃列表的最大高度。

删除过程和插入过程大致相同,这里我只是实现了插入过程。redis里的更新操作是先删除过插入。有兴趣的同学可以接着往下实现。

原创文章 15 获赞 22 访问量 6941

猜你喜欢

转载自blog.csdn.net/qq_31387317/article/details/93973414