Spark源码阅读系列停更了好久,因为一直没能达到想要的效果。写了一篇Spark物理计划生成,读下来味同嚼蜡,暂时不打算放出来了。最近偶然遇到《如何阅读一份源代码》,文中说“写代码是表达自己,读代码是在理解别人”,我想既然没有理解别人,为什么要写一篇水文欺骗自己呢?
最近开始看Kafka的书,书里面提到kafka的文件offset查找用到了“跳表”这种数据结构。跳表是一种随机化数据结构,查找、添加、删除操作都可以在对数期望时间下完成;当你设计一种存储结构,嫌数组O(n)的插入慢,又嫌链表的O(n)查找慢,可以尝试下跳表,既可以O(log n)的插入,又可以O(log n)的查找,唯一的代价是多出小于一倍的链表存储空间。
首先看一个Redis有序集合(Sorted Set)的例子, 向有序集合sc中插入三个元素x,y,z,排序评分为6, 10, 15;
localhost:~ renq$ redis-cli
127.0.0.1:6379> zadd sc 6 x 10 y 15 z
(integer) 3
127.0.0.1:6379> zrange sc 0 -1
1) "x"
2) "y"
3) "z"
127.0.0.1:6379> zrange sc 0 -1 WITHSCORES
1) "x"
2) "6"
3) "y"
4) "10"
5) "z"
6) "15"
127.0.0.1:6379>
一般地,跳表的结构如下图;每一层的节点随机向上形成索引,直到最上层只有一个索引节点。查找时从最上层索引开始,索引的存在加速了查找过程。
Redis内部采用了改进的跳表结构存储上述信息,简化成下图所示;
具体Redis的实现,有序集合叫做zset, 由一个字典dict和一个跳表zsl组成。
typedef struct zset {
// 字典
dict *dict;
// 跳表
zskiplist *zsl;
} zset;
字典用来在O(1)的时间检查集合中指定元素是否存在,在O(1)时间内根据键取出给定的值;跳表用来在O(log n)的时间内查找指定键值对,进行范围查找。
Redis完整的zset结构如下所示。
做一个当天NBA球员得分表score, 使用zadd插入球员的得分,注意先插入value(得分),后插入key(球员名)。使用zrange获取得分排行榜;
127.0.0.1:6379> zadd score 54 harden
(integer) 1
127.0.0.1:6379> zadd score 38 curry
(integer) 1
127.0.0.1:6379> zadd score 20 klay
(integer) 1
127.0.0.1:6379> zadd score 0 lowery
(integer) 1
127.0.0.1:6379> zcard score
(integer) 4
127.0.0.1:6379> zrange score 0 -1
1) "lowery"
2) "klay"
3) "curry"
4) "harden"
127.0.0.1:6379> zrange score 0 -1 WITHSCORES
1) "lowery"
2) "0"
3) "klay"
4) "20"
5) "curry"
6) "38"
7) "harden"
8) "54"
注意上面查询得到的是分数从低到高。想要倒序求分数最高的前几个,只需先-n, 再-1即可。例如查询得分前3和前5:
127.0.0.1:6379> zrange score -3 -1
1) "klay"
2) "curry"
3) "harden"
127.0.0.1:6379> zrange score -5 -1
1) "lowery"
2) "klay"
3) "curry"
4) "harden"
选择第2、第3高分:
127.0.0.1:6379> zrange score -3 -2
1) "klay"
2) "curry"
统计得分在某个区间的球员数量:
127.0.0.1:6379> zcount score 0 30
(integer) 2
127.0.0.1:6379> zcount score 0 60
(integer) 4
127.0.0.1:6379> zcount score 0 10
(integer) 1