文章目录
- 前言
- 一、Redis是什么?
- 二、Redis的优缺点是什么?
- 三、Redis为什么这么快?
- 四、Redis为什么使用单线程?
- 五、Redis6.0为何引入多线程?
- 六、Redis应用场景有哪些?
- 七、Memcached和Redis的区别?
- 八、Redis 数据类型有哪些?
- 九、keys命令存在的问题?(了解)
- 十、ZSet和List异同点?
- 十一、Redis事务
- 十二、Redis持久化(重点、面试常问)
- 十三、RDB和AOF如何选择?
- 十四、Redis的发布订阅
- 十五、主从复制
- 十六、哨兵Sentinel
- 十七、缓存穿透(重点)
- 十八、缓存击穿
- 十九、缓存雪崩
- 二十、分布式锁
- 二十一、什么是RedLock?
- 二十二、Redis中的Zset为什么使用跳表(skiplist)?(重要)
前言
Redis面试总结,自己学习使用。
一、Redis是什么?
Redis是一个使用C语言编写的,高性能非关系型的键值对数据库,与传统的数据库不同的是,Redis的数据是存在内存中的,所以读写速度非常快,被广泛应用于缓存方向,Redis可以将数据写入磁盘中,保证了数据的安全不丢失,而且Redis的操作是原子性的。默认端口号是6379
二、Redis的优缺点是什么?
优点:
- 基于内存操作,内存读写速度快。
- Redis是单线程,避免线程切换开销及多线程的竞争问题。单线程是指网络请求使用一个线程来处理,即一个线程处理所有网络请求,Redis 运行时不止有一个线程,比如数据持久化的过程会另起线程。
- 支持多种数据类型:String、set、List、Hash、ZSet等。
- 支持持久化。Redis支持RDB和AOF两种持久化机制,持久化功能可以有效地避免数据丢失问题。
- 支持事务。Redis的所有操作都是原子性的,同时Redis还支持对几个操作合并后的原子性执行。
- 支持主从复制。主节点会自动将数据同步到从节点,可以进行读写分离。
缺点:
- Redis 不具备自动容错和恢复功能,主机从机的宕机都会导致前端部分读写请求失败,需要等待机器重启或者手动切换前端的IP才能恢复。
- 主机宕机,宕机前有部分数据未能及时同步到从机,切换IP后还会引入数据不一致的问题,降低了系统的可用性。
- 数据库的容量受到物理内存的限制,不适合作为海量数据的高性能读写,因此Redis适合的场景主要局限在较小的数据量的操作。
- Redis较难支持在线的扩容,在集群容量达到上限时在线扩容会变得复杂。
三、Redis为什么这么快?
- 基于内存:Redis是使用内存存储,没有磁盘IO上的开销。数据存在内存中,读写速度快。
- 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。
- IO多路复用模型:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。
- 高效的数据结构:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。
四、Redis为什么使用单线程?
- 避免过多的上下文切换开销:程序始终运行在进程中的单个线程内,没有多线程切换的场景。
- 避免同步机制开销,如果 Redis选择多线程模型,需要考虑数据同步的问题,则必然会引入某些同步机制,会导致在操作数据过程中带来更多的开销,增加程序复杂度的同时还会降低性能。
- 实现简单,方便维护。如果 Redis使用多线程模式,那么所有的底层数据结构的设计都必须考虑线程安全问题,那么 Redis 的实现将会变得更加复杂。
为什么是单线程?
因为Redis是基于内存的操作,CPU不是Redis的瓶颈,Redis的瓶颈最有可能是机器内存的大小或者网络带宽。既然单线程容易实现,而且CPU不会成为瓶颈,那就顺理成章地采用单线程的。
五、Redis6.0为何引入多线程?
Redis支持多线程主要有两个原因:
- 可以充分利用服务器 CPU 资源,单线程模型的主线程只能利用一个cpu;
- 多线程任务可以分摊 Redis 同步 IO 读写的负荷。
六、Redis应用场景有哪些?
- 缓存热点数据,缓解数据库的压力。
- 利用Redis原子性自增操作,可以实现计数器功能,比如,统计用户点赞数,用户访问数等。
- 简单的消息队列。可以使用Redis自身的发布订阅模式或者list来实现简单的消息队列,实现异步操作。
- 限速器,可用于限制某个用户访问某个接口的频率,比如秒杀场景用于防止用户快速点击带来的不必要的压力。
- 好友关系,利用集合的一些命令,比如交集,并集,差集等。实现共同好友、共同爱好之类的功能。
七、Memcached和Redis的区别?
- Redis 只使用单核,而 Memcached 可以使用多核。
- MemCached 数据结构单一,仅用来缓存数据,而 Redis 支持多种数据类型。
- MemCached 不支持数据持久化,重启后数据会消失。Redis 支持数据持久化。
- Redis 提供主从同步机制和 cluster 集群部署能力,能够提供高可用服务。Memcached 没有提供原生的集群模式,需要依靠客户端实现往集群中分片写入数据。
- Redis 的速度比 Memcached 快很多。
- Redis 使用单线程的多路 IO 复用模型,Memcached使用多线程的非阻塞 IO 模型。
八、Redis 数据类型有哪些?
基本数据类型:
- String:最常用的一种数据类型,String类型的值可以是字符串、数字或者二进制,但值最大不能超过512MB。
- Hash:Hash 是一个键值对集合。
- Set:无序去重的集合。Set 提供了交集、并集等方法,对于实现共同好友、共同关注等功能特别方便。
- List:有序可重复的集合,底层是依赖双向链表实现的。
- Zset:有序Set。内部维护了一个 score 的参数来实现。适用于排行榜和带权重的消息队列等场景。
特殊的数据类型:
-
Bitmaps:位图,可以认为是一个以位为单位数组,数组中的每个单元只能存0或者1,数组的下标在Bitmap 中叫做偏移量。Bitmap的长度与集合中元素个数无关,而是与基数的上限有关。可以用来统计网站中活跃的人数。
-
HyperLogLog。HyperLogLog 是用来做基数统计的算法,其优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定的、并且是很小的。典型的使用场景是统计独立访客。
-
Geospatial :主要用于存储地理位置信息,并对存储的信息进行操作,适用场景如定位、附近的人等。
九、keys命令存在的问题?(了解)
Redis的单线程的。keys指令会导致线程阻塞一段时间,直到执行完毕,服务才能恢复。scan采用渐进式遍历的方式来解决keys命令可能带来的阻塞问题,每次scan命令的时间复杂度是 O(1) ,但是要真正实现keys的功能,需要执行多次scan。
scan的缺点:在scan的过程中如果有键的变化(增加、删除、修改),遍历过程可能会有以下问题:新增的键可能没有遍历到,遍历出了重复的键等情况,也就是说scan并不能保证完整的遍历出来所有的键。
十、ZSet和List异同点?
相同点:
- 都是有序的
- 都可以获取某个范围的元素
不同点:
- 列表基于双向链表实现,获取两端元素速度快,访问中间元素速度慢
- 有序集合基于散列表和跳跃表实现,访问中间元素时间复杂度是Olog(n)
- 列表不能简单的调整某个元素的位置,有序列表可以(更改元素的分数)
- 有序集合更加消耗内存
十一、Redis事务
Redis的单条命令是保证原子性的,但是事务是不保证原子性的。
Redis事务的本质:一组命令的集合,一个事务中的所有命令都会被序列化,在事务执行过程中,会按照顺序执行,一次性、顺序性、排他性,执行这些命令。
Redis事务没有隔离级别的概念,也就是没有幻读,脏读这些概念。
事物的生命周期:
- 开启事务(multi)
- 命令入队()
- 执行事务(exec)
放弃事务:
监控:Watch(重要)
悲观锁:认为什么时候都会出问题,无论做什么都会加锁。
乐观锁:认为什么时候都不会出错,所以不会上锁,更新数据的时候去判断一下,在此期间是否有人修改过这个数据。version
WATCH 命令可以监控一个或多个键,一旦其中有一个键被修改,之后的事务就不会执行(类似于乐观锁)。执行 EXEC 命令之后,就会自动取消监控。
十二、Redis持久化(重点、面试常问)
Redis提供了两种持久化方式,分别是RDB持久化和AOF持久化。
RDB持久化:RDB持久化会在指定的时间间隔内将内存中的数据快照存储到硬盘上,这个快照文件就是RDB文件。RDB文件是一个二进制文件,它包含了Redis在某个时间点上的所有数据。当需要恢复数据时,Redis会读取RDB文件,并根据文件中的内容来还原数据库。RDB持久化的优点是备份恢复数据速度快,适合用于备份和灾难恢复。缺点是如果在Redis发生故障时,最后一次持久化的数据可能会丢失。
AOF持久化:AOF持久化会将Redis的所有写操作追加到一个日志文件中,这个日志文件就是AOF文件。当Redis需要恢复数据时,它会读取AOF文件,并重新执行文件中保存的写操作,来还原数据库。AOF持久化的优点是可以保证更高的数据安全性,因为它记录的是每个写操作,即使Redis在持久化时发生故障,也可以通过AOF文件来还原数据。缺点是相比于RDB持久化,AOF持久化的性能较低。
在实际应用中,可以根据业务场景和数据安全要求选择不同的持久化方式,或者将两种持久化方式结合使用。例如,可以使用AOF持久化来保证数据安全性,再使用RDB持久化来定期备份数据。
持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。
Redis支持两种方式的持久化,一种是 RDB 的方式,一种是 AOF 的方式。前者会根据指定的规则定时将内存中的数据存储在硬盘上,而后者在每次执行完命令后将命令记录下来。一般将两者结合使用。
Redis是内存数据库,如果不将内存中的数据库状态保存到磁盘,那么一旦服务器进程退出,服务器中的数据库状态也会消失。所以Redis提供了持久化功能。
12.1 RDB(Redis DataBase)方式
RDB 是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个 dump.rdb 文件。Redis 重启会加载 dump.rdb 文件恢复数据。
在指定的时间间隔内将内存中的数据集快照写入磁盘,也就是行话讲的 Snapshot快照,它恢复时是将快照文件直接读到内存里。
Redis会单独创建(fork)ー个子进程来进行持久化,会先将数据写入到一个临时文件中,待持久化过程都结束了,再用这个临时文件替換上次持久化好的文件。整个过程中,主进程是不进行任何I/O操作的。这就确保了极高的性能。如果需要进行大规模数据的恢复,且对于数据恢复的完整性不是非常敏感,那RDB方式要比AOF方式更加的高效。RDB的缺点是最后一次持久化后的数据可能丢失。我们默认的就是RDB,一般情況下不需要修改这个配置。
触发机制:
- save的规则满足的情况下,会自动触发rdb规则,生成一个 dump.rdb 文件。
- 执行flushall命令,也会触发rdb规则,生成一个 dump.rdb 文件。
- 退出Redis,也会生成一个 dump.rdb 文件。
优势:
- 方便备份,我们可以很容易的将一个一个RDB文件移动到其他的存储介质上RDB。
- 在恢复大数据集时的速度比 AOF 的恢复速度要快。
- RDB 可以最大化 Redis 的性能:父进程在保存 RDB 文件时唯一要做的就是 fork 出一个子进程,然后这个子进程就会处理接下来的所有保存工作,父进程无须执行任何磁盘 I/O 操作。
劣势:
- 如果你需要尽量避免在服务器故障时丢失数据,那么 RDB 不适合你。 虽然 Redis 允许你设置不同的保存点(save point)来控制保存 RDB 文件的频率, 但是, 因为RDB 文件需要保存整个数据集的状态, 所以它并不是一个轻松的操作。 因此你可能会至少 5 分钟才保存一次 RDB 文件。 在这种情况下, 一旦发生故障停机, 你就可能会丢失好几分钟的数据。
- 每次保存 RDB 的时候,Redis 都要 fork() 出一个子进程,并由子进程来进行实际的持久化工作。在数据集比较庞大时, fork() 可能会非常耗时,造成服务器在某某毫秒内停止处理客户端; 如果数据集非常巨大,并且 CPU 时间非常紧张的话,那么这种停止时间甚至可能会长达整整一秒。 虽然AOF 重写也需要进行 fork() ,但无论 AOF 重写的执行间隔有多长,数据的耐久性都不会有任何损失。
12.2 AOF(Append Only File)方式
AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,AOF 是Redis持久化的主流方式。
默认情况下Redis没有开启AOF方式的持久化,可以通过 appendonly 参数启用: appendonly yes 。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进 aof_buf 缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。
默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过 appendfsync 参数设置同步的时机。
appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置
appendfsync everysec //既保证性能也保证安全,建议配置
appendfsync no //由操作系统决定何时进行同步操作
如果AOF文件大于64M,太大了,fork一个新的进程来将我们的文件进行重写。
AOF 持久化执行流程:
- 所有的写入命令会追加到 AOP 缓冲区中。
- AOF 缓冲区根据对应的策略向硬盘同步。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
- 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。
12.3 RDB和AOF的优缺点
RDB的优点:
-
体积更小:相同的数据量rdb数据比aof的小,因为rdb是紧凑型文件
-
恢复更快:因为rdb是数据的快照,基本上就是数据的复制,不用重新读取再写入内存
-
性能更高:父进程在保存rdb时候只需要fork一个子进程,无需父进程的进行其他io操作,也保证了服务器的性能。
缺点:
-
故障丢失:因为rdb是全量的,我们一般是使用shell脚本实现30分钟或者1小时或者每天对redis进行rdb备份,(注,也可以是用自带的策略),但是最少也要5分钟进行一次的备份,所以当服务死掉后,最少也要丢失5分钟的数据。
-
耐久性差:相对aof的异步策略来说,因为rdb的复制是全量的,即使是fork的子进程来进行备份,当数据量很大的时候对磁盘的消耗也是不可忽视的,尤其在访问量很高的时候,fork的时间也会延长,导致cpu吃紧,耐久性相对较差。
AOF的优点:
-
数据保证:我们可以设置fsync策略,一般默认是everysec,也可以设置每次写入追加,所以即使服务死掉了,咱们也最多丢失一秒数据
-
自动缩小:当aof文件大小到达一定程度的时候,后台会自动的去执行aof重写,此过程不会影响主进程,重写完成后,新的写入将会写到新的aof中,旧的就会被删除掉。但是此条如果拿出来对比rdb的话还是没有必要算成优点,只是官网显示成优点而已。
缺点:和rdb相反,毕竟只有两种。
-
性能相对较差:它的操作模式决定了它会对redis的性能有所损耗
-
体积相对更大:尽管是将aof文件重写了,但是毕竟是操作过程和操作结果仍然有很大的差别,体积也毋庸置疑的更大。
-
恢复速度更慢:
十三、RDB和AOF如何选择?
通常来说,应该同时使用两种持久化方案,以保证数据安全。
- 如果数据不敏感,且可以从其他地方重新生成,可以关闭持久化。
- 如果数据比较重要,且能够承受几分钟的数据丢失,比如缓存等,只需要使用RDB即可。
- 如果是用做内存数据,要使用Redis的持久化,建议是RDB和AOF都开启。
- 如果只用AOF,优先使用everysec的配置选择,因为它在可靠性和性能之间取了一个平衡。
当RDB与AOF两种方式都开启时,Redis会优先使用AOF恢复数据,因为AOF保存的文件比RDB文件更完整。
十四、Redis的发布订阅
Redis发布订阅是一种消息通信模式,发送者(pub)发送消息,订阅者(sub)接收消息,应用于微信公众号,微博,关注系统
Redis客户端可以订阅任意数量的频道。
十五、主从复制
原理:
- 当启动一个从节点时,它会发送一个 PSYNC 命令给主节点;
- 如果是从节点初次连接到主节点,那么会触发一次全量复制。此时主节点会启动一个后台线程,开始生成一份 RDB 快照文件;
- 同时还会将从客户端 client 新收到的所有写命令缓存在内存中。 RDB 文件生成完毕后, 主节点会将 RDB 文件发送给从节点,从节点会先将 RDB 文件写入本地磁盘,然后再从本地磁盘加载到内存redis-server //启动Redis实例作为主数据库 redis-server --port 6380 --slaveof 127.0.0.1 6379 //启动另一个实例作为从数据库 slaveof 127.0.0.1 6379 SLAVEOF NO ONE //停止接收其他数据库的同步并转化为主数据库中;
- 接着主节点会将内存中缓存的写命令发送到从节点,从节点同步这些数据;
- 如果从节点跟主节点之间网络出现故障,连接断开了,会自动重连,连接之后主节点仅会将部分缺失的数据同步给从节点。
十六、哨兵Sentinel
主从复制存在不能自动故障转移、达不到高可用的问题。哨兵模式解决了这些问题。通过哨兵机制可以自动切换主从节点。
客户端连接Redis的时候,先连接哨兵,哨兵会告诉客户端Redis主节点的地址,然后客户端连接上Redis并进行后续的操作。当主节点宕机的时候,哨兵监测到主节点宕机,会重新推选出某个表现良好的从节点成为新的主节点,然后通过发布订阅模式通知其他的从服务器,让它们切换主机。
工作原理:
- 每个 Sentinel 以每秒钟一次的频率向它所知道的 Master , Slave 以及其他 Sentinel 实例发送一个 PING 命令。
- 如果一个实例距离最后一次有效回复 PING 命令的时间超过指定值, 则这个实例会被 Sentine 标记为主观下线。
- 如果一个 Master 被标记为主观下线,则正在监视这个 Master 的所有 Sentinel 要以每秒一次的频率确认 Master 是否真正进入主观下线状态。
- 当有足够数量的 Sentinel (大于等于配置文件指定值)在指定的时间范围内确认 Master 的确进入了主观下线状态, 则 Master 会被标记为客观下线 。若没有足够数量的 Sentinel 同意 Master已经下线, Master 的客观下线状态就会被解除。 若 Master 重新向 Sentinel 的 PING 命令返回有效回复, Master 的主观下线状态就会被移除。
- 哨兵节点会选举出哨兵 leader,负责故障转移的工作。
- 哨兵 leader 会推选出某个表现良好的从节点成为新的主节点,然后通知其他从节点更新主节点信息。
十七、缓存穿透(重点)
概念:
缓存穿透的概念很简单,用户想要查询一个数据,发现Redis内存数据库中没有,也就是缓存没有命中,于是想持久层数据库查询。发现也没有,于是本次查询失败,当用户很多的时候,缓存都没有命中,于是都去请求了持久层数据库,这就会给持久层数据库造成很大的压力,这时候就相当于是缓存穿透。
解决方案:
- 缓存空值,不会查数据库
- 设置可访问的白名单:使用bitmaps类型定义一个可以访问的名单,名单id作为bitmaps的偏移量,每次访问和bitmaps里面的id进行比较,如果访问id不在bitmaps里面,进行拦截,不允许访问。
- 采用布隆过滤器,将所有可能存在的数据哈希到一个足够大的 bitmaps 中,查询不存在的数据会被这个 bitmaps 拦截掉,从而避免了对数据库的查询压力。
布隆过滤器的原理:当一个元素被加入集合时,通过K个散列函数将这个元素映射成一个位数组中的K个点,把它们置为1。查询时,将元素通过散列函数映射之后会得到k个点,如果这些点有任何一个0,则被检元素一定不在,直接返回;如果都是1,则查询元素很可能存在,就会去查询Redis和数据库。 - 进行实时监控:当发现Redis的命中率开始急速降低,需要排查访问对象和访问的数据,和运维人员配合,可以设置黑名单限制服务。
十八、缓存击穿
缓存击穿:大量的请求同时查询一个 key 时,此时这个 key 正好失效了,就会导致大量的请求都落到数据库。缓存击穿是查询缓存中失效的 key,而缓存穿透是查询不存在的 key。
解决方法:
- 加分布式锁,第一个请求的线程可以拿到锁,拿到锁的线程查询到了数据之后设置缓存,其他的线程获取锁失败会等待50ms然后重新到缓存取数据,这样便可以避免大量的请求落到数据库。
- 设置热点数据永不过期
- 预先设置热门数据:在redis高峰访问之前,把一些热点数据提前存入到redis里面,加大这些热门数据key的时长。
十九、缓存雪崩
缓存雪崩是指在我们设置缓存时采用了相同的过期时间,导致缓存在某一时刻同时失效,请求全部转发到数据库,数据库瞬时压力过重挂掉。
解决方案:
- 构建多级缓存架构:nginx缓存+redis缓存+其他缓存(ehcache)
- 使用锁或者队列:用加锁或者队列的方式来保证不会有大量的线程对数据库一次性进行读写,从而避免失效时大量的并发请求落在底层存储系统上,不适用高并发情况。
- 设置过期标志更新缓存记录缓存数据是否过期(设置提前量),如果过期会触发通知另外线程在后台去更新实际key的缓存
- 将缓存失效时间分散开:不如我们可以在原有的失效时间基础上增加一个随机值,比如1-5分钟随机,这样每一个缓存的过期时间的重复率就会降低,就很难引发集体失效的事件。
二十、分布式锁
问题描述:
随着业务发展的需要,原单体单机部署的系统被演化成分布式集群系统后,由于分布式系统多线程、多进程并且分布在不同机器上,这将使原单机部署情况下的并发控制锁策略失效,单纯的Java API并不能提供分布式锁的能力,为了解决这个问题就需要一种跨JVM的互斥机制来控制共享资源的访问,这就是分布式锁要解决的问题。
分布式锁主流的实现方案:
- 基于数据库实现分布式锁
- 基于缓存(Redis等),性能最高
- 基于Zookeeper,可靠性最高
Redis分布式锁机制,主要借助setnx和expire两个命令完成。
SETNX命令:SETNX 是SET If Not Exists的简写。将 key 的值设为 value,当且仅当 key 不存在; 若给定的 key 已经存在,则 SETNX 不做任何动作。
127.0.0.1:6379> set lock "unlock"
OK
127.0.0.1:6379> setnx lock "unlock"
(integer) 0
127.0.0.1:6379> setnx lock "lock"
(integer) 0
127.0.0.1:6379>
expire命令:expire命令为 key 设置生存时间,当 key 过期时(生存时间为 0 ),它会被自动删除. 其格式为:
127.0.0.1:6379> expire lock 10
(integer) 1
127.0.0.1:6379> ttl lock
8
以上简单redis分布式锁的问题:
如果出现了这么一个问题:如果setnx是成功的,但是expire设置失败,一旦出现了释放锁失败,或者没有手工释放,那么这个锁永远被占用,其他线程永远也抢不到锁。
所以,需要保障setnx和expire两个操作的原子性,要么全部执行,要么全部不执行,二者不能分开。
解决的办法有两种:
- 使用set的命令时,同时设置过期时间,不再单独使用 expire命令
- 使用lua脚本,将加锁的命令放在lua脚本中原子性的执行
方法一:简单加锁:使用set的命令时,同时设置过期时间使用set的命令时,同时设置过期时间的示例如下:
127.0.0.1:6379> set users "234" nx ex 10
OK
127.0.0.1:6379> ttl users //查看过期时间
二十一、什么是RedLock?
Redis 官方站提出了一种权威的基于 Redis 实现分布式锁的方式名叫 Redlock,此种方式比原先的单节点的方法更安全。它可以保证以下特性:
- 安全特性:互斥访问,即永远只有一个 client 能拿到锁。
- 避免死锁:最终 client 都可能拿到锁,不会出现死锁的情况,即使原本锁住某资源的 client 挂掉了。
- 容错性:只要大部分 Redis 节点存活就可以正常提供服务。
二十二、Redis中的Zset为什么使用跳表(skiplist)?(重要)
Redis中的Zset(有序集合)是一种基于跳表(Skip List)实现的数据结构。跳表是一种基于链表的数据结构,它允许快速查找、插入和删除操作,同时还保证了元素的有序性。Zset作为一种有序集合,需要保证元素的有序性和快速查找。
在Zset中,每个元素都有一个权重值(score),元素按照权重值从小到大排序,相同权重的元素按照字典序排序。Zset需要支持添加、删除元素,以及根据权重值范围查找元素、计算元素排名等操作。使用跳表可以快速定位元素的位置,支持快速查找和修改,同时也保证了元素的有序性。
跳表在维护有序性的同时,还具有较高的插入、删除效率,其时间复杂度为O(log n),比平衡二叉树的时间复杂度O(log n)要小,而且实现起来比较简单,所以跳表是实现Zset的一种较好的选择。
总的来说,Redis中的Zset使用跳表实现可以提供快速的元素查找、插入和删除操作,同时保证元素的有序性,是一种性能和效率较好的数据结构。
22.1、skiplist与平衡树、哈希表的比较
- skiplist和各种平衡树(如AVL、红黑树等)的元素是有序排列的,而哈希表不是有序的。因此,在哈希表上只能做单个key的查找,不适宜做范围查找。所谓范围查找,指的是查找那些大小在指定的两个值之间的所有节点。
- 在做范围查找的时候,平衡树比skiplist操作要复杂。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在skiplist上进行范围查找就非常简单,只需要在找到小值之后,对第1层链表进行若干步的遍历就可以实现。
- 平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而skiplist的插入和删除只需要修改相邻节点的指针,操作简单又快速。
- 从内存占用上来说,skiplist比平衡树更灵活一些。一般来说,平衡树每个节点包含2个指针(分别指向左右子树),而skiplist每个节点包含的指针数目平均为1/(1-p),具体取决于参数p的大小。如果像Redis里的实现一样,取p=1/4,那么平均每个节点包含1.33个指针,比平衡树更有优势。
- 查找单个key,skiplist和平衡树的时间复杂度都为O(log n),大体相当;而哈希表在保持较低的哈希值冲突概率的前提下,查找时间复杂度接近O(1),性能更高一些。所以我们平常使用的各种Map或dictionary结构,大都是基于哈希表实现的。
- 从算法实现难度上来比较,skiplist比平衡树要简单得多。
22.2、Redis中的skiplist实现
见参考