Redis面试详解:

一、Redis基本概念

  • 面试官心理: 靠!手上活都没干完又叫我过来面试,这不耽误我事么,今儿又得加班补活了........咦,这小伙子简历不错啊,先考考它redis..........

  • 面试官: 谈谈你对Redis的理解?

  • 我: Redis是 ANSI C 语言编写的一个基于内存的高性能键值对(key-value)的NoSQL数据库,一般用于架设在Java程序与数据库之间用作缓存层来弥补DB性能与Java程序之间的差距所带来的请求阻塞造成的响应缓慢以及DB并发吞吐跟不上系统并发量时避免请求直接落入DB从而起到保护DB的作用,而Redis一般除了缓存DB数据之外还可以利用它丰富的数据类型及指令来实现一些其他功能,比如:计数器、用户在线状态、排行榜、session存储等,同时Redis的性能也非常可观,通过官方给出的数据显示能够达到10w/s的QPS处理,但是在生产环境的实测结果大概读取QPS在7-9w/s,写入QPS在6-8w/s左右(注:与机器性能也有关),同时Redis也提供事务、持久化、高可用等一些机制的支持。

二、Redis基本数据类型与常用指令

  • 面试官: 刚刚听你提到了可以利用它丰富的数据类型及指令来实现一些其他功能,那你跟我讲讲redis的一些常用指令。

  • 我: Redis常用的一些命令的话一般是都是对于基本数据类型的操作指令以及一些全局指令.....叭啦叭啦叭......,如下:

当然了,一般也是记得一些常用的命令,但是 更多命令参考:Redis命令大全,因为redis命令和JVM参数一样,只要记得可以这样做就行了,但是具体的可以去参考相关文档资料。

  • 面试官: 嗯嗯,不错,那再接着讲讲redis的基本数据类型以及你是在项目中怎么使用它们的吧!

  • 我:Redis数据类型在之前是五种,但是现在的版本中存在九种,分别为:字符串(strings/string)、散列(hashes/hash)、列表(lists/list)、集合(sets/set)、有序集合(sorted sets/zset)以及后续的四种数据类型:bitmaps、hyperloglogs、地理空间( geospatial)、消息(Streams),不过无论是哪种数据类型Redis都不会直接将它放在内存中存储,而是转而内部使用redisObject来存储以及表示所有类型的key-value(说着说着我拿出了纸和笔,给面试官画了一张图):

Redis面试详解:_第1张图片

  • redisObject对象

    Redis内部使用一个redisObject对象来表示所有的key和value,redisObject最主要的信息如上图所示:type表示一个value对象具体是何种数据类型,encoding是不同数据类型在redis内部的存储方式。比如:type=string表示value存储的是一个普通字符串,那么encoding可以是raw或者int,而关于其他数据类型的内部编码实现我顿时再拿起笔 chua~ chua~ chua:

Redis面试详解:_第2张图片

  • redis内部编码实现

  • 我接着回答: 下面我再简单讲讲redis的基本数据类型以及它们的应用场景:

PS:HyperLogLog 的优点是,在输入元素的数量或者体积非常非常大时,计算基数所需的空间总是固定 的、并且是很小的。在 Redis 里面,每个 HyperLogLog 键只需要花费 12 KB 内存,就可以计算接近 2^64 个不同元素的基 数。这和计算基数时,元素越多耗费内存就越多的集合形成鲜明对比。但是,因为 HyperLogLog 只会根据输入元素来计算基数,而不会储存输入元素本身,所以 HyperLogLog 不能像集合那样,返回输入的各个元素(核心是基数估算算法,最终数值存在一定误差误差范围:基数估计的结果是一个带有 0.81% 标准错误的近似值,耗空间极小,每个hyperloglog key占用了12K的内存用于标记基数,pfadd命令不是一次性分配12K内存使用,会随着基数的增加内存逐渐增大,Pfmerge命令合并后占用的存储空间为12K,无论合并之前数据量多少)

三、Redis缓存及一致性、雪崩、击穿与穿透问题

  • 面试官提问: 那么你们在使用redis做为缓存层的时候是怎么通过Java操作redis的呢?

  • 我的心理: 这问题不是送命题吗.....

  • 我: Java操作redis的客户端有很多,比如springData中的RedisTemplate,也有SpringCache集成Redis后的注解形式,当然也会有一些Jedis、Lettuce、Redisson等等,而我们使用的是Lettuce以及Redisson........

  • 面试官提问: 那你们在使用redis作为缓存的时候有没有遇到什么问题呢?

  • 我: 咳咳,是的,确实遇到了以及考虑到了一些问题,比如缓存一致性、雪崩、穿透与击穿,关于redis与MySQL之间的数据一致性问题其实也考虑过很多方案,比如先删后改,延时双删等等很多方案,但是在高并发情况下还是会造成数据的不一致性,所以关于DB与缓存之间的强一致性一定要保证的话那么就对于这部分数据不要做缓存,操作直接走DB,但是如果这个数据比较热点的话那么还是会给DB造成很大的压力,所以在我们的项目中还是采用先删再改+过期的方案来做的,虽然也会存在数据的不一致,但是勉强也能接受,因为毕竟使用缓存访问快的同时也能减轻DB压力,而且本身采用缓存就需要接受一定的数据延迟性和短暂的不一致性,我们只能采取合适的策略来降低缓存和数据库间数据不一致的概率,而无法保证两者间的强一致性。合适的策略包括合适的缓存更新策略,合适的缓存淘汰策略,更新数据库后及时更新缓存、缓存失败时增加重试机制等。

  • 面试官话锋一转: 打断一下,你刚刚提到了使用缓存能让访问变快,那么你能不能讲讲redis为什么快呢?

  • 我的心理: 好家伙,这一手来的我猝不及防......

  • 硬着头发回答:

    Redis快的原因嘛其实可以从多个维度来看待:

    • 一、Redis完全基于内存

    • 二、Redis整个结构类似于HashMap,查找和操作复杂度为O(1),不需要和MySQL查找数据一样需要产生随机磁盘IO或者全表

    • 三、Redis对于客户端的处理是单线程的,采用单线程处理所有客户端请求,避免了多线程的上下文切换和线程竞争造成的开销

    • 四、底层采用select/epoll多路复用的高效非阻塞IO模型

    • 五、客户端通信协议采用RESP,简单易读,避免了复杂请求的解析开销

  • 面试官露出姨父般的慈笑: 嗯嗯,还不错,那你继续谈谈刚刚的缓存雪崩、穿透与击穿的问题吧

  • 我:

    好的,先说缓存雪崩吧,缓存雪崩造成的原因是因为我们在做缓存时为了保证内存利用率,一般在写入数据时都会给定一个过期时间,而就是因为过期时间的设置有可能导致大量的热点key在同一时间内全部失效,此时来了大量请求访问这些key,而redis中却没有这些数据,从而导致所有请求直接落入DB查询,造成DB出现瓶颈或者直接被打宕导致雪崩情况的发生。关于解决方案的的话也可以从多个维度来考虑:

    • 一、设置热点数据永不过期,避免热点数据的失效导致大量的相同请求落入DB

    • 二、错开过期时间的设置,根据业务以及线上情况合理的设置失效时间

    • 三、使用分布式锁或者MQ队列使得请求串行化,从而避免同一时间请求大量落入DB(性能会受到很大的影响)

  • 面试官: 那缓存穿透呢?指的是什么?又该怎么解决?

  • 我喝了口水接着回答: 缓存穿透这个问题是由于请求参数不合理导致的,比如对外暴露了一个接口getUser?userID=xxx,而数据库中的userID是从1开始的,当有黑客通过这个接口携带不存在的ID请求时,比如:getUser?userID=-1,请求会先来到Redis中查询缓存,但是发现没有对应的数据从而转向DB查询,但是DB中也无此值, 所以也无法写入数据到缓存,而黑客就通过这一点利用“肉鸡”等手段疯狂请求这个接口,导致出现大量redis不存在数据的请求落入DB,从而导致DB出现瓶颈或者直接被打宕机,整个系统陷入瘫痪。

  • 面试官: 嗯,那又该如果避免这种情况呢?

  • 我:

    解决方案也有好几种呢:

    • 一、做IP限流与黑名单,避免同一IP一瞬间发送大量请求

    • 二、对于请求做非法校验,对于携带非法参数的请求直接过滤

    • 三、对于DB中查询不存在的数据写入Redis中“Not Data”并设置短暂的过期时间,下次请求能够直接被拦截在redis而不会落入DB

    • 四、布隆过滤器

  • 面试官: 那接下来的缓存击穿呢?又是怎么回事?怎么解决?

  • 我:

    这个简单,缓存击穿和缓存雪崩有点类似,都是由于请求的key过期导致的问题,但是不同点在于失效key的数量,对于雪崩而言指的是大量的key失效导致大量请求落入DB,而对于击穿而言,指的是某一个热点key突然过期,而这个时候又突然又大量的请求来查询它,但是在redis中却并没有查询到结果从而导致所有请求全部打向DB,导致在这个时刻DB直接被打穿。解决方案的话也是有多种:

    • 一、设置热点key永不过期

    • 二、做好redis监控,请求串行化访问(性能较差)

    • 使用mutex。简单地来说,就是在缓存失效的时候(判断拿出来的值为空),不是立即去load db,而是先使用缓存工具的某些带成功操作返回值的操作(比如Redis的SETNX或者Memcache的ADD)去set一个mutex key,当操作返回成功时,再进行load db的操作并回设缓存;否则,就重试整个get缓存的方法,代码实现如下:

public Result get(int ID){
redisResult = redis.get(ID);
if(redisResult != null){
return redisResult;
}
if(redis.setnx("update:" + ID) != "0"){
DBResult = DB.selectByID(ID);
if(DBResult != null){ // 避免缓存穿透
redis.set(ID,DBResult);
redis.del( + ID);
return DBResult;
}
redis.set(ID,"Not Data");
return "抱歉,当前查询暂时没有找到数据......";
}
Thread.sleep(2);
return get(ID);
}

四、Redis八种淘汰策略与三种删除策略

4.1. 八种键淘汰(过期)策略

  • 面试官: 你前面提到过,redis的数据是全部放在内存中的,那么有些数据我也没有设置过期时间,导致了大量的内存浪费,当我有新的数据需要写入内存不够用了怎么办?

  • 我的内心: 好家伙,问个redis淘汰策略这么拐弯抹角.......

  • 我: 我想你是想问内存淘汰策略吧,redis在5.0之前为我们提供了六种淘汰策略,而5.0为我们提供了八种,但是大体上来说这些lru、lfu、random、ttl四种类型,如下:

  • 我喘了口气接着说:

    • 一、在Redis中,数据有一部分访问频率较高,其余部分访问频率较低,或者无法预测数据的使用频率时,设置allkeys-lru是比较合适的。

    • 二、如果所有数据访问概率大致相等时,可以选择allkeys-random。

    • 三、如果研发者需要通过设置不同的ttl来判断数据过期的先后顺序,此时可以选择volatile-ttl策略。

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