面经 | Redis常见面试题

Posted 结构化思维wz

tags:

篇首语:本文由小常识网(cha138.com)小编为大家整理,主要介绍了面经 | Redis常见面试题相关的知识,希望对你有一定的参考价值。

Redis 常见面试题

目录结构:

文章目录

简单来说 Redis 就是一个使用 C 语言开发的数据库,不过与传统数据库不同的是 Redis 的数据是存在内存中的 ,也就是它是内存数据库,所以读写速度非常快,因此 Redis 被广泛应用于缓存方向。

常用命令

# 查看所有符合条件的key
keys [pattern]
keys h?llo

# 删除key
del key [key ...]

# 检查是否存在
exists key

# 给key设置超时时间
#秒级:
expire key 5
#毫秒级:
pexpire key 5

#查看key的剩余时间
ttl key
pttl key

# 返回存储类型
type key

数据结构

String

最常规的set/get操作,value可以是String也可以是数字。一般做一些复杂的计数功能的缓存。

介绍

string 数据结构是简单的 key-value 类型。虽然 Redis 是用 C 语言写的,但是 Redis并没有使用 C 的字符串表示,而是自己构建了一种 简单动态字符串(simple dynamic string,SDS)。相比于 C 的原生字符串,Redis 的 SDS 不光可以保存文本数据还可以保存二进制数据,并且获取字符串⻓度复杂度为 O(1)(C 字符串为 O(N)),除此之外,Redis 的SDS API 是安全的,不会造成缓冲区溢出。

常用命令:

set:

set key value [ex seconds] [px milliseconds] [nx|xx]

#设置键为Hello,值为World的键值对
set Hello World

# set命令有几个选项:
ex seconds:为键设置秒级过期时间。
px milliseconds:为键设置毫秒级过期时间。
nx:键必须不存在,才可以设置成功,用于添加。
xx:与nx相反,键必须存在,才可以设置成功,用于更新。

#批量设置
mset key1 value1 [key2 value2..... ]

get:

get key

#获取键为Hello的值
get Hello

#批量获取
mget key [key2....]

应用场景

字符串可以说是Redis应用最广泛的数据结构,我们来看一下在实际的开发中一些典型的应用场景。

  • 缓存

  • 计数

    许多应用都会使用Redis作为计数的基础工具,它可以实现快速计数、 查询缓存的功能,同时数据可以异步落地到其他数据源。例如记录文章的阅读次数。

  • 共享Session

  • 分布式锁

    利用setnx命令可以实现分布式锁,由于Redis的单线程命令处理机制,如果有多个客户端同时执行setnx key value, 根据setnx的特性只有一个客户端能设置成功,所以setnx可以作为分布式锁的一种实现方案。

哈希

Redis 的字典和 Java 语言里面的 HashMap类似。在Redis中,哈希类型是指键值本身又是一个键值对结构,形如value=field1,value1,…fieldN,valueN

常用命令

hset:

hset key field value

#key为 user1的一组hash
hset user1 name wz

hget:

hget key field

hget user1 naem

hdel

#删除 field
hdel key field[field...]

hdel user1 name

应用场景:

  • 存储对象

    hash的filed-value的结构非常适合用来存储对象,field用来存储属性名称,value用来存储属性值。在一些客户端里也提供了json序列化器。

List

Redis 的列表类似于 Java 语言里面的 LinkedList,同样地list 的插入和删除操作非常快,时间复杂度为 O(1),但是索引定位很慢,时间复杂度为O(n)。

常用操作:

操作类型操作
添加rpush lpush linsert
查找lrange lindex llen
删除lpop rpop lren ltrim
修改lset
阻塞操作blpop brpop

添加

#从右边插入
rpush key value[value....]

# 从左边插入
lpush key value[value.....]

#向某个元素(pivot)前或者后插入元素
linsert key before|after pivot value

查找

#查找指定范围内的元素
lrange key start end

#获取指定下标的元素
lindex key index

#列表长度
llen key

删除

#从左侧弹出元素
lpop key

#从右侧弹出元素
rpop key

#删除指定元素
lrem key count value

#修改
lset key index newValue

#阻塞形式弹出
blpop key[key....] timeout
brpop key[key....] timeout

应用场景:

  • 消息队列

  • list可以灵活组合,在不同的场景使用,总结如下:

    • lpush+lpop=Stack(栈)
    • lpush+rpop=Queue(队列)
    • lpsh+ltrim=Capped Collection(有限集合)
    • lpush+brpop=Message Queue(消息队列)

Set

集合类似Java语言中的HashSet,集合中不允许有重复元素,并且集合中的元素是无序的,不能通过索引下标获取元素。

常用操作:

添加

sadd key element[element...]

删除

srem key element[element....]

从集合中随机弹出

spop key

集合的操作

#交集
sinter key[key....]

#并集
suinon key [key...]

#差集
sdiff key [key...]

应用场景:

  • 标签

    集合类型比较典型的使用场景是标签(tag)。例如一个用户可能对娱乐、体育比较感兴趣,另一个用户可能对历史、新闻比较感兴趣,这些兴趣点就是标签。有了这些数据就可以得到喜欢同一个标签的人,以及用户的共 同喜好的标签,这些数据对于用户体验以及增强用户黏度比较重要。

  • 共同关注

Zset 有序集合

zset 可能是 Redis 提供的最为特色的数据结构,它类似于 Java 的 SortedSet 和 HashMap 的结合体,一方面它是一个 set,保证了内部value 的唯一性,另一方面它可以给每个 value 赋予一个 score,代表这个 value 的排序权重。

相比于set增加了一个权重score,底层使用跳表

常用命令:

zadd key score member [score member...]

zadd user:rank 5 吴老狗

计算

#计算排名
zrank key member

#计算成员个数
zcard key member

#删除成员
zrem key member

应用场景:

可以用于统计博客视频网站等作品的点赞数,可以根据点赞数对作品进行排行

持久化机制

持久化就是把内存的数据写到磁盘中,防止服务宕机导致内存数据丢失。

RDB快照

将某一个时刻的内存数据,以二进制的方式写入磁盘。

RDB是 Redis 默认的持久化方案。RDB持久化时会将内存中的数据写入到磁盘中,在指定目录下生成一个dump.rdb文件。Redis 重启会加载dump.rdb文件恢复数据。

bgsave是主流的触发 RDB 持久化的方式,执行过程如下:

Redis启动时会读取RDB快照文件,将数据从硬盘载入内存。通过 RDB 方式的持久化,一旦Redis异常退出,就会丢失最近一次持久化以后更改的数据。

触发持久化的方式:

  • 手动触发: 用户执行savebgsave命令。save命令会阻塞所有客户端的请求,bgsave异步进行快照操作。

  • 被动触发:

    • 根据规则进行自动快照, bgsave 100 10(100秒内至少10个键被修改则触发快照)

    • 从节点进行全量复制操作,主节点会自动执行bgsave生成RDB文件发给从节点。

    • 默认情况下执行shutdown命令时,如果没有开启AOF持久化功能,自动执行bgsvae

优缺点:

  1. Redis 加载 RDB 恢复数据远远快于 AOF 的方式
  2. 使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能
  3. RDB方式数据无法做到实时持久化。因为BGSAVE每次运行都要执行fork操作创建子进程,属于重量级操作,频繁执行成本比较高。
  4. RDB 文件使用特定二进制格式保存,Redis 版本升级过程中有多个格式的 RDB 版本,存在老版本 Redis 无法兼容新版 RDB 格式的问题

AOF

类似于MySQL的binlog

AOF(append only file)持久化:以独立日志的方式记录每次写命令,Redis重启时会重新执行AOF文件中的命令达到恢复数据的目的。AOF的主要作用是解决了数据持久化的实时性,AOF 是Redis持久化的主流方式。

默认情况下Redis没有开启AOF方式的持久化,可以通过appendonly参数启用:appendonly yes。开启AOF方式持久化后每执行一条写命令,Redis就会将该命令写进aof_buf缓冲区,AOF缓冲区根据对应的策略向硬盘做同步操作。

默认情况下系统每30秒会执行一次同步操作。为了防止缓冲区数据丢失,可以在Redis写入AOF文件后主动要求系统将缓冲区数据同步到硬盘上。可以通过appendfsync参数设置同步的时机。

appendfsync always //每次写入aof文件都会执行同步,最安全最慢,不建议配置
appendfsync everysec  //既保证性能也保证安全,建议配置
appendfsync no //由操作系统决定何时进行同步操作

注意事项:

  1. 所有的写入命令会追加到 AOP 缓冲区中。
  2. AOF 缓冲区根据对应的策略向硬盘同步。
  3. 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
  4. 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。

优缺点:

优点:

  1. AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次fsync操作,如果Redis进程挂掉,最多丢失1秒的数据。
  2. AOF以append-only的模式写入,所以没有磁盘寻址的开销,写入性能非常高。

缺点

  1. 对于同一份文件AOF文件比RDB数据快照要大。
  2. 数据恢复比较慢。

混合使用

在 Redis 4.0 后,增加了 AOF 和 RDB 混合的数据持久化机制: 把数据以 RDB 的方式写入文件,再将后续的操作命令以 AOF 的格式存入文件,既保证了 Redis 重启速度,又降低数据丢失风险。

Redis为什么要线执行命令,在把数据写入日志?

主要是由于Redis在写入日志之前,不对命令进行语法检查,所以只记录执行成功的命令,避免出现记录错误命令的情况,而且在命令执行后再写日志不会阻塞当前的写操作。

那写后日志有什么风险呢?

  • 数据可能会丢失:如果 Redis 刚执行完命令,此时发生故障宕机,会导致这条命令存在丢失的风险。
  • 可能阻塞其他操作:AOF 日志其实也是在主线程中执行,所以当 Redis 把日志文件写入磁盘的时候,还是会阻塞后续的操作无法执行。

缓存问题

常用的分布式缓存Redis单机并发量能达到万级,常用的关系型数据库mysql一般并发量是千级,他们支持的并发量可能差十倍,所以要尽可能把流量拦截在缓存层。

缓存击穿

一个并发访问量比较大的key在某个时间过期,导致所有的请求直接打在DB上。

解决方案:

  • 让热点数据永不失效,或者快要过期时,设置一个守护线程为该数据重新设置过期时间
  • 加锁更新,如果查询缓存发现不存在,加锁,让其他线程等待,只让一个线程去更新缓存。

缓存穿透

缓存穿透指的查询缓存和数据库中都不存在的数据,这样每次请求直接打到数据库,就好像缓存不存在一样。

解决方案:

  • 缓存空值/默认值

  • 使用布隆过滤器

    布隆过滤器里会保存数据是否存在,如果判断数据不不能再,就不会访问存储。

缓存雪崩

指大量缓存数据在同一时刻失效,使这些访问都直接访问数据库,使数据库崩溃。

解决方案:

提高缓存可用性

  • 集群部署:通过集群来提升缓存的可用性,可以利用Redis本身的Redis Cluster或者第三方集群方案如Codis等。
  • 多级缓存:设置多级缓存,第一级缓存失效的基础上,访问二级缓存,每一级缓存的失效时间都不同。

过期时间

  • 均匀过期:为了避免大量的缓存在同一时间过期,可以把不同的 key 过期时间随机生成,避免过期时间太过集中。
  • 热点数据永不过期。

熔断降级

  • 服务熔断:当缓存服务器宕机或超时响应时,为了防止整个系统出现雪崩,暂时停止业务服务访问缓存系统。
  • 服务降级:当出现大量缓存失效,而且处在高并发高负荷的情况下,在业务系统内部暂时舍弃对一些非核心的接口和数据的请求,而直接返回一个提前准备好的 fallback(退路)错误处理信息。

数据一致性问题

缓存和数据库的强一致性无法实现!

一致性问题

如果采用先更新数据库,再删除缓存的方式,我们更新数据库成功,接下来还没来删除缓存,或者删除缓存失败怎么办?

解决方案:

延迟双删

先删除缓存,再更新数据库,休眠一定时间,再删除缓存。

  • 休眠的目的:确保读请求结束,写请求可以删除读请求造成的缓存脏数据。
  • 弊端:这样最差的情况就是在超时时间内数据存在不一致,而且又增加了写请求的耗时。

异步更新缓存(基于订阅binlog的同步机制)

读redis当中的数据,mysql负责增删改操作,然后利用消息队列异步更新Redis当中的缓存。

一旦mysql当中的进行了增删改的操作之后,就会记录到binlog当中,就可以把binlog相关的消息推送至Redis,Redis再根据binlog中的记录,对Redis进行更新。

单线程问题

Redis的单线程指的是执行命令时的单线程

单线程为什么还这么快?

通常来讲,单线程处理能力要比多线程差,那么为什么Redis使用单线程模型会达到每秒万级别的处理能力呢?

  • 纯内存访问
  • 非阻塞I/O,Redis使用epoll作为I/O多路复用技术的实现,再加上Redis自身的时间处理模型将epoll中的连接、读写、关闭都转为事件,不在网络 I/O 上浪费过多的时间。

IO多路复用:

引用知乎上一个高赞的回答来解释什么是I/O多路复用。假设你是一个老师,让30个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:

  1. 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
  2. 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者线程处理连接。
  3. 第三种选择,你站在讲台上等,谁解答完谁举手。这时C、D举手,表示他们解答问题完毕,你下去依次检查C、D的答案,然后继续回到讲台上等。此时E、A又举手,然后去处理E和A。

第一种就是阻塞IO模型,第三种就是I/O复用模型,Linux下的select、poll和epoll就是干这个的。将用户socket对应的fd注册进epoll,然后epoll帮你监听哪些socket上有消息到达,这样就避免了大量的无用操作。此时的socket应该采用非阻塞模式

单线程也会有一个问题:对于每个命令的执行时间是有要求的。如果某个命令执行过长,会造成其他命令的阻塞,对于Redis这种高性能的服务来说是致命的,所以Redis是面向快速执行场景的数据库。

Redis6.0引入了多线程的特性,这个多线程是在哪里呢?——「是对处理网络请求过程采用了多线程」

集群问题

主从复制

将从前的一台 Redis 服务器,同步数据到多台从 Redis 服务器上,即一主多从的模式,这个跟MySQL主从复制的原理一样。

哨兵模式

使用 Redis 主从服务的时候,会有一个问题,就是当 Redis 的主从服务器出现故障宕机时,需要手动进行恢复,为了解决这个问题,Redis 增加了哨兵模式(因为哨兵模式做到了可以监控主从服务器,并且提供自动容灾恢复的功能)。

Redis集群

Redis Cluster 是一种分布式去中心化的运行模式,是在 Redis 3.0 版本中推出的 Redis 集群方案,它将数据分布在不同的服务器上,以此来降低系统对单主节点的依赖,从而提高 Redis 服务的读写性能。

为什么需要使用集群?哨兵还差点什么呢?

哨兵模式归根节点还是主从模式,在主从模式下我们可以通过增加salve节点来扩展读并发能力,但是没办法扩展写能力和存储能力,存储能力只能是master节点能够承载的上限。所以为了扩展写能力和存储能力,我们就需要引入集群模式。

集群中那么多Master节点,redis cluster在存储的时候如何确定选择哪个节点呢?

Redis Cluster采用的是类一致性哈希算法实现节点选择的

事务问题

Redis 事务的原理是将一个事务范围内的若干命令发送给Redis。然后再让Redis依次执行这些命令。

事务的生命周期:

  1. 使用 MULTI 开启一个事务

  2. 在开启事务的时候,每次操作的命令将会呗插入到一个队列中,同时这个命令并不会被真正的执行;

  3. EXEC 命令进行提交事务

    一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性

  4. discard:取消事务

  5. watch:监视

如何保证原子性

使用Lua脚本可以保证事务的原子性。

Redis 通过 LUA 脚本创建具有原子性的命令:当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。

在Redis中执行Lua脚本有两种方法:evalevalshaeval命令使用内置的 Lua 解释器,对 Lua 脚本进行求值。

# 第一个参数是lua脚本,第二个参数是键名参数个数,剩下的是键名参数和附加参数
> eval "return KEYS[1],KEYS[2],ARGV[1],ARGV[2]" 2 key1 key2 first second
1) "key1"
2) "key2"
3) "first"
4) "second"

lua脚本作用

1、Lua脚本在Redis中是原子执行的,执行过程中间不会插入其他命令。

2、Lua脚本可以将多条命令一次性打包,有效地减少网络开销。

Redis事务为什么不能回滚

总结起来主要就是 3 个原因:

  • Redis 作者认为发生事务回滚的原因大部分都是程序错误导致,这种情况一般发生在开发和测试阶段,而生产环境很少出现。
  • 对于逻辑性错误,比如本来应该把一个数加 1 ,但是程序逻辑写成了加 2,那么这种错误也是无法通过事务回滚来进行解决的。
  • Redis 追求的是简单高效,而传统事务的实现相对比较复杂,这和 Redis 的设计思想相违背。

分布式锁问题

分布式锁的常用实现方式

布式锁需要满足谁申请谁释放原则,不能释放别人的锁,也就是说,分布式锁,是要有归属的。

set lock key nx ex 3;
检查是否存在,不存在才能加锁并且设置超时时间

问题:执行完毕后,检查锁,再释放,这些操作不是原子化的。可能锁获取时还是自己的,删除时却已经是别人的了。这可怎么办呢?

有了Lua的特性,Redis才真正在分布式锁、秒杀等场景,有了用武之地,下面便是改造之后的流程:

集群处理分布式锁

前面我们谈及的内容,基本是基于单机考虑的,如果Redis挂掉了,那锁就不能获取了。这个问题该如何解决呢?

  • 主从容灾

    最简单的一种方式,就是为Redis配置从节点,当主节点挂了,用从节点顶包。(但是主从切换需要人工参与,可以使用哨兵模式灵活自动切换。)

    这种方式由于同步有延迟,可能导致丢掉一部分数据,分布式锁可能失效。

  • 多机部署(假设集群中有五个主节点)

    • 向5个Redis申请加锁
    • 只要超过一半,那么就是获取锁成功,如果超过一半失败,需要向每一个Redis发送解锁命令。
    • 由于向5个Redis发送请求,会有一定耗时,所以锁剩余持有时间,需要减去请求时间。这个可以作为判断依据,如果剩余时间已经为0,那么也是获取锁失败。
    • 使用完成之后,向5个Redis发送解锁请求。

其他问题

Redis为什么这么快

  • 基于内存:Redis是使用内存存储,没有磁盘IO上的开销。数据存在内存中,读写速度快。
  • 单线程实现( Redis 6.0以前):Redis使用单个线程处理请求,避免了多个线程之间线程切换和锁资源争用的开销。
  • IO多路复用模型:Redis 采用 IO 多路复用技术。Redis 使用单线程来轮询描述符,将数据库的操作都转换成了事件,不在网络I/O上浪费过多的时间。
  • 高效的数据结构:Redis 每种数据类型底层都做了优化,目的就是为了追求更快的速度。

内存淘汰策略有哪些?

当Redis的内存超过最大允许的内存之后,Redis 会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器正常运行。

Redisv4.0前提供 6 种数据淘汰策略

  • volatile-lru:LRU(Least Recently Used),最近使用。利用LRU算法移除设置了过期时间的key
  • allkeys-lru:当内存不足以容纳新写入数据时,从数据集中移除最近最少使用的key
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止删除数据,当内存不足以容纳新写入数据时,新写入操作会报错

Redisv4.0后增加以下两种

  • volatile-lfu:LFU,Least Frequently Used,最少使用,从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
  • allkeys-lfu:当内存不足以容纳新写入数据时,从数据集中移除最不经常使用的key。

内存淘汰策略可以通过配置文件来修改,相应的配置项是maxmemory-policy,默认配置是noeviction


本文参考:

三分恶微信公众号

超过最大允许的内存之后,Redis 会触发内存淘汰策略,删除一些不常用的数据,以保证Redis服务器正常运行。

Redisv4.0前提供 6 种数据淘汰策略

  • volatile-lru:LRU(Least Recently Used),最近使用。利用LRU算法移除设置了过期时间的key
  • allkeys-lru:当内存不足以容纳新写入数据时,从数据集中移除最近最少使用的key
  • volatile-ttl:从已设置过期时间的数据集中挑选将要过期的数据淘汰
  • volatile-random:从已设置过期时间的数据集中任意选择数据淘汰
  • allkeys-random:从数据集中任意选择数据淘汰
  • no-eviction:禁止删除数据,当内存不足以容纳新写入数据时,新写入操作会报错

Redisv4.0后增加以下两种

  • volatile-lfu:LFU,Least Frequently Used,最少使用,从已设置过期时间的数据集中挑选最不经常使用的数据淘汰。
  • allkeys-lfu:当内存不足以容纳新写入数据时,从数据集中移除最不经常使用的key。

内存淘汰策略可以通过配置文件来修改,相应的配置项是maxmemory-policy,默认配置是noeviction


本文参考:

三分恶微信公众号

JavaGuide面经

以上是关于面经 | Redis常见面试题的主要内容,如果未能解决你的问题,请参考以下文章

面经Java岗位常见面试题

面经软件测试岗位常见面试题全套合集系列4-1

面试题Redis篇-常见面试题p1

面试题Redis篇-常见面试题p1

Redis常见面试题

测开常见面试题什么是redis