Redis面试题

Redis

仅对以下资料做整理!!! 如果版权问题,立刻删除!

参考资料:

图解Redis介绍 | 小林coding

新版Java面试专题视频教程,java八股文面试全套真题+深度详解(含大厂高频面试真题)_哔哩哔哩_bilibili

尚硅谷Redis零基础到进阶,最强redis7教程,阳哥亲自带练(附redis面试题)_哔哩哔哩_bilibili

Redis面试题

总体

1、什么是Redis?(Redis的优点,相比Memcached的好处)

  • Redis是一个高性能内存数据存储系统,也可以称为键值存储系统。它支持多种数据结构,包括字符串、哈希、列表、集合、有序集合等,还提供了一些高级功能,如发布订阅、事务、Lua脚本等。Redis的特点是数据存储在内存中,可以快速读写,同时支持数据持久化到磁盘中。Redis还具有分布式特性,可以通过分片和赋值来实现高可用和高扩展性。
  • Redis主要应用于缓存会话存储消息队列、排行榜等场景,具有快速、稳定、可靠等优点。由于其出色的性能和易用性,Redis已经成为最受欢迎的内存数据库之一。

2、你了解Redis事务吗?

  • Redis中的事务是一组命令的集合,是Redis的最小执行单位。它可以保证一次执行多个命令,每个事务是一个单独的隔离操作,事务中的所有命令都会被序列化、按顺序地执行,服务端在执行事务的过程中,不会被其他客户端发送来的命令请求打断。Redis事务通过MULTI(开启一个事务)、EXEC(提交事务,从命令队列中取出提交的操作命令,进行实际执行)、DISCARD(放弃一个事务,清空命令队列)、WATCH(检测一个或多个键的值在事务执行期间是否发生变化,如果发生变化,那么当前事务放弃执行)等命令来实现的。

  • Redis事务是不支持回滚的。但是执行的命令如果有语法错误,Redis会执行失败,这些问题可以从程序层面捕获并解决。但是如果出现其他问题,则依然会继续执行剩下的命令。

    继续探究:Redis事务为什么不支持回滚?

    • 因为回滚需要增加很多工作,而不支持回滚可以保持简单、快速的特性。

缓存

1、哪些场景用到了Redis?

  • 缓存
  • 分布式锁
  • 消息队列

2、什么是缓存穿透 ? 怎么解决 ?

  • 缓存穿透:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库
  • 解决方法:
    • 缓存空数据。问题:经常缓存空数据会带来巨大的内存消耗
    • 布隆过滤器(数据存在一定放行,不存在可能会误判为存在)

3、什么是布隆过滤器?

  • 目的:用于检索一个元素是否在一个集合中
  • 实现方式:redisson
  • 原理:bitmap + 多个哈希函数
  • 缺点:不存在可能误判为存在(哈希函数的特性造成的)

4、什么是缓存击穿 ? 怎么解决 ?

  • 缓存击穿:给某一个key(热点数据)设置了过期时间,当key过期的时候,恰好这时间点对这个key有大量的并发请求过来,这些并发的请求可能会瞬间把DB压垮
  • 解决方式
    • 逻辑过期 (高可用 + 性能优)
      • 在设置key的时候,设置一个过期时间字段一块存入缓存中,不给当前 key设置过期时间
      • 当查询的时候,从redis取出数据后判断时间是否过期
      • 如果过期则开通另外一个线程进行数据同步,当前线程正常返回数据, 这个数据不是最新
    • 互斥锁(强一致 + 性能差)
      • 当缓存失效时,不立即去load db,先使用如 Redis 的 setnx 去设置一个互斥锁,当操作成功返回时再进行 load db的操作并回设缓存,否则重试get缓存的方法

5、什么是缓存雪崩 ? 怎么解决 ?

  • 缓存雪崩:在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。
  • 解决方法
    • 给不同的Key的TTL添加随机值
    • 利用Redis集群提高服务的可用性
    • 给缓存业务添加降级限流策略
    • 给业务添加多级缓存

6、Redis的数据过期策略有哪些?

  • 惰性删除(CPU友好、内存不友好),在设置该key过期时间后,我们不去管它,当需要该key 时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key。
  • 定期删除,就是说每隔一段时间,我们就对一些key进行检查,删 除里面过期的key。
  • 继续探究:定期清理有哪几种模式?
    • SLOW模式是定时任务,执行频率默认为10hz,每次不超过25ms,以通过修改配 置文件redis.conf 的 hz 选项来调整这个次数。
    • FAST模式执行频率不固定,每次事件循环会尝试执行,但两次间隔不低于2ms, 每次耗时不超过1ms。

7、Redis的数据淘汰策略有哪些 ?

  • 默认是noeviction,不删除任何数据,内 部不足直接报错
  • LRU:最少最近使用,用当前时间减去最后一次访问时间,这个值 越大则淘汰优先级越高。
  • LFU:最少频率使用。会统计每个key的访问频率,值越小淘汰优先级 越高
  • 8种:不做事 + TTL比较 + (随机 + LRU + LFU) * (所有键 + 设置了TTL的键)

8、怎么样提高缓存的命中率?

  1. 预热缓存:在系统启动的时候,将一些热点数据提前加载到缓存中,可以避免在系统运行时出现缓存穿透和缓存雪崩的情况。
  2. 增加缓存容量:增加缓存容量可以缓存更多的数据,从而提高缓存命中率。
  3. 优化缓存设计:合理的缓存设计是提高缓存命中率的前提,包括选择合适的数据结构、缓存过期时间、缓存的key命名等。
  4. 使用多级缓存:多级缓存可以将热点数据缓存在更快速、容量更小的缓存中,减少从慢速缓存或者数据库中读取数据的次数。
  5. 缓存穿透处理:针对一些缓存中不存在,但是经常被查询的数据,可以采取布隆过滤器或设置空值等方式来进行预判,避免缓存穿透的情况。
  6. 建立读写分离的架构:将读请求和写请求分别处理,读请求可以直接从缓存中读取数据,写请求更新数据库后再更新缓存,从而避免缓存和数据库的一致性问题。

9、什么事缓存降级?有哪些常见的Redis缓存降级策略?

缓存降级:指在缓存失效缓存访问异常时,为了保证系统的可用性,通过一些机制,将请求转发到其他服务或者直接返回默认值,从而避免系统崩溃或者因为缓存故障导致业务受损

  • 常见的Redis缓存降级策略包括:
    1. 熔断降级:当Redis缓存故障或者超时时,系统会进入熔断状态,所有请求转发到备用服务或者直接返回默认值
    2. 限流降级:当Redis缓存无法处理所有请求时,系统会采用限流策略,限制访问流量,保护系统资源,避免系统崩溃。
    3. 数据降级:当Redis缓存故障时,系统可以返回默认值,避免因缓存故障导致业务受损。

持久化

1、你是如何实现双写一致性的?

  • 双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致
  • 因为要求时效性比较高, 我们当时采用的读写锁保证的强一致性。我们采用的是redisson实现的读写锁,在读的时候添加共享锁,可以保证读 读不互斥,读写互斥。当我们更新数据的时候,添加排他锁,它是读写,读 读都互斥,这样就能保证在写数据的同时是不会让其他线程读数据的,避免 了脏数据。这里面需要注意的是读方法和写方法上需要使用同一把锁才行。
  • 继续深究:排他锁是如何保证读写、读读互斥的呢?
    • 其实排他锁底层使用也是setnx,保证了同时只能有一个线程操作 锁住的方法。为了锁的可重入性,用了hsetnx。
  • 数据同步可以有一定的延时的场景:采用的阿里的canal组件实现数据同步:不需要更改业务代码,部署 一个canal服务。canal服务把自己伪装成mysql的一个从节点,当mysql数据 更新以后,canal会读取binlog数据,然后在通过canal的客户端获取到数据, 更新缓存即可。

2、什么是延迟双删?为什么不用它呢?

  • 对于写操作,我们先把缓存中的数据删除,然后更新数据库,最后再延时删除缓存中的数据
  • 由于这个延时多久不太好确定,在延时的过程中可能会出现脏数据,并不能保证强一致性,所以没有采用它。

3、redis做为缓存,数据的持久化是怎么做的?

  • 在Redis中提供了两种数据持久化的方式:1、RDB 2、AOF

  • RDB是一个快照文件,它是把redis内存存储的数据写到磁盘上,当 redis实例宕机恢复数据的时候,方便从RDB的快照文件中恢复数据。

  • AOF的含义是追加文件,当redis操作写命令的时候,都会存储这个文件中, 当redis实例宕机恢复数据的时候,会从这个文件中再次执行一遍命令来恢复 数据。

  • RDB因为是二进制文件,在保存的时候体积也是比较的,它恢复 的比较快,但是它有可能会丢数据,我们通常在项目中也会使用AOF来恢复 数据,虽然AOF恢复的速度慢一些,但是它丢数据的风险要小很多,在AOF 文件中可以设置刷盘策略,我们当时设置的就是每秒批量写入一次命令。

    RDB AOF
    持久化方式 定时对整个内存做快照 记录每一次执行的命令
    数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
    文件大小 会有压缩,文件体积小 记录命令,文件体积很大
    宕机恢复速度 很快
    数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
    系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源 但AOF重写时会占用大量CPU和内存资源
    使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

4、RDB的执行原理是什么?

  • bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据。完成fork后读取内存数据并写入 RDB 文件。fork采用的是copy-on-write技术:
    • 当主进程执行读操作时,访问共享内存;

    • 当主进程执行写操作时,则会拷贝一份数据,执行写操作。

                

Redis面试题_第1张图片

5、AOF有哪几种刷盘方式?

配置项 刷盘时机 优点 缺点
Always 表示每执行一次写命令,立即记录到AOF文件 可靠性高,几乎不丢数据 性能影响大
everysec 写命令执行完先放入AOF缓冲区,然后表示每隔1秒将缓冲区数据写到AOF文件,是默认方案 性能适中 最多丢失1秒数据
no 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘 性能最好 可靠性较差,可能丢失大量数据

6、AOF持久化方式会存在哪些问题?如何解决?

  • 因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义。
  • 解决方案:执行bgrewriteaof命令,可以让AOF文件执行重写功能,用最少的命令达到相同效果。

分布式锁

1、Redis分布式锁如何实现 ?

  • 由于redis的单线程的,通过setnx(SET if not exists)命令,只能有一个客户端对某一个key设置值,在没有过期或删除key的时候是其他客户端是不能设置这个key的。为了锁的可重入性,使用hsetnx命令。如果是当前线程持有的锁就会计 数,如果释放锁就会在计算上减一。key是当前线程的唯一标识,value 是当前线程重入的次数。

  • 解锁是有两个操作(保证执行操作的客户端就是加锁的客户端以及删除锁),这时就需要 Lua 脚本来保证解锁的原子性。

    // 释放锁时,先比较 unique_value 是否相等,避免锁的误释放
    if redis.call("get",KEYS[1]) == ARGV[1] then
        return redis.call("del",KEYS[1])
    else
        return 0
    end
    

2、那你如何控制Redis实现分布式锁有效时长呢?

  • 首先设置一个给定时间,比如30ms。然后引入一个看门狗机制,每隔一段时间就检查当前业务是否还持有锁,如果持有就增加加锁的持有时间,当业务执行完成之后需要使用释放锁就可以了。

3、redisson实现的分布式锁能解决主从一致性的问题吗?如何解决?

  • 不能。比如,当线程1加锁成功后,master节点数据会异 步复制到slave节点,此时当前持有Redis锁的master节点宕机,slave节点被 提升为新的master节点,假如现在来了一个线程2,再次加锁,会在新的 master节点上加锁成功,这个时候就会出现两个节点同时持有一把锁的问题。
  • 解决方案:
    • 不建议:在多个redis实例上创建锁,红锁中要求是redis的节点数量要过半。这 样就能避免线程1加锁成功后master节点宕机导致线程2成功加锁到新的 master节点上的问题了。但是,如果使用了红锁,因为需要同时在多个节点上都添加锁,性能就变的 很低了,并且运维维护成本也非常高。
    • 使用zookeeper实现的分布式锁,它是可以保证强一致性的。

高可用

1、请你介绍一下主从复制

  • 动机:单节点Redis的并发能力是有上限的,要进一步提 高Redis的并发能力,可以搭建主从集群,实现读写分离。一般都是一主多 从,主节点负责写数据,从节点负责读数据,主节点写入数据之后,需要把 数据同步到从节点中
  • 流程:
    • 阶段一:全量同步

Redis面试题_第2张图片

  •  从节点请求主节点同步数据,其中从节点会携带自己的replication id 和offset偏移量。
  •  主节点判断是否是第一次请求,主要判断的依据就是,主节点与从节 点是否是同一个replication id,如果不是,就说明是第一次同步,那主节点 就会把自己的replication id和offset发送给从节点,让从节点与主节点的信息 保持一致。
  • 在同时主节点会执行bgsave,生成rdb文件后,发送给从节点去执 行,从节点先把自己的数据清空,然后执行主节点发送过来的rdb文件,这 样就保持了一致

        注:如果在rdb生成执行期间,依然有请求到了主节点,而主节点会以命令的方式记录到缓冲区,缓冲区是一个日志文件,最后把这个日志文件发送给从节点,这样就能保证主节点与从节点完全一致了,后期再同步数据的时 候,都是依赖于这个日志文件,这个就是全量同步

  • 阶段二:增量同步

    • 当从节点服务重启之后,数据就不一致了,所以这个时 候,从节点会请求主节点同步数据,主节点还是判断不是第一次请求,不是 第一次就获取从节点的offset值,然后主节点从命令日志中获取offset值之后 的数据,发送给从节点进行数据同步

2、怎么保证Redis的高并发高可用?

  • 首先可以搭建主从集群,再加上使用redis中的哨兵模式。哨兵模式 可以实现主从集群的自动故障恢复,里面就包含了对主从服务的监控自动故障恢复通知;如果master故障,Sentinel会将一个slave提升为master。 当故障实例恢复后也以新的master为主;同时Sentinel也充当Redis客户端的 服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户 端,所以一般项目都会采用哨兵的模式来保证redis的高并发高可用
  • 我们当时使用的是主从(1主1从)加哨兵。一般单节点不超 过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节 点。尽量不做分片集群。因为集群维护起来比较麻烦,并且集群之间的心跳 检测和数据通信会消耗大量的网络带宽,也没有办法使用lua脚本和事务

3、redis集群脑裂是什么?怎么解决?

  • 集群脑裂:由于主节点和从节点和sentinel处于不同的网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当网络恢复后,sentinel会将老的主节点降为从节点,这时再从新master同步数据,就会导致数据丢失。
  • 解决方案
    • 设置最少的从节点数量,比如设置至少要有一个从节点才能同步数据,
    • 缩短主从数据同步的延迟时间,达不到要求就拒绝请求

4、redis的分片集群有什么作用?

  • 分片集群主要解决的是,海量数据存储的问题,集群中有多个 master,每个master保存不同数据,并且还可以给每个master设置多个slave 节点,就可以继续增大集群的高并发能力。
  • 每个master之间通过ping监 测彼此健康状态,就类似于哨兵模式了。当客户端请求可以访问集群任意节点,最终都会被转发到正确节点。

5、Redis分片集群中数据是怎么存储和读取的?

  • Redis 集群引入了哈希槽的概念,有 16384 个哈希槽,集群中每个主节点绑 定了一定范围的哈希槽范围, key通过 CRC16 校验后对 16384 取模来决定放 置哪个槽,通过槽找到对应的节点进行存储。

底层原理

1、Redis是单线程的,但是为什么还那么快?

  • 完全基于内存的,C语言编写
  • 采用单线程,避免不必要的上下文切换可竞争条件
  • 使用多路I/O复用模型,非阻塞IO
  • 丰富的数据结构,全程采用哈希结构,读取速度非常快,对数据存储进行了一些优化,例如压缩表、跳表等。
  • 同时,bgsave 和 bgrewriteaof 都是在后台执行操作,不影响主线程的正常 使用,不会产生阻塞

2、可以解释一下I/O多路复用模型吗?

  • 阻塞IO在两个阶段必须阻塞等待:
    • 等待数据加载到内核
    • 数据从内核缓冲区拷贝到用户缓冲区
  • 非阻塞IO只会阻塞在第二个阶段
    • 第一阶段会循环询问,直到数据就绪(忙等机制可能会导致CPU空转,CPU使用率暴增)
    • 数据从内核缓冲区拷贝到用户缓冲区仍然阻塞
  • IO多路复用模型
    • 利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU 资源。目前的I/O多路复用都是采用的epoll模式实现,它会在通知用户进程 Socket就绪的同时,把已就绪的Socket写入用户空间,不需要挨个遍历 Socket来判断是否就绪,提升了性能。
  • 其中Redis的网络模型就是使用I/O多路复用结合事件的处理器来应对多个 Socket请求,比如,提供了连接应答处理器、命令回复处理器,命令请求处 理器; 在Redis6.0之后,为了提升更好的性能,在命令回复处理器使用了多线程来 处理回复事件,在命令请求处理器中,将命令的转换使用了多线程,增加命 令转换速度,在命令执行的时候,依然是单线程。

Redis面试题_第3张图片

3、pipeline有什么好处,为什么redis要是用Pipeline?

  • 可以将多次I/O往返的时间缩短为一次,从而提高Redis的吞吐量和性能。Pipeline允许客户端将多个Redis命令打包成一次请求发送给Redis服务器,Redis服务器收到后,将多个命令按顺序执行,并将执行结果按照请求的顺序返回给客户端,这样就避免了每次请求都要进行网络通信的开销

4、Redis为什么设计成单线程的?

  • 避免上下文切换:在多线程环境下,线程的切换会涉及到上下文的切换,这个切换本身就就会消耗CPU资源和时间。
  • 避免锁竞争。
  • 减少内存分配:在多线程环境下,线程之间需要共享内存,而内存共享会涉及到内存分配和管理的开销。

5、为什么Redis的操作是原子性的,怎么保证原子性?

  • Redis的操作是原子性的,是因为Redis是单线程的。
  • Redis保证原子性的方式主要有两种:事务Lua脚本
  • Redis还提供了一些原子性操作,例如INCR、DECR等。

常见工程性问题

1、什么是Bigkey?会存在什么影响?

  • Bigkey指的是Redis中的大键,即占用内存较多的键值对。
  • 影响如下:
    • 占用大量的内存资源
    • 增加网络传输的负担
    • 由于Bigkey占用的空间较大并且Redis采用单线程模型,所以Redis在对其操作时,可能会消耗过长的时间,导致客户端超时阻塞
    • Redis中出现内存碎片,从而影响Redis的内存使用效率,导致Redis内存占用率上升。

2、Redis常见性能问题和解决方案有哪些?

  1. 网络延迟:Redis的性能很大程度上受限于网络延迟,因此需要尽可能减少网络传输次数和数据量,避免过多的网络IO操作
    • 解决方案:可以使用Redis的Pipline特性,将多个请求打包发送,减少网络传输的次数;也可以使用Redis的批量操作命令,将多个数据一次性提交,减少网络传输的数据量。
  2. 大量的数据读写:Redis的单线程模型会在高并发读写的情况下出现性能瓶颈,导致响应时间变长。
    • 解决方案:可以使用Redis的主从复制和集群特性,将数据分布在多个节点上,增加系统的读写并发能力。
  3. 慢查询:当Redis中存在大量慢查询操作时,会影响Redis的整体性能。
    • 解决方案:可以使用Redis的slowlog功能,记录Redis的慢查询操作,并使用Redis的监控工具进行监控,及时发现慢查询问题。
  4. 内存使用过多:Redis需要将所有的数据存储在内存中,当数据量过大时,会占用大量的内存资源,导致Redis的性能下降。
    • 解决方案:可以使用Redis的持久化功能,将数据写入磁盘中,以释放内存空间;也可以使用Redis的内存优化技巧,如删除不必要的数据合理使用Redis的数据结构等。
  5. 阻塞操作:当Redis执行某些操作时,会阻塞其他操作的执行,从而影响Redis的整体性能。
    • 解决方案:可以使用Redis的异步操作特性,将阻塞操作转化为异步操作,以提高Redis的性能和吞吐量。

3、如果Redis中有1亿个key,其中有10w个key是以某个固定的已知前缀开头的,如何将它们全部找出来?

  • 使用scan命令,千万不要使用keys命令。

    优点:

    • 在执行过程中不会阻塞线程
    • scan命令是通过游标方式查询的,所以不会导致Redis出现假死问题(Redis实例在进行某些耗时操作时,由于Redis是单线程的,所以这个操作会导致Redis线程被阻塞,从而导致Redis无法处理其他请求,造成Redis服务不可用的状态。)。

    缺点:

    • 相对来说,scan命令查找花费的时间会比keys命令长。
    • Redis在查询过程中会把游标返回给客户端,单词返回控制且游标不为0,则说明遍历还没有结束,客户端继续遍历查询。但是,scan命令在检索的过程中,被删除的元素是不会被查询出来的,如果在迭代过程中有元素被修改,scan命令也不能保证查询出对应的元素。

4、什么情况下可能会导致Redis阻塞?

  • 的计算复杂度较高,在数据量较大的情况下,如果直接执行这些计算,会导致 Redis 实例阻塞。
  • Redis主线程在执行阻塞命令(如BRPOPBLPOPBRPOPLPUSHSUBSCRIBE等)时,会阻塞其他客户端的请求,直到命令执行完毕才能继续处理其他请求。
  • Redis主线程在执行某些耗时的命令(如SORTKEYS、Set 的差集、并集和交集等)时,也会阻塞其他客户端的请求,同样需要等待命令执行完毕后才能继续处理其他请求。
  • Redis内存使用达到最大限制时,执行写操作(如SETINCR等)可能会导致Redis阻塞。这是因为Redis需要执行内存回收操作以释放内存空间,如果回收操作耗时过长,就会导致Redis阻塞。
  • Redis主从同步过程中,如果主库无法及时响应从库的同步请求,就会导致从库阻塞,无法继续进行数据同步。

进一步探究:如何解决?

  1. 尽可能使用非阻塞命令,例如LPUSHRPOP代替BLPOP,使用Lua脚本实现多个操作的原子性等。
  2. 尽量避免使用耗时的命令或对大数据集进行操作,如果必须使用,可以考虑将这些操作放在后台进行。
  3. 设置合理的内存使用上限,同时使用内存淘汰策略来控制内存使用情况。
  4. 配置合理的主从架构,避免主库过于繁忙,导致从库同步阻塞。

5、Redis缓存不足如何处理?

  • 增加物理内存。
  • 删除一些已经不再使用的数据,或者将一些数据进行持久化,以释放内存。
  • 调整Redis配置文件中的一些参数,如maxmemory等,增加Redis可用内存。
  • 使用Redis集群:可以将数据分散在多个Redis节点中,每个节点存储一部分数据,从而减少单个Redis实例的内存使用量。

数据结构

1、Redis有哪些数据结构?对应的底层实现、应用场景和常见命令有哪些?

  • 常见的有五种:String(字符串),Hash(哈希),List(列表),Set(集合)、Zset(有序集合)。
  • 面又支持了四种数据类型: BitMap(2.2 版新增)、HyperLogLog(2.8 版新增)、GEO(3.2 版新增)、Stream(5.0 版新增)

String :最基本的 key-value 结构,key 是唯一标识,value 是具体的值,value其实不仅是字符串, 也可以是数字(整数或浮点数),value 最多可以容纳的数据长度是 512M。底层实现:intSDS(简单动态字符串)

SDS相比于C 的原生字符串的优点?

  • SDS 不仅可以保存文本数据,还可以保存图片、音频、视频、压缩文件这样的二进制数据
  • SDS 获取字符串长度的时间复杂度是 O(1)
  • Redis 的 SDS API 是二进制安全的,拼接字符串不会造成缓冲区溢出

字符串对象的内部编码是什么样子的?

Redis面试题_第4张图片

  • 保存的是整数值,并且这个整数值可以用long类型来表示,那么字符串对象会将整数值保存在字符串对象结构的ptr属性里面(将void*转换成 long),并将字符串对象的编码设置为int

    Redis面试题_第5张图片

  • 保存的是一个字符串,并且这个字符申的长度小于等于 32 字节。那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为embstr, embstr编码是专门用于保存短字符串的一种优化编码方式:

  • 保存的是一个字符串,并且这个字符串的长度大于 32 字节(redis 2.+版本),那么字符串对象将使用一个简单动态字符串(SDS)来保存这个字符串,并将对象的编码设置为raw

    Redis面试题_第6张图片

embstr 编码和 raw 编码的边界在 redis 不同版本中一样吗?

  • redis 2.+ 是 32 字节
  • redis 3.0-4.0 是 39 字节
  • redis 5.0 是 44 字节

embstr会通过一次内存分配函数来分配一块连续的内存空间来保存redisObjectSDS,而raw编码会通过调用两次内存分配函数来分别分配两块空间来保存redisObjectSDS,这样的优点和缺点是什么?

  • 优点:内存分配次数从 raw 编码的两次降低为一次;内存释放也只需要一次,字符串对象的所有数据都保存在一块连续的内存里面可以更好的利用 CPU 缓存提升性能。
  • 缺点:如果字符串的长度增加需要重新分配内存时,整个redisObject和sds都需要重新分配空间,所以embstr编码的字符串对象实际上是只读的。

String有哪些常用指令?

# 设置 key-value 类型的值
> SET name lin
OK
# 根据 key 获得对应的 value
> GET name
"lin"
# 判断某个 key 是否存在
> EXISTS name
(integer) 1
# 返回 key 所储存的字符串值的长度
> STRLEN name
(integer) 3
# 删除某个 key 对应的值
> DEL name
(integer) 1

批量设置 key-value 类型的值
> **MSET** key1 value1 key2 value2 
OK
# 批量获取多个 key 对应的 value
> **MGET** key1 key2 
1) "value1"
2) "value2"

# 充当计数器,字符串的内容为整数的时候可以使用
# 设置 key-value 类型的值
> SET number 0
OK
# 将 key 中储存的数字值增一
> INCR number
(integer) 1
# 将key中存储的数字值加 10
> INCRBY number 10
(integer) 11
# 将 key 中储存的数字值减一
> DECR number
(integer) 10
# 将key中存储的数字值键 10
> DECRBY number 10
(integer) 0

# 过期时间的相关设置
# 设置 key 在 60 秒后过期(该方法是针对已经存在的key设置过期时间)
> EXPIRE name  60 
(integer) 1
# 查看数据还有多久过期
> TTL name 
(integer) 51

#设置 key-value 类型的值,并设置该key的过期时间为 60 秒
> SET key  value EX 60
OK
> SETEX key  60 value
OK

# 不存在就插入(not exists)
>SETNX key value
(integer) 1

String有哪些应用场景?

  • 缓存数据:如直接缓存整个对象的 JSON

  • 常规计数功能:用于实现问次数、点赞、转发、库存数量等等

  • 分布式锁:有个 NX 参数可以实现「key不存在才插入」,还会对分布式锁加上过期时间(为了避免客户端发生异常而无法释放锁)。SET lock_key unique_value NX PX 10000

  • 分布式系统中共享Session

    Redis面试题_第7张图片

List:简单的字符串列表,按照插入顺序排序,可以从头部尾部向 List 列表添加元素。列表的最大长度为 2^32 - 1,也即每个列表支持超过 40 亿个元素。

List的底层数据结构是什么?

  • 如果列表的元素个数小于 512 个(默认值,可由 list-max-ziplist-entries 配置),列表每个元素的值都小于 64 字节(默认值,可由 list-max-ziplist-value 配置),Redis 会使用压缩列表作为 List 类型的底层数据结构;
  • 如果列表的元素不满足上面的条件,Redis 会使用双向链表作为 List 类型的底层数据结构;
  • 在 Redis 3.2 版本之后,List 数据类型底层数据结构就只由 quicklist 实现了,替代了双向链表和压缩列表。

List的常用指令有哪些?

# 将一个或多个值value插入到key列表的表头(最左边),最后的值在最前面
LPUSH key value [value ...] 
# 将一个或多个值value插入到key列表的表尾(最右边)
RPUSH key value [value ...]
# 移除并返回key列表的头元素
LPOP key     
# 移除并返回key列表的尾元素
RPOP key 

# 返回列表key中指定区间内的元素,区间以偏移量start和stop指定,从0开始
LRANGE key start stop

# 从key列表表头弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BLPOP key [key ...] timeout
# 从key列表表尾弹出一个元素,没有就阻塞timeout秒,如果timeout=0则一直阻塞
BRPOP key [key ...] timeout

List有哪些应用场景?

  • 消息队列:
    • 消息保序:使用 LPUSH + RRPOP命令。BRPOP命令也称为阻塞式读取,客户端在没有读到队列数据时,自动阻塞,直到有新的数据写入队列,再开始读取新数据。因此可以实现及时处理消息。
    • 处理重复的消息:List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一ID
    • 保证消息可靠性:当消费者程序从 List 中读取一条消息后,List 就不会再留存这条消息了。如果消费者程序在处理消息的过程出现了故障或宕机,就会导致消息没有处理完成,那么,消费者程序再次启动后,就没法再次从 List 中读取消息了。为了留存消息,List 类型提供了 BRPOPLPUSH 命令,这个命令的作用是让消费者程序从一个 List 中读取消息,同时,Redis 会把这个消息再插入到另一个 List(可以叫作备份 List)留存

List 作为消息队列有什么缺陷?

  • List 不支持多个消费者消费同一条消息,因为一旦消费者拉取一条消息后,这条消息就从 List 中删除了,无法被其它消费者再次消费。

Hash:一个键值对(key - value)集合

Hash类型的底层实现?

  • 如果哈希类型元素个数小于 512 个(默认值,可由 hash-max-ziplist-entries 配置),所有值小于 64 字节(默认值,可由 hash-max-ziplist-value 配置)的话,Redis 会使用压缩列表作为 Hash 类型的底层数据结构;
  • 如果哈希类型元素不满足上面条件,Redis 会使用哈希表作为 Hash 类型的 底层数据结构。
  • 在 Redis 7.0 中,压缩列表数据结构已经废弃了,交由 listpack 数据结构来实现了。

Hash类型有哪些常用命令?

# 存储一个哈希表key的键值
HSET key field value   
# 获取哈希表key对应的field键值
HGET key field

# 在一个哈希表key中存储多个键值对
HMSET key field value [field value...] 
# 批量获取哈希表key中多个field键值
HMGET key field [field ...]       
# 删除哈希表key中的field键值
HDEL key field [field ...]    

# 返回哈希表key中field的数量
HLEN key       
# 返回哈希表key中所有的键值
HGETALL key 

# 为哈希表key中field键的值加上增量n
HINCRBY key field n

Hash的应用场景有哪些?

  • 缓存对象:如购物车

Set:无序并唯一的键值集合,一个集合最多可以存储 2^32-1 个元素。

Set内部实现是什么样子的?

  • 如果集合中的元素都是整数且元素个数小于 512 (默认值,set-maxintset-entries配置)个,Redis 会使用整数集合作为 Set 类型的底层数据结构;
  • 如果集合中的元素不满足上面条件,则 Redis 使用哈希表作为 Set 类型的底层数据结构。

Set中有哪些常见命令?

# 往集合key中存入元素,元素存在则忽略,若key不存在则新建
SADD key member [member ...]
# 从集合key中删除元素
SREM key member [member ...] 
# 获取集合key中所有元素
SMEMBERS key
# 获取集合key中的元素个数
SCARD key

# 判断member元素是否存在于集合key中
SISMEMBER key member

# 从集合key中随机选出count个元素,元素不从key中删除
SRANDMEMBER key [count]
# 从集合key中随机选出count个元素,元素从key中删除
SPOP key [count]

# 交集运算
SINTER key [key ...]
# 将交集结果存入新集合destination中
SINTERSTORE destination key [key ...]

# 并集运算
SUNION key [key ...]
# 将并集结果存入新集合destination中
SUNIONSTORE destination key [key ...]

# 差集运算
SDIFF key [key ...]
# 将差集结果存入新集合destination中
SDIFFSTORE destination key [key ...]

Set有哪些应用场景?

  • 点赞:可以保证一个用户只能点一个赞。key 是文章id,value 是用户id。
  • 共同关注:交集
  • 抽奖:同一个用户不会中奖两次

Zset:相比于 Set 类型多了一个排序属性 score。

Zset的底层实现?

  • 如果有序集合的元素个数小于 128 个,并且每个元素的值小于 64 字节时,Redis 会使用压缩列表作为 Zset 类型的底层数据结构;
  • 如果有序集合的元素不满足上面的条件,Redis 会使用跳表作为 Zset 类型的底层数据结构;

Zset常见的命令有哪些?

# 往有序集合key中加入带分值元素
ZADD key score member [[score member]...]   
# 往有序集合key中删除元素
ZREM key member [member...]                 
# 返回有序集合key中元素member的分值
ZSCORE key member
# 返回有序集合key中元素个数
ZCARD key 

# 为有序集合key中元素member的分值加上increment
ZINCRBY key increment member 

# 正序获取有序集合key从start下标到stop下标的元素
ZRANGE key start stop [WITHSCORES]
# 倒序获取有序集合key从start下标到stop下标的元素
ZREVRANGE key start stop [WITHSCORES]

# 返回有序集合中指定分数区间内的成员,分数由低到高排序。
ZRANGEBYSCORE key min max [WITHSCORES] [LIMIT offset count]

# 返回指定成员区间内的成员,按字典正序排列, 分数必须相同。
ZRANGEBYLEX key min max [LIMIT offset count]
# 返回指定成员区间内的成员,按字典倒序排列, 分数必须相同
ZREVRANGEBYLEX key max min [LIMIT offset count]

# 并集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZUNIONSTORE destkey numberkeys key [key...] 
# 交集计算(相同元素分值相加),numberkeys一共多少个key,WEIGHTS每个key对应的分值乘积
ZINTERSTORE destkey numberkeys key [key...]

Zset的应用场景有哪些?

  • 排行榜
  • 电话、姓名排序

BitMap:一串连续的二进制数组(0和1),可以通过偏移量(offset)定位元素。

BitMap的内部实现?

  • Bitmap 本身是用 String 类型作为底层数据结构实现的一种统计二值状态的数据类型。
  • String 类型是会保存为二进制的字节数组,所以,Redis 就把字节数组的每个 bit 位利用起来,用来表示一个元素的二值状态,你可以把 Bitmap 看作是一个 bit 数组。

BitMap常用的命令有哪些?

# 设置值,其中value只能是 0 和 1
SETBIT key offset value

# 获取值
GETBIT key offset

# 获取指定范围内值为 1 的个数
# start 和 end 以字节为单位
BITCOUNT key start end

BitMap有哪些应用场景?

  • 签到统计
  • 判断用户登录状态

HyperLogLog:Redis 2.8.9 版本新增的数据类型,是一种用于「统计基数」的数据集合类型,基数统计就是指统计一个集合中不重复的元素个数,不是非常准确,标准误算率是 0.81%。但是每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基数,非常节省空间。

HyperLogLog常见命令有哪些?

# 添加指定元素到 HyperLogLog 中
PFADD key element [element ...]

# 返回给定 HyperLogLog 的基数估算值。
PFCOUNT key [key ...]

# 将多个 HyperLogLog 合并为一个 HyperLogLog
PFMERGE destkey sourcekey [sourcekey ...]

HyperLogLog有哪些应用场景?

  • 百万级以上的网页 UV 的场景

GEO:Redis 3.2 版本新增的数据类型,主要用于存储地理位置信息,并对存储的信息进行操作。GEO 本身并没有设计新的底层数据结构,而是直接使用了 Sorted Set 集合类型。

# 存储指定的地理空间位置,可以将一个或多个经度(longitude)、纬度(latitude)、位置名称(member)添加到指定的 key 中。
GEOADD key longitude latitude member [longitude latitude member ...]

# 从给定的 key 里返回所有指定名称(member)的位置(经度和纬度),不存在的返回 nil。
GEOPOS key member [member ...]

# 返回两个给定位置之间的距离。
GEODIST key member1 member2 [m|km|ft|mi]

# 根据用户给定的经纬度坐标来获取指定范围内的地理位置集合。
GEORADIUS key longitude latitude radius m|km|ft|mi [WITHCOORD] [WITHDIST] [WITHHASH] [COUNT count] [ASC|DESC] [STORE key] [STOREDIST key]

应用场景:主要是一些位置服务,如滴滴快车

Stream:Redis 5.0 版本新增加的数据类型,Redis 专门为消息队列设计的数据类型。

2、键值对数据库是怎么实现的?

Redis 是使用了一个「哈希表」保存所有键值对,哈希表的最大好处就是让我们可以用 O(1) 的时间复杂度来快速查找到键值对。哈希表其实就是一个数组,数组中的元素叫做哈希桶

哈希桶存放的是指向键值对数据的指针(dictEntry*),这样通过指针就能找到键值对数据,然后因为键值对的值可以保存字符串对象和集合数据类型的对象,所以键值对的数据结构中并不是直接保存值本身,而是保存了 void * key 和 void * value 指针,分别指向了实际的键对象和值对象,这样一来,即使值是集合数据,也可以通过 void * value 指针找到。

Redis面试题_第8张图片

  • redisDb 结构,表示 Redis 数据的结构,结构体里存放了指向了 dict 结构的指针;

  • dict 结构,结构体里存放了 2 个哈希表,正常情况下都是用「哈希表1」,「哈希表2」只有在 rehash 的时候才用;

  • ditctht 结构,表示哈希表的结构,结构里存放了哈希表数组,数组中的每个元素都是指向一个哈希表节点结构(dictEntry)的指针;

  • dictEntry 结构,表示哈希表节点的结构,结构里存放了void * key 和 void * value 指针;

  • void * key 和 void * value 指针指向的是 Redis 对象,Redis 中的每个对象都由 redisObject 结构表示,如下图:

    Redis面试题_第9张图片

3、C语言字符串的缺陷有哪些?

  • 字符数组的结尾位置就用“\0”表示,因此除了字符串的末尾之外,字符串里面不能含有 “\0” 字符,这个限制使得 C 语言的字符串只能保存文本数据,不能保存像图片、音频、视频文化这样的二进制数据。
  • C 语言获取字符串长度的时间复杂度是 O(N)。
  • 字符串操作函数不高效且不安全,比如有缓冲区溢出的风险,有可能会造成程序运行终止。因为C 语言的字符串是不会记录自身的缓冲区大小的。

4、SDS是如何设计的?优点是什么?

Redis面试题_第10张图片

  • len,记录了字符串长度
  • alloc,分配给字符数组的空间长度当判断出缓冲区大小不够用时,Redis 会自动将扩大 SDS 的空间大小,以满足修改所需的大小。
    • 如果所需的 sds 长度小于 1 MB,那么最后的扩容是按照翻倍扩容来执行的,即 2 倍的newlen
    • 如果所需的 sds 长度超过 1 MB,那么最后的扩容长度应该是 newlen + 1MB
  • flags,用来表示不同类型的 SDS。一共设计了 5 种类型,分别是 sdshdr5、sdshdr8、sdshdr16、sdshdr32 和 sdshdr64。
    • sdshdr16 类型的 len 和 alloc 的数据类型都是 uint16_t,表示字符数组长度和分配空间大小不能超过 2 的 16 次方。
    • sdshdr32 则都是 uint32_t,表示表示字符数组长度和分配空间大小不能超过 2 的 32 次方。
  • buf[],字符数组,用来保存实际数据。不仅可以保存字符串,也可以保存二进制数据。

优点:

  • O(1)复杂度获取字符串长度
  • 二进制安全
  • 不会发生缓冲区溢出
  • 节省内存空间
    • SDS 设计不同类型的结构体,是为了能灵活保存不同大小的字符串,从而有效节省内存空间。比如,在保存小字符串时,结构头占用空间也比较少。
    • 使用了专门的编译优化来节省内存空间,即在 struct 声明了 __attribute__ ((packed)) ,它的作用是:告诉编译器取消结构体在编译过程中的优化对齐,按照实际占用字节数进行对齐

5、双向链表的结构是什么样子的?有什么优缺点?

Redis面试题_第11张图片

Redis 的链表实现优点如下:

  • listNode 链表节点的结构里带有 prev 和 next 指针,获取某个节点的前置节点或后置节点的时间复杂度只需O(1),而且这两个指针都可以指向 NULL,所以链表是无环链表
  • list 结构因为提供了表头指针 head 和表尾节点 tail,所以获取链表的表头节点和表尾节点的时间复杂度只需O(1)
  • list 结构因为提供了链表节点数量 len,所以获取链表中的节点数量的时间复杂度只需O(1)
  • listNode 链表节使用 void* 指针保存节点值,并且可以通过 list 结构的 dup、free、match 函数指针为节点设置该节点类型特定的函数,因此链表节点可以保存各种不同类型的值

链表的缺陷也是有的:

  • 链表每个节点之间的内存都是不连续的,意味着无法很好利用 CPU 缓存。能很好利用 CPU 缓存的数据结构就是数组,因为数组的内存是连续的,这样就可以充分利用 CPU 缓存来加速访问。
  • 还有一点,保存一个链表节点的值都需要一个链表节点结构头的分配,内存开销较大

6、压缩列表是如何设计的?缺点是什么?

由连续内存块组成的顺序型数据结构,有点类似于数组。

Redis面试题_第12张图片

  • zlbytes,记录整个压缩列表占用对内存字节数;
  • zltail,记录压缩列表「尾部」节点距离起始地址由多少字节,也就是列表尾的偏移量;
  • zllen,记录压缩列表包含的节点数量;
  • zlend,标记压缩列表的结束点,固定值 0xFF(十进制255)。
  • prevlen,记录了「前一个节点」的长度,目的是为了实现从后向前遍历;
  • encoding,记录了当前节点实际数据的「类型和长度」,类型主要有两种:字符串和整数。
  • data,记录了当前节点的实际数据,类型和长度都由 encoding 决定;

缺陷:

  • 不能保存过多的元素,否则查询效率就会降低;
  • 新增或修改某个元素时,压缩列表占用的内存空间需要重新分配,甚至可能引发连锁更新的问题(当新插入的元素较大时,可能会导致后续元素的 prevlen 占用空间都发生变化,导致每个元素的空间都要重新分配,造成访问压缩列表性能的下降。。

7、Redis中哈希表节点是如何设计的?

typedef struct dictEntry {
    //键值对中的键
    void *key;
  
    //键值对中的值
    union {
        void *val;
        uint64_t u64;
        int64_t s64;
        double d;
    } v;
    //指向下一个哈希表节点,形成链表
    struct dictEntry *next;
} dictEntry;
  • Redis 采用了「链式哈希」来解决哈希冲突,dictEntry 结构里不仅包含指向键和值的指针,还包含了指向下一个哈希表节点的指针,这个指针可以将多个哈希值相同的键值对链接起来,以此来解决哈希冲突的问题,这就是链式哈希。
  • dictEntry 结构里键值对中的值是一个「联合体 v」定义的,因此,键值对中的值可以是一个指向实际值的指针,或者是一个无符号的 64 位整数或有符号的 64 位整数或double 类的值。这么做的好处是可以节省内存空间,因为当「值」是整数或浮点数时,就可以将值的数据内嵌在 dictEntry 结构里,无需再用一个指针指向实际的值,从而节省了内存空间。

8、请解释一下rehash操作?

Redis面试题_第13张图片

  • Redis 定义一个 dict 结构体,这个结构体里定义了两个哈希表(ht[2])。
  • 在正常服务请求阶段,插入的数据,都会写入到「哈希表 1」,此时的「哈希表 2 」 并没有被分配空间。
  • 随着数据逐步增多,触发了 rehash 操作,这个过程分为三步:
    • 给「哈希表 2」 分配空间,一般会比「哈希表 1」 大 2 倍;
    • 将「哈希表 1 」的数据迁移到「哈希表 2」 中;
    • 迁移完成后,「哈希表 1 」的空间会被释放,并把「哈希表 2」 设置为「哈希表 1」,然后在「哈希表 2」 新创建一个空白的哈希表,为下次 rehash 做准备。

继续探究:如果「哈希表 1 」的数据量非常大,那么在迁移至「哈希表 2 」的时候,因为会涉及大量的数据拷贝,此时可能会对 Redis 造成阻塞,无法服务其他请求,如何处理?

  • Redis 采用了渐进式 rehash,将数据的迁移的工作不再是一次性迁移完成,而是分多次迁移。步骤如下:
    • 给「哈希表 2」 分配空间;
    • 在 rehash 进行期间,每次哈希表元素进行新增、删除、查找或者更新操作时,Redis 除了会执行对应的操作之外,还会顺序将「哈希表 1 」中索引位置上的所有 key-value 迁移到「哈希表 2」 上
    • 随着处理客户端发起的哈希表操作请求数量越多,最终在某个时间点会把「哈希表 1 」的所有 key-value 迁移到「哈希表 2」,从而完成 rehash 操作。

继续探究:什么时情况下会触发 rehash 操作呢?

  • 当负载因子大于等于 1 ,并且 Redis 没有在执行 bgsave 命令或者 bgrewiteaof 命令,也就是没有执行 RDB 快照或没有进行 AOF 重写的时候,就会进行 rehash 操作。
  • 当负载因子大于等于 5 时,此时说明哈希冲突非常严重了,不管有没有有在执行 RDB 快照或 AOF 重写,都会强制进行 rehash 操作。

9、介绍一下整数集合?

  • 整数集合是 Set 对象的底层实现之一。当一个 Set 对象只包含整数值元素,并且元素数量不大时,就会使用整数集这个数据结构作为底层实现。

  • 整数集合本质上是一块连续内存空间,它的结构定义如下(contents 数组并不保存任何 int8_t 类型的元素,contents 数组的真正类型取决于 intset 结构体里的 encoding 属性的值。):

    typedef struct intset {
        //编码方式
        uint32_t encoding;
        //集合包含的元素数量
        uint32_t length;
        //保存元素的数组
        int8_t contents[];
    } intset;
    
  • 整数升级:

Redis面试题_第14张图片

Redis面试题_第15张图片

Redis面试题_第16张图片

原本的数组上扩展空间,然后在将每个元素按间隔类型大小分割。不支持降级操作。

10、可以介绍一下跳表吗?

  • Redis 只有 Zset 对象的底层实现用到了跳表,跳表的优势是能支持平均 O(logN) 复杂度的节点查找。

  • zset 结构体里有两个数据结构:一个是跳表,一个是哈希表。这样的好处是既能进行高效的范围查询,也能进行高效单点查询。

    typedef struct zset {
        dict *dict;
        zskiplist *zsl;
    } zset;
    
  • Zset 对象能支持范围查询(如 ZRANGEBYSCORE 操作),这是因为它的数据结构设计采用了跳表,而又能以常数复杂度获取元素权重(如 ZSCORE 操作),这是因为它同时采用了哈希表进行索引。

  • 跳表是在链表基础上改进过来的,实现了一种「多层」的有序链表。

Redis面试题_第17张图片

11、为什么用跳表而不用平衡树?

  • 从内存占用上来比较,跳表比平衡树更灵活一些。平衡树每个节点包含 2 个指针(分别指向左右子树),而跳表每个节点包含的指针数目平均为 1/(1-p),具体取决于参数 p 的大小。如果像 Redis里的实现一样,取 p=1/4,那么平均每个节点包含 1.33 个指针,比平衡树更有优势。
  • 在做范围查找的时候,跳表比平衡树操作要简单。在平衡树上,我们找到指定范围的小值之后,还需要以中序遍历的顺序继续寻找其它不超过大值的节点。如果不对平衡树进行一定的改造,这里的中序遍历并不容易实现。而在跳表上进行范围查找就非常简单,只需要在找到小值之后,对第 1 层链表进行若干步的遍历就可以实现。
  • 从算法实现难度上来比较,跳表比平衡树要简单得多。平衡树的插入和删除操作可能引发子树的调整,逻辑复杂,而跳表的插入和删除只需要修改相邻节点的指针,操作简单又快速。

12、quicklist是如何设计的,从而避免连锁更新问题?

Redis面试题_第18张图片

  • 在向 quicklist 添加一个元素的时候,不会像普通的链表那样,直接新建一个链表节点。而是会检查插入位置的压缩列表是否能容纳该元素,如果能容纳就直接保存到 quicklistNode 结构里的压缩列表,如果不能容纳,才会新建一个新的 quicklistNode 结构。
  • quicklist 会控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来规避潜在的连锁更新的风险,但是这并没有完全解决连锁更新的问题。

继续探究:quicklist 虽然通过控制 quicklistNode 结构里的压缩列表的大小或者元素个数,来减少连锁更新带来的性能影响,但是并没有完全解决连锁更新的问题。那怎么办?

  • Redis 在 5.0 新设计一个数据结构叫 listpack,最大特点是 listpack 中每个节点不再包含前一个节点的长度了。

Redis面试题_第19张图片

listpack 没有压缩列表中记录前一个节点长度的字段了,listpack 只记录当前节点的长度,当我们向 listpack 加入一个新元素的时候,不会影响其他节点的长度字段的变化,从而避免了压缩列表的连锁更新问题。

你可能感兴趣的:(面试题,redis,缓存)