这里整理一些面试题目,很多已经在前面的文章中详细说明了,这里算是总结一下。也补充了一点新的知识。

Redis有哪些数据结构?

字符串 String、字典 Hash 、列表 List 、集合 Set 、有序集合 SortedSet

如果你是Redis中高级用户,还需要加上下面几种数据结构HyperLogLogGeoPub/Sub

String类型的底层数据结构

Redis 是一个键值对数据库, 数据库的值可以是字符串、集合、列表等多种类型的对象, 而数据库的键则总是字符串对象。

对于那些包含字符串值的字符串对象来说, 每个字符串对象都包含一个 sds 值。

“包含字符串值的字符串对象”,这种说法初听上去可能会有点奇怪, 但是在 Redis 中, 一个字符串对象除了可以保存字符串值之外, 还可以保存 long 类型的值, 所以为了严谨起见, 这里需要强调一下: 当字符串对象保存的是字符串时, 它包含的才是 sds 值, 否则的话, 它就是一个 long 类型的值。

举个例子, 以下命令创建了一个新的数据库键值对, 这个键值对的键和值都是字符串对象, 它们都包含一个 sds 值:

1
2
3
4
5
redis> SET book "Mastering C++ in 21 days"
OK

redis> GET book
"Mastering C++ in 21 days"

目前来说, 只要记住这个事实即可: 在 Redis 中, 客户端传入服务器的协议内容、 aof 缓存、 返回给客户端的回复, 等等, 这些重要的内容都是由 sds 类型来保存的。

在 C 语言中,字符串可以用一个 \0 结尾的 char 数组来表示。

比如说, hello world 在 C 语言中就可以表示为 "hello world\0"

这种简单的字符串表示,在大多数情况下都能满足要求,但是,它并不能高效地支持长度计算和追加(append)这两种操作:

  • 每次计算字符串长度(strlen(s))的复杂度为 θ(N) 。
  • 对字符串进行 N 次追加,必定需要对字符串进行 N 次内存重分配(realloc)。

Redis 内部, 字符串的追加和长度计算很常见, 而 APPENDSTRLEN 更是这两种操作,在 Redis 命令中的直接映射, 这两个简单的操作不应该成为性能的瓶颈。

另外, Redis 除了处理字符串之外, 还需要处理单纯的字节数组, 以及服务器协议等内容, 所以为了方便起见, Redis 的字符串表示还应该是二进制安全的: 程序不应对字符串里面保存的数据做任何假设, 数据可以是以 \0 结尾的 C 字符串, 也可以是单纯的字节数组, 或者其他格式的数据。

考虑到这两个原因, Redis 使用 sds 类型替换了 C 语言的默认字符串表示: sds 既可高效地实现追加和长度计算, 同时是二进制安全的。

在前面的内容中, 我们一直将 sds 作为一种抽象数据结构来说明, 实际上, 它的实现由以下两部分组成:

1
2
3
4
5
6
7
8
9
10
11
12
13
14
typedef char *sds;


struct sdshdr {

// buf 已占用长度
int len;

// buf 剩余可用长度
int free;

// 实际保存字符串数据的地方
char buf[];
};

其中,类型 sdschar * 的别名(alias),而结构 sdshdr 则保存了 lenfreebuf 三个属性。

作为例子,以下是新创建的,同样保存 hello world 字符串的 sdshdr 结构:

1
2
3
4
5
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0"; // buf 的实际长度为 len + 1
};

通过 len 属性, sdshdr 可以实现复杂度为 θ(1) 的长度计算操作。

另一方面, 通过对 buf 分配一些额外的空间, 并使用 free 记录未使用空间的大小, sdshdr 可以让执行追加操作所需的内存重分配次数大大减少。

为了易于理解,我们用一个 Redis 执行实例作为例子,解释一下,当执行以下代码时, Redis 内部发生了什么:

1
2
3
4
5
6
7
8
redis> SET msg "hello world"
OK

redis> APPEND msg " again!"
(integer) 18

redis> GET msg
"hello world again!"

首先, SET 命令创建并保存 hello world 到一个 sdshdr 中,这个 sdshdr 的值如下:

1
2
3
4
5
struct sdshdr {
len = 11;
free = 0;
buf = "hello world\0";
}

当执行 APPEND 命令时,相应的 sdshdr 被更新,字符串 " again!" 会被追加到原来的 “hello world” 之后:

1
2
3
4
5
struct sdshdr {
len = 18;
free = 18;
buf = "hello world again!\0 "; // 空白的地方为预分配空间,共 18 + 18 + 1 个字节
}

在这个例子中, 保存 “hello world again!” 共需要 18 + 1 个字节, 但程序却为我们分配了 18 + 18 + 1 = 37 个字节 —— 这样一来, 如果将来再次对同一个 sdshdr 进行追加操作, 只要追加内容的长度不超过 free 属性的值, 那么就不需要对 buf 进行内存重分配。

这种分配策略会浪费内存吗? 执行过 APPEND 命令的字符串会带有额外的预分配空间, 这些预分配空间不会被释放, 除非该字符串所对应的键被删除, 或者等到关闭 Redis 之后, 再次启动时重新载入的字符串对象将不会有预分配空间。

因为执行 APPEND 命令的字符串键数量通常并不多, 占用内存的体积通常也不大, 所以这一般并不算什么问题。

另一方面, 如果执行 APPEND 操作的键很多, 而字符串的体积又很大的话, 那可能就需要修改 Redis 服务器, 让它定时释放一些字符串键的预分配空间, 从而更有效地使用内存。

当然, sds 也对操作的正确实现提出了要求 —— 所有处理 sdshdr 的函数,都必须正确地更新 lenfree 属性,否则就会造成 bug 。

更多参见:简单动态字符串

从海量数据中查询某一固定前缀的key

使用keys指令可以扫出指定模式的key列表。

对方接着追问:如果这个redis正在给线上的业务提供服务,那使用keys指令会有什么问题?

这个时候你要回答redis关键的一个特性:redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会停顿,直到指令执行完毕,服务才能恢复。这个时候可以使用scan指令,scan指令可以无阻塞的提取出指定模式的key列表,但是会有一定的重复概率,在客户端做一次去重就可以了,但是整体所花费的时间会比直接用keys指令长。

基于游标的迭代器,需要使用上一次游标延续之前的迭代过程。游标为0的时候代表开始或结束。

1
2
3
4
#模式
scan cursor match pattern count
#示例
scan 0 match k* count 10

Redis做异步队列

一般使用list结构作为队列,rpush生产消息,lpop消费消息。当lpop没有消息的时候,要适当sleep一会再重试。

如果对方追问可不可以不用sleep呢?list还有个指令叫blpop,在没有消息的时候,它会阻塞住直到消息到来。

如果对方追问能不能生产一次消费多次呢?使用pub/sub主题订阅者模式,可以实现1:N的消息队列。

如果对方追问pub/sub有什么缺点?在消费者下线的情况下,生产的消息会丢失,得使用专业的消息队列如rabbitmq等。

如果对方追问redis如何实现延时队列?我估计现在你很想把面试官一棒打死如果你手上有一根棒球棍的话,怎么问的这么详细。但是你很克制,然后神态自若的回答道:使用sortedset,拿时间戳作为score,消息内容作为key调用zadd来生产消息,消费者用zrangebyscore指令获取N秒之前的数据轮询进行处理。

如果有大量的key需要设置同一时间过期,一般需要注意什么?

如果大量的key过期时间设置的过于集中,到过期的那个时间点,redis可能会出现短暂的卡顿现象。一般需要在时间上加一个随机值,使得过期时间分散一些。

Redis如何做持久化的?

bgsave做镜像全量持久化,aof做增量持久化。因为bgsave会耗费较长时间,不够实时,在停机的时候会导致大量丢失数据,所以需要aof来配合使用。在redis实例重启时,会使用bgsave持久化文件重新构建内存,再使用aof重放近期的操作指令来实现完整恢复重启之前的状态。

对方追问那如果突然机器掉电会怎样?取决于aof日志sync属性的配置,如果不要求性能,在每条写指令时都sync一下磁盘,就不会丢失数据。但是在高性能的要求下每次都sync是不现实的,一般都使用定时sync,比如1s1次,这个时候最多就会丢失1s的数据。

对方追问bgsave的原理是什么?你给出两个词汇就可以了,fork和cow。fork是指redis通过创建子进程来进行bgsave操作,cow指的是copy on write,子进程创建后,父子进程共享数据段,父进程继续提供读写服务,写脏的页面数据会逐渐和子进程分离开来。

Pipeline有什么好处,为什么要用pipeline?

可以将多次IO往返的时间缩减为一次,前提是pipeline执行的指令之间没有因果相关性。使用redis-benchmark进行压测的时候可以发现影响redis的QPS峰值的一个重要因素是pipeline批次指令的数目。

Redis的同步机制了解么?

Redis可以使用主从同步,从从同步。第一次同步时,主节点做一次bgsave,并同时将后续修改操作记录到内存buffer,待完成后将rdb文件全量同步到复制节点,复制节点接受完成后将rdb镜像加载到内存。加载完成后,再通知主节点将期间修改的操作记录同步到复制节点进行重放就完成了同步过程。

是否使用过Redis集群,集群的原理是什么?

Redis Sentinal着眼于高可用,在master宕机时会自动将slave提升为master,继续提供服务。

Redis Cluster着眼于扩展性,在单个redis内存不足时,使用Cluster进行分片存储。

整理自: