Redis常见面试题

CopyRight:大白

可以访问语雀观看更多面试题:https://www.yuque.com/greedy-9i38g/tzpwui?# 《面试题》

在CSDN更新纯属看心情 <(_ _)>

目录

1、Redis为啥快?

2、Redis的合适应用场景?

3、Redis的高级特性?

4、Redis的过期策略?

5、Redis内存淘汰机制?

6、执行什么命令的时候可能会阻塞Redis?

7、如何保证缓存与数据库双写时的数据一致性?

8、Memcached和Redis的区别?

9、什么是BigKey?

10、怎么提高缓存命中率

11、Redis集群有哪些功能限制?

12、Redis常见性能问题和解决方案?

13、击穿、穿透、雪崩?

14、哨兵模式


1、Redis为啥快?

  • 纯内存访问
  • 单线程,不会存在上下文切换
  • 渐进式Rehash、缓存系统时间戳

2、Redis的合适应用场景?

  • String
  • Hash:用户添加的购物车信息,Key->Field-Value(用户ID->商品ID-商品信息)
  • List:朋友圈点赞列表,评论列表
  • Set:共同好友
  • Geospatial:计算最近商铺,最近用户之类的
  • Hyperloglog:统计排行榜之类的操作,Sorted Set也可以实现,但是它比Hyperloglog要浪费超级多的内存,内存浪费单位一个是G级一个是K级
  • Bitmap:布隆过滤器

3、Redis的高级特性?

高级数据类型

  • Geospatial:经纬度数据类型
  • Hyperloglog:不重复计数类型
    • Hyperloglog是专门用于存放不重复且需要计数的数据的,例如一篇文章的浏览量;当然这种数据也可以存放到set类型中,因为set类型也是不重复的,但如果数据量很大,则存放到set中会占用大量的内存,基于此,Redis 在2.8.9 版本中更新了 Hyperloglog 数据结构专门用于存放不重复计数类型的数据,其特点是占用的内存很小且固定,存放了2^64 个不同元素的key,只需要废 12KB内存,所以,hyperloglog是存放不重复计数类型的最佳类型
  • Bitmap:位存储数据类型
    • 其key值为map类型,但是其map的key只能为integer类型,value值只能为0或1。使用场景:打卡等

支持事务

  • 可以将多个命令作为一个整体,进行事务执行,其原理是把多个命令放在队列中执行,只是保证了,事务中的命令串行执行。并没有给事务添加原子性的保障,命令执行失败了不会进行回滚
  • 如果在执行提交事务之前发生了错误,例如某个要入队的命令语法错误,会自动取消当前这个任务。如果是在执行某个命令时出错,并不会回滚已经执行的命令,而是跳过当前出错的命令继续往下执行,可以在最后的执行结果中看到每个命令的执行情况

支持Lua脚本

  • 在一个脚本中编写多条Redis命令,确保多条命令执行时的原子性
  • 分布式锁可以结合Lua脚本实现,获取锁、比较锁、删除锁是一个原子性动作

支持pipeline管道

  • 管道可以实现打包一批指令,一次性发送给Redis服务器,Redis服务器会一次性返回这老些指令的执行结果。减少了与Reids服务器的频繁连接所浪费的网络通信时间开销、
  • 与Redis事务相比
    • 相同点:都是多个命令让服务器执行,整个操作都不具备原子性,即时某个命令出错了,也不影响其它命令继续执行
    • 不同点:Redis事务具有隔离性,不会穿插执行其它客户端的命令。随让两者都是顺序执行命令,但是pipeline管道中的命令可能会被其它客户端的执行插队。管道适合对顺序性要求不高的场景
  • 原生的mset、mget实际上就是批量执行set、get指令,但是其具有原子性
  • 一次性封装过多执行,或者是发送大key,可能会导致网络阻塞

支持发布/订阅

  • 发布/订阅(publish/subscribe)是一种消息通信模式,Reids客户端订阅Reids服务器上的一个或多个channel(管道、频道),客户端发布消息到Reids服务器中的某个channel后,Reids服务器会将消息推送给所有订阅了此channel的Reids客户端
  • Reids的发布订阅不能提供可靠的消息投递,一般只用于日志、流水等对消息投递可靠性要求不高的场景

可实现队列

  • 使用List类型
    • 生产者用Rpush命令实现消息入队,消费者使用Blpop阻塞获取元素实现消息出队
    • List有序,可以实现顺序消费
    • 一个消息只会被一个消费者消费
  • 使用Zset实现延时队列
    • 把消息内容作为Key,消息发送的时间转为时间戳,作为score,Zadd命令实现消息入队
    • 消费者间隔指定时间用Zrangebyscore命令获取一定时间范围内的key进行处理

4、Redis的过期策略?

这个跟下面的内存淘汰策略是我人生中第一次面试的题 (●'◡'●),面的亚信实习生。因为我嫌弃问的太基础,面试官直接就问了这个问题>︿<

Redis会将每个设置了过期时间的Key放入到一个独立的字典中,以后会定时遍历这个字典来删除过期的Key。除了定时遍历外,它还会使用惰性策略来删除过期的Key,所谓的惰性策略就是在客户端访问这个Key的时候,Redis会对其进行过期时间的检查,如果过期了就立马删除。定时删除时集中处理,惰性删除是分散处理

定时扫描策略:Redis默认会每秒进行10次过期扫描,过期扫描不会遍历所有字典中的Key,而是采用了一种简单的贪心策略。从字典中随机抽取20个Key,删除这20个Key中已经过期的Key,如果过期的Key比率超过1/4,那就重复抽取20个Key的操作

带来的问题,大量Key在同一时间过期,Redis会不断抽取20个Key,进行删除操作,直到过期的Key在字典中变得稀疏,这会导致线上读写出现明显卡顿

5、Redis内存淘汰机制?

当Redis内存超出物理内存的限制时,内存的数据会开始和磁盘产生频繁的交换。交换会让Redis的性能急剧下降,对比访问量比较频繁的Redis来说,这样的龟速存取效率,还不如直接查库。

  • NO-EVICTION(默认淘汰策略):此时的Redis可以读可以删操作,但是不能写操作
  • VOLATILE-LRU:尝试淘汰设置了过期时间的Key,与最少使用的Key。没有设置过期时间的不会删除,这样确保了需要持久化的数据不会丢失
  • VOLATILE-TTL:淘汰将要过期的Key,ttl 越小越优先被淘汰
  • VOLATILE-RANDOM:从设置了过期时间的Key中,随机抽取Key进行淘汰
  • ALLKEYS-LRU:区别于volatile-lru,这个策略要淘汰的 key 对象是全体的 key 集合,而不只是过期的 key 集合。这意味着没有设置过期时间的 key 也会被淘汰
  • ALLKEYS-RANDOM:allkeys-random跟上面一样,不过淘汰的策略是随机的 key

6、执行什么命令的时候可能会阻塞Redis?

DEL命令

  • Redis官方文档描述过:
    • Key是String类型,DEL时间复杂度为O(1)
    • Key是List/Hash/Set/ZSet类型,DEL时间复杂度为O(M),M为其元素个数
  • 删除Key时,Redis需要依次释放每个元素的内存,元素越多,过程就会越耗时

RANDOMKEY命令

  • RANDOMKEY在随机拿出一个 key 后,首先会先检查这个 key 是否已过期。如果该 key 已经过期,那么 Redis 会删除它,这个过程就是懒惰清理。直到Redis找到一个没过期的Key返回给客户端
  • 如果在Redis从库上执行此命令,可能更耗时,原因在于从节点不会自己删除Key,只有等主库删除完当前过期的Key后,把删除命令发送给从库,从库才会删除该条过期的Key。

7、如何保证缓存与数据库双写时的数据一致性?

1、新增数据类

  • 如果是新增数据到数据库,不会对缓存进行操作。缓存中压根没有,所以就不会存在数据一致性的问题

2、更新数据类

  • 先更新缓存,再更新数据库:这种方案基本不要考虑,如果缓存更新成功了,但是更新数据库的时候发生异常,导致缓存与库数据不一致。而且这种现象还不容易被发现
  • 先更新数据库,再更新缓存:这种方案基本不要考虑,一样的道理,如果库更新成功了,缓存没更新成功,还是有数据一致性问题。同时还会有下列问题:
    • 并发问题:同时有请求A请求B进行更新操作,那么会出现
      • 线程A更新了数据库
      • 线程B更新了数据库
      • 线程B更新了缓存
      • 线程A更新了缓存
    • 正常按照顺序应该是线程A先更新缓存才对,但是因为网络问题,导致线程B先更新了缓存,这就出现了脏数据
    • 业务场景问题:如果是写操作比较多,而读操作比较少的业务场景,采用先更库后更缓存的方案就会导致数据压根还没读取到,缓存被频繁的更新,浪费性能
  • 先更新数据库,再删除缓存:主要取决于:"更新缓存的时间复杂度",哪种方式代价小,倾向于哪种方式

3、删除缓存类

  • 先删缓存,再更新数据库:也会出现问题
    • 同时来了两个请求,A(更新)B(查询)
      • A先去删除缓存,再去更新库
      • 此时B缓存未命中,去查库,但是此时A还没有更新完库,B返回的就会是旧值
      • 这时产生了缓存与库数据不一致
    • 如何解决?最简单的方式是延时双删。A先删除缓存->更新数据库(更新后线程不会死亡,而是休眠几秒)->休眠后唤醒时,再次进行删除缓存操作。这么做就会把脏数据删除掉
    • 但是上面的解决方案还存在一个问题,如果MySQL采用的是读写分离的架构,主从之间还会存在时间差
      • A更新库,删除了缓存,主库进行更新操作,同时发消息给从库进行数据同步
      • B查询,缓存未命中,去查从库,此时主从之间同步还未完成,B返回的还是旧值
    • 如何解决?一、在延时双删的基础上,让线程休眠时间更长。二、如果是Redis进行填充式的查询时,强制让其查询数据主库。这样做两个问题:降低系统吞吐量、第二个删除操作失败咋办

4、先更新库,再更新缓存

  • 读的时候,先读缓存,缓存没有的话,就读数据库,然后取出数据后放入缓存,同时返回响应
  • 更新的时候,先更新数据库,然后再删除缓存

8、Memcached和Redis的区别?

Redis

Memcached

整体类型

支持内存/非关系型数据库

支持内存/key-value

数据类型

String/List/Set....

文本型/二进制类型

操作类型

单个事务/批量操作/事务支持

少量命令

附加功能

发布订阅/主从高可用/支持Lua脚本/序列化支持

多线程服务支持

网络IO模型

执行命令--单线程

网络操作--多线程

多线程

非阻塞IO

持久化

RDB/AOF

不支持

9、什么是BigKey?

首先说明一个事情:BigKey是指Value非常大,而不是说Key非常大。再者说key基本都是业务逻辑的名称,以英文冒号分隔,再长也占用不了多大的内存哇。了解Redis它的存储结构就会发现,为了实现从键到值的快速访问,Redis 使用了一个哈希表来保存所有键值对。一个哈希表,其实就是一个数组,数组的每个元素称为一个哈希桶。所以,我们常说,一个哈希表是由多个哈希桶组成的,每个哈希桶中保存了键值对数据

  • 字符串类型的Big Key体现在,它的Value非常大
  • 非字符串类型的Big Key体现在,它的元素非常多
  • Big Key带来的危害
    • 集群模式下,导致内存分配不均衡
    • 超时阻塞,因为Redis是单线程,操作其会比较耗时,意味着阻塞Redis的可能性加大
    • 网络阻塞,每次获取Big Key时,产生的网络流量大

10、怎么提高缓存命中率

  • 提前加载,把数据库中的一部分数据提前加载到缓存中
  • 增加Redis的存储空间,提升可以存储数据的数量,以达到提高命中率的效果
  • 调整缓存的存储类型:例如线程A需要获取用户信息,用户信息以String类型存储在缓存中,如果想要命中只能通过用户ID,才能命中。如果把用户信息改用Hash结构存储,就可以实现多条件命中
  • 提升缓存的更新频次:例如可以搭配定时任务去更新、监控MySQL的Binlog日志、MySQL更新时将数据推送至MQ,让MQ去更新缓存

11、Redis集群有哪些功能限制?

  • Key批量操作支持有限。如MSet、MGet,目前只支持具有相同slot(槽)值得key执行批量操作。对于映射为不同槽值的Key由于执行mget、mset等操作可能存在于多个节点上因此不被支持
  • 事务功能支持有限。与上面同理,多个Key只支持在同一节点上事务操作
  • Key作为数据分区最小单位,因此不能将一个BigKey拆分到不同节点
  • 不支持多数据库,单机的Redis是有16(0~15)个数据库空间可用的,集群模式下只有一个,即db 0
  • 复制结构只支持一层,从节点只能复制主节点,不支持嵌套树状复制结构

12、Redis常见性能问题和解决方案?

  • 主库最好不要做任何持久化工作,包括RDB与AOF,让从节点去做持久化,如果数据比较关键,某个从节点开启AOF持久化,策略定为每秒同步一次
  • 为了主从之间复制速度与连接稳定性,主从机器最好在同一个局域网内
  • 尽量避免在压力较大的主库上增加从库,因为主库压力本来就大了,还要给从库同步数据.....
  • 主节点调用bgRewriteAOF重写AOF文件,AOF会在重写的时候占用大量CPU与内存资源,导致阻塞情况
  • 为了集群稳定性,主从复制不要用树状结构,而是用单向列表结构。即主从关系:Master<-Slave1<-Slave2<-Slave3...,这样的结构也方便解决单点故障问题,即时主挂掉了,Slave1立刻接手,而不用让其它从节点也跟着换主

13、击穿、穿透、雪崩?

  • 穿透:缓存不存在,数据库不存在,高并发,少量Key
  • 击穿:缓存不存在,数据库存在,高并发,少量Key
  • 雪崩:缓存不存在,数据库存在,高并发,大量Key

无论是多少个Key只要是高并发,查询就相当于乘以N了。它们三个的共同点都是缓存不存在,所以无论数据它是否存在于数据库,我们只要限制一个Key能够访问数据库(让时间复杂度降为为O 1),并返回数据,就可以解决上面的问题。上面三种都可以使用互斥锁来保证数据库的稳定性

下面逐个分析

缓存穿透

是指客户端请求的数据在缓存中和数据库中都不存在,这样缓存永远不会生效,这些请求都会打到数据库。常见的解决方案有两种:

  • 缓存空对象
    • 优点:实现简单、维护方便
    • 缺点:额外的内存消耗、可能造成短期的数据不一致
  • 布隆过滤器
    • 优点:内存占用少,算法上称为位图
    • 缺点:实现复杂、可能存在误判的情况

缓存空对象:这个数据在数据库中不存在,我们也把这个数据存入到Redis中去,这样,下次用户过来访问这个不存在的数据,那么在Redis中也能找到这个数据就不会进入到数据库了

布隆过滤:通过一个庞大的二进制数组,走哈希思想去判断当前这个要查询的这个数据是否存在,如果布隆过滤器判断存在,则放行,这个请求会去访问Redis,哪怕此时Redis中的数据过期了,但是数据库中一定存在这个数据,在数据库中查询出来这个数据后,再将其放入到Redis中。但是只要是哈希思想必有哈希冲突,所以可以对其请求进行多次哈希,如果两次哈希的结果一致则认为你可以继续访问Redis

缓存雪崩

缓存雪崩是指在同一时段大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来巨大压力。解决方案:

  • 给不同的Key的TTL添加随机值
  • 利用Redis集群提高服务的可用性
  • 给缓存业务添加降级限流策略
  • 给业务添加多级缓存

缓存击穿

缓存击穿问题也叫热点Key问题,就是一个被高并发访问并且缓存重建业务较复杂的key突然失效了,无数的请求访问会在瞬间给数据库带来巨大的冲击。常见的解决方案有两种:

  • 互斥锁
    • 优点:没有额外的内存消耗、保证数据一致性、实现简单(加锁删锁的事情)
    • 缺点:线程需要等待,降低系统吞吐量、可能有死锁的风险
  • 逻辑过期
    • 线程无需等待,性能好
    • 不能保证数据一致性、需要额外内存消耗、实现复杂

14、哨兵模式

哨兵模式是一种特殊的模式,首先Redis提供了哨兵的命令,哨兵是一个独立的进程,作为进程,它会独立运行。其原理是哨兵通过发送命令,等待Redis服务器响应,从而监控运行的多个Redis实例对于男孩子来说这个模式很吊的有木有 (≧∀≦)ゞ

哨兵两个作用:

发送命令,让每个被监控的Redis服务器返回其运行状态

当哨兵监测到master宕机,会自动将slave切换成master,然后通过发布订阅模式通知其他的从服务器,修改配置文件,让它们切换主机 (๑ˉ∀ˉ๑)

一个哨兵监控的时候可能会有偷懒的时候 ╮(╯▽╰)╭ ,这个时候就需要多个哨兵啦,而且让哨兵之间互相监督不许偷懒  ̄へ ̄

文字叙述一下failover过程,假设哨兵A发现主机挂掉啦,不会马上进行故障转移,因为这仅仅是它自己主观认为主机挂掉了,这个现象网上称为主观下线。等到发现主机挂掉到达一定数量的其他哨兵,它们会集合起来开会,最后决定由一个哨兵去进行故障转移。切换成功后,就会通过发布订阅模式,让各个哨兵把自己监控的从机切换为主机,这个过程称为客观下线。这样对于外部而言,一切都是透明的

评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值