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