深入理解跳跃链表在Redis中的应用
0.前言
前面写了一篇关于跳表基本原理和特性的文章,本次继续介绍跳表的概率平衡和工程实现,跳表在Redis、LevelDB、ES中都有应用,本文以Redis为工程蓝本,分析跳表在Redis中的工程实现。
通过本文你将了解到以下内容:
- Redis基本的数据类型和底层数据结构
- Redis的有序集合的实现方法
- Redis的跳表实现细节
1.Redis的数据结构
Redis对外共有约五种类型的对象:
- 字符串(String)
- 列表(List)
- 哈希(Hash)
- 集合(Set)
- 有序集合(SortedSet)
redis源码文件src/server.h中对于5种结构的定义:

1 /* The actual Redis Object */ 2 #define OBJ_STRING 0 /* String object. */ 3 #define OBJ_LIST 1 /* List object. */ 4 #define OBJ_SET 2 /* Set object. */ 5 #define OBJ_ZSET 3 /* Sorted set object. */ 6 #define OBJ_HASH 4 /* Hash object. */

Redis对象由redisObject结构体表示,从src/server.h可以看到该结构的定义如下:

1 typedef struct redisObject { 2 unsigned type:4; 3 unsigned encoding:4; 4 unsigned lru:LRU_BITS; 5 int refcount; 6 void *ptr; 7 } robj;

redisObject明确了对象类型、对象编码方式、过期设置、引用计数、内存指针等,从而完整表示一个key-value键值对。
由于Redis是基于内存的,Antirez在实现这5种数据类型时在底层创建了多种数据结构,在对象底层选择采用哪种结构来实现,需要根据对象大小以及单个元素大小来进行确定,从而提高空间使用率和效率。
如图展示了Redis对外使用的数据类型和底层的数据结构:
有序集合对象的编码可以是ziplist或者skiplist,在元素小于128并且元素长度小于64Byte时才会选择压缩列表实现,一般使用skiplist跳表实现。
2.Redis的ZSet
ZSet结构同时包含一个字典和一个跳跃表,跳跃表按score从小到大保存所有集合元素。
字典保存着从member到score的映射。这两种结构通过指针共享相同元素的member和score,不会浪费额外内存。
1 typedef struct zset { 2 dict *dict; 3 zskiplist *zsl; 4 } zset;
ZSet中的字典和跳表布局:
注:图片源自网络
3.ZSet中跳表的实现细节
- 随机层数的实现原理
跳表是一个概率型的数据结构,元素的插入层数是随机指定的。Willam Pugh在论文中描述了它的计算过程如下:
- 指定节点最大层数 MaxLevel,指定概率 p, 默认层数 lvl 为1
- 生成一个0~1的随机数r,若r<p,且lvl<MaxLevel ,则lvl ++
- 重复第 2 步,直至生成的r >p 为止,此时的 lvl 就是要插入的层数。
论文中生成随机层数的伪码:
论文中关于随机层数的伪码
在Redis中对跳表的实现基本上也是遵循这个思想的,只不过有微小差异,看下Redis关于跳表层数的随机源码src/z_set.c:

1 /* Returns a random level for the new skiplist node we are going to create. 2 * The return value of this function is between 1 and ZSKIPLIST_MAXLEVEL 3 * (both inclusive), with a powerlaw-alike distribution where higher 4 * levels are less likely to be returned. */ 5 int zslRandomLevel(void) { 6 int level = 1; 7 while ((random()&0xFFFF) < (ZSKIPLIST_P * 0xFFFF)) 8 level += 1; 9 return (level<ZSKIPLIST_MAXLEVEL) ? level : ZSKIPLIST_MAXLEVEL; 10 }

其中两个宏的定义在redis.h中:
1 #define ZSKIPLIST_MAXLEVEL 32 /* Should be enough for 2^32 elements */ 2 #define ZSKIPLIST_P 0.25 /* Skiplist P = 1/4 */
可以看到while中的:
1 (random()&0xFFFF) < (ZSKIPLIST_P*0xFFFF)
第一眼看到这个公式,因为涉及位运算有些诧异,需要研究一下Antirez为什么使用位运算来这么写?
最开始的猜测是random()返回的是浮点数[0-1],于是乎在线找了个浮点数转二进制的工具,输入0.25看了下结果:
可以看到0.25的32bit转换16进制结果为0x3e800000,如果与0xFFFF做与运算结果是0,好像也符合预期,再试一个0.5:
可以看到0.5的32bit转换16进制结果为0x3f000000,如果与0xFFFF做与运算结果还是0,不符合预期。
我印象中C语言的math库好像并没有直接random函数,所以就去Redis源码中找找看,于是下载了3.2版本代码,也并没有找到random()的实现,不过找到了其他几个地方的应用:
- random()在dict.c中的使用:
- random()在cluster.c中的使用:
看到这里的取模运算,后知后觉地发现原以为random()是个[0-1]的浮点数,但是现在看来是uint32才对,这样Antirez的式子就好理解了。
由于ZSKIPLIST_P=0.25,所以相当于0xFFFF右移2位变为0x3FFF,假设random()比较均匀,
在进行0xFFFF与运算之后高16位清零之后,低16位取值就落在0x0000-0xFFFF之间,这样while为真的概率只有1/4,更一般地说为真的概率为1/ZSKIPLIST_P。
对于随机层数的实现并不统一,重要的是随机数的生成,在LevelDB中对跳表层数的生成代码是这样的:

1 template <typename Key, typename Value> 2 int SkipList<Key, Value>::randomLevel() { 3 4 static const unsigned int kBranching = 4; 5 int height = 1; 6 while (height < kMaxLevel && ((::Next(rnd_) % kBranching) == 0)) { 7 height++; 8 } 9 assert(height > 0); 10 assert(height <= kMaxLevel); 11 return height; 12 } 13 14 uint32_t Next( uint32_t& seed) { 15 seed = seed & 0x7fffffffu; 16 17 if (seed == 0 || seed == 2147483647L) { 18 seed = 1; 19 } 20 static const uint32_t M = 2147483647L; 21 static const uint64_t A = 16807; 22 uint64_t product = seed * A; 23 seed = static_cast<uint32_t>((product >> 31) + (product & M)); 24 if (seed > M) { 25 seed -= M; 26 } 27 return seed; 28 }

可以看到leveldb使用随机数与kBranching取模,如果值为0就增加一层,这样虽然没有使用浮点数,但是也实现了概率平衡。
- 跳表结点的平均层数
我们很容易看出,产生越高的节点层数出现概率越低,无论如何层数总是满足幂次定律越大的数出现的概率越小。
幂次定律:如果某件事的发生频率和它的某个属性成幂关系,那么这个频率就可以称之为符合幂次定律。幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其余的大多数事件只占整个发生频率的一个小部分。
幂次定律应用到跳表的随机层数来说就是大部分的节点层数都是黄色部分,只有少数是绿色部分,并且概率很低。
定量的分析如下:
- 节点层数至少为1,大于1的节点层数满足一个概率分布。
- 节点层数恰好等于1的概率为p^0(1-p)。
- 节点层数恰好等于2的概率为p^1(1-p)。
- 节点层数恰好等于3的概率为p^2(1-p)。
- 节点层数恰好等于4的概率为p^3(1-p)。
- 依次递推节点层数恰好等于K的概率为p^(k-1)(1-p)
因此如果我们要求节点的平均层数,那么也就转换成了求概率分布的期望问题了,灵魂画手大白再次上线:
表中P为概率,V为对应取值,给出了所有取值和概率的可能,因此就可以求这个概率分布的期望了。
方括号里面的式子其实就是高一年级学的等比数列,常用技巧错位相减求和,从中可以看到结点层数的期望值与1-p成反比。
对于Redis而言,当p=0.25时结点层数的期望是1.33。
小结:在Redis源码中有详尽的关于插入和删除调整跳表的过程,本文就不再展开了,代码并不算难懂,都是纯C写的没有那么多炫技的特效,放心大胆读起来。
4.参考资料
- http://note.huangz.me/algorithm/arithmetic/power-law.html
- https://juejin.im/post/5cb885a8f265da03973aa8a1
- https://epaperpress.com/sortsearch/download/skiplist.pdf
- https://www.h-schmidt.net/FloatConverter/IEEE754.html
- http://www.ruanyifeng.com/blog/2010/06/ieee_floating-point_representation.html
- https://cyningsun.github.io/06-18-2018/skiplist.html
深入理解跳跃链表在Redis中的应用的更多相关文章
- 深入理解跳表在Redis中的应用
本文首发于:深入理解跳表在Redis中的应用微信公众号:后端技术指南针持续输出干货 欢迎关注 前面写了一篇关于跳表基本原理和特性的文章,本次继续介绍跳表的概率平衡和工程实现, 跳表在Redis.Lev ...
- Redis中的基本数据结构
Redis基础数据结构 基础数据结构 sds简单动态字符串 数据结构 typedef struct sdstr{ int len // 字符串分配的字节 int free // 未使用的字节数 cha ...
- 深入理解Redis中的主键失效及其实现机制
参考:http://blog.sina.com.cn/s/articlelist_1221155353_0_1.html 作为一种定期清理无效数据的重要机制,主键失效存在于大多数缓存系统中,Reids ...
- Redis中7种集合类型应用场景&redis常用命令
Redis常用数据类型 Redis最为常用的数据类型主要有以下五种: String Hash List Set Sorted set 在具体描述这几种数据类型之前,我们先通过一张图了解下Redis内部 ...
- Redis中的数据结构
1. 底层数据结构, 与Redis Value Type之间的关系 对于Redis的使用者来说, Redis作为Key-Value型的内存数据库, 其Value有多种类型. String Hash L ...
- 用Python深入理解跳跃表原理及实现
最近看 Redis 的实现原理,其中讲到 Redis 中的有序数据结构是通过跳跃表来进行实现的.第一次听说跳跃表的概念,感到比较新奇,所以查了不少资料.其中,网上有部分文章是按照如下方式描述跳跃表的: ...
- Redis中为什么使用跳表---------转自http://blog.csdn.net/u010412301/article/details/64923131
最近在研究数据库的一些底层实现,百度的面试官问到了跳表,当时没有回答上来,在csdn上看到了这篇文章,感觉写的比较好,希望大家可以多多交流. Redis里面使用skiplist是为了实现sorted ...
- 快速整明白Redis中的字典到底是个啥
字典简介 字典是一种用于保存键值对的数据结构,可以通过键值对中的键快速地查找到对应的值.在Redis所使用的C语言中,并没有内置字典,所以Redis自己实现了字典. 整个Redis数据库的所有的键和值 ...
- Redis中5种数据结构的使用场景介绍
转载于:http://www.itxuexiwang.com/a/shujukujishu/redis/2016/0216/108.html?1455861435 一.redis 数据结构使用场景 原 ...
随机推荐
- group 状压dp
应某些人要求,我把标签删掉了 这是一道好题. 一看$c<=16$果断状压,但是怎么压? 一个很显然的思路是,枚举上下两层的状态,每一层的状态极限有$C(c,c/2)$,c=16的时候有13000 ...
- C# WebApi 根据实体类检查传参或字典检查参数
根据实体类或字典检查传参,是否缺少参数并返回缺少参数 值类型必须声明可空 /// <summary> /// 根据 Dictionary<string, string> 得到实 ...
- 使用Samba或实现文件共享
安装Samba服务软件包: 命令:yum install samba 使用Samba服务口令验证方式可以让共享文件更加的安全,做到仅让信任的用户访问,而且验证过程也很简单 第1步:检查当前是否为use ...
- salesforce lightning零基础学习(十四) Toast 浅入浅出
本篇参考: https://developer.salesforce.com/docs/component-library/bundle/force:showToast/specification h ...
- python——掌握sorted函数的用法
看本篇文章的前提是掌握 00函数的基本概念.01函数参数传递方式 可参考本人博客文章 sorted函数 是一个内建函数,接收一个可迭代对象,按照指定类型.指定顺序进行排序,特点是返回一个新的列表,不改 ...
- JVM 运行参数 & 代码监控
1.Java代码监控 JDK提供java.lang.management包, 其实就是基于JMX技术规范,提供一套完整的MBean,动态获取JVM的运行时数据,达到监控JVM性能的目的. packag ...
- JavaScript with Image:创建缩略图
当图片很大,直接把图片从Server下载到浏览器上看是一种很不明智的做法,浪费了服务器的资源,网络带宽和客户端的资源.所以,通常Server和Client之间会传输缩略图,只有当Client请求某张图 ...
- C++中对C的扩展学习新增内容———面向对象(继承)函数扩展性及虚函数机制
1.c语言中的多态,动态绑定和静态绑定 void do_speak(void(*speak)()) { speak(); } void pig_speak() { cout << &quo ...
- PHP+Swoole并发编程的魅力
PHP语言是一个短生命周期的Web编程语言,很多PHPer已经形成了fpm下编程的思维定势.实际上在Swoole出现之后,这种串行化编程的模式早已被打破.使用Swoole完全可以轻易实现更灵活的并发编 ...
- [LC]876题 Middle of the Linked List (链表的中间结点)(链表)
①中文题目 给定一个带有头结点 head 的非空单链表,返回链表的中间结点. 如果有两个中间结点,则返回第二个中间结点. 示例 1: 输入:[1,2,3,4,5]输出:此列表中的结点 3 (序列化形式 ...