Redis面试

1.关于使用String还是Hash

  • String存储的是序列化后的对象数据,存放的是整个对象,而Hash是对对象的每个字段单独存储,可以获取部分字段的信息,也可修改或者添加某些字段,节省网络流量。如果对象中的字段需要经常变动,就用Hash
  • String相对来书更加节省内存,缓存相同数据量,String使用的内存约为Hash的一半,并且存储多层嵌套对象也方便很多。如果对性能和资源消耗十分敏感的话,使用String。
  • 在绝大部分情况下使用String

2.购物车信息用String还是Hash好?

由于购物车信息经常变动,所以我们推荐使用Hash 存储。用户id为key,商品id为Field,商品数量为value

那用户购物车信息的维护具体应该怎么操作呢?

  • 用户添加商品就是往 Hash 里面增加新的 field 与 value;
  • 查询购物车信息就是遍历对应的 Hash;
  • 更改商品数量直接修改对应的 value 值(直接 set 或者做运算皆可);
  • 删除商品就是删除 Hash 中对应的 field;
  • 清空购物车直接删除对应的 key 即可。

3.使用 Redis 实现一个排行榜怎么做?

使用sorted set 相关的redis命令 ZRANGE, ZREVRANGE(降序),ZREVRANK(指定元素排名)

4.使用 Set 实现抽奖系统需要用到什么命令?

  • SPOP key count : 随机移除并获取指定集合中一个或多个元素,适合不允许重复中奖的场景。
  • SRANDMEMBER key count : 随机获取指定集合中指定数量的元素,适合允许重复中奖的场景。

5.Redis 线程模型

对于读写命令来说,redis一直是单线程模型,不过在4.0以后,引入了多线程来执行一些较大的键值对的异步删除操作。6.0后引入了多线程来处理网络请求。

6. 单线程模型:

redis基于reactor模式开发了一套高效的事件处理模型,也就是文件处理器,由于文件处理器是单线程的,所以一般说redis是单线程模型。

6.1 文件事件处理器:

  • 使用I/O多路复用模式,同时监听多个套接字,并根据套接字目前执行的任务来为套接字关联不同的事件处理器。
  • 当被监听的套接字准备好链接应答,读取,写入,关闭等操作后,与操作相应的文件事件就会产生,这时文件事件处理器就会调用套接字之前关联好的事件处理器进行处理这些事件。

简单来说,就是不再去主动等待事件的到来,而是提前注册好对应事件的处理方法,当事件到来时,直接调用准备好的处理方法。

主要包含4个部分

  • 多个客户端: 多个socket
  • io多路复用程序 :支持多个客户端链接
  • 文件事件分派器  将socket关联到相应的事件处理器
  • 事件处理器  链接应答处理器,,命令请求处理器,命令回复处理器。

Redis面试_第1张图片

7.为什么redis之前不使用多线程?

虽然redis'是单线程,但是4.0之后加入了多线程支持,主要是大键值对的删除。

原因有三个:

  1. 单线程编程更容易维护
  2. Redis的性能瓶颈不再CPU,而在网络和内存
  3. 多线程就可能出现死锁,线程上下文切换的问题。甚至会影响性能。

8.  Redis 6.0之后为何引入多线程?

主要是为了提高io读写性能,因为这个算是Redis的性能瓶颈,但是主要就是用在网络数据的读写这种消耗时间的操作上,执行命令仍然是单线程。

多线程默认是禁用的,需要手动再redis.conf文件开启

不建议开启多线程(提升不大)

9. Redis内存管理

9.1 Redis给数据设置过期时间的作用:

避免oom问题,在Redis中除了字符串可以使用setex命令设置过期时间外,其他数据类型都要使用 expire key 60 同时使用 persist 命令可以移除过期时间。

除了用于避免oom,过期时间还会用于 1. 验证码时效,2.登录状态时效。

用传统的数据库处理需要自己判断过期时间,十分不变

9.2 Redis如何判断数据过期?

在redis中,数据通过一个叫做过期字典的东西来存储数据的过期时间,过期字典类似哈希表,键指向Redis中的某个key,而value则是过期时间。

Redis面试_第2张图片

typedef struct redisDb {
    ...

    dict *dict;     //数据库键空间,保存着数据库中所有键值对
    dict *expires   // 过期字典,保存着键的过期时间
    ...
} redisDb;

9.3 过期数据删除策略

1.懒汉式: 只有在加载数据的时候才去判断数据是否过期了。对CPU友好

2.定期删除:每隔固定时间就去判断是否有过期的key需要清除。对内存友好

但是这两者都不能保证一定清除全部的过期key,而redis采用的是二者结合的方式。

解决不能删除干净的办法就是内存淘汰机制

9.4 内存淘汰机制

  1. volatile-lru(least recently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最近最少使用的数据淘汰
  2. volatile-ttl:从已设置过期时间的数据集(server.db[i].expires)中挑选将要过期的数据淘汰
  3. volatile-random:从已设置过期时间的数据集(server.db[i].expires)中任意选择数据淘汰
  4. allkeys-lru(least recently used):当内存不足以容纳新写入数据时,在键空间中,移除最近最少使用的 key(这个是最常用的)
  5. allkeys-random:从数据集(server.db[i].dict)中任意选择数据淘汰
  6. no-eviction:禁止驱逐数据,也就是说当内存不足以容纳新写入数据时,新写入操作会报错。这个应该没人使用吧!

4.0 版本后增加以下两种:

  1. volatile-lfu(least frequently used):从已设置过期时间的数据集(server.db[i].expires)中挑选最不经常使用的数据淘汰
  2. allkeys-lfu(least frequently used):当内存不足以容纳新写入数据时,在键空间中,移除最不经常使用的 key

10 持久化机制

两种,RDB(快照) + AOF(追加文件),redis使用两者混合 

10.1 RDB持久化

Redis可以通过创建快照的方式来获得存储在内存里面的数据在某个时间节点上的副本,redis创建快照后,可以对快照进行备份,将其发送到其他节点上(比如主从架构)。

快照持久化是redis默认开启的配置。

10.2 RDB创建时会阻塞主进程么

Redis提供过了两个方式来创建快照,

        save 同步保存,会阻塞

        bgsave : 会fork出一个子进程,不会阻塞主线程,默认选项

10.3 AOF持久化

目前的主流方案就是AOF,默认关闭,手动开启

开启后,会记录redis执行的每一条指令,类似,MySQL的binlog,在执行每条命令后,都会写入到内存缓存中,然后根据appendfsync配置决定何时将其同步到硬盘上。 fsync想起了mysql的redo log  先读数据进入buffer poll 然后对操作记录到 redo log buffer 然后刷到 page cache中

AOF三种方式

always 每次数据同步都写入AOF,严重降低速度

everysec 每秒写一次(推荐)

no 让操作系统决定什么时候同步


10.4 AOF日志

日志记录的执行时机: 在每次命令执行结束之后

原因:        

  1.   避免额外的检查开销。
  2. 不会阻塞当前的命令

缺点:

  1.         数据丢失风险(执行完命令就宕机)
  2.         阻塞后续程序运行      

10.5 AOF重写

AOF太大了以后,会在后台重写AOF,在重写期间会创建一个缓冲区,这个缓冲区会记录在重写AOF期间的全部Redis的写操作,在AOF重写结束后,将其追加到AOF末尾。

11. 如何选择RDB和AOF

RDB比AOF强的地方:

  1. RDB存储到的文件是二进制压缩的形式,占用的体积小
  2. RDB存储的是快照,恢复数据的速度很快。

AOF比RDB强的地方:

  1. AOF是行级数记录,记录的粒度更细,可以实现秒级记录,同时生成RDB的过程是繁重的,虽然可以采用bgsave的形式,但是对CPU资源仍然会造成巨大的负载。
  2. RDB用二进制文件保存,无法阅读。低版本不能阅读高版本数据。
  3. AOF用一种较容易理解的格式包含所有的操作日志,可以轻松的导出文件并进行分析。

如果把混合持久化打开,AOF 重写的时候就直接把 RDB 的内容写到 AOF 文件开头。这样做的好处是可以结合 RDB 和 AOF 的优点, 快速加载同时避免丢失过多的数据。当然缺点也是有的, AOF 里面的 RDB 部分是压缩格式不再是 AOF 格式,可读性较差。

12. Redis 事务

通过MULTI,EXEC, DISCARD,WATCH命令来实现

使用了MULTI后,可以输入多个命令,Redis不会立即执行这些命令,而是将他们放到队列里,执行了EXEC后,在执行所有的命令。同样,可以通过WATCH监控某个变量,如果这个变量在其他的线程中被修改了,则事务执行失败。

# 客户端 1
SET PROJECT "RustGuide"
OK
WATCH PROJECT
OK
MULTI
OK
SET PROJECT "JavaGuide"
QUEUED

# 客户端 2
# 在客户端 1 执行 EXEC 命令提交事务之前修改 PROJECT 的值
SET PROJECT "GoGuide"

# 客户端 1
# 修改失败,因为 PROJECT 的值被客户端2修改了
EXEC
(nil)
GET PROJECT
"GoGuide"

如果 WATCH 与 事务 在同一个 Session 里,并且被 WATCH 监视的 Key 被修改的操作发生在事务内部,这个事务是可以被执行成功的

SET PROJECT "JavaGuide"
OK
WATCH PROJECT
OK
MULTI
OK
SET PROJECT "JavaGuide1"
QUEUED
SET PROJECT "JavaGuide2"
QUEUED
SET PROJECT "JavaGuide3"
QUEUED
EXEC
1) OK
2) OK
3) OK
127.0.0.1:6379> GET PROJECT
"JavaGuide3"

13. Redis事务是否支持原子性

不支持,甚至不支持持久性

Redis在事务执行出错的情况下,不会回滚,仅仅出错的命令不会执行,剩余命令全部执行。并且不支持回滚。

14. 解决Redis事务缺陷

Lua脚本

Redis 从 2.6 版本开始支持执行 Lua 脚本,它的功能和事务非常类似。我们可以利用 Lua 脚本来批量执行多条 Redis 命令,这些 Redis 命令会被提交到 Redis 服务器一次性执行完成,大幅减小了网络开销。

一段 Lua 脚本可以视作一条命令执行,一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰。

不过,如果 Lua 脚本运行时出错并中途结束,出错之后的命令是不会被执行的。并且,出错之前执行的命令是无法被撤销的,无法实现类似关系型数据库执行失败可以回滚的那种原子性效果。因此, 严格来说的话,通过 Lua 脚本来批量执行 Redis 命令实际也是不完全满足原子性的。

如果想要让 Lua 脚本中的命令全部执行,必须保证语句语法和命令都是对的。

另外,Redis 7.0 新增了 Redis functionsopen in new window 特性,你可以将 Redis functions 看作是比 Lua 更强大的脚本。

15. Redis 性能优化

15.1 使用批处理操作减少网络传输

redis命令可以简化为以下4步

发送命令 命令排队 命令执行 返回结果

rtt为发送命令和返回结果所用的时间。主要是减少这个两个的时间。

15.2 原生批量操作命令:

mget 获取一个或多个key的值

hmget 获取哈希表中的一个或者多个指定字段的值

sadd 想指定集合添加一个或者多个元素

但是在官方提供的分片集群解决方案下,使用这些原生命令,存在一些问题。

比如mget 无法保证所有的key都在一个哈希槽上,mget可能需要经过多次网络传输才能找到数据,这样原子操作也无法保证。但是相比较非批量操作,还是能节省不少的网络传输次数。

整个步骤的简化版如下(通常由 Redis 客户端实现,无需我们自己再手动实现):

  1. 找到 key 对应的所有 hash slot;
  2. 分别向对应的 Redis 节点发起 mget 请求获取数据;
  3. 等待所有请求执行结束,重新组装结果数据,保持跟入参 key 的顺序一致,然后返回结果。

如果想要解决这个多次网络传输的问题,比较常用的办法是自己维护 key 与 slot 的关系。不过这样不太灵活,虽然带来了性能提升,但同样让系统复杂性提升

15.3 pipline 

pipline可以将一组命令封装成一个包内,将这个包传递给redis,只需要一次网络传输,不过要注意传输的命令的数量,比如在500以下,避免网络传输的数据量过大。

与mget, hmget一样,都会存在一些问题,原因类似,无法保证所有的可以都在一个hash slot上,如果使用,需要自己维护客户端和服务端。

同时pipline不支持顺序执行,比如前一个命令的结果要给后一个命令执行,那么是不能用pipline执行的。

15.4 LUA脚本

Lua 脚本同样支持批量操作多条命令。一段 Lua 脚本可以视作一条命令执行,可以看作是原子操作。一段 Lua 脚本执行过程中不会有其他脚本或 Redis 命令同时执行,保证了操作不会被其他指令插入或打扰,这是 pipeline 所不具备的。

并且,Lua 脚本中支持一些简单的逻辑处理比如使用命令读取值并在 Lua 脚本中进行处理,这同样是 pipeline 所不具备的。

不过, Redis Cluster 下 Lua 脚本的原子操作也无法保证了,原因同样是无法保证所有的 key 都在同一个 hash slot(哈希槽)上

16. 大量key集中过期

定期删除执行过程中,如果突然遇到大量过期 key 的话,客户端请求必须等待定期清理过期 key 任务线程执行完成,因为这个这个定期任务线程是在 Redis 主线程中执行的。这就导致客户端请求没办法被及时处理,响应速度会比较慢。

如何解决呢?下面是两种常见的方法:

  1. 给 key 设置随机过期时间。
  2. 开启 lazy-free(惰性删除/延迟释放) 。lazy-free 特性是 Redis 4.0 开始引入的,指的是让 Redis 采用异步方式延迟释放 key 使用的内存,将该操作交给单独的子线程处理,避免阻塞主线程。

个人建议不管是否开启 lazy-free,我们都尽量给 key 设置随机过期时间

17. Redis bigkey

如果一个key对应的内存比较大,那么这个key就可以看成是一个bigkey, string类型 的 vlaue超过10kb,复合类型的value包含的元素超过5000个

危害

消耗更多的存储空间 + 拖慢运行速度

如何发现? 

  1. 使用redis自带的 --bigkeys ,会扫描全部的redis中的所有key,会对redis的性能有一点影响。只能找出每种数据结构top1的bigkey
  2. 分析RDB文件, 现有工具: redis-rdb-tools, rdb_bigkeys

18. Redis生产问题

缓存穿透

# 什么是缓存穿透?

缓存穿透说简单点就是大量请求的 key 是不合理的,根本不存在于缓存中,也不存在于数据库中 。这就导致这些请求直接到了数据库上,根本没有经过缓存这一层,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

举个例子:某个黑客故意制造一些非法的 key 发起大量请求,导致大量请求落到数据库,结果数据库上也没有查到对应的数据。也就是说这些请求最终都落到了数据库上,对数据库造成了巨大的压力。

# 有哪些解决办法?

最基本的就是首先做好参数校验,一些不合法的参数请求直接抛出异常信息返回给客户端。比如查询的数据库 id 不能小于 0、传入的邮箱格式不对的时候直接返回错误消息给客户端等等。

1)缓存无效 key

如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期时间,具体命令如下: SET key value EX 10086 。这种方式可以解决请求的 key 变化不频繁的情况,如果黑客恶意攻击,每次构建不同的请求 key,会导致 Redis 中缓存大量无效的 key 。很明显,这种方案并不能从根本上解决此问题。如果非要用这种方式来解决穿透问题的话,尽量将无效的 key 的过期时间设置短一点比如 1 分钟。

2)布隆过滤器

布隆过滤器是一个非常神奇的数据结构,通过它我们可以非常方便地判断一个给定数据是否存在于海量数据中。我们需要的就是判断 key 是否合法,有没有感觉布隆过滤器就是我们想要找的那个“人”。

具体是这样做的:把所有可能存在的请求的值都存放在布隆过滤器中,当用户请求过来,先判断用户发来的请求的值是否存在于布隆过滤器中。不存在的话,直接返回请求参数错误信息给客户端,存在的话才会走下面的流程。

加入布隆过滤器之后的缓存处理流程图如下。

Redis面试_第3张图片

但是,需要注意的是布隆过滤器可能会存在误判的情况。总结来说就是: 布隆过滤器说某个元素存在,小概率会误判。布隆过滤器说某个元素不在,那么这个元素一定不在。

为什么会出现误判的情况呢? 我们还要从布隆过滤器的原理来说!

我们先来看一下,当一个元素加入布隆过滤器中的时候,会进行哪些操作:

  1. 使用布隆过滤器中的哈希函数对元素值进行计算,得到哈希值(有几个哈希函数得到几个哈希值)。
  2. 根据得到的哈希值,在位数组中把对应下标的值置为 1。

我们再来看一下,当我们需要判断一个元素是否存在于布隆过滤器的时候,会进行哪些操作:

  1. 对给定元素再次进行相同的哈希计算;
  2. 得到值之后判断位数组中的每个元素是否都为 1,如果值都为 1,那么说明这个值在布隆过滤器中,如果存在一个值不为 1,说明该元素不在布隆过滤器中。

然后,一定会出现这样一种情况:不同的字符串可能哈希出来的位置相同。 (可以适当增加位数组大小或者调整我们的哈希函数来降低概率)

缓存击穿

# 什么是缓存击穿?

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期) 。这就可能会导致瞬时大量的请求直接打到了数据库上,对数据库造成了巨大的压力,可能直接就被这么多请求弄宕机了。

Redis面试_第4张图片

举个例子 :秒杀进行过程中,缓存中的某个秒杀商品的数据突然过期,这就导致瞬时大量对该商品的请求直接落到数据库上,对数据库造成了巨大的压力。

# 有哪些解决办法?

  • 设置热点数据永不过期或者过期时间比较长。
  • 针对热点数据提前预热,将其存入缓存中并设置合理的过期时间比如秒杀场景下的数据在秒杀结束之前不过期。
  • 请求数据库写数据到缓存之前,先获取互斥锁,保证只有一个请求会落到数据库上,减少数据库的压力。

# 缓存穿透和缓存击穿有什么区别?

缓存穿透中,请求的 key 既不存在于缓存中,也不存在于数据库中。

缓存击穿中,请求的 key 对应的是 热点数据 ,该数据 存在于数据库中,但不存在于缓存中(通常是因为缓存中的那份数据已经过期)

# 缓存雪崩

# 什么是缓存雪崩?

我发现缓存雪崩这名字起的有点意思,哈哈。

实际上,缓存雪崩描述的就是这样一个简单的场景:缓存在同一时间大面积的失效,导致大量的请求都直接落到了数据库上,对数据库造成了巨大的压力。 这就好比雪崩一样,摧枯拉朽之势,数据库的压力可想而知,可能直接就被这么多请求弄宕机了。

另外,缓存服务宕机也会导致缓存雪崩现象,导致所有的请求都落到了数据库上。

Redis面试_第5张图片

举个例子 :数据库中的大量数据在同一时间过期,这个时候突然有大量的请求需要访问这些过期的数据。这就导致大量的请求直接落到数据库上,对数据库造成了巨大的压力。

# 有哪些解决办法?

针对 Redis 服务不可用的情况:

  1. 采用 Redis 集群,避免单机出现问题整个缓存服务都没办法使用。
  2. 限流,避免同时处理大量的请求。

针对热点缓存失效的情况:

  1. 设置不同的失效时间比如随机设置缓存的失效时间。
  2. 缓存永不失效(不太推荐,实用性太差)。
  3. 设置二级缓存。

# 缓存雪崩和缓存击穿有什么区别?

缓存雪崩和缓存击穿比较像,但缓存雪崩导致的原因是缓存中的大量或者所有数据失效,缓存击穿导致的原因主要是某个热点数据不存在与缓存中(通常是因为缓存中的那份数据已经过期)。

如何保证缓存和数据库数据的一致性?

细说的话可以扯很多,但是我觉得其实没太大必要(小声 BB:很多解决方案我也没太弄明白)。我个人觉得引入缓存之后,如果为了短时间的不一致性问题,选择让系统设计变得更加复杂的话,完全没必要。

下面单独对 Cache Aside Pattern(旁路缓存模式) 来聊聊。

Cache Aside Pattern 中遇到写请求是这样的:更新 DB,然后直接删除 cache 。

如果更新数据库成功,而删除缓存这一步失败的情况的话,简单说两个解决方案:

  1. 缓存失效时间变短(不推荐,治标不治本) :我们让缓存数据的过期时间变短,这样的话缓存就会从数据库中加载数据。另外,这种解决办法对于先操作缓存后操作数据库的场景不适用。
  2. 增加 cache 更新重试机制(常用): 如果 cache 服务当前不可用导致缓存删除失败的话,我们就隔一段时间进行重试,重试次数可以自己定。如果多次重试还是失败的话,我们可以把当前更新失败的 key 存入队列中,等缓存服务可用之后,再将缓存中对应的 key 删除即可。

你可能感兴趣的:(redis,redis,面试,缓存)