Halo

A magic place for coding

0%

实习面经 --Redis 基础

面试总结之 Redis 基础

  Redis 是一个高速缓存数据库,可以在后端配合数据库使用,提高服务的性能,是后端开发面试常考的知识,这里就为大家总结一下常见的考点。总结的部分是结合了教科书、其他面经以及个人的面试经历,相信能够帮助到大家。如果有不足的地方,欢迎大家指出!

   本博客长期更新,欢迎大家收藏、订阅!


1.redis 是什么

   简单来说,redis 是一个 ** 非关系型数据库 ,是一个基于 **key-value 的数据结构服务器。它主要是在内存中操作数据,因此我们常常说 **redis 缓存 **。(如果你还不了解 ** 关系型数据库 ** 和 ** 非关系型数据库 ** 的区别,请跳转到 浅谈关系型数据库与非关系型数据库

2.redis 数据类型

redis 支持物种数据类型,分别是:

  • 字符串(String):二进制安全,可以包含任何数据,比如 jpg 图片或者序列化后的对象(bytes 数组)
  • 哈希(Hash):相对于将某个对象的每个字段都存储成单个 string 类型,将这个对象直接存成 hash 类型会占用更少的内存。因为刚开始新建一个 hash 类型的时候用的是 zipmap,zipmap 是连续的 key-value 集合,它的特点是维护 map 需要的额外信息很少,代价是时间复杂度是 $O (N)$,但是对于一般的对象,包含的字段是比较少的,因此查询的时间会很短,所以在查找时间几乎一样的情况下,hash 类型占用的内存比 string 更少。
  • 列表(List):双端链表实现
  • 集合(Set):set 是 ** 无序集合 ,基于 hash table 实现,所以 insertdeletesearch 的时间复杂度都是 $O (1)$。但是 hash table 会随着添加或删除自动调整大小,调整过程中的同步操作(获取写锁)会阻塞其他的读写操作。set 支持取 ** 并集(union)、交集(intersecton)、差集(difference),这些操作可以很容易地实现 SNS 中的好友推荐和 blog 的 tag 功能。
  • 有序集合(Sorted Set):有序集合可以理解是在 set 的基础上增加了一个顺序属性(权重),这一属性在添加或修改元素的时候可以指定,每次制定后,set 会重新排序。

3. redis 分布式锁

工作原理是:先拿 setnx 来争抢锁,抢到后再用 expire 给锁加一个过期时间,防止锁忘记释放。

如果在拿到锁之后,在 expire 之前遇到了 crash,这个锁就永远得不到释放了,我们可以通过将 setnxexpire 合并成一条指令使用,set 提供了这个操作。


4. 在海量数据中找出已知前缀的 key

有两个方法:

  • keys 指令:单线程,会导致县城阻塞一定的时间,线上服务会停顿,直到命令执行完毕;
  • scan 指令:无阻塞地提取出指定模式的 key 列表,会有一定的重复概率。我们可以在客户端做一次去重,获得 key 列表,但是这个整体花费的时间比用 key 指令要长得多。

5. 队列

使用 list 结构作为底层,使用 rpush () 生产消息,使用 lpop () 消费消息。当调用 lpop () 时如果队列为空,将会 sleep () 后再重试。也可以使用 blpop (),阻塞直到队列中有消息可以消费。

满足 ** 生产一次,消费多次 ** 的需求,可使用 **pub/sub 主题订阅者模式 **。缺点是,消费者下线时,生产的消息会丢失。解决这个问题要使用专业的队列,如 rabbitmq。

延时队列的实现使用 sortedset,时间作为 score,消息作为 key,生产者调用 zadd 生产消息,消费者使用 zrangebyscore 来获取一定时间内的数据进行处理。


6. 大量 key 设置同一个时间过期

如果大量的 key 都是同一个时间过期的话,redis 可能会在该时间点卡顿。解决方法是:在时间上增加一个随机值,使过期时间分散开。


7.redis 持久化

为什么要持久化

前面提到,redis 通常是跑在内存的数据库,优点是速度非常快,缺点就是不稳定,只要服务升级、机器停电或其他故障,那么内存的数据就会丢失。我们需要数据持久化功能将内存中的数据持久化到磁盘。

方式 1: RDB Snapshotting(default)

简单来说就是将内存中的数据以快照形式一次性写入到二进制文件中。通常使用 savebgsave 命令。

  • save 操作是在 master 线程中保存快照,因为 redis 也是用 master 来处理客户端请求的,所以在保存期间会 ** 阻塞所有的客户端请求 **。
  • bgsave 则是子进程进行,不阻塞 master 线程。
    • fork:redis 通过创建子进程进行 bgsave 操作
    • cow:Copy On Write。父子进程共享数据段。父进程提供读写服务给客户端,写脏的页面数据会通过复制逐渐和子进程分离开来。
  • 这种方式为镜像全量持久化,缺点是耗时长,因此只能隔一定时间做一次,而且当停机时会丢失丢失最后一次快照后的所有数据。

方式 2: AOF

AOF 增量持久化的方式是 redis 将每一个收到的写命令都通过 write 函数追加到文件中。当 redis 重启时会通过重新执行文件中保存的写命令来在内存中重建整个数据库。值得一提的是,这个方法仍然是有可能丢失部分修改的,因为通过 write 函数写入文件的操作会被操作系统的内核缓存,如果在这段时间内系统故障,那这个期间的 write 的修改就会丢失。为了解决这个问题,我们可以通过配置文件告诉 redis 我们想要通过 fsync 函数强制 OS 写入磁盘的时间,有三种写入的方式:

  • always:每次收到 write 命令就强制写入磁盘,速度比较慢,但能保证完全持久化。
  • everysec:每一秒强制写入一次,是性能与持久化的折中。(** 推荐 **)
  • no:完全依赖操作系统,速度最快,但持久化没有保证。

小结

所以,redis 整个的重启策略可以是两种方法的结合:redis 实例重启后,使用 bgsave 持久化文件重建构建内存,再使用 aof 重放近期的操作指令来恢复重启之间的状态。


8.pipeline 操作

如果指令之间没有因果关系,可以实现 pipeline。将多次 IO 往返时间缩短为 1 次。


9. redis 虚拟内存

redis 虚拟内存和操作系统的虚拟内存是两个概念,但是其目的与思路是相似的 —— 把访问频率少的数据从内存中交换到磁盘。redis 作为一个运行在内存中的数据库,空间是非常有限的。为了解决这个问题,除了部署多个 redis 服务器外,我们还能够使用虚拟内存的技术提高数据库的使用效率。这对于 SNS 类型的业务而言更是如此,因为 SNS 数据具有很强的集中性,比如网页中总是只有一部分人活跃。

值得注意的是,redis 没有使用操作系统提供的虚拟内存机制,而是自己在用户态实现了虚拟内存。主要出于以下三个原因:

  1. 页的大小不在一个数量级。对于操作系统来说,一个交换页的单位是 4K,而 redis 大多数对象都远小于 4K,所以一个操作系统页上就会有多个 redis 对象。因此如果只有 10% 的 key 是被经常访问的,而这 10% 的 key 又均匀分布在每个操作系统页上,那么系统会认为所有的页都是活跃的,此时只有当内存真正耗尽的时候 OS 才会进行换页,这就起不到虚拟内存的效果了。
  2. redis 的对象可以进行压缩,去除指针和元数据信息,一般压缩后的对象会比内存中的对象小 10 倍,这样 redis 的虚拟内存就比 OS 中的虚拟内存少做很多 IO 操作了。

10.redis 数据淘汰策略

  1. volatile-LRU:在 ** 已设置过期时间的数据集 ** 中选 ** 最近最少使用的数据 ** 进行淘汰。
  2. volatile-TTL:在 ** 已设置过期时间的数据集 ** 中选 ** 将要过期的数据 ** 进行淘汰。
  3. volatile-random:在 ** 已设置过期时间的数据集 ** 中 ** 随机选取数据 ** 进行淘汰。
  4. allkeys-LRU:在 ** 全数据集 ** 中选 ** 最近最少使用的数据 ** 进行淘汰。
  5. allkeys-random:在 ** 全数据集 ** 中 ** 随机选取数据数据 ** 进行淘汰。
  6. no-enviction:禁止淘汰数据。

Reference

  1. Redis 面试题刁难大全
  2. Redis 面经总结
  3. Redis 跳跃表
  4. Redis zipmap

Welcome to my other publishing channels