关于有序集合的实现,从 zaddGenericCommand函数说起:
从代码中我们发现有序集合在存储时似乎使用了两种方案:一种Zset,另一种是zsetlistpack。主要是根据zset_max_listpack_entries和zset_max_listpack_value来判断。这两个字段什么意思呢,我们在redis.conf中找到了这两个参数的说明:
什么意思呢?同Hashes、lists相似,有序集合为了节约大量存储空间进行了特殊的编码梳理。这种编码仅在有序集合存储的元素的长度在符合zset_max_listpack_entries和zset_max_listpack_value的设置时才生效。结合上面的代码就是,小数据用zsetlistpack,大数据用zset。或者设置zset_max_listpack_entries强制使用zset。这两种数据类型究竟是个什么玩意儿呢?
zsetlistpack就是简单的listpack结构,说到底最终是一段malloc分配的地址。而zset就相对复杂,它是这样的:
看过笔者redis源码系列的朋友应该知道dict是个什么样的数据结构。但zskiplist是什么鬼呢?上图中的注释大致说这是skiplist的一个变种。也就是说这玩意儿的本质还是skiplist。那么skiplist是什么呢?skiplist专业术语叫跳跃链表(跳表)。也就是说这玩意儿是一种链表。但是跳跃是什么呢?怎么跳。我们知道链表的访问是从头到尾式逐个遍历,这种方式在数据少的时候没啥问题,但是如果数据多了,就很耗时间。为了改善这种情况,有大佬就想,能不能不要逐个遍历,比如跳过紧邻的直接访问下一个呢?来讨论具体实现可能更容易理解:
在跳表中,和1节点对比,如果大于1节点则直接到3节点。如果目标值小于3节点则回到2节点,如果大于3节点则跳转5节点。依次这样,整个查询过程中也就减少了遍历的节点个数,这种思想就是跳表。基于这种思想我们可以对该结构进行扩充至跳过3个甚至更多节点,这样也就大大减少了遍历时间。更多跳跃表的细节这里就不过多解读,毕竟能力有限。到这里key的存储解决了,具体的值是怎么存储的呢?
具体的插入因为有序集合采用了两种数据结构,因此是两种插入方式,首先是数据量小时用的listpack:
至于具体的规则是从前往后还是从后往前,需要具体研究lpseek,此处不过多解读,因为此处是一个类似双向链表的遍历。
对于跳表结构,过程类似,只是redis的跳表中增加了一个随机层数,也就是前面所说的那个跳跃步长,选择随机步长主要用于解决redis在存储过程中增删改的具体问题。