秒杀面试官系列 - Redis zset底层是怎么实现的
前言
如果Redis面试有什么必问的面试题,那么Zset的底层原理一定是排名前三的。
在上篇文章中我们提到ZSET
的底层实现是ziplist
压缩列表 和skiplist
跳跃表。
这篇文章我们就详细讲解:
- zset是什么
- zset怎么用
- zset的原理是什么。
让你轻松秒杀面试官,获得Offer。
Zset是什么
Redis ZSET(Sorted Set) 有序集合,ZSET中的成员是有序排列的。
它和 set 集合的相同之处在于:
- 集合中的每一个成员都是字符串类型,并且不允许重复
而它们最大区别是:
- zset 是有序的
- set 是无序的
这是因为有序集合中每个成员都会关联一个 double64
(双精度浮点数)类型的 score
(分数值),Redis 正是通过 score 实现了对集合成员的排序。
zset 是 Redis 常用数据类型之一,它适用于排行榜类型的业务场景。
Redis 使用以下命令创建一个有序集合:
|
|
- key:指定一个键名;
- score:分数值,用来描述 member,它是实现排序的关键;
- member:要添加的成员(元素)。
也就是,一个ZSET里面有多个 <score, member>
的pair,通过member的score来排序,最终你就得到了一个有序的member列表。
比如:member是用户ID,score是这个用户ID的战力值。那么你使用ZSET很容易得到一个排行榜,以score排好序的member列表
当 key 不存在时,将会创建一个新的有序集合,并把分数/成员(score/member)添加到有序集合中;当 key 存在时,但 key 并非 zset 类型,此时就不能完成添加成员的操作,同时会返回一个错误提示。
在有序集合中,成员是唯一存在的,但是分数(score)却可以重复。有序集合的最大的成员数为 2^32 - 1 (大约 40 多亿个)。
**注意:**score使用的是double64,精度只有53bit
。所以如果使用int64作为score的请注意,不要超过53位,否则会截断,导致排序不符合你的预期。
官网页面有描述精度问题:Redis ZAdd
ZSET怎么用
最简单的增删改查,统计数量,范围查找,交集并集等等都是很方便的支持的。完成的命令可以查看官网文档:sorted-set
几个最简单的例子:
|
|
你也可以自己在线测试Redis的命令来熟悉。https://try.redis.io/
ZSET底层实现
我们讲了ZSET是什么,以及ZSET怎么用,相信你已经在脑海中对Redis ZSET有了初步的概念。那下一步我们就要讲解ZSET的核心,底层是怎么实现的。让你了解这个数据结构的设计有多优秀。
上面我们说过,ZSET底层使用ziplist和skiplist实现。
并不是两个都会用到,而是二选一的。在满足以下条件的时候会使用ziplist:
- 保存元素(member)数量小于128
- 每个元素(member)长度小于64字节
注意:这两个参数可以通过redis.conf
里面的zset-max-ziplist-entries
选项和 zset-max-ziplist-value
进行修改。
1. ziplist 压缩列表
压缩列表(ziplist)是Redis为了节省内存而开发的,是由一系列特殊编码的连续内存块组成的顺序型数据结构,一个压缩列表可以包含任意多个节点(entry)。ziplist就是为了节省内存redis会用时间换空间的典型例子。
解决问题
- 内存紧凑型列表,节省内存空间、提升内存使用率
存在问题
- 查询效率O(n)
- 内存重分配
- 连锁更新
当 ziplist
保存的元素过多时,查找中间数据的复杂度就增加了。更糟糕的是,如果 ziplist 里面保存的是字符串,ziplist 在查找某个元素时,还需要逐一判断元素的每个字符,这样又进一步增加了复杂度。查询效率O(n)
。
所以Redis zset使用ziplist要限制元素的总数量和每个元素的长度上限。因为超过这个值之后使用ziplist的优势就没有了,就会自动切换成skiplist。
这里你应该明白了,为什么在数据量和长度比较小的时候使用ziplist了,因为Redis的瓶颈之一就是内存,所以Redis的设计极致的考虑了内存的使用。在数据量较少的时候,ziplist充分体现了它内存的优势,又不会影响整体的查询效率。
zset中entry的添加
|
|
自动转换编码格式
如果发现满足ziplist的规则(长度小于等于128且最大的元素的长度小于等于64)
|
|
可以看到,在作为zset实现的时候,每当插入一个<member, score>
pair,就insert了两个连续的entry,第一个为member,第二个为score。
总结
ziplist是为节省内存空间而生的。 ziplist是一个为Redis专门提供的底层数据结构之一,本身可以有序也可以无序。当作为list和hash的底层实现时,节点之间没有顺序;当作为zset的底层实现时,节点之间会按照大小顺序排列。 如果长度变化的时候会检查是否满足规则,底层结构会在ziplist和skiplist之间转换。
2. skiplist 跳跃表
概念
跳跃表是一种有序的数据结构,它通过在每个节点中维持多个指向其它节点的指针,从而达到快速访问节点的目的。
跳跃表支持平均O(logN)复杂度的节点查找,还可以通过顺序性操作来批量处理节点。
如果任意一个条件不满足,就会Convert为SKIPLIST
底层实现。
|
|
这里的OBJ_ENCODING_SKIPLIST
,实际上是skiplist
(跳跃表)+dict
(字典)。
为什么要这么设计呢?我们知道数据量大的时候才会使用skiplist,skiplist的查询时间复杂度为O(logN)。
dict
保存着member到score的映射,这样就可以使用O(1)
的复杂度来查找member对应的score值。虽然同时使用两种结构,但它们会通过指针来共享相同元素的 member 和 score,因此不会浪费额外的内存。
就是为了性能而做的考虑。
为什么使用跳跃表(skiplist)而不使用平衡树
Redis使用SkipList而不是用平衡树的主要原因有:
- 平衡树不适合范围查找。需要中序遍历继续寻找其它节点。而Skiplist就非常简单了,使用
lv1
的指针进行向右遍历即可。 - 平衡树的插入和删除引发子树调整,逻辑复杂,SkipList相对简单很多
- 平衡树每个节点包含两个指针,SkipList平均不到2个指针,内存上更有优势。
- 从算法实现难度上来比较,Skiplist 比平衡树要简单得多。
结论
看了Redis ZSET的实现,不得不感叹,Redis的设计真的优秀,每个细节都考虑到了,对内存的使用有着完美的执着。这也就是为什么Redis能火,成为互联网公司必使用的技术之一。也是程序员面试的必面类型之一。了解Redis不仅能让你轻松通过面试,也能让你掌握更多知识,轻松应付工作中的各种需求。
<全文完>