本文首发于:深刻理解跳表在Redis中的应用
微信公众号:后端技术指南针
持续输出干货 欢迎关注html
前面写了一篇关于跳表基本原理和特性的文章,本次继续介绍跳表的几率平衡和工程实现,node
跳表在Redis、LevelDB、ES中都有应用,git
本文以Redis为工程蓝本,分析跳表在Redis中的工程实现。github
经过本文你将了解到如下内容:redis
1.Redis的数据结构后端
Redis对外共有约五种类型的对象:缓存
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在论文中描述了它的计算过程以下:
论文中生成随机层数的伪码:
论文中关于随机层数的伪码
在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()是个[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就增长一层,这样虽然没有使用浮点数,可是也实现了几率平衡。
咱们很容易看出,产生越高的节点层数出现几率越低,不管如何层数老是知足幂次定律越大的数出现的几率越小。
若是某件事的发生频率和它的某个属性成幂关系,那么这个频率就能够称之为符合幂次定律。幂次定律的表现是少数几个事件的发生频率占了整个发生频率的大部分, 而其他的大多数事件只占整个发生频率的一个小部分。幂次定律
幂次定律应用到跳表的随机层数来讲就是大部分的节点层数都是黄色部分,只有少数是绿色部分,而且几率很低。
定量的分析以下:
所以若是咱们要求节点的平均层数,那么也就转换成了求几率分布的指望问题了,灵魂画手大白再次上线:
表中P为几率,V为对应取值,给出了全部取值和几率的可能,所以就能够求这个几率分布的指望了。
方括号里面的式子其实就是高一年级学的等比数列,经常使用技巧错位相减求和,从中能够看到结点层数的指望值与1-p成反比。
对于Redis而言,当p=0.25时结点层数的指望是1.33。
小结:在Redis源码中有详尽的关于插入和删除调整跳表的过程,本文就再也不展开了,代码并不算难懂,都是纯C写的没有那么多炫技的特效,放心大胆读起来。
4.参考资料
5.推荐阅读
白话布隆过滤器BloomFilter
理解缓存系统的三个问题
几种高性能网络模型
二叉树及其四大遍历
理解Redis单线程运行模式
Linux中各类锁及其基本原理
理解Redis持久化
深刻理解IO复用之epoll
深刻理解跳跃链表[一]
理解堆和堆排序
理解堆和优先队列
6.关于本公众号
开号不久做者力争持续输出原创干货,若是文章有帮助到你,
但愿朋友们多多转发和分享,做者会更加有动力,推出更好的文章,共同进步。
微信公众号:后端技术指南针