跳跃表(zskiplist)在redis中作为sorted set的底层实现,今天来学习一下。

/* ZSETs use a specialized version of Skiplists */
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;

zskiplist中有头指针和尾指针,尾指针方便从尾部遍历。zskiplistNode中有个结构体zskiplistLevel,这个结构体是跳跃表的实现,level是个结构体数组,其中forward指针有用跨度,可以指向非相邻的结点。span表示forward指向的结点距离本结点多远。

/* Create a new skiplist. */
zskiplist *zslCreate(void) {//创建并返回一个新的跳跃表int j;zskiplist *zsl;zsl = zmalloc(sizeof(*zsl));//跳跃表zsl->level = 1;zsl->length = 0;zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);for (j = 0; j < ZSKIPLIST_MAXLEVEL; j++) {zsl->header->level[j].forward = NULL;zsl->header->level[j].span = 0;}zsl->header->backward = NULL;zsl->tail = NULL;return zsl;
}

刚开始默认结点最大层级是1,指针赋空。

int zslRandomLevel(void) {//返回一个随机值,用作新跳跃表节点的层数int level = 1;  //0xFFFF 65535 //random()返回一个[0...1)的随机数while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF))//random()&0xFFFF始终小于65535level += 1;return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL;
}

random是一个伪随机数,执行多次之后,返回的level也是一个固定的序列,后面会测试一下。

/* Insert a new node in the skiplist. Assumes the element does not already* exist (up to the caller to enforce that). The skiplist takes ownership* of the passed SDS string 'ele'. */
//创建一个成员为 ele ,分值为 score 的新节点,并将这个新节点插入到跳跃表 zsl 中
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {  //mapanzskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;unsigned int rank[ZSKIPLIST_MAXLEVEL];int i, level;serverAssert(!isnan(score));x = zsl->header;  //取出头for (i = zsl->level-1; i >= 0; i--) {//在各个层查找节点的插入位置/* store rank that is crossed to reach the insert position */rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];//沿着前进指针遍历跳跃表//首先比较分值while (x->level[i].forward &&(x->level[i].forward->score < score || (x->level[i].forward->score == score && sdscmp(x->level[i].forward->ele,ele) < 0) )){rank[i] += x->level[i].span;x = x->level[i].forward;}update[i] = x;// 记录将要和新节点相连接的节点}/* we assume the element is not already inside, since we allow duplicated* scores, reinserting the same element should never happen since the* caller of zslInsert() should test in the hash table if the element is* already inside or not. */level = zslRandomLevel();if (level > zsl->level) {// 如果新节点的层数比表中其他节点的层数都要大for (i = zsl->level; i < level; i++) {// 初始化未使用层rank[i] = 0;update[i] = zsl->header;update[i]->level[i].span = zsl->length;}zsl->level = level; // 更新表中节点最大层数}x = zslCreateNode(level,score,ele);// 将前面记录的指针指向新节点,并做相应的设置for (i = 0; i < level; i++) {   //1x->level[i].forward = update[i]->level[i].forward;// 设置新节点的 forward 指针update[i]->level[i].forward = x;// 将沿途记录的各个节点的 forward 指针指向新节点/* update span covered by update[i] as x is inserted here */x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);// 计算新节点跨越的节点数量update[i]->level[i].span = (rank[0] - rank[i]) + 1;}/* increment span for untouched levels */// 未接触的节点的 span 值也需要增一,这些节点直接从表头指向新节点for (i = level; i < zsl->level; i++) {update[i]->level[i].span++;}// 设置新节点的后退指针x->backward = (update[0] == zsl->header) ? NULL : update[0];if (x->level[0].forward) //设置新建节点前面的后退指针x->level[0].forward->backward = x;elsezsl->tail = x;zsl->length++;return x;
}

跳跃表的插入函数,我们写一个程序测试一下:

int t_zsetTest()        //mapan
{zskiplist *p = zslCreate();zslInsert(p,1.1,"a");zslInsert(p,3.3,"b");zslInsert(p,2.2,"c");zslInsert(p,4.4,"d");zslInsert(p,0.9,"e");zslInsert(p,3.8,"f");zslInsert(p,5.5,"g");printf("----------------\n");zslInsert(p,1.3,"h");return 0;
}

先看一下,执行对应插入操作所获取的层级:

mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000
mapan@mapan-virtual-machine:~/redis-5.0.5/src$ ./redis-server
level=1,score=1.100000
level=2,score=3.300000
level=1,score=2.200000
level=1,score=4.400000
level=1,score=0.900000
level=1,score=3.800000
level=1,score=5.500000
----------------
level=2,score=1.300000

你每次运行所对应的层级是一个固定的序列。3.3和1.3对应的层级是2,其他都是1。当执行到zslInsert(p,5.5,"g")时,我们看一下跳跃表的结构。

最下面一层对应都是L1。这时候在执行zslInsert(p,1.3,"h");操作,就会先遍历L2对应的那层,在遍历L1对应的那层。

/* Delete an element with matching score/element from the skiplist.* The function returns 1 if the node was found and deleted, otherwise* 0 is returned.** If 'node' is NULL the deleted node is freed by zslFreeNode(), otherwise* it is not freed (but just unlinked) and *node is set to the node pointer,* so that it is possible for the caller to reuse the node (including the* referenced SDS string at node->ele). *///从跳跃表 zsl 中删除包含给定节点 score 并且带有指定对象 ele 的节点
int zslDelete(zskiplist *zsl, double score, sds ele, zskiplistNode **node) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;int i;x = zsl->header;for (i = zsl->level-1; i >= 0; i--) { //逐层遍历,遍历完后要么x指向对应节点,要么找不到while (x->level[i].forward &&(x->level[i].forward->score < score ||(x->level[i].forward->score == score &&sdscmp(x->level[i].forward->ele,ele) < 0))){x = x->level[i].forward;}update[i] = x;// 记录沿途节点printf("score=%lf,i=%d\n",update[i]->score,i);}/* We may have multiple elements with the same score, what we need* is to find the element with both the right score and object. */x = x->level[0].forward;if (x && score == x->score && sdscmp(x->ele,ele) == 0) {printf("x->score=%lf\n",x->score);zslDeleteNode(zsl, x, update);if (!node)zslFreeNode(x);else*node = x; //node用来保存结点,用于重复使用return 1;}return 0; /* not found */
}

删除结点首先也要遍历结点。

/* Update the score of an elmenent inside the sorted set skiplist.* Note that the element must exist and must match 'score'.* This function does not update the score in the hash table side, the* caller should take care of it.** Note that this function attempts to just update the node, in case after* the score update, the node would be exactly at the same position.* Otherwise the skiplist is modified by removing and re-adding a new* element, which is more costly.** The function returns the updated element skiplist node pointer. *///更新排序后的跳跃表中元素的score。
zskiplistNode *zslUpdateScore(zskiplist *zsl, double curscore, sds ele, double newscore) {zskiplistNode *update[ZSKIPLIST_MAXLEVEL], *x;int i;/* We need to seek to element to update to start: this is useful anyway,* we'll have to update or remove it. */x = zsl->header;for (i = zsl->level-1; i >= 0; i--) {while (x->level[i].forward &&(x->level[i].forward->score < curscore ||(x->level[i].forward->score == curscore &&sdscmp(x->level[i].forward->ele,ele) < 0))){x = x->level[i].forward;}update[i] = x;}/* Jump to our element: note that this function assumes that the* element with the matching score exists. */x = x->level[0].forward;serverAssert(x && curscore == x->score && sdscmp(x->ele,ele) == 0);  //确保找到对应的结点printf("assert\n");/* If the node, after the score update, would be still exactly* at the same position, we can just update the score without* actually removing and re-inserting the element in the skiplist. */if ((x->backward == NULL || x->backward->score < newscore) &&(x->level[0].forward == NULL || x->level[0].forward->score > newscore)) //newscore修正后,要注意集合的顺序{x->score = newscore;return x;}/* No way to reuse the old node: we need to remove and insert a new* one at a different place. */zslDeleteNode(zsl, x, update);  zskiplistNode *newnode = zslInsert(zsl,newscore,x->ele);/* We reused the old node x->ele SDS string, free the node now* since zslInsert created a new one. */x->ele = NULL;zslFreeNode(x);return newnode;
}

更新跳跃表中的元素时,要保证更新后还是有序的,这就可能需要删除原来的阶段,重新插入新的结点。

redis之zskiplist相关推荐

  1. 聊一聊 Redis 数据内部存储使用到的数据结构

    点击上方 好好学java ,选择 星标 公众号 重磅资讯.干货,第一时间送达 今日推荐:2020年7月程序员工资统计,平均14357元,又跌了,扎心个人原创100W+访问量博客:点击前往,查看更多 R ...

  2. java使用xml存储数据_聊一聊 Redis 数据内部存储使用到的数据结构

    Redis 数据库虽然一直都在使用,但是对其内部存储结构之类的,都没有研究过,哪怕是面试的时候都没有准备过这方面的东西.最近在看一门网课,里面有讲到过这一块的内容,结合了<Redis 设计与实现 ...

  3. redis 系列7 数据结构之跳跃表

    redis 系列7 数据结构之跳跃表 原文:redis 系列7 数据结构之跳跃表 一.概述 跳跃表(skiplist)是一种有序数据结构,它通过在每个节点中维持多个指向其他节点的指针,从而达到快速访问 ...

  4. Redis数据类型使用场景及有序集合SortedSet底层实现详解

    Redis常用数据类型有字符串String.字典dict.列表List.集合Set.有序集合SortedSet,本文将简单介绍各数据类型及其使用场景,并重点剖析有序集合SortedSet的实现. Li ...

  5. Redis之跳跃表实现

    http://redisbook.com/index.html 跳跃表的实现 Redis 的跳跃表由 redis.h/zskiplistNode 和 redis.h/zskiplist 两个结构定义, ...

  6. redis字符串匹配_Redis设计原理

    1.简介 Redis中的每个Key-Value在内存中都会被划分成DictEntry.RedisObject以及具体对象,其中DictEntry又分别包含指向Key和Value的指针(以RedisOb ...

  7. redis 底层数据结构详解

    目录 1.字符串 1.1 SDS定义 1.2 SDS1好处 2.列表 2.1 void 实现多态 3 字典 3.1   底层实现是hash表 3.2 字典结构 3.3 哈希算法 3.3.1 rehas ...

  8. Redis设计与实现-笔记(一)

    文章目录 缓存理解 带来问题 本地缓存解决方案 分布式缓存 缓存读写模式/更新策略 正文 第一部分 数据结构与对象 第2章 简单动态字符串 2.1 SDS的定义 2.2 SDS与C字符串的区别 2.2 ...

  9. Redis中ZSet的底层数据结构跳跃表skiplist,你真的了解吗?

    欢迎大家关注我的微信公众号[老周聊架构],Java后端主流技术栈的原理.源码分析.架构以及各种互联网高并发.高性能.高可用的解决方案. 一.前言 老周写这篇文章的初衷是这样的,之前项目中有大量使用 R ...

最新文章

  1. java程序无法启动_由于Java程序,Tomcat无法启动
  2. 谨慎使用PHP的引用
  3. Design Pattern IDisposable Pattern C
  4. 以太坊节点布置(3) 启动geth客户端
  5. 反汇编学习笔记2 函数的本质
  6. 我女朋友让我删前任,我明明删了她还是要分手...
  7. [数据结构]链表中销毁和清空的区别
  8. 模仿JavaAppArguments.java示例
  9. ASP.NET Atlas简单控件介绍——Sys.Component基类与Sys.UI.Control基类
  10. Oracle 10g Create Database
  11. QQ目录下各文件用途不完全揭密
  12. Linux driver读书笔记(2) - Bus Types总线类型(mybus/mydevice/mydriver实例)
  13. Android Studio生成APP方法及其所在位置
  14. Windows10系统提示 为了对电脑进行保护,已经阻止此应用(管理员已阻止你运行此应用。有关详细信息,请与管理员联系。)的解决办法
  15. Tempo超进化,提效降本好帮手,工时管理小当家 Timewise
  16. SQL Server异地数据库每日同步作业操作步骤
  17. [4G5G专题-79]:流程 - 4G LTE 寻呼流程Paging
  18. 用anacnda创建虚拟环境用不用指定python版本
  19. C++之重载:函数名的鱼塘
  20. Java中的finalize方法

热门文章

  1. Java HashMap的put操作(Java1.8)
  2. C语言建立有向图的邻接表及其遍历操作
  3. 在当前进程下取得当前登陆用户
  4. js点击获取连接的内容
  5. 清空sqlserver当前日志信息!
  6. 函数式编程与REST的思考
  7. fork()与pid
  8. 用户进程与内核进程通信netlink实例
  9. Typescript 基本类型
  10. 感謝有PPStream這種好東西