Redis学习手册(五)--Redis 学习补充
1、键和值用什么结构组织?
- 为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有的键值对,在哈希表的每一个桶中都保存了键值对数据。
- 在这个哈希表的 entry 结构中,哈希桶中的元素保存的并不是值本身,而是指向具体值的指针,也就是说,不管 value 存放的数据类型是 string ,还是集合类型,哈希桶中的元素其实都只是指向它们的指针。
- Redis 解决哈希冲突的方法是拉链法
2、Redis 全局哈希表的 rehash 操作
2.1、Redis 如何进行 rehash 操作?
- 使用拉链法来解决哈希冲突时会存在一个问题,也就是随着哈希表中的数据的写入,某个桶上的链表可能会变得非常长,这样在这个桶上查找元素时需要逐一遍历这个桶上的元素,效率不高,所以 Redis 会对哈希表进行 rehash 操作,也就是增加现有哈希桶的数量。
- 为了 rehash 操作更加高效,Redis 默认使用了两个全局哈希表:分别记为哈希表 1 和哈希表 2 ,一开始插入数据时,默认使用哈希表 1 ,此时哈希表 2 并没有被分配空间。随着数据增多,Redis 需要进行 rehash ,这个过程分为三步:
- 为哈希表 2 分配更大的空间,比如说是当前哈希表 1 大小的两倍
- 将哈希表 1 中的数据重新映射到哈希表 2 中
- 释放哈希表 1 的空间
然后将哈希表 2 作为新的操作表,而释放完空间的哈希表 1 就作为下一次 rehash 时扩容的备用表。
2.2、渐进式 rehash
上面的过程看似简单,但是在 rehash 的第二步中涉及了大量的数据拷贝,如果一次性将哈希表 1 的数据全部迁移完,那么势必会造成 Redis 线程的阻塞,从而使其无法服务其他请求。
- 为了避免这个问题, Redis 使用了渐进式 rehash
在第二步拷贝数据时, Redis 仍然正常处理客户端请求,没处理一个请求,都从哈希表 1 中的第一个索引位置开始,顺带着将这个桶上的所有 entry 拷贝到哈希表 2 中;处理下一个请求时,顺带拷贝哈希表 1 下一个桶的 entry
3、单线程的 Redis 为什么可以那么快?
3.1、说明
Redis 的单线程主要是指 Redis 的网络 IO 和键值对读写是由一个线程来完成的。而 Redis 的其他功能,比如说持久化,异步删除,集群数据同步等都是由额外的线程来执行。
3.2、Redis 为什么使用单线程?
如果没有良好的系统设计,那么使用多线程反而会使得系统的吞吐量降低。
如果使用多线程,那么还需要考虑多线程共享资源的正确性,也就是为资源加锁,这会导致额外的开销
线程上下文的切换也可能会导致性能下降
3.3、Redis 为什么那么快?
- Redis 绝大部分操作都是在内存中完成的,同时 Redis 使用了高效的数据结构(例如哈希表和跳表),这是它高性能的一个重要原因。
- Redis 采用了多路复用机制,使其在网络 IO 中能并发处理大量的客户端请求,实现高吞吐率。
4、AOF 重写会阻塞吗?
- 与 AOF 日志由主线程写回不同,AOF 的重写过程是由子进程 bgrewriteaof 来完成的,这也是为了避免阻塞主线程,导致 Redis 性能下降
AOF 重写的过程可以总结为一个拷贝,两处日志
- 一个拷贝指每次执行重写时,主线程会 fork 出一个后台的
bgrewriteaof
子进程,此时,fork 会把主线程的内存拷贝一份给bgwriteaof
子进程,这里面包含了 Redis 的最新数据。然后子进程在不影响主线程的情况下,将拷贝的数据写成操作,记入重写日志 - 第一处日志指正在使用的 AOF 日志,Redis 会将这个操作写到它的缓冲区。这样一来,即使 Redis 宕机,那么这个 AOF 日志中的内容也是齐全的,可以用于恢复数据。
- 第二处日志指新的 AOF 重写日志。这个操作也会被写到重写日志的缓冲区。这样重写日志也不会丢失最新的操作。等到拷贝数据的所有操作记录重写完成后,重写日志记录的这些最新操作也会被写入到新的 AOF 文件,以保证 Redis 最新状态的记录。
最后,就可以使用新的 AOF 文件代替原来的旧文件了。
总的来说,每次 AOF 重写时,Redis 会先执行一个内存拷贝,用于重写;然后使用两个日志保证重写过程中,新写入的数据不丢失。而且,因为 Redis 采用额外的线程进行数据重写,所以重写过程不会阻塞主线程。
5、Redis 哨兵
在 Redis 中,如果主库挂了,那么我们就需要运行一个新主库,比如说将一个从库切换为一个主库,把它当成主库,这其中涉及到三个问题:
- 主库真的挂了吗?
- 该选择哪个从库作为主库?
- 怎么把新主库的相关信息通知给从库和客户端?
Redis 的哨兵机制是实现主从库自动切换的关键机制,它有效地解决了主从复制模式下故障转移的这三个问题。
5.1、哨兵机制的基本流程
- 哨兵其实就是一个运行在特殊模式下的 Redis 进程,主从库实例运行的同时,它也在运行。哨兵主要负责三个任务,即监控、选主 和 通知
- 监控:哨兵在运行过程中,会周期性地给所有的主从库发送 PING 命令,检测它们是否存活。
如果从库没有在限定时间内响应哨兵的 PING 命令,那么哨兵会将其标记为下线状态;同样,如果主库没有在规定时间内响应哨兵的 PING 命令,那么哨兵就会判断主库下线,然后开始自动切换主库的流程。
- 选主:当主库下线后,哨兵需要从很多个从库中,按照一定的规则选择一个从库实例,把它作为新的主库,在这一步完成后,现有的主从集群里就有了新主库
- 通知:在新的主库出现后,哨兵会执行最后一个任务:通知。
在执行通知任务时,哨兵会把新主库的连接信息发送给其他从库,让它们执行
replicaof
命令,和新主库建立连接,并进行数据复制。同时,哨兵会把新主库的连接信息发送给客户端,让它们将请求操作发到新主库上。
- 哨兵的通知任务相对来说比较简单,哨兵只需要将新主库的信息发送给从库和客户端,让它们与新主库建立连接就行,并不涉及决策的逻辑
- 在监控和选主两个任务中,哨兵需要做出两个决策:
- 在监控任务中,哨兵需要判断主库是否处于下线状态
- 在选主任务中,哨兵需要决定哪个从库实例升级为主库
5.2、主观下线和客观下线
哨兵对主库的下线判断有主观下线和客观下线两种。
- 主观下线:
哨兵进程会使用 PING 命令检测它自己和主、从库的网络连接状况,用来判断实例的状态。如果哨兵发现主库或从库对 PING 命令的响应超时了,那么就会先把它标记为主观下线
- 如果检测的是从库,那么哨兵简单地将其标记为主观下线就可以了,因为从库的下线一般影响不大,集群对外服务不会间断。
- 如果检测的是主库,那么哨兵还不能简单地将其标记为主观下线,开启主从切换,因为可能存在这么一种情况:那就是哨兵误判了,其实主库没有故障。而一旦启动了主从切换,那么后续的选主和通知操作都会带来额外的开销。
哨兵的误判一般会发生在集群网络压力大,网络拥塞等情况。那么应该如何减少误判的发生?
哨兵一般采用多实例组成的集群模式进行部署,这也被称为哨兵集群。引入多个哨兵实例一起来判断,就可以避免因为单个哨兵网络不好,从而误判主库下线的情况。
- 客观下线:只有当哨兵集群中大多数哨兵实例都认为主库已经下线时,主库才会被标记为客观下线,如果走到这一步,那么证明主库的下线已经是客观事实了,此时需要进行主从切换。
当有 N 个哨兵实例时,最好要有 (N / 2) + 1 个实例判断主库为主观下线,才能最终判定主库为客观下线。
5.3、如何选定新主库?
一般来说,将哨兵选择新主库的过程称为筛选 + 打分。简单来说,我们从多个从库中,先按照一定的筛选条件,将不符合条件的从库去掉,然后再从剩下的从库中,按照一定规则打分,最终选择得分最高的从库作为新主库。
- 筛选条件:
- 需要是在线的从库才有资格参与选主(选新太子应该从活的皇子里选吧?)
- 需要判断从库的网络连接状态,如果某个在线的从库的网络状态差,那么也不能让它进入打分阶段,这是为了避免将其选为新主后,其因为网络连接状态差而挂掉。(身体有残疾或者得了重病的皇子应该不选,避免下一代皇帝也是短命鬼)
- 如何判断从库网络连接状况?
通过 Redis 的配置项
down-after-milliseconds
,其中 down-after-milliseconds 是我们认定主从库断连的最大超时时间。如果在 down-after-milliseconds 时间内,主从节点都没有通过网络连接上,那么我们就可以认为主从节点断连。如果断连次数超过十次,那么就说明这个从库的网络状况不好,不适合作为主库。
- 打分工作:
接下来就是要给剩下的从库进行打分,我们分别按照三个规则进行三轮打分,这三个规则分别是从库优先级、从库复制进度和从库 ID ,只要在某一轮中,有从库得分最高,那么它就是新主库了,如果没有出现得分最高的从库,那么继续下一轮。
- 优先级最高的从库得分高
用户可以通过
slave-priority
配置项,给不同的从库设置不同的优先级。如果在众多从库中有一个从库优先级最高,那么直接将其指定为新主库,如果从库的优先级都一样,那么进入第二轮打分。
- 和旧主库同步程度最接近的从库得分高
这个规则的依据是,如果选择和旧主库同步最接近的那个从库作为主库,那么,这个新主库上就有最新的数据。
这里使用主库记录的
master_repl_offset
和 从库的slave_repl_offset
,从库的slave_repl_offset
记录的就是复制进度,应该选择slave_repl_offset
与主库的master_repl_offset
最接近的从库作为新主库。
在上图中,我们应该选择从库 2 作为新主库,如果有多个从库的同步程度相同,那我们需要进行新一轮打分
- ID 小的从库得分高
每一个从库实例都会有一个 ID ,这个 ID 相当于从库的编号,Redis 在选择主库时有一个默认规定:在优先级和复制进度相同的前提下,选择 ID 较小的从库作为新主库。
6、为什么主从库之间的复制不使用 AOF ?
- RDB 文件是二进制文件,无论是要数据写入磁盘,还是网络传输,RDB 的 IO 效率都比 AOF 高
- 在从库进行文件恢复时,RDB 的恢复效率要远远高于 AOF
7、在主从切换过程中,客户端能否正常地进行请求操作?
主从集群一般采用读写分离模式,当主库故障后,客户端仍然可以将读请求发送给从库,让从库服务,但是,对于写请求操作,客户端就无法执行了。
8、Redis 什么时候进行 rehash?
和 HashMap 的扩容一样, Redis 使用负载因子(load factor)来判断是否需要进行 rehash ,其中 load factor = 哈希表中所有的 entry 个数 / 哈希表中的桶数
- 当
load factor >= 1
时,哈希表允许被进行 rehash
当 load factor 大于 1 时,此时表示至少有一个桶上有两个元素,此时当有数据写入时,会对哈希表中查询较慢的桶进行 rehash
- 当
load factor >= 5
时
此时表示保存的数据量远大于哈希桶的个数,哈希桶中存在大量哈希冲突的情况,此时要立马开始做 rehash
- 采用渐进式 rehash 时,即使实例暂时没有收到新请求,那么也会通过定时任务执行渐进式的 rehash 操作。
9、Redis 缓存的淘汰策略
Redis 一共有 8 种缓存淘汰策略
- Redis 4.0 之前一共实现了 6 种缓存淘汰策略
- noeviction:不进行数据淘汰
- volatile-lru:使用 LRU (最近最少使用)算法对存在过期时间的缓存进行淘汰
- volatile-ttl:淘汰那些即将过期的缓存
- volatile-random:随机淘汰那些即将过期的缓存
- allkeys-random:在所有缓存中随机淘汰那些即将过期的缓存
- allkeys-lru:在所有缓存中使用 LRU 算法进行淘汰
- Redis 4.0 后增加了两种缓存淘汰策略
- volatile-lfu:使用 LFU 算法对存在过期时间的缓存进行淘汰
- allkeys-lfu:使用 LFU 算法对所有缓存进行淘汰
10、Redis 的两种原子操作方法
将多个操作在 Redis 中实现成一个操作,也就是单命令操作。
将多个操作写到一个 LUA 脚本中,以原子性的方式执行单个命令
当我们要执行的操作不是简单的增减值,而是有更复杂的判断逻辑或者是其他操作,那么,Redis 的单命令操作已经无法保证多个操作的互斥执行了,此时我们需要使用 LUA 脚本。
Redis 会将整个 LUA 脚本作为一个整体执行,在执行过程中不会被其他命令打断,从而保证了 LUA 脚本中操作的原子性。
10.1、使用 Redis + LUA 脚本实现限制某个客户端在一定时间内的访问次数
- 我们可以将客户端 IP 作为 Key,将客户端的访问次数作为 value ,保存在 Redis 中,客户端每访问一次,就使用 INCR 命令增加访问次数。
由于在这个操作中包含了判断逻辑,我们需要保证这段逻辑的原子性,此时使用 Redis 自带的单命令不能保证原子性,此时我们可以使用 LUA 脚本完成
1 | // 获取 ip 对应的访问次数 |
- 在 Redis 中,我们可以使用 Redis 的 EVAL 命令来执行 LUA 脚本
11、Redis 6 新增的数据类型 – BitMaps
11.1、简介
现代计算机使用二进制(位)作为信息的基础单位,一个字节等于 8 位,即(1 byte = 8 bit),例如 “abc” 字符串由三个字节组成,但实际在计算机存储时将其使用二进制表示,”abc” 分别对应的 ASCII 码为 97 98 99 ,对应的二进制位为
01100001
、01100010
和01100011
,如下图
合理使用操作位可以有效地提高内存利用率和开发效率,Redis 提供了
Bitmaps
这个数据类型,它可以实现对位的操作:
Bitmaps
本身不是一种数据类型,实际上它就是字符串(Key-Value),但它可以对字符串的位进行操作。Bitmaps
单独提供了一套命令,所以在 Redis 中使用Bitmaps
和使用字符串的方法不太相同。可以将 Bitmaps 想象成一个以位为单位的数组,数组中的每个单元只能存储 0 和 1 ,数组的下标在 Bitmaps 中叫做偏移量。
11.2、setbit 命令
作用:设置 bitmaps 中某个偏移量的值(只能设置为 0 或 1)
格式:
1 | set <key> <offset> <value> |
offset (偏移量)从 0 开始
- 实例:判断用户是否访问过网站
我们将用户 id 作为数组偏移量,如果用户在该天访问过用户,那么将数组对应偏移量的值设置为 1 ,否则设置为 0 ,假设现在有 20 个用户,其中用户 id 为 1 , 6 , 11 , 15 , 19 的用户对网站进行了访问,那么当前
Bitmaps
初始化结果为:
其中键
unique:users:20201106
代表在 2020-11-06 这一天用户访问记录的Bitmaps
- 很多应用的用户 id 都会以一个指定数字开头,如果直接将用户 id 和 Bitmaps 的偏移量对应势必会造成一定的浪费,通常的做法是每次做 setbit 操作时将用户 id 减去这个数字。
- 在第一次初始化 Bitmaps 时,如果偏移量非常大,那么整个初始化过程执行会比较慢,可能会造成 Redis 的阻塞。
11.3、getbit 命令
- 作用:用于获取
Bitmaps
中某个偏移量的值 - 格式:
1 | getbit <key> <offset> |
获取键对应 bitmaps 的第 offset 位的值(offset 从 0 开始)
- 实例:判断用户 id 为 8 的用户在 2020-11-06 这一天是否访问过网站
1 | getbit unique:users:20201106 8 |
结果为 0 ,证明 id 为 8 的用户在 2020-11-06 这一天没有访问过网站
11.4、bitcount 命令
- 作用:这个命令用于统计位图中被设置为 1 的 bit 数。
一般情况下,给定的整个位图都会被进行计数,通过指定额外的
start
和end
参数,可以让计数只在特定的位上进行。
start
和end
参数都可以使用负数值:比如说 -1 表示最后一个位,而 -2 表示倒数第二个位,**start
和end
是指位图中 offset 的下标数,二者皆包含,即 [start
,end
]**
- 格式:
1 | bitcount <key> [start end] |
- 实例:统计 2020-11-06 这天访问网站的用户数
1 | bitcount unique:users:20201106 |
11.5、bitop 命令
作用:
bitop
是一个符合操作,它可以做多个 Bitmaps 的 and(交集)、or(并集)、not(非)、xor(异或)操作并将结果保存到 destkey 中格式
1 | bitop and(or/not/xor) <destkey> [key...] |
- 实例:统计 2020-11-06 和 2020-11-07 两天都访问过网站的用户
- 假设 2020-11-06 访问网站的 userid = 1,2,5,9
1 | setbit unique:users:2020-11-06 1 1 |
- 假设 2020-11-07 访问网站的 userid = 0,1,4,9
1 | setbit unique:users:2020-11-07 0 1 |
- 我们使用
bitop
命令将两天都访问过网站的用户计算出来,并保存到 key 为unique:users:2020110620201107
中
1 | bitop and unique:users:2020110620201107 unique:users:2020-11-06 unique:users:2020-11-07 |
使用 bitcount 统计 key 为
unique:users:2020110620201107
的位图的用户数
1 | bitcount unique:users:2020110620201107 |
示意图如下
11.6、Bitmaps 与 Set 对比
假设网站有一亿用户,每天独立访问的用户有 5 千万,如果每天用集合类型和 Bitmaps 分别存储活跃用户可以得到表如下
很明显,这种情况下使用 Bitmaps 可以节省非常多空间,尤其是随着时间推移节省的内存十分可观
但 Bitmaps 不是万金油,假设网站每天访问的独立用户非常少,例如只有 10 万,那么二者的对比如下表所示
这个时候使用 Bitmaps 就不太合适了,因为此时基本上大部分位都是 0
12、Redis Cluster 集群
12.1、特点
- 多主多从,去中心化,从节点不提供服务,而只是单纯作为主节点的备用节点
- 不支持处理多个 Key:因为数据分散在多个节点,在数据量高并发情况下会影响性能。
- 支持动态扩展节点
- 节点之间相互通信,不再依赖哨兵,保证及时故障转移
- Redis 集群不像单机 Redis 那样支持多数据库功能, 集群只使用默认的
0
号数据库
12.2、Redis Cluster 集群的键分布模型
- Redis 集群的键空间被分割为
16384(16 * 1024)
个槽(slot),集群的最大节点数量也是16384
个,每个键值对会根据它的 key ,然后被映射到一个哈希槽中。 - Redis 会自动把这个槽平均分布在集群实例上。例如,如果集群中有 N 个实例,那么每个实例上的槽个数为
16384 / N
个槽
映射过程分为两大步:
- 首先根据键值对的 key ,按照 CRC16 算法计算一个 16 bit 的值
- 再用这个 16 bit 的值对
16384
进行取模,得到一个0 - 16383
之间的值
HASH_SLOT = CRC16(key) mod 16384
12.3、Redis Cluster 是如何保证高可用的?
Redis Cluster 保证高可用主要是依靠:故障检测和故障转移两种策略
- 故障转移
我们为 Redis Cluster 的实例添加从节点,从节点不提供服务,只复制主节点,当集群中的实例下线时,就可以在下线实例的从节点中选举一个新的主节点。
比如说现在实例中有 4 个实例,其中实例一存在两个从节点
当
ip1:6379
下线时,我们就从它的从节点中选取出一个新的主节点,代替原来下线的实例
当
ip1:6379
重新上线时,它会作为ip5:6379
的新的从节点
- 故障检测
- Redis Cluster 采用一种相互监督的方式来检测集群中的实例是否健康,每个实例都会定期地向集群中的其他节点发送 PING 消息,以检测对方是否在线;
如果接收 Ping 消息的节点没有在规定的时间内回传 PONG 消息,那么发送 PING 消息的节点就会将该节点标记为疑似下线(PFAIL)
- 如果在集群中,超过半数以上负责处理槽的主节点都将某个节点 X 标记为疑似下线,那么某个主节点就会将 X 标记为已下线,然后广播这条消息
这样其他所有主节点都会将 X 标记为已下线,此时需要进行故障转移。
13、Redis 有序集合 Zset (Sorted Set)
13.1、简介
- 有序集合 Zset 与普通集合 set 非常相似,它是一个没有重复元素的字符串集合
不同之处是有序集合的每个成员都关联了一个评分(score),这个评分被用于从最低分到最高分的方式排序集合中的成员。
集合中的成员是唯一的,但是评分可以是重复的。
因为集合是有序的,所以我们可以很快地根据评分或者次序来获取一个范围内的元素
访问有序集合的中间元素也是非常快的,因此我们可以使用有序集合作为一个 没有重复成员的只能列表。
13.2、zadd 命令
作用:将一个或者多个 member 元素及其 score 加入到有序集合 key 中
格式:
1 | zadd <key> <score1> <value1> <score2> <value2> |
- 实例:
1 | zadd topN 200 java 300 CPP 400 C |
13.3、zrange 命令
作用:返回有序集合 key 中,下标在 [start, stop] 之间的元素,如果带上 WITHSCORES ,那么可以让分数一起和值返回到结果值。
格式:
1 | zrange <key> <start> <stop> [WITHSCORES] |
- 实例:
- 取出有序集合中的所有内容
1 | zrange topN 0 -1 |
将值与分数一同返回到结果集中
1 | zrange topN 0 -1 WITHSCORES |
- 返回有序集合中下标在 [0,1] 之间的元素,连同分数一起返回
1 | zrange topN 0 1 WITHSCORES |
13.4、zrangebyscore 命令
- 作用:返回有序集合 key 中,所有 score 值处于 [min , max] 之间的成员,按照 score 从小到大排序
- 格式:
1 | zrangebyscore key min max [withscores] [limit offset count] |
- 实例:返回有序集合 topN 中 score 处于 [300, 500] 之间的成员,连带他们的 score 一起返回
1 | zrangebyscore topN 300 500 WITHSCORES |
13.5、zrevrangebyscore 命令
- 作用:返回有序集合 key 中,所有 score 值处于 [min , max] 之间的成员,按照 score 从大到小排序
- 格式:
1 | zrevrangebyscore key max min [withscores] [limit offset count] |
- 实例:
- 逆序排序所有成员
1 | zrevrangebyscore topN +inf -inf |
- 逆序排序所有成员并取前 2 条
1 | zrevrangebyscore topN +inf -inf limit 0 2 |
13.6、zincrby 命令
- 作用:为元素的 score 值加上增量
- 格式:
1 | zincrby <key> <increment> <value> |
13.7、zrem 命令
- 作用:删除该集合下指定元素的集合
- 格式:
1 | zrem <key> <value> |
13.8、zcount 命令
- 作用:统计该集合中,分数处于 [min, max] 区间的元素个数
- 格式:
1 | zcount <key> <min> <max> |
13.9、zrank 命令
- 作用:返回该值在集合中的排名,从 0 开始
- 格式:
1 | zrank <key> <value> |
14、Zset 的底层实现
14.1、概述
ZSet 底层的存储结构包括压缩列表
ziplist
和**跳表skiplist
**。
- Redis 如何决定使用 ziplist 还是 skiplist?
在 Redis 配置文件中存在两个属性,分别是
zset-max-ziplist-entries 128
和zset-max-ziplist-value 64(字节)
,当集合内元素个数小于 128 个且集合内保存的所有元素的长度小于 64 字节时使用 ziplist,否则使用 skiplist
从上面的源码可以看到,只要两个条件(集合内元素个数 大于 128 或集合内元素长度有一个大于 64 字节)有一个满足时,就会执行
zsetConvert
方法,这个方法会将底层数据结构由 ziplist 变为 skiplist
- 集合内存储的元素分值相同时,Redis 会如何进行处理
所有节点的分值是按从小到大的方式进行排序的,当有序集合中成员分值相同时,节点会按照成员的字典序进行排序。
14.2、有序集合的结构体
1 | /** |
- 当使用 ziplist 作为底层存储结构时,每个集合元素使用两个紧挨在一起的压缩列表节点来保存,第一个节点保存元素的成员,第二个节点保存元素的分值。
- 当使用 skiplist 作为底层存储结构时,使用 skiplist 保存元素及分值,使用一个字典来保存元素和分值的映射关系。
跳跃表的查找复杂度为平均 O(logN),最坏 O(N),效率堪比红黑树,却远比红黑树实现简单。跳跃表是在链表的基础上,通过增加索引来提高查找效率的。
字典和跳表会通过指针来共享相同元素的成员和分值,所以不会产生重复的成员和分支,也就不会造成空间浪费。
14.3、为什么有序集合需要引入字典?
其实有序集合单独使用字典或者跳跃表其中一种元素都可以实现,但是 Redis 将两种数据结构组合起来实现 Zset 。
- 如果单独使用字典,那么我们虽然可以以 O(1) 的时间复杂度查找成员的分值,但是由于字典是以无需的方式来保存集合元素的,所以每次进行范围操作时我们都需要对无序的数据进行排序。
- 如果单独使用跳跃表来实现,那么我们虽然可以很方便地进行范围操作,但是查找操作的时间复杂度变为了 O(logN),所以 Redis 使用了两个数据结构来共同实现有序集合。
14.4、为什么跳表不使用树形结构?
- Zset 有一个核心操作:范围查找,而跳表效率比红黑树高,跳表可以做到在 O(logN) 时间复杂度内快速查找,我们只需要找到范围数据的起点,然后向后遍历即可。
- 跳表的实现比红黑树简单易懂,且没有红黑树的 rebalance 操作