面经 | 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异常退出,就会丢失最近一次持久化以后更改的数据。
触发持久化的方式:
-
手动触发: 用户执行
save
或bgsave
命令。save命令会阻塞所有客户端的请求,bgsave异步进行快照操作。 -
被动触发:
-
根据规则进行自动快照,
bgsave 100 10(100秒内至少10个键被修改则触发快照)
-
从节点进行全量复制操作,主节点会自动执行
bgsave
生成RDB文件发给从节点。 -
默认情况下执行
shutdown
命令时,如果没有开启AOF持久化功能,自动执行bgsvae
-
优缺点:
- Redis 加载 RDB 恢复数据远远快于 AOF 的方式。
- 使用单独子进程来进行持久化,主进程不会进行任何 IO 操作,保证了 Redis 的高性能。
- RDB方式数据无法做到实时持久化。因为
BGSAVE
每次运行都要执行fork
操作创建子进程,属于重量级操作,频繁执行成本比较高。 - 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 //由操作系统决定何时进行同步操作
注意事项:
- 所有的写入命令会追加到 AOP 缓冲区中。
- AOF 缓冲区根据对应的策略向硬盘同步。
- 随着 AOF 文件越来越大,需要定期对 AOF 文件进行重写,达到压缩文件体积的目的。AOF文件重写是把Redis进程内的数据转化为写命令同步到新AOF文件的过程。
- 当 Redis 服务器重启时,可以加载 AOF 文件进行数据恢复。
优缺点:
优点:
- AOF可以更好的保护数据不丢失,可以配置 AOF 每秒执行一次
fsync
操作,如果Redis进程挂掉,最多丢失1秒的数据。 - AOF以
append-only
的模式写入,所以没有磁盘寻址的开销,写入性能非常高。
缺点:
- 对于同一份文件AOF文件比RDB数据快照要大。
- 数据恢复比较慢。
混合使用
在 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个学生解答一道题目,然后检查学生做的是否正确,你有下面几个选择:
- 第一种选择:按顺序逐个检查,先检查A,然后是B,之后是C、D。。。这中间如果有一个学生卡主,全班都会被耽误。这种模式就好比,你用循环挨个处理socket,根本不具有并发能力。
- 第二种选择:你创建30个分身,每个分身检查一个学生的答案是否正确。这种类似于为每一个用户创建一个进程或者线程处理连接。
- 第三种选择,你站在讲台上等,谁解答完谁举手。这时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依次执行这些命令。
事务的生命周期:
-
使用
MULTI
开启一个事务 -
在开启事务的时候,每次操作的命令将会呗插入到一个队列中,同时这个命令并不会被真正的执行;
-
EXEC
命令进行提交事务一个事务范围内某个命令出错不会影响其他命令的执行,不保证原子性
-
discard:取消事务
-
watch:监视
如何保证原子性
使用Lua脚本可以保证事务的原子性。
Redis 通过 LUA 脚本创建具有原子性的命令:当lua脚本命令正在运行的时候,不会有其他脚本或 Redis 命令被执行,实现组合命令的原子操作。
在Redis中执行Lua脚本有两种方法:eval
和evalsha
。eval
命令使用内置的 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
。
本文参考:
以上是关于面经 | Redis常见面试题的主要内容,如果未能解决你的问题,请参考以下文章