Redis 有 5 种基础数据结构,分别为:string (字符串)、list (列表)、set (集合)、hash (哈希) 和 zset (有序集合)。
常见数据结构与操作指令如下
string (字符串)
key 和 value 都是字符串
缓存信息:信息结构体使用 JSON 序列化成字符串,然后将序列化后的字符串塞进 Redis 来缓存。同样,取信息会经过一次反序列化的过程。
list (列表)
Redis 的列表相当于 Java 语言里面的 LinkedList,注意它是链表而不是数组。这意味着list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n)。
hash (字典)
数组 + 链表二维结构,但是只能是字符串。
set (集合)
它内部的键值对是无序的唯一的。它的内部实现相当于一个特殊的字典,字典中所有的 value 都是一个值 NULL。
zset (有序列表)
一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。并且,它的内部实现用的是一种叫着「跳跃列表」的数据结构。
跳跃列表
zset 内部的排序功能是通过「跳跃列表」数据结构来实现的。
插入一个新元素"23"的步骤:
-
遍历链表 c,只有一个元素 “19“,发现 23 大于 19,所以在 “19“ 后面找。
-
回到链表 b,查询到第一个大于 23 的元素 “26” 。
-
回到链表 a,发现 21 比 23 小,所以插入到 “21” 和 “26” 节点之间。
可以看到,跳跃列表其实是实现了二分查找,将定位的时间复杂度O(n)降低至O(log n)。
思考:小明问 如果按照上下层元素个数1:2的比例构建跳跃列表,会不会有问题?
影响:之后新插入一个节点之后,就会破坏了上下相邻两层链表上节点个数 2:1 的比例关系。要维持这种对应关系,就必须把新插入的节点后面的所有节点(也包括新插入的节点)重新进行调整,这会让时间复杂度又降低成了 O(n),删除节点同样如此。
思考:Redis的跳跃列表会怎么做?
其实,Redis的跳跃列表不要求上下相邻两层链表之间的节点个数有严格的比例关系,而是每个节点随机一个层数 level ,以此去构建跳跃列表。
这里先给出其特点:
-
第一层链表永远保存着最完整的元素数据。
-
位于第n层的元素,也肯定在 1~n-1层 之中。
-
新插入一个元素不会影响其它元素的层数。
结合特点来看看构建的过程。
构建跳跃列表的步骤:
思考:既然跳跃列表是一个依赖随机层级的“概率型” 的数据结构,那么”随机“如何体现,又是如何保证查找性能的呢。
实际上决定层数的随机计算对跳表的查找性能有着很大影响,这并不是一个普通的服从均匀分布的随机数,它的计算过程如下:
-
指定一个节点最大的层数 MaxLevel,指定一个概率 p, 层数 level 默认为 1 。
-
生成一个 0~1 的随机数 r,若 r < p,且 level < MaxLevel ,则执行 level++。
-
重复第 2 步,直至生成的 r > p 为止,此时的 level 就是要插入的层数。
randomLevel() level = 1 // random()返回一个[0...1)的随机数 while random() < p and level < MaxLevel do level := level + 1 return level
在 Redis 的 skiplist 实现中,p=1/4 ,MaxLevel=64。
也就是说,一个节点每次在当前层想继续深入只有1/4的概率能够拥有下一层的”权限“,且最深只能是64层。