Redis面试问题

Redis

1.简介

Redis是一种非关系型(NoSQL)内存键值数据库,可以存储键和五种不同类型的值之间的映射。

键的类型只能为字符串,保证不可变性,值的类型只有五种:字符串、列表、集合、哈希表、有序集合。

Redis面试问题的主要出发点:数据类型、跳表、缓存、持久化、数据淘汰策略。

2.数据类型

数据类型 可以存储的值 操作
STRING 字符串、整数或者浮点数 对整个字符串或者字符串的其中一部分执行操作,对整数和浮点数执行自增或者自减操作
LIST 列表 从两端压入或者弹出元素 ,对单个或者多个元素进行修剪, 只保留一个范围内的元素
SET 无序集合 添加、获取、移除单个元素, 检查一个元素是否存在于集合中, 计算交集、并集、差集, 从集合里面随机获取元素
HASH 包含键值对的无序散列表 添加、获取、移除单个键值对, 获取所有键值对,检查某个键是否存在
ZSET 有序集合 添加、获取、删除元素,根据分值范围或者成员来获取元素, 计算一个键的排名

3.数据结构

字典

字典是HASH的底层结构,是一种散列表结构,使用的是拉链法解决哈希冲突。Redis 的字典 dict 中包含两个哈希表 dictht,这是为了方便进行 rehash 操作。在扩容时,将其中一个 dictht 上的键值对 rehash 到另一个 dictht 上面,完成之后释放空间并交换两个 dictht 的角色。有点类似于CopyandWriteList中扩容的操作。rehash 操作不是一次性完成,而是采用渐进方式,这是为了避免一次性执行过多的 rehash 操作给服务器带来过大的负担。

跳表

是有序集合的底层实现之一。跳跃表是基于多指针有序链表实现的,可以看成多个有序链表。在查找时,从上层指针开始查找,找到对应的区间之后再到下一层去查找。

与红黑树等平衡树相比,跳跃表具有以下优点:

  • 插入速度非常快速,因为不需要进行旋转等操作来维护平衡性;
  • 更容易实现;
  • 支持无锁操作。

4. redis使用场景

使用场景 详细
计数器 可以对 String 进行自增自减运算,从而实现计数器功能。Redis 这种内存型数据库的读写性能非常高,很适合存储频繁读写的计数量。
缓存 一般和关系型数据库进行配合,将热点数据放到内存中,设置内存的最大使用量以及淘汰策略来保证缓存的命中率。
查找表 例如 DNS 记录就很适合使用 Redis 进行存储。查找表和缓存类似,也是利用了 Redis 快速的查找特性。但是查找表的内容不能失效,而缓存的内容可以失效,因为缓存不作为可靠的数据来源
消息队列 List 是一个双向链表,可以通过 lpush 和 rpop 写入和读取消息
会话缓存 可以使用 Redis 来统一存储多台应用服务器的会话信息。
分布式锁 利用SETNX实现分布式锁,或者是ReadLock实现分布式锁。

5.键的过期时间

Redis 可以为每个键设置过期时间,当键过期时,会自动删除该键。

对于散列表这种容器,只能为整个键设置过期时间(整个散列表),而不能为键里面的单个元素设置过期时间。散列表算作值。

6.数据淘汰策略

因为我们的内存是有限的,所以对于部分数据我们总会要实行内存淘汰策略,以免超出内存发生大小。

Redis具体有6中淘汰策略:

策略 描述
volatile-lru 从已设置过期时间的数据集中挑选最近最少使用的数据淘汰
volatile-ttl 从已设置过期时间的数据集中挑选将要过期的数据淘汰
volatile-random 从已设置过期时间的数据集中任意选择数据淘汰
allkeys-lru 从所有数据集中挑选最近最少使用的数据淘汰
allkeys-random 从所有数据集中任意选择数据进行淘汰
noeviction 禁止驱逐数据

使用 Redis 缓存数据时,为了提高缓存命中率,需要保证缓存数据都是热点数据。可以将内存最大使用量设置为热点数据占用的内存量,然后启用 allkeys-lru 淘汰策略,将最近最少使用的数据淘汰。

Redis 4.0 引入了 volatile-lfu 和 allkeys-lfu 淘汰策略,LFU 策略通过统计访问频率,将访问频率最少的键值对淘汰。

一般情况下还是使用前三种最好,因为不设置过期时间可能确实这些数据比较重要。

7.持久化

Redis 是内存型数据库,为了保证数据在断电后不会丢失,需要将内存中的数据持久化到硬盘上。

RDB 持久化

将某个时间点的所有数据都存放到硬盘上。

可以将快照复制到其它服务器从而创建具有相同数据的服务器副本。

如果系统发生故障,将会丢失最后一次创建快照之后的数据。

如果数据量很大,保存快照的时间会很长。

持久化

将写命令添加到 AOF 文件(Append Only File)的末尾。

使用 AOF 持久化需要设置同步选项,从而确保写命令同步到磁盘文件上的时机。这是因为对文件进行写入并不会马上将内容同步到磁盘上,而是先存储到缓冲区,然后由操作系统决定什么时候同步到磁盘。有以下同步选项:

选项 同步频率
always 每个写命令都同步
everysec 每秒同步一次
no 让操作系统来决定何时同步
  • always 选项会严重减低服务器的性能;
  • everysec 选项比较合适,可以保证系统崩溃时只会丢失一秒左右的数据,并且 Redis 每秒执行一次同步对服务器性能几乎没有任何影响;
  • no 选项并不能给服务器性能带来多大的提升,而且也会增加系统崩溃时数据丢失的数量。

随着服务器写请求的增多,AOF 文件会越来越大。Redis 提供了一种将 AOF 重写的特性,能够去除 AOF 文件中的冗余写命令。大致思路是将redis文件中所有的数据作为插入指令重新写入一个文件中,之后将以前文件丢弃即可。以后的命令都写入新的AOF文件。

8.哨兵机制

Sentinel(哨兵)可以监听集群中的服务器,并在主服务器进入下线状态时,自动从从服务器中选举出新的主服务器。

9. 缓存问题

缓存穿透

指的是对某个一定不存在的数据进行请求,该请求将会穿透缓存到达数据库。

解决方案:

  • 对这些不存在的数据缓存一个空数据;
  • 对这类请求进行过滤

缓存雪崩

指的是由于数据没有被加载到缓存中,或者缓存数据在同一时间大面积失效(过期),又或者缓存服务器宕机,导致大量的请求都到达数据库。

在有缓存的系统中,系统非常依赖于缓存,缓存分担了很大一部分的数据请求。当发生缓存雪崩时,数据库无法处理这么大的请求,导致数据库崩溃。

解决方案:

  • 为了防止缓存在同一时间大面积过期导致的缓存雪崩,可以通过观察用户行为,合理设置缓存过期时间来实现;
  • 为了防止缓存服务器宕机出现的缓存雪崩,可以使用分布式缓存,分布式缓存中每一个节点只缓存部分的数据,当某个节点宕机时可以保证其它节点的缓存仍然可用。最好的解决方案。增加了系统的高可用性。
  • 也可以进行缓存预热,避免在系统刚启动不久由于还未将大量数据进行缓存而导致缓存雪崩。

缓存一致性

缓存一致性一般就是redis数据库和mysql数据库之间在更新和删除时候的问题。其实一般情况下设置过期时间,一段时间之后缓存中数据都会消失,但是就怕在过期时间内又缓存更新的情况。

先更新数据库,然后再立即去删除缓存。再读缓存的时候判断缓存是否是最新的,如果不是先进行更新。

每日一题

在一个 2 x 3 的板上(board)有 5 块砖瓦,用数字 1~5 来表示, 以及一块空缺用 0 来表示.

一次移动定义为选择 0 与一个相邻的数字(上下左右)进行交换.

最终当板 board 的结果是 [[1,2,3],[4,5,0]] 谜板被解开。

给出一个谜板的初始状态,返回最少可以通过多少次移动解开谜板,如果不能解开谜板,则返回 -1 。

遇到这种问题基本很多都是广度优先搜索,这些可以将二维数组转换为1维。按照行优先的顺序给2×3 的谜板进行编号。因此,我们在 status 中找出 00 所在的位置 x,对于每一个与 x 相邻的位置 y,我们将 status[x] 与 status[y] 进行交换,即等同于进行了一次操作。就是对位置进行标号,找出位置的相邻关系,然后进行广度优先搜索。

 1
 2
 3
 4
 5
 6
 7
 8
 9
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
44
45
46
47
48
49
var neighbors = [6][]int{{1, 3}, {0, 2, 4}, {1, 5}, {0, 4}, {1, 3, 5}, {2, 4}}
// 这种最少通过多少次变换,很多都是使用广度优先搜索
func slidingPuzzle(board [][]int) int {
    const target = "123450"

    s := make([]byte, 0, 6)
    for _, r := range board {
        for _, v := range r {
            s = append(s, '0'+byte(v))
        }
    }
    start := string(s)
    if start == target {
        return 0
    }

    // 枚举 status 通过一次交换操作得到的状态
    get := func(status string) (ret []string) {
        s := []byte(status)
        x := strings.Index(status, "0")
        for _, y := range neighbors[x] {
            s[x], s[y] = s[y], s[x]
            ret = append(ret, string(s))
            s[x], s[y] = s[y], s[x]
        }
        return
    }

    type pair struct {
        status string
        step   int
    }
    q := []pair{{start, 0}}
    seen := map[string]bool{start: true}
    for len(q) > 0 {
        p := q[0]
        q = q[1:]
        for _, nxt := range get(p.status) {
            if !seen[nxt] {
                if nxt == target {
                    return p.step + 1
                }
                seen[nxt] = true
                q = append(q, pair{nxt, p.step + 1})
            }
        }
    }
    return -1
}

参考资料