Redis核心面试题

Redis基础知识

Redis为什么快

  1. 纯内存操作:数据全存内存,避免磁盘 IO(数据库慢的核心原因),内存读写速度比磁盘快万倍以上
  2. 单线程模型:无多线程上下文切换开销,也无需处理线程安全问题,减少性能损耗
  3. 高效数据结构:底层用跳表、哈希表等结构,查询/插入/删除效率均为 O(1) 或 O(logn)
  4. IO模型优化:用 IO 多路复用(epoll/kqueue),单线程处理上万并发连接,无连接阻塞
  5. 轻量设计:代码简洁(核心代码仅几万行),无复杂逻辑,减少运行时资源占用

Redis的数据结构

String类型:二进制存储数据,比较安全,小字符串用embstr编码,大字符串使用raw编码。常用来存储token和简单数据。

Hash类型:存储键值对集合,元素较少时用压缩列表,较多时采用hashtable。常用来存用户信息和商品信息。

List类型:双向列表,短列表用压缩列表,长列表使用linkedlist双向列表。可以用作消息队列。

Set类型:无序不可重复集合,常用来去重复,整数集合使用intset,其他类型使用hashtable。

ZSet类型:有序集合,每个元素会带有一个score值用来排序,底层采用跳表+哈希表,小集合用压缩列表。

Redis的其他数据结构

Redis还支持:

  • Bitmap:位图,用于状态统计
  • HyperLogLog:基数统计,用于去重计数
  • Geospatial:地理位置,用于附近的人等功能

缓存问题

缓存穿透

定义:查询一个不存在的数据,MySQL查询不到数据也不会直接写入缓存,就会导致每次请求都查数据库,有可能导致数据库压力增大而宕机。

解决方案

  1. 缓存空数据:查询返回的数据为空,仍把这个空结果进行缓存
    • 优点:简单
    • 缺点:消耗内存,当后面真正存储这个元素的时候,可能会发生不一致的问题
  2. 布隆过滤器:在请求之前添加一个布隆过滤器,先判断这个值是否不存在,不存在直接拦截,存在才让它去redis或者数据库进行查询
    • 优点:内存占用少,没有多余的key
    • 缺点:实现复杂,存在误判

缓存击穿

定义:对于设置了过期时间的Key,缓存在某个时间点过期的时候,恰好这个时间点对这个key有大量的并发请求过来,发现缓存过期,就会去数据库加载这些数据并回设到缓存中,这些大量的并发请求可能会瞬间压垮数据库。

解决方案

  1. 互斥锁方案:当缓存失效的时候,不会立即去访问数据库,而是使用Redis的Setnx命令添加一个互斥锁,确保只有一个线程去查询数据库,其它线程等待加载完之后再从缓存中获取
    • 优点:数据强一致性
    • 缺点:性能低,有可能产生死锁的问题
  2. 逻辑过期方案:设置当前key的逻辑过期(高可用,性能高,但是数据同步做不到强一致性)
    • 在设置key的时候,设置一个过期时间字段一起存入缓存,不对key设置过期时间
    • 当查询的时候,从Redis取出数据后判断是否过期
    • 如果过期,就开启另外一个线程进行数据同步,当前线程返回正常数据(这个数据可能不是最新的)

缓存雪崩

定义:在设置缓存的时候采用了相同的过期时间,导致某一时刻缓存全部失效,就全部去访问数据库,导致数据库压力增大而雪崩。

解决方案:在原有的失效时间基础上设置一个随机值,这样每个缓存的过期时间重复率降低,就很难引发集体失效的问题。

数据一致性

Redis作为缓存,MySQL的数据如何与Redis进行同步

数据不一致可能是因为:更新延迟、缓存失效、并发冲突、系统异常而导致的数据不一致。

解决方案

  1. 旁路缓存模式

    • 读操作:先查询缓存,如果缓存命中,则返回数据;如果缓存未命中,则从数据库读取数据,并将数据写入缓存
    • 写操作:先更新数据库,再删除缓存,确保缓存中的数据无效化
  2. 双写一致:写操作同时更新数据库和缓存,读操作仅从缓存中获取数据

  3. 延时双删策略:写删除缓存中的数据,然后更新数据库,经过一段时间再去删除缓存中的数据

  4. Binlog同步:数据库更新时产生Binlog,同步工具Canal实时解析BinLog将变更事件通知给缓存服务,缓存服务根据BinLog更新或删除缓存

延时双删

延时双删就是先删除缓存中的数据,然后更新数据库,最后延时删除缓存中的数据。这个问题就是我们不知道延时多久,在这个过程中可能会出现脏数据,无法保证数据的强制一致性。

先写数据库和先写缓存的问题

  • 先写缓存再写数据库:假如在写数据库的时候失败了,缓存更新了,数据库是旧值,后续都命中缓存了,读到都是脏数据
  • 先写数据库再写缓存:会有并发安全性的问题,比如线程1更新缓存100-200,线程2也过来了更新缓存为300,并强行修改数据库为300,后续线程1再更新数据库为200,那现在缓存是300,数据库是200

缓存一致性方案选择

  • 如果业务对于并发情况数据一致性要求并没有那么高,旁路缓存延迟双删就够用了
  • 如果要保证强一致性,可以使用分布式锁,在读写操作之前必须要获取到锁才能操作
  • 最终一致性方案:使用阿里的Canal,订阅数据库的Binlog日志,优点是解耦了,缺点增加了运维成本

数据持久化

Redis数据持久化方式

Redis数据持久化有两种方式:RDBAOF

RDB(快照文件):将redis内存存储的数据保存到磁盘上,当redis宕机后,就会从RDB快照文件中恢复数据。因为是二进制文件,保存的时候体积比较小,所以恢复数据比较快,但是可能会导致数据的丢失。

AOF(追加文件):每次执行命令的时候会将命令追加到文件中,当redis宕机需要恢复数据,就会去文件中再次执行这个命令来恢复数据。虽然恢复的慢,但数据丢失的风险小很多。在AOF文件中可以设置刷盘策略,每秒批量写入一次命令。

混合持久化(Redis 4.0+):融合RDB的"快速恢复"和AOF的"数据完整"优势,将全量数据用RDB以二进制存储,增量数据用AOF,以后统一放入AOF文件中,恢复先加载二进制的RDB再加载AOF。

过期策略和淘汰策略

Redis的数据过期策略

Redis的过期策略有两种方式:

  1. 惰性删除:在设置key的过期时间之后,不去管它,等到下一次再使用这个数据的时候,会判断这个key是否过期,如果过期就删除

  2. 定期删除:定期检查key是否过期,过期就删除。定时删除又分为两种模式:

    • Slow模式:定时任务,执行频率是10hz,每次不超过25ms
    • Fast模式:执行频率不固定,每次事件循环尝试执行,间隔不低于2ms,不超过1ms

Redis过期删除策略是:惰性删除+定期删除配合使用

Redis的数据淘汰策略

Redis数据淘汰策略有8种,分为不进行数据淘汰的策略进行数据淘汰的策略

不进行数据淘汰的策略

  • noeviction:redis默认的淘汰策略,当使用内存达到上限的时候,不淘汰数据,这时如果有新的数据写入,则会禁止写入。如果是单纯的查询或删除,则可以正常工作

进行数据淘汰的策略

在设置了过期时间的数据中淘汰:

  • volatile-random:随机淘汰设置了过期时间的任意键值
  • volatile-ttl:优先淘汰更早过期的键值
  • volatile-lru:淘汰设置了过期时间最久未使用的键值
  • volatile-lfu:淘汰设置了过期时间最少使用的键值

所有数据范围内淘汰:

  • allkeys-random:随机淘汰任意键值
  • allkeys-lru:淘汰整个键值中最久未使用的键值
  • allkeys-lfu:淘汰整个键值中最少使用的键值

热点数据缓存策略

当数据库有1000w数据,Redis只能缓存20条时,如何保证Redis都是热点数据?可以采用allkeys-lru数据淘汰策略进行删除,挑选最近最少访问的数据删除,留下的就都是热点数据了。

分布式锁

Redis分布式锁实现

在Redis中提供了一个命令叫做SETNX(SET if not exists)。由于Redis是单线程的,用了这个命令之后,只能有一个客户端对某一个key设置值,当这个key没有过期或者被删除,其它客户端是不能对这个key进行操作的。

Redisson分布式锁原理

Redisson 分布式锁是基于 Redis 实现的高性能、高可用分布式锁方案,核心原理可概括为:利用 Redis 的单线程特性 + 原子命令 + Lua 脚本保证锁的安全性,通过可重入设计、自动续期、集群容错等机制解决分布式场景下的各种问题。

锁的有效时长控制 - 看门狗机制

通过Redisson框架来实现,需要手动添加锁,并且能够控制锁的失效时间和控制时间。当一个业务还没有执行完之前,Redisson会引入一个看门狗机制,每间隔一段时间就会判断当前业务是否还持有锁,如果有,就会增加锁的持有时间。当业务执行完之后,需要使用释放锁就可以了。通过事件通知机制,能够在锁释放时就立即响应。

分布式锁的可重入性

Redis实现的分布式锁是支持可重入的,这样做是为了避免死锁。其内部实现原理是:判断是否是当前线程持有的锁,如果是就会进行计数;释放锁时会在计数上减一。在存储数据的时候采用的是hash结构,大key可以根据业务逻辑指定,其中小key是当前线程的唯一标识,value是当前线程重入的次数。

主从复制和哨兵模式

Redis的集群方案

Redis集群有三种方式:

  1. 主从复制(一主多从)
  2. 哨兵模式
  3. Redis分片集群

主从同步

Redis单节点的并发能力是有上限的,所以为了进一步提高Redis的并发能力,我们可以搭建主从集群,实现读写分离,一般是一主多从。主节点负责写数据,从节点负责读数据。当主节点写入数据之后,就需要把数据同步到从节点。

主从同步流程

主从同步分为两个阶段:全量同步增量同步

  1. 全量同步:当从节点首次连接主节点,或者从节点因为某些原因导致数据丢失,就会执行SYNC命令实现一次全量同步。具体流程:首先从节点会执行SYNC命令,向主节点发送请求开始同步。主节点接收到SYNC命令之后,会执行bgSave命令生成一个RDB快照文件,然后将RDB快照文件发送给从节点,从节点会清空之前的数据集,载入RDB快照文件中的数据。在RDB文件生成和发送期间,主服务器会记录所有接收到的写命令到replication backlog buff,一旦RDB文件传输完成,就会将里面的命令发送给从服务器,从服务器执行这些命令,来保证数据的一致性。

  2. 增量同步:当从节点服务重启之后,数据与主节点不一致了,这个时候就会请求主节点同步数据,判断是不是第一次同步,如果不是就会获取从节点的offset值,然后主节点从命令日志中获取offset值之后的数据,发送给从节点进行数据同步。

哨兵模式

哨兵模式可以实现主从集群的故障恢复,里面包含了对主从服务的监控、自动故障恢复、通知。当master遇到故障,Sentinel就会选举一个slave作为新的master,并将新的master相关信息通知给slave和客户端。

哨兵机制的选主节点算法

哨兵机制的选主节点算法分为三个步骤:

  1. 故障节点主观下线:Sentinel集群的每一个Sentinel节点都会对Redis集群中的所有节点发心跳包检测该节点是否正常,如果没有在规定时间回复Sentinel节点的心跳包,那么该Redis节点就被该Sentinel认为是主观下线

  2. 故障节点客观下线:当一个Sentinel节点认为该Redis节点主观下线后,该Sentinel节点会去询问其它Sentinel节点,如果超过一定数量的Sentinel节点认为该Redis节点主观下线,则该Redis节点被标记为客观下线

  3. Sentinel集群选举Leader:如果需要从Redis集群选举一个节点作为主节点,首先需要从Sentinel集群选举一个Sentinel节点作为Leader。每一个Sentinel节点都可以成为Leader,当一个Sentinel节点确认Redis集群的主节点主观下线后,会请求其他Sentinel节点要求将自己选举为Leader。被请求的Sentinel节点如果没有同意过其他Sentinel节点的选举请求,则同意该请求(选举票数+1),否则不同意。如果一个Sentinel节点获得的选举票数达到Leader最低票数(quorum和Sentinel节点数/2+1的最大值),则该Sentinel节点被选举为Leader;否则重新进行选举

  4. Sentinel Leader决定新主节点:当Sentinel集群选举出的Sentinel节点作为Leader后,由该Leader从Redis中选举一个节点作为master节点,选择标准如下:

    • 过滤故障的节点
    • 选择优先级slave-priority最大的从节点作为主节点
    • 选择复制偏移量最大的从节点作为主节点
    • 选择runid最小的从节点作为主节点

集群脑裂问题

定义:Redis的集群脑裂是因为网络等原因出现的问题,导致Redis的主节点和从节点还有Sentinel处于不同的网络分区。使得哨兵没有能够心跳感知到主节点,所以会通过选举的方式选举一个slave作为新的主节点。这样就存在两个master,就像大脑分裂一样。这样会导致客户端在老的主节点那里写入数据,新主节点无法同步数据,当网络恢复,Sentinel会将老的主节点降为从节点,这时候再从新的主节点那里同步数据,会导致老的主节点丢失大量的数据。

解决方案:可以在Redis配置中设置:

  1. 设置最少的slave节点个数。比如最少有一个从节点才会写入数据
  2. 设置主从复制和同步的延时时间,如果达不到要求就不会写入数据,就避免了大量的数据丢失

分片集群

Redis分片集群的作用

分片集群主要解决的是海量数据的存储问题。在集群中有多个master,每个master保存的是不同的数据。每个master可以设置多个slave节点。这样又增大了集群的高并发能力。同时每个master之间都通过ping来监测彼此的心跳。

分片集群的数据存储和读取

Redis集群引入了哈希槽概念,有16384个哈希槽。集群中的主节点都绑定了一定范围的哈希槽范围。key通过CRC16校验之后对16384进行取模计算出存储的槽位,然后根据槽位找到存储在哪个节点。取值的逻辑也是一样的。

hash槽说明

  • 16384个槽对应16384个二进制位(bit),刚好是2048字节(2KB)(16384 ÷ 8 = 2048)
  • 16384个槽的设计是为了平衡内存占用和槽位管理的效率