vlambda博客
学习文章列表

就说说面试如何准备Redis的问题(含缓存穿透,持久化和数据一致性等知识点)

Redis能用来展示缓存方面的技能,而且也不难准备,在本文里,就讲详细讲讲Redis面试该如何准备。

在介绍项目的时候,就说,我们项目用到了Redis做缓存,同时我还解决过了缓存穿透等方面的问题,如果按本文的方式准备到位,还可以加一句,本人还了解Redis的细节内容。

当然按照本人在之前文章里给出的引导做法,在项目介绍时不必展开,之后面试官自然会细问。这时大家就可以按如下的方法展开。

先结合项目业务介绍Redis的用法。在我们项目的仓库查询系统中,由于在一定时刻的并发量比较大,此时如果把请求全压到数据库里,就会压垮数据库,所以就引入缓存。

缓存里用到了货物ID作为键,用List的方式来存储货物信息,同时在设置时需要设置超时时间为1个小时。我们是用RedisTemplate在Spring Boot(或SSM等)框架里使用Redis。

刚才还讲到了解决了缓存穿透问题,如果面试官问起,你可以按如下的思路来说明。

一般缓存整合数据库的做法是,每次查询前先用键到Redis里去读数据,如果读到了就不走数据库,这样能提升性能,如果找不到再走数据库,每次在数据库里找到数据后,会把这条数据写入缓存,这样下次找该条数据,就能从缓存中找,而不必走数据库,这样能提升性能。

而缓存穿透是指,大量请求在查找数据时,这些数据的键值对不存在于缓存,这样每次请求都会走数据库,就像缓存不存在那样,这样在大并发量的前提下,依然会压垮数据库。

当然用布隆过滤器可以解决此类问题,不过如果面试者Java方面的能力一般,想准备些基础些的说辞,那可以这样说,我们项目里,遇到在数据库里找不到的数据,也会缓存到数据库里,比如键是该ID值,对应的值是null,或者是empty,或是一个不存在的值,但需要设置个较短的超时时间。这样下次找这个不存在的数据时,虽然还是找不到,但不会走数据库,直接从缓存这里就打回去了,所以依然能提升数据库层面的性能。

再准备些Redis数据结构方面的问题。

当你说好你是用Redis列表来缓存数据,那么面试官就有可能问,你还知道Redis哪些数据结构?你就可以说出如下的一些话。

redis的数据结构有字符串对象string、列表对象list、哈希对象hash、集合对象set、有序集合对象zset。

同时,Redis底层的数据结构如下:

  1. 字符串:redis是自己实现了名为简单动态字符串SDS的抽象类型。SDS则保存了长度信息,这样将获取字符串长度的时间由O(N)降低到了O(1),同时避免了缓冲区溢出和减少修改字符串长度时所需的内存分配次数。

  2. 链表linkedlist:这是个双向的非环状的链表结构,每个链表节点由一个listNode结构来表示,由于是双向,所以每个节点都有前驱和后继指针。

  3. 跳跃表skiplist:跳跃表是有序集合的底层实现。跳跃表由zskiplist和zskiplistNode组成,zskiplist用于保存跳跃表信息,即表头、表尾节点和长度信息等。而zskiplistNode用于表示表跳跃节点,每个跳跃表的层高都是1-32的随机数,在同一个跳跃表中,多个节点可以包含相同的分值,但是每个节点的成员对象必须是唯一。

  4. 整数集合intset:用于保存整数值的数据,不会出现重复元素,底层实现为数组。

  5. 压缩列表ziplist:压缩列表是为节约内存而开发的顺序性数据结构,可包含多个节点,每个节点可以保存一个字节数组或者整数值。

同时大家可以说下redis数据结构和底层实现之间的对应关系。

  1. 字符串对象string:是由简单动态字符串即SDS构成。

  2. 列表对象list由ziplist和linkedlist构成

  3. 哈希对象hash对应于ziplist和hashtable

  4. 集合对象set对应intset、hashtable

  5. 有序集合对象zset对应ziplist、skiplist

面试官也有可能问及热键问题。所谓热key问题是,高并发场景下在短时间内有大量请求,比如几十万数据量的请求去访问redis缓存中的某个或某些特定的key,这样会造成流量过于集中,达到服务器网卡的上限流量,从而导致redis的服务器宕机,从而拖垮整个缓存甚至数据库系统,引发雪崩效应。

在回答这类问题时,千万要记得,如果你项目里并发量不大,未必会遇到并解决热键问题,此时你就可以大大方方地说,我们项目没遇到,但我知道热键问题的解决方案:

  1. 提前把热key打散到不同的服务器,降低压力,比如最简单的做法是用put命令,在不同的redis服务器里缓存不同的热键,或者每次发布前,写个脚本做。

  2. 加入二级缓存,即redis找不到再到二级缓存中找,同时提前加载热key数据到二级缓存的内存中,如果redis宕机,走内存查询。


由于刚才你还提到了Redis的超时时间,所以面试官可能会问及删除策略。

先说下超时时间的概念,这块你甚至可以引出OOM问题。比如设置缓存数据的超时时间是1个小时,那么1个小时后Redis会用如下的删除策略删数据,但如果没有设置超时时间,那么所缓存的数据会一直在内存中,这样久而久之就会有OOM问题。下面就说下具体的删除超时数据的两种策略。

惰性删除是指,是当查询key的时候才对key进行检测,如果已超过过期时间则删除。这种策略的缺点是如果过期的key没被访问,那么就无法被删除,会一直占用内存,相关的流程如下所示。

定期删除指的是redis每隔一段时间会统一检查,删除过期key。由于如果所有key去做轮询再删除,这个效率太低,所以redis会每次抽样性地获取一些key去做检查,过期则删除。

但是用上述两种机制可能还是无法删除过期的键,此时就会引用redis的内存淘汰机制。

  1. volatile-lru:从已设置过期时间的key中,移除最近最少使用的key

  2. volatile-ttl:从已设置过期时间的key中,移除将要过期的key

  3. volatile-random:从已设置过期时间的key中随机选择key淘汰

  4. allkeys-lru:从key中选择最近最少使用的进行淘汰

  5. allkeys-random:从key中随机选择key进行淘汰

  6. noeviction:当内存达到阈值的时候,新写入操作报错

不过话说回来,大多数项目里,会对Redis所在的内存进行监控,比如用Zabbix或Cat甚至new relic监控,当内存用量超过一定的值,比如80%,就会告警并人为介入,所以很多项目是不会用到Redis的内存淘汰机制的。

持久化也是面试官经常会问题到的问题,比如问你知道的Reddis持久化方式有哪些?

redis持久化方案一般分为RDB和AOF两种,在详细讲这两种方案前,请大家务必记得,在不少项目里,是不对Redis持久化的。Redis是用来缓存内容,而不是像数据库一样持久化数据,这些缓存的数据,一般也有超时时间让它们自动失效,所以在大多数场景里,是没必要持久化当前Redis缓存的内存镜像的。

比如我做面试官的时候,有人告诉我,他们项目是用Redis RDB或AOF持久化,我就会问,为什么要持久化?再问细节,比如持久化文件一般多大?恢复一次要多久?此时求职者就说不上了。所以这方面,你也可以大大方方地说,我们项目不对Redis持久化,但我知道Redis持久化的两种方法。

RDB持久化可以手动执行,也可以根据配置定期执行,这种持久化的做法是,把某个时间点的所有缓存内容保存到RDB文件中,RDB文件是一个压缩的二进制文件,用它能还原某个时刻缓存的状态。

在这种持久化过程中,可用SAVE或者BGSAVE来生成RDB文件。但Save命令会阻塞redis进程,即在RDB文件生成完毕前,该redis服务器不能处理任何命令,而如果缓存数据量大,持久化动作甚至可以持续几十分钟,在这段时间内缓存不起作用,会引发数据库性能问题,所以一般不直接用save命令,这显然是不合适的。

BGSAVE命令是后台处理,即是会fork出一个子进程,由子进程负责生成RDB文件,在这过程中,父进程还可以继续处理命令请求,不会阻塞进程。

AOF和RDB不同,AOF是通过保存redis服务器所执行的写命令来记录数据库状态的。

AOF通过追加、写入、同步三个步骤来实现持久化机制。

  1. 当AOF持久化处于工作状态时,服务器执行完写命令之后,会把该命令追加append到aof_buf缓冲区的末尾,请注意这里只保存写命令,读命令不保存。

  2. 在服务器每结束一个事件循环之前,将会调用flushAppendOnlyFile函数决定是否要将aof_buf的内容保存到AOF文件中。

  3. 这样当需要数据恢复时,就用AOF文件里的写命令恢复数据。

有时候面试官还会问Redis集群。 这块大家千万要量力而行,别勉强。

在并发量不高的前提下,用单台redis足够了,大不了redis服务器出故障,再告警,再人工介入。如果并发量高,用主从架构就行。现在哨兵集群不怎么用了,因为要用额外的节点来监控,如果并发量高,还能用Cluster集群,但如果大家说项目用到cluster集群,面试官就可能会问细节了。所以你如果感觉没把握,但想说集群,那么就可以说主从集群。

主从模式是最简单的实现高可用的方案,核心就是主从同步,这和mysql主从集群是很相似的。

  1. slave从机发送psync命令到master

  2. master收到psync命令后,执行bgsave,生成RDB全量文件

  3. master把slave的写命令记录到缓存

  4. bgsave命令执行完后,发送RDB文件到slave,slave执行

  5. master发送缓存中的写命令到slave,slave执行

如下内容供参考,面试时千万量力而行。

主从方案里,假设master宕机,那么就不能写入数据,整个架构就不可用了哨兵(sentinel)集群还具备自动故障转移、集群监控、消息通知等功能。但哨兵集群会耗费机器。


有的面试官可能还会问Redis的事务,这里请大家注意如下的要点。

1 Redis可以通过MULTI、EXEC、WATCH等命令来实现事务机制,但Redis支持的事务不像mysql里一样,比如很难做到回滚。

2 有些面试题里,会介绍用redis的一些数据结构来实现分布式事务。这里给大家多多建议是,就说项目里分布式事务是通过seata等组件的API和配置来实现,别说项目组自己用redis一些机制来实现事务,第一自己实现的分布式事务要考虑的问题太多,说一通的话未必能说全,尤其是通过背但没做过分布式事务的人来说,更说不好。第二很多项目确实是用seata等组件来实现。

下面说下Redis事务机制的一些细节。

1 redis是通过MULTI、EXEC、WATCH等命令来实现事务机制,事务以MULTI开始。

2 如果客户端正处于事务状态,则会把事务放入队列同时返回给客户端QUEUED,反之如果不在事务状态,则直接执行该命令

3 当收到客户端EXEC命令时,WATCH命令监视整个事务中的key是否有被修改,如果有则事务失败,否则redis会遍历整个事务队列,执行队列中保存的所有命令,最后返回结果给客户端

从中大家能看到,Redis的事务其实只能确保命令要么被全部执行,要么全都不执行,一般是没有回滚机制的,所以redis的事务不能称为像mysql数据库里的传统意义上的事务。

然后再说说mysql和redis中数据一致性的问题。比如Mysql里数据有更新甚至删除了,怎么更新到redis缓存里。

这里要说明的是,应当确保数据库和缓存的最终一致性,即通过某些操作,两者会一致。但千万别说通过某种机制会确保数据库和缓存的强一致性。强一致性代价太大,没项目组会这样用,如果你面试时说是用强一致性,面试官一定会认为你没用过redis。

第一种做法是,就设置个超时时间。在大多数场景里真就这样干的,这样等到数据超时后,再从数据库拉取,这样从长时间的角度来看,能确保两者数据一致性。

但这种做法会出现脏数据或者是数据延迟,不过很多项目里,对数据实时性要求不高,比如评论信息可以过15分钟再更新,而且很多数据的跨度都很大的,比如公司名信息,基本不会变。所以在大多数项目里,用这种做法真能确保最终一致性。

第二种做法是,在更新数据库时同时把redis里相关缓存删除。这样下次访问该条更新数据时,由于redis里没数据,那么会再从数据库里拉,这样redis里的数据就不会有延迟了。当然在这种情况下,redis数据的超时时间还得设置,从而确保数据超时后,再到数据库里去拿。

第三种做法是,用消息中间件来更新,但这会提升系统的复杂度。具体做法是,用spring的aop机制,在更新数据库时向消息队列中写这条更新语句,用另一端监听该消息队列,一旦有内容,则再用一个代码更新缓存,这样能确保两者的一致性。

但如果在面试中给出了这套解决方法,就同时得解释为什么要采用额外的代价来确保数据库和缓存的实时一致性。除非是在促销活动等高实时场景下,在大多数场景里,是没必要再搭建一套消息中间件的,所以大家可以说,知道这个解决方案,但项目里就用第一或第二套解决方法确保两者一致性。

也写了那么多了,这里做个总结。

本文是从介绍项目的引导话术说起,告诉大家如何在面试中准备redis缓存的问题和亮点,同时也讲了一些redis的高频面试题。在讲题的时候,并没有直接枯燥地说答案,而是告诉大家如何结合项目说,这样能通过结合项目,证明自己确实用过redis,也确实知道如何解决一些redis的实际问题。

本人之前是从方法论方面讲如何准备面试,如何引导面试官,如何准备分布式组件等方面的亮点,最近打算再具体结合说辞讲,相关还有如下的文章,这样再结合本人讲的面试技巧,甚至大家都能在没redis等实际项目经验的基础上在面试中证明相关能力。