热门标签 | HotTags
当前位置:  开发笔记 > 编程语言 > 正文

【带你redis从入门到不会系列】了解跳跃表只看这篇大概不够

前言oh肉丝oh杰克youjump!ijump!yeah一起沉浸在jump的喜悦里今天跟大家来聊聊redis的跳跃表everybody都需要了解的跳跃表不仅仅是概念还有代码强烈建议


前言


oh 肉丝


oh 杰克


you jump! i jump!


yeah 一起沉浸在jump的喜悦里


今天跟大家来聊聊 redis 的跳跃表


everybody 都需要了解的跳跃表 不仅仅是概念 还有代码 强烈建议阅读《Redis5 设计与源码分析》陈雷编著



有序集合


我们先看看我们平时常用的关于 redis有序集合
大家常用的命令如下


127.0.0.1:6379> zadd jump 1 jack
(integer) 1
127.0.0.1:6379> zadd jump 2 rose
(integer) 1
127.0.0.1:6379> zrange jump 0 1
1) "jack"
2) "rose"

redis 的有序集合的复杂度


ZADD O(M*log(N)), N 是有序集的基数, M 为成功添加的新成员的数量。


ZRANGE O(log(N)+M), N 为有序集的基数,而 M 为结果集的基数


ZRANK O(log(N))


ZSCORE O(1)


我们不妨可以思考下,什么样的数据结构能满足上述,来这时候拿出我们的数据结构知识储备了。 线性表链表栈与队列树它全家系列 想想哪种能符合上述


也是常见问题 为什么使用跳表当数据结构


zscore

而且树的实现有多蛋疼,可以经常听到以下对话


你可以手撕 ** 树吗


我可以爬 ** 树 你看可以吗?



zscore 返回有序集 key 中,成员 member 的 score 值。如果是O(1)的话 有链表那味


那么我猜跳表就是链表的它私生子,绝对不是树的私生子。


跳跃表


事实上,跳表是一个多层的有序双向链表


有一座楼,有三层高,每个人都会影子分身术,可以每层楼放自己影子(放不放看每个人的心情,一旦定了就不能后悔),并且每个房间都有通往下一层的楼梯。这里有3个人分别狗蛋A、狗蛋B、狗蛋C,有一次考试结束


狗蛋A 考了60分


狗蛋B 考了80分


狗蛋C 考了100分


每个狗蛋根据分数自己排了顺序,考的好的站后面。


狗蛋他爸住在最顶楼(第二层) 想问问谁考了100分


大概图长这个样子



1.狗蛋他爸问狗蛋A:你多少分。 狗蛋A说:60,这层后面没狗蛋了。


2.狗蛋他爸在狗蛋A的房间往下一层走。


4.看到了狗蛋B:狗蛋B说80分,后面没有狗蛋了。


5.狗蛋他爸在狗蛋B的房间往下走,发现了狗蛋C,并且就是考了100的那的狗蛋。


路线大概是这样子



有人肯定会说,这个还不如有序链表啊 搞什么分层



毕竟是洪光光为了省事瞎画的图,我们再把图精致点,我们再品品这种结构的好处



看了源码你会发现每个狗蛋几层高真的是看狗蛋心情。


分析源码


有人说:跳跃表概念我也懂,有本事直接撸源码。


撸就撸,来,大家一起撸 ~~~~~~~ 源码



结构体


/**
* 跳跃表节点的结构体
*/
typedef struct zskiplistNode {
// 用动态字符串的数据key(member)的类型 如果头节点 NULL
sds ele;
// 对应的排序分值 如果头节点 NULL
double score;
// 指向前一个节点的戒指 当然头节点指向的NULL
struct zskiplistNode *backward;
// 柔性数组 数组大小 参照zslRandomLevel(void) 长度为1到 64
struct zskiplistLevel {
// 指向前一个节点的戒指 当然头节点指向的NULL
struct zskiplistNode *forward;
// 表示该层下的节点数量
unsigned long span;
} level[];
} zskiplistNode;
/**
* 跳跃表结构体
*/
typedef struct zskiplist {
// 指向跳跃表的头和尾部
struct zskiplistNode *header, *tail;
// 跳跃表长度 除去头节点
unsigned long length;
// 跳跃表的层级
int level;
} zskiplist;

根据上述的狗蛋图和跳跃表的结构体,看看能不能在心中有个实现思路。如果有的话,就不要往下看了,我怕看了你被我带偏。



跳跃表的基本知识点


1.层高最多64层,具体层高看心情(随机生成,越大越概率越低)


2.每层都是有序双向链表(有前进指针和后退指针)


3.header节点就是类似大学的宿管(知道每层的房间数和当层下一个房间的指针)


4.每一层最后的一个节点都是指向NULL(就是用下个节点是为NULL判断是不是本层最后一个)


5.第0层也就是最后一层的节点上数量就是跳跃表的实际长度(想想上述例子,地基都打在第一层,当然能知道整个具体长度)


源码分析


zslRandomLevel


#define ZSKIPLIST_MAXLEVEL 64 /* Should be enough for 2^64 elements */
#define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */

/**
* 随机生产跳跃表节点的层高
* @return
*/
int zslRandomLevel(void) {
// 默认层高 1层
int level = 1;
// &0xFFFF = 65535 只取低16位 相当于只获取 1 ~ 65535
// ZSKIPLIST_P -> 0.25
while ((random()&0xFFFF) <(ZSKIPLIST_P * 0xFFFF))
level += 1;
// 最终的概率为 1/(1 - ZSKIPLIST_P)
return (level}

你看吧每个狗蛋的层高的真的看心情。


zslCreate


/**
* 初始化跳跃表
* @return
*/
zskiplist *zslCreate(void) {
int j;
zskiplist *zsl;
// 初始化跳跃表内存
zsl = zmalloc(sizeof(*zsl));
// 默认一层
zsl->level = 1;
// 长度默认0
zsl->length = 0;
// 初始化默认头节点
zsl->header = zslCreateNode(ZSKIPLIST_MAXLEVEL,0,NULL);
// 默认跳跃表为64层
for (j = 0; j zsl->header->level[j].forward = NULL;
zsl->header->level[j].span = 0;
}
// 后退指针
zsl->header->backward = NULL;
// 尾指针
zsl->tail = NULL;
return zsl;
}

zslInsert


/** 插入跳跃表节点
*
* @param zsl 管理跳跃表
* @param score 分值
* @param ele 字符串
* @return
*/
zskiplistNode *zslInsert(zskiplist *zsl, double score, sds ele) {
zskiplistNode *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--) {
rank[i] = i == (zsl->level-1) ? 0 : rank[i+1];
// 有下节点 且 下个节点小于当前的分值 且 (如果节点分值相同的情况下 比较key的字典)
while (x->level[i].forward &&
(x->level[i].forward->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;
}
// update[i] 记录每个层的前节点
// rank[i] 记录每个层的需要更新的步长
// 初始化需要插入节点的层高
level = zslRandomLevel();
// 如果插入的节点层高 大于 跳跃表的层高
if (level > zsl->level) {
// 需要将多出的层高
for (i = zsl->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 // 每一层为有序链表 参照插入链表的做法
x->level[i].forward = update[i]->level[i].forward;
update[i]->level[i].forward = x;
x->level[i].span = update[i]->level[i].span - (rank[0] - rank[i]);
// 更新步长
update[i]->level[i].span = (rank[0] - rank[i]) + 1;
}
// 因为插入一个节点 需要每层的span都需要+1
for (i = level; i 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;
else
// 如果没有节点 则 插入的节点为最后一个节点 尾指针指向自己
zsl->tail = x;
// 增加跳跃表的长度 因为插入一个节点
zsl->length++;
return x;
}

现在看不懂具体代码实现没关系,因为本篇文章没打算让你看懂具体实现,具体代码可以自己下载redis 5.0版本里t_zset.c文件或者可以购买《Redis5 设计与源码分析》陈雷编著 (强烈推荐后者)。



最后


如果你以后成为面试官,问别人有序集合的底层实现是什么?


如果那个人说是跳跃表。


请你杠回去,有序集合底层实现使用 跳跃表压缩列表 根据参数配置 zset-max-ziplust-entriszset-max-ziplist-value 决定


/* Lookup the key and create the sorted set if does not exist. */
zobj = lookupKeyWrite(c->db,key);
if (zobj == NULL) {
if (xx) goto reply_to_client; /* No key + XX option: nothing to do. */
if (server.zset_max_ziplist_entries == 0 ||
server.zset_max_ziplist_value argv[scoreidx+1]->ptr))
{
zobj = createZsetObject();
} else {
zobj = createZsetZiplistObject();
}
dbAdd(c->db,key,zobj);
}

毕竟面试官都是杠精



文章如果有描述错误的地方,恳请留言矫正。




推荐阅读
  • 深入解析Android 4.4中的Fence机制及其应用
    在Android 4.4中,Fence机制是处理缓冲区交换和同步问题的关键技术。该机制广泛应用于生产者-消费者模式中,确保了不同组件之间高效、安全的数据传输。通过深入解析Fence机制的工作原理和应用场景,本文探讨了其在系统性能优化和资源管理中的重要作用。 ... [详细]
  • 深入探索HTTP协议的学习与实践
    在初次访问某个网站时,由于本地没有缓存,服务器会返回一个200状态码的响应,并在响应头中设置Etag和Last-Modified等缓存控制字段。这些字段用于后续请求时验证资源是否已更新,从而提高页面加载速度和减少带宽消耗。本文将深入探讨HTTP缓存机制及其在实际应用中的优化策略,帮助读者更好地理解和运用HTTP协议。 ... [详细]
  • 本指南从零开始介绍Scala编程语言的基础知识,重点讲解了Scala解释器REPL(读取-求值-打印-循环)的使用方法。REPL是Scala开发中的重要工具,能够帮助初学者快速理解和实践Scala的基本语法和特性。通过详细的示例和练习,读者将能够熟练掌握Scala的基础概念和编程技巧。 ... [详细]
  • 单链表的高效遍历及性能优化策略
    本文探讨了单链表的高效遍历方法及其性能优化策略。在单链表的数据结构中,插入操作的时间复杂度为O(n),而遍历操作的时间复杂度为O(n^2)。通过在 `LinkList.h` 和 `main.cpp` 文件中对单链表进行封装,我们实现了创建和销毁功能的优化,提高了单链表的使用效率。此外,文章还介绍了几种常见的优化技术,如缓存节点指针和批量处理,以进一步提升遍历性能。 ... [详细]
  • 手指触控|Android电容屏幕驱动调试指南
    手指触控|Android电容屏幕驱动调试指南 ... [详细]
  • 在尝试对 QQmlPropertyMap 类进行测试驱动开发时,发现其派生类中无法正常调用槽函数或 Q_INVOKABLE 方法。这可能是由于 QQmlPropertyMap 的内部实现机制导致的,需要进一步研究以找到解决方案。 ... [详细]
  • 如何将Python与Excel高效结合:常用操作技巧解析
    本文深入探讨了如何将Python与Excel高效结合,涵盖了一系列实用的操作技巧。文章内容详尽,步骤清晰,注重细节处理,旨在帮助读者掌握Python与Excel之间的无缝对接方法,提升数据处理效率。 ... [详细]
  • PTArchiver工作原理详解与应用分析
    PTArchiver工作原理及其应用分析本文详细解析了PTArchiver的工作机制,探讨了其在数据归档和管理中的应用。PTArchiver通过高效的压缩算法和灵活的存储策略,实现了对大规模数据的高效管理和长期保存。文章还介绍了其在企业级数据备份、历史数据迁移等场景中的实际应用案例,为用户提供了实用的操作建议和技术支持。 ... [详细]
  • 本文介绍了如何利用Shell脚本高效地部署MHA(MySQL High Availability)高可用集群。通过详细的脚本编写和配置示例,展示了自动化部署过程中的关键步骤和注意事项。该方法不仅简化了集群的部署流程,还提高了系统的稳定性和可用性。 ... [详细]
  • 使用 ListView 浏览安卓系统中的回收站文件 ... [详细]
  • Python 伦理黑客技术:深入探讨后门攻击(第三部分)
    在《Python 伦理黑客技术:深入探讨后门攻击(第三部分)》中,作者详细分析了后门攻击中的Socket问题。由于TCP协议基于流,难以确定消息批次的结束点,这给后门攻击的实现带来了挑战。为了解决这一问题,文章提出了一系列有效的技术方案,包括使用特定的分隔符和长度前缀,以确保数据包的准确传输和解析。这些方法不仅提高了攻击的隐蔽性和可靠性,还为安全研究人员提供了宝贵的参考。 ... [详细]
  • 深入解析CAS机制:全面替代传统锁的底层原理与应用
    本文深入探讨了CAS(Compare-and-Swap)机制,分析了其作为传统锁的替代方案在并发控制中的优势与原理。CAS通过原子操作确保数据的一致性,避免了传统锁带来的性能瓶颈和死锁问题。文章详细解析了CAS的工作机制,并结合实际应用场景,展示了其在高并发环境下的高效性和可靠性。 ... [详细]
  • C++ 异步编程中获取线程执行结果的方法与技巧及其在前端开发中的应用探讨
    本文探讨了C++异步编程中获取线程执行结果的方法与技巧,并深入分析了这些技术在前端开发中的应用。通过对比不同的异步编程模型,本文详细介绍了如何高效地处理多线程任务,确保程序的稳定性和性能。同时,文章还结合实际案例,展示了这些方法在前端异步编程中的具体实现和优化策略。 ... [详细]
  • 深入理解排序算法:集合 1(编程语言中的高效排序工具) ... [详细]
  • 本文全面解析了JavaScript中的DOM操作,并提供了详细的实践指南。DOM节点(Node)通常代表一个标签、文本或HTML属性,每个节点都具有一个nodeType属性,用于标识其类型。文章深入探讨了DOM节点的创建、查询、修改和删除等操作,结合实际案例,帮助读者更好地理解和掌握DOM编程技术。 ... [详细]
author-avatar
muc4093631
这个家伙很懒,什么也没留下!
PHP1.CN | 中国最专业的PHP中文社区 | DevBox开发工具箱 | json解析格式化 |PHP资讯 | PHP教程 | 数据库技术 | 服务器技术 | 前端开发技术 | PHP框架 | 开发工具 | 在线工具
Copyright © 1998 - 2020 PHP1.CN. All Rights Reserved | 京公网安备 11010802041100号 | 京ICP备19059560号-4 | PHP1.CN 第一PHP社区 版权所有