redis/redis 面试题

redis/redis 面试题

redis 主要用来实现缓存功能。

缓存的主流应用架构如下:


1. 客户端请求数据,如果数据在缓存层里面,那么直接返回。 2. 如果数据不在缓存层里面,那么就到数据库里查询,这个过程叫做穿透查询。并把数据回种到缓存层。 3. 此外,这种架构还能实现熔断机制:如果我们发现数据库挂了,那么就可以将请求拦在缓存层,无论数据是否在缓存层中,都直接返回。

Mencache 和 Redis 的区别

  • Mencache:代码层次类似 Hash
    • 支持简单数据
    • 不支持数据持久化存储
    • 不支持主从
    • 不支持分片(不支持将大量数据分布到多个物理节点)
  • Redis
    • 数据类型丰富
    • 支持数据磁盘持久化存储
    • 支持主从同步
    • 支持分片

根据业务特性来选择适合自己的缓存:

  • 如果是简单类型,并且不需要持久化,那么可以考虑用 Mencache
  • 否则使用 Redis

为什么 Redis 能这么快

  • 完全基于内存。
  • 数据结构简单,对数据的操作也简单。
  • 采用单线程,主线程负责IO 事件、集群协调、过期键的处理等。除了 IO 事件之外,其他事件会被周期性低执行。由于采用了单线程,多个客户端对同一个键修改,就不会出现并发的问题,避免频繁的上下文切换和锁竞争。再配合 IO 多路复用,单线程可以达到 10万+ 的 QPS。也可以在多核的服务器中启动多个 Redis 实例。这里的单线程是指处理网络请求时只使用一个线程。在持久化等操作中,会启动另外的线程。
  • IO 多路复用(Select 系统调用)。

Redis 采用的 IO 多路复用函数有:epoll、kqueue、evport、select。

  • epoll、kqueue、evport 等的时间复杂度都是 O(1),但这些函数不是每个平台都有。具体来说,会根据不同系统调用不同的函数,优先选择 O(1) 的函数。
  • 如果当前系统没有实现 epoll、kqueue、evport 等函数,那么就会选择时间复杂度为 O(n) 的 select。
  • 这些都是基于 react 的设计模式监听 IO 事件。当 accept、write、close 等事件产生时,会回调 FD 绑定的事件处理器。通过 IO 多路复用就可以监听多个 FD,使得 Redis 高效。

Redis 的数据类型

  • String:最基本的数据类型,是二进制安全的,可以存储任何数据,如图片、序列化的对象。使用简单动态字符串实现
  • Hash:String 元素组成的字段,适合用于存储对象。
  • List:列表,按照 String 元素插入顺序排序。先进后出,类似于栈
  • Set:String 元素组成的无序集合,通过哈希表实现,不允许重复。
  • Sorted Set:通过分数为集合中的成员从小到大排序,使用跳表实现。
  • 用于计数的 HyperLogLog,用于支持存储地理位置信息的 Geo。

从海量 Key 里查询出某一固定前缀的 Key

使用 keys 指令会卡顿,对线上业务造成影响。

1
keys k1*

应该使用 SCAN cursor [MATCH pattern] [COUNT count]。

1
SCAN 0 MATCH k1* COUNT count

会返回不大于 count 数量的数据,已经下一个 cursor。我们在 Java 代码中可以使用 Jedis 库,循环使用 SCAN 取出 某一固定前缀的 Key,注意需要使用 HashSet 存储,因为可能会重复取出数据。

如何通过 Redis 实现分布式锁

  • 分布式锁要解决的问题
    • 互斥性:同一时刻只能有一个客户端获取锁。
    • 安全性:锁只能被持有该锁的客户端删除,不能由其他客户端删除。
    • 死锁:获取锁的客户端因为某些原因宕机,无法释放锁。
    • 容错:当部分 Redis 节点宕机时,需要保证客户端依然能够获取锁。

SETNX 可以用来实现分布式锁,如果设置成功(返回 1),表示获取锁成功;如果设置失败(返回 1),则获取锁失败。

1
setnx locknx test

然后设置过期时间

1
expire locknx 2

但程序可能执行完第一个 setnx 后就宕机了,无法设置过期时间,导致无法释放锁。这是因为两个方法组合起来不满足原子性。

解决方法是使用 SET 命令,在设置 key 的时候同时设置过期时间。

1
SET key value [EX seconds] [PX milliseconds] [NX|XX]
  • NX 表示只有键不存在时,才对键进行设置操作。
  • XX 表示只有键存在时,才对键进行设置操作。
  • SET 操作成功时,返回 OK,否则返回 nil。
1
set locktarget 12345 ex 10 nx

大量的 Key 同时过期的注意事项

如果大量的 Key 同时过期,由于删除大量的 key 很耗时,会出现短暂的卡顿现象。

解决方法是:

  • 在设置 key 的过期时间时,给每个 key 加上随机值

如何使用 Redis 做异步队列

使用 List 作为队列,RPUSH 生产消息,LPOP 消费信息。

  • 缺点:没有等待队列里有值就直接消费。
  • 弥补方案:可以通过在应用层使用 sleep 机制调用 LPOP 重试。

更好的方案是使用 BLPOP [key] timeout 阻塞,直到队列有消息或者超时。

1
BLPOP testlist 10

上面生产出来的胡数据只能给一个消费者消费。

可以使用发布订阅模式来实现一个消息给多个消费者消费。

  • pub 发送消息,sub 接收消息
  • 订阅者可以订阅任意数量的 topic。

但这种方式是有缺点的:消息的发布是无状态的,无法保证可达。

Redis 持久化

stop-writes-on-basave-error 设置为 yes。在备份进程出错时,主进程就停止接收新的写入操作,保护持久化数据一致性的问题。

修改 redis.conf 的 save 配置,就可以修改 RDB 持久化。

一般使用 BGSAVE,它会 fork 出一个子进程来创建 RDB 文件,不阻塞服务器进行。

  • 自动化触发 RDB 持久化的方式

    • 根据 redis.conf 配置里的 SAVE m n 定时触发
    • 主从复制时,主节点自动触发。
    • 执行 Debug Reload
    • 执行 Shutdown 且没有开启 AOF 持久化
  • BGSAVE 的原理

    使用系统调用 fork():创建进行,实现了 Copy on Write。内核只为子进程创建虚拟空间,父子进程使用相同的物理空间。只有发生改变时,才会为子进程分配新的物理空间。

使用 lastsave 可以查看上次成功备份的时间。

  • RDB 持久化
    • 内存数据的全量同步,数据量大会由于 IO 而严重影响性能。
    • 可能会丢死最近一次快照之后的数据
  • AOF 持久化:保存写状态
    • 记录下除了查询以外的所有变更数据库状态的指令
    • 以 append 的形式追加保存到 AOF 文件中
1
config set appendonly yes

AOF 持久化,可以通过日志重写解决 AOF 文件大小不断增大的问题,原理如下:

  • 调用 fork(),创建一个子进程。
  • 子进程把新的 AOF 写到临时文件里,不依赖原来的 AOF 文件
  • 主进程持续将新的变动写到内存和原来的 AOF 里
  • 主进程获取子进程重写 AOF 的完成信号,往新的 AOF 同步增量变动。
  • 使用新的 AOF 文件替换旧的 AOF 文件

AOF 和 RDB 的缺点

  • RDB 优点:全量数据快照,文件小,恢复快。
  • RDB 缺点:无法保存最近一次快照之后的数据。
  • AOF 优点:可读性高,适合保存增量数据,数据不易丢失
  • AOF 缺点:文件体积大,恢复时间长

Redis 4.0 之后,默认使用 RDB + AOF 混合持久化方式。

先以 RDB 格式写入全量数据,再追加增量数据。

BGSAVE 做镜像全量持久化,AOF 做增量持久化。

在恢复时,先恢复 RDB 的全量数据,再使用 AOF 重放历史命令。

Pipeline

使用 Pipeline 批量执行命令,节省多次 IO 往返的时间。

Redis 的同步机制有全同步过程和增量同步过程。

  • 全同步过程
    • Slave 发送 sync 命令道 Master
    • Master 启动一个后台进程,将 Redis 中的数据快照保存到文件中
    • Master 将保存数据快照期间接收到的命令缓存起来
    • Master 完成写文件操作后,将该文件发送给 Salve。
    • 使用新的 AOF 文件替换旧的 AOF 文件
    • Master 将这期间收集的增量写命令发送给 Slave 端
  • 增量同步过程

Redis 通过 哨兵机制来解决主从同步时,Master 宕机后的主从切换问题。

Redis 集群

如何从海量数据里快速找到需要的数据?

  • 分片:按照某种规则区划分数据,分散存储在多个节点上
  • 常规的按照哈希划分(Hash 对节点数量取模)无法实现节点的动态增加和减少

因此使用一致性哈希算法,(IP 地址+ 主机名)对 \(2^{32}\) 取模,将哈希值空间组织成虚拟的圆环。

这种方法只需要重新定位环中一小部分的数据,有比较好的容错性和扩展性,可以做到最小化的有损服务。

但在节点少时,可能会出现数据倾斜问题。引入虚拟节点解决数据倾斜的问题,对每个服务器节点计算多个 hash,映射到多个虚拟节点上。

将虚拟节点设置为 32 或者更大。还可以引入主从同步,哨兵机制来进一步确保高可用性。

总结

  • 主流应用架构
  • Mencache 和 Redis 区别
  • 单线程 + IO 多路复用
  • Redis 常用数据类型
  • 利用 SCAN 在海量数据里筛选某一固定前缀的 key
  • 使用 SETNX 实现简单的分布式锁
  • 使用 List 和 BLPOP 实现异步消息队列
  • 持久化原理
    • 使用 BGSAVE 的 RDB 快照持久化
    • 基于操作指令的 AOF 增量持久化
    • 混合模式
  • Redis 主从原理
  • Redis 哨兵原理
  • Redis 集群原理

评论