Redis面试高频

Redis面试高频问题

Redis 是一种基于内存的数据库,对数据的读写操作都是在内存中完成,因此读写速度非常快,常用于缓存,消息队列、分布式锁等场景。Redis 还支持事务 、持久化、Lua 脚本、多种集群方案(主从复制模式、哨兵模式、切片机群模式)、发布/订阅模式,内存淘汰机制、过期删除机制等等。

11.1 redis缓存雪崩、缓存击穿、缓存穿透

在这里插入图片描述
(图片来源:小林coding公众号)

缓存雪崩原因
  • 大量数据同时过期;
  • Redis 故障宕机;

缓存雪崩处理方式

  • 均匀设置过期时间;
  • 互斥锁;
  • 双 key 策略;
  • 后台更新缓存;
  • 服务熔断或请求限流机制;
  • 构建 Redis 缓存高可靠集群;
缓存击穿原因

某个热点数据过期

缓存击穿处理方式

  • 互斥锁方案,保证同一时间只有一个业务线程更新缓存,未能获取互斥锁的请求,要么等待锁释放后重新读取缓存,要么就返回空值或者默认值。
  • 不给热点数据设置过期时间,由后台异步更新缓存,或者在热点数据准备要过期前,提前通知后台线程更新缓存以及重新设置过期时间;
缓存穿透原因

当用户访问的数据,既不在缓存中,也不在数据库中,导致请求在访问缓存时,发现缓存缺失,再去访问数据库时,发现数据库中也没有要访问的数据,没办法构建缓存数据,来服务后续的请求。那么当有大量这样的请求到来时,数据库的压力骤增,这就是缓存穿透的问题。

缓存穿透解决方式

第一种方案,非法请求的限制

第二种方案,缓存空值或者默认值

第三种方案,使用布隆过滤器快速判断数据是否存在,避免通过查询数据库来判断数据是否存在。

内存淘汰策略和过期删除策略
  • 如何设置过期时间

    • expire <key> <n>:设置 key 在 n 秒后过期
    • pexpire <key> <n>:设置 key 在 n 毫秒后过期
    • expireat <key> <n>:设置 key 在某个时间戳(精确到秒)之后过期
    • pexpireat <key> <n>:设置 key 在某个时间戳(精确到毫秒)之后过期
    • set <key> <value> ex <n> :设置键值对的时候,同时指定过期时间(精确到秒);
    • set <key> <value> px <n> :设置键值对的时候,同时指定过期时间(精确到毫秒);
    • setex <key> <n> <valule> :设置键值对的时候,同时指定过期时间(精确到秒)。
  • 如何判断key过期?

    key 设置了过期时间时,Redis 会把该 key 带上过期时间存储到一个过期字典(expires dict)中。过期字典是Hash结构

    • 过期字典的 key 是一个指针,指向某个键对象;
    • 过期字典的 value 是一个 long long 类型的整数,这个整数保存了 key 的过期时间;
    • 查询时如果key不在,则正常读取键值;
    • 如果key存在,则会获取该 key 的过期时间,然后与当前系统时间进行比对,如果比系统时间大,那就没有过期,否则判定该 key 已过期。

    过期删除策略

    • 定时删除;(内存友好,cpu不友好)在设置 key 的过期时间时,同时创建一个定时事件,当时间到达时,由事件处理器自动执行 key 的删除操作。

    • 惰性删除;(内存不友好,cpu友好)不主动删除过期键,每次从数据库访问 key 时,都检测 key 是否过期,如果过期则删除该 key。

    • 定期删除;每隔一段时间「随机」从数据库中取出一定数量的 key 进行检查,并删除其中的过期key。

      Redis 选择「惰性删除+定期删除」这两种策略配和使用

      1. 从过期字典中随机抽取 20 个 key;
      2. 检查这 20 个 key 是否过期,并删除已过期的 key;
      3. 如果本轮检查的已过期 key 的数量,超过 5 个(20/4),也就是「已过期 key 的数量」占比「随机抽取 key 的数量」大于 25%,则继续重复步骤 1;如果已过期的 key 比例小于 25%,则停止继续删除过期 key,然后等待下一轮再检查。

    内存淘汰策略

    不进行数据淘汰类和进行数据淘汰类(对所有key进行淘汰和对过期key进行淘汰)

    1、不进行数据淘汰的策略

    noeviction :它表示当运行内存超过最大设置内存时,不淘汰任何数据,而是不再提供服务,直接返回错误。

    2、进行数据淘汰的策略

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

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

    在所有数据范围内进行淘汰:

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

常见的缓存更新策略共有3种:

  • Cache Aside(旁路缓存)策略;

  • Read/Write Through(读穿 / 写穿)策略;

  • Write Back(写回)策略;

    Redis 和 MySQL 的更新策略用的是 Cache Aside,另外两种策略应用不了

    写策略的步骤:

    • 先更新数据库中的数据,再删除缓存中的数据。不能反过来原因是在「读+写」并发的时候,会出现缓存和数据库的数据不一致性的问题。

    读策略的步骤:

    • 如果读取的数据命中了缓存,则直接返回数据;
    • 如果读取的数据没有命中缓存,则从数据库中读取数据,然后将数据写入到缓存,并且返回给用户。

    Read/Write Through(读穿 / 写穿)策略

    Read Through 策略

    先查询缓存中数据是否存在,如果存在则直接返回,如果不存在,则由缓存组件负责从数据库查询数据,并将结果写入到缓存组件,最后缓存组件将数据返回给应用。

    Write Through 策略

    当有数据更新的时候,先查询要写入的数据在缓存中是否已经存在:

    • 如果缓存中数据已经存在,则更新缓存中的数据,并且由缓存组件同步更新到数据库中,然后缓存组件告知应用程序更新完成。

    • 如果缓存中数据不存在,直接更新数据库,然后返回;

    Write Back(写回)策略

    Write Back(写回)策略在更新数据的时候,只更新缓存,同时将缓存数据设置为脏的,然后立马返回,并不会更新数据库。对于数据库的更新,会通过批量异步更新的方式进行。



11.2 redis持久化方案
AOF日志方式
Redis 每执行一条写操作命令,就把该命令以追加的方式写入到一个文件里,然后重启 Redis 的时候,先去读取这个文件里的命令,并且执行它,**注意只会记录写操作命令,读操作命令是不会被记录的**

在这里插入图片描述

AOF先执行写再写入文件好处:

第一个好处,避免额外的检查开销。(若先写入文件则命令可能存在语法错误)

第二个好处,不会阻塞当前写操作命令的执行,因为当写操作命令执行成功后,才会将命令记录到 AOF 日志。

缺点:

第一个风险,执行写操作命令和记录日志是两个过程,那当 Redis 在还没来得及将命令写入到硬盘时,服务器发生宕机了,这个数据就会有丢失的风险

第二个风险,前面说道,由于写操作命令执行成功后才记录到 AOF 日志,所以不会阻塞当前写操作命令的执行,但是可能会给「下一个」命令带来阻塞风险

AOF三种写回策略:
  • Always,每次写操作命令执行完后,同步将 AOF 日志数据写回硬盘;

  • Everysec,每次写操作命令执行完后,先将命令写入到 AOF 文件的内核缓冲区,然后每隔一秒将缓冲区里的内容写回到硬盘;

  • No,意味着不由 Redis 控制写回硬盘的时机,转交给操作系统控制写回的时机。

在这里插入图片描述

AOF三种写回策略优缺点:

在这里插入图片描述

AOF重写

如果当 AOF 日志文件过大就会带来性能问题,重启 Redis 后,需要读 AOF 文件的内容以恢复数据,如果文件过大,整个恢复的过程就会很慢。所以,Redis 为了避免 AOF 文件越写越大,提供了 AOF 重写机制

重写机制过程:在重写时,读取当前数据库中的所有键值对,然后将每一个键值对用一条命令记录到「新的 AOF 文件」,等到全部记录完后,就将新的 AOF 文件替换掉现有的 AOF 文件。这样一来,一个键值对在重写日志中只用一条命令就行了。

AOF后台重写:重写过程很耗时,所以重写的操作不能放在主进程里。Redis 的重写 AOF 过程是由后台子进程 bgrewriteaof 来完成的

好处:

  • 子进程进行 AOF 重写期间,主进程可以继续处理命令请求,从而避免阻塞主进程;
  • 子进程带有主进程的数据副本(数据副本怎么产生的后面会说),这里使用子进程而不是线程,因为如果是使用线程,多线程之间会共享内存,那么在修改共享内存数据的时候,需要通过加锁来保证数据的安全,而这样就会降低性能。而使用子进程,创建子进程时,父子进程是共享内存数据的,不过这个共享的内存只能以只读的方式,而当父子进程任意一方修改了该共享内存,就会发生「写时复制」,于是父子进程就有了独立的数据副本,就不用加锁来保证数据安全。

重写 AOF 日志过程中,如果主进程修改了已经存在 key-value,此时这个 key-value 数据在子进程的内存数据就跟主进程的内存数据不一致了,这时要怎么办呢?

Redis 设置了一个 AOF 重写缓冲区,这个缓冲区在创建 bgrewriteaof 子进程之后开始使用。

在重写 AOF 期间,当 Redis 执行完一个写命令之后,它会同时将这个写命令写入到 「AOF 缓冲区」和 「AOF 重写缓冲区」

也就是说,在 bgrewriteaof 子进程执行 AOF 重写期间,主进程需要执行以下三个工作:

  • 执行客户端发来的命令;
  • 将执行后的写命令追加到 「AOF 缓冲区」;
  • 将执行后的写命令追加到 「AOF 重写缓冲区」;

当子进程完成 AOF 重写工作后,会向主进程发送一条信号,信号是进程间通讯的一种方式,且是异步的。

主进程收到该信号后,会调用一个信号处理函数,该函数主要做以下工作:

  • 将 AOF 重写缓冲区中的所有内容追加到新的 AOF 的文件中,使得新旧两个 AOF 文件所保存的数据库状态一致;
  • 新的 AOF 的文件进行改名,覆盖现有的 AOF 文件。

信号函数执行完后,主进程就可以继续像往常一样处理命令了。

在这里插入图片描述


RDB快照方式

RDB 快照就是记录某一个瞬间的内存数据,记录的是实际数据,而 AOF 文件记录的是命令操作的日志,而不是实际的数据。Redis 的快照是全量快照,也就是说每次执行快照,都是把内存中的「所有数据」都记录到磁盘中。

RDB两种使用方式:
  • 执行了 save 命令,就会在主线程生成 RDB 文件,由于和执行操作命令在同一个线程,所以如果写入 RDB 文件的时间太长,会阻塞主线程
  • 执行了 bgsava 命令,会创建一个子进程来生成 RDB 文件,这样可以避免主线程的阻塞默认
RDB发生时,主进程可以进行数据修改

如果主线程要修改共享数据里的某一块数据(比如键值对 A)时,就会发生写时复制,于是这块数据的物理内存就会被复制一份(键值对 A’),然后主线程在这个数据副本(键值对 A’)进行修改操作。与此同时,bgsave 子进程可以继续把原来的数据(键值对 A)写入到 RDB 文件

在这里插入图片描述

RDB使用save方式时机

那么当 Redis 内存数据高达几十 G,甚至上百 G 的时候,如果用 bgsave 进行 RDB 快照的话,在创建子进程的时候,会因为复制太大的页表而导致 Redis 阻塞在 fork() 函数,主线程无法继续执行,相当于停顿了。所以针对这种情况建议用 sava。


混合持久化方式

当开启了混合持久化时,在 AOF 重写日志时,fork 出来的重写子进程会先将与主线程共享的内存数据以 RDB 方式写入到 AOF 文件,然后主线程处理的操作命令会被记录在重写缓冲区里,重写缓冲区里的增量命令会以 AOF 方式写入到 AOF 文件写入完成后通知主进程将新的含有 RDB 格式和 AOF 格式的 AOF 文件替换旧的的 AOF 文件。

也就是说,使用了混合持久化,AOF 文件的前半部分是 RDB 格式的全量数据,后半部分是 AOF 格式的增量数据


11.3 redis集群搭建(主从模式)

在这里插入图片描述

主从复制

保证多台服务器之间数据一致性

主服务器可以进行读写操作,当发生写操作时自动将写操作同步给从服务器,而从服务器一般是只读,并接受主服务器同步过来写操作命令,然后执行这条命令

在这里插入图片描述

主从复制流程

主从服务器间的第一次同步的过程可分为三个阶段:(主从服务器建立长链接)

  • 第一阶段是建立链接、协商同步;
  • 第二阶段是主服务器同步数据给从服务器;(首次同步采用全量复制
  • 第三阶段是主服务器发送新写操作命令给从服务器。

在这里插入图片描述


主从复制之增量复制

在这里插入图片描述

哨兵

在这里插入图片描述

哨兵机制产生的原因

在 Redis 的主从架构中,由于主从模式是读写分离的,如果主节点(master)挂了,那么将没有主节点来服务客户端的写操作请求,也没有主节点给从节点(slave)进行数据同步了。

哨兵(Sentinel)机制,它的作用是实现主从节点故障转移。它会监测主节点是否存活,如果发现主节点挂了,它就会选举一个从节点切换为主节点,并且把新主节点的相关信息通知给从节点和客户端。
在这里插入图片描述

哨兵机制工作原理

哨兵节点主要负责三件事情:监控、选主、通知

监控

  1. 哨兵会周期性地给所有主从节点发送 PING 命令,当主从节点收到 PING 命令后,会发送一个响应命令给哨兵,这样就可以判断它们是否在正常运行。如果主节点或者从节点没有在规定的时间内响应哨兵的 PING 命令,哨兵就会将它们标记为「主观下线」。
  2. 当一个哨兵判断主节点为「主观下线」后,就会向其他哨兵发起命令,其他哨兵收到这个命令后,就会根据自身和主节点的网络状况,做出赞成投票或者拒绝投票的响应。
  3. 当这个哨兵的赞同票数达到哨兵配置文件中的 quorum 配置项设定的值后,这时主节点就会被该哨兵标记为「客观下线」。

选主

首先把已经下线的从节点过滤掉,然后把以往网络连接状态不好的从节点也给过滤掉。

如果在 down-after-milliseconds 毫秒内,主从节点都没有通过网络联系上,我们就可以认为主从节点断连了。如果发生断连的次数超过了 10 次,就说明这个从节点的网络状况不好,不适合作为新主节点。

  • 第一轮考察:哨兵首先会根据从节点的优先级来进行排序,优先级越小排名越靠前,
  • 第二轮考察:如果优先级相同,则查看复制的下标,哪个从「主节点」接收的复制数据多,哪个就靠前。
  • 第三轮考察:如果优先级和下标都相同,就选择从节点 ID 较小的那个。

由哪个哨兵进行故障转移

哪个哨兵节点判断主节点为「客观下线」,这个哨兵节点就是候选者,所谓的候选者就是想当 Leader 的哨兵。

候选者会向其他哨兵发送命令并让所有其他哨兵对它进行投票。

那么在投票过程中,任何一个「候选者」,要满足两个条件:

  • 第一,拿到半数以上的赞成票;

  • 第二,拿到的票数同时还需要大于等于哨兵配置文件中的 quorum 值。

在这里插入图片描述

通知通过 Redis 的发布者/订阅者机制来实现

哨兵集群搭建原理

在这里插入图片描述


11.4 redis数据结构类型

在这里插入图片描述

数据结构应用场景:

String:缓存对象、常规计数、分布式锁(SETNX OR SETPX)

在这里插入图片描述

List:消息队列

消息队列在存取消息时,必须要满足三个需求,分别是消息保序、处理重复的消息和保证消息可靠性

List 并不会为每个消息生成 ID 号,所以我们需要自行为每个消息生成一个全局唯一ID

在用 List 做消息队列时,如果生产者消息发送很快,而消费者处理消息的速度比较慢,这就导致 List 中的消息越积越多,给 Redis 的内存带来很大压力

要解决这个问题,就要启动多个消费者程序组成一个消费组,一起分担处理 List 中的消息。但是,List 类型并不支持消费组的实现

**Hash:**缓存对象、购物车以用户 id 为 key,商品 id 为 field,商品数量为 value

**Set: **点赞、共同关注、抽奖活动

**Zset:**排行榜、电话、姓名排序


11.5 redis单线程
Redis 是单线程吗?

Redis 单线程指的是「接收客户端请求->解析请求 ->进行数据读写等操作->发生数据给客户端」这个过程是由一个线程(主线程)来完成的,即「从网络 IO 处理到实际的读写命令处理」都是由单个线程完成的

之所以 Redis 为「关闭文件、AOF 刷盘、释放内存」这些任务创建单独的线程来处理,是因为这些任务的操作都是很耗时的,如果把这些任务都放在主线程来处理,那么 Redis 主线程就很容易发生阻塞,这样就无法处理后续的请求了。

Redis 单线程模式是怎样的?

Redis 初始化的时候,会做下面这几年事情:

  • 首先,调用 epoll_create() 创建一个 epoll 对象和调用 socket() 一个服务端 socket
  • 然后,调用 bind() 绑定端口和调用 listen() 监听该 socket;
  • 然后,将调用 epoll_crt() 将 listen socket 加入到 epoll,同时注册「连接事件」处理函数。

初始化完后,主线程就进入到一个事件循环函数,主要会做以下事情:

  • 首先,先调用处理发送队列函数,看是发送队列里是否有任务,如果有发送任务,则通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发生完,就会注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。
  • 接着,调用 epoll_wait 函数等待事件的到来:
    • 如果是连接事件到来,则会调用连接事件处理函数,该函数会做这些事情:调用 accpet 获取已连接的 socket -> 调用 epoll_ctr 将已连接的 socket 加入到 epoll -> 注册「读事件」处理函数;
    • 如果是读事件到来,则会调用读事件处理函数,该函数会做这些事情:调用 read 获取客户端发送的数据 -> 解析命令 -> 处理命令 -> 将客户端对象添加到发送队列 -> 将执行结果写到发送缓存区等待发送;
    • 如果是写事件到来,则会调用写事件处理函数,该函数会做这些事情:通过 write 函数将客户端发送缓存区里的数据发送出去,如果这一轮数据没有发生完,就会继续注册写事件处理函数,等待 epoll_wait 发现可写后再处理 。

在这里插入图片描述

Redis 采用单线程为什么还这么快?

Redis 的大部分操作都在内存中完成,并且采用了高效的数据结构

Redis 采用单线程模型可以避免了多线程之间的竞争,省去了多线程切换带来的时间和性能上的开销,而且也不会导致死锁问题。

Redis 采用了 I/O 多路复用机制处理大量的客户端 Socket 请求,IO 多路复用机制是指一个线程处理多个 IO 流,就是我们经常听到的 select/epoll 机制。实现了一个 Redis 线程处理多个 IO 流的效果。


11.6 redis分布式锁

Redis 的 SET 命令有个 NX 参数可以实现「key不存在才插入」,所以可以用它来实现分布式锁:

  • 如果 key 不存在,则显示插入成功,可以用来表示加锁成功;
  • 如果 key 存在,则会显示插入失败,可以用来表示加锁失败。

set 指令有非常复杂的参数,这个应该是可以同时把 setnx 和expire 合成一条指令来用的!

在这里插入图片描述

基于 Redis 实现分布式锁的优点
  1. 性能高效(这是选择缓存实现分布式锁最核心的出发点)。
  2. 实现方便。很多研发工程师选择使用 Redis 来实现分布式锁,很大成分上是因为 Redis 提供了 setnx 方法,实现分布式锁很方便。
  3. 避免单点故障(因为 Redis 是跨集群部署的,自然就避免了单点故障)。
基于 Redis 实现分布式锁的缺点
  • 超时时间不好设置
  • Redis 主从复制模式中的数据是异步复制的,这样导致分布式锁的不可靠性。(红锁让客户端和多个独立的 Redis 节点依次请求申请加锁,如果客户端能够和半数以上的节点成功地完成加锁操作,那么我们就认为,客户端成功地获得分布式锁,否则加锁失败。**)
16.7 redis作异步队列

在这里插入图片描述

  • 0
    点赞
  • 1
    收藏
    觉得还不错? 一键收藏
  • 1
    评论
评论 1
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值