老面:小伙子,咱来聊聊Redis,你晓得什么是Redis吗?
解读:这个必须晓得,不晓得间接挂~,但面试官必定不是想听你晓得两个字,他是想让你简略的介绍下。
笑小枫:Redis是咱们罕用的缓存中间件,是一个基于内存的高性能的key-value数据库。Redis不仅仅反对简略的key-value类型的数据,同时还提供多种数据结构的存储。Redis反对数据的长久化,能够将内存中的数据保留在磁盘中,重启的时候能够再次加载进行应用。
老面:除了key-value类型的数据,你还晓得Redis的哪几种数据结构?
解读:当然晓得,就是有点不想通知你
笑小枫:Redis反对五种数据类型:string(字符串),hash(哈希),list(列表),set(汇合)及zsetsortedset:(有序汇合)。
咱们理论我的项目中比拟罕用的是string,hash。
PS:如果你是Redis中高级用户,还须要加上上面几种数据结构HyperLogLog、Geo、Pub/Sub。如果你说还玩过RedisModule,像BloomFilter,RedisSearch,Redis-ML,面试官得眼睛就开始发亮了。这里就不开展一一细说了,感兴趣的能够本人去搜搜,前面问题会波及一二。
老面:你说说在什么场景下应用了Redis吗?
解读:不就是应用Redis的场景嘛~没用过,你也要编出来,这个太常见了
笑小枫:Redis罕用的有缓存用户登录后的token,热点数据做缓存,利用Redis的原子递增生成订单惟一编号,用Redis来做浏览量的计算器,应用Redis做分布式锁,应用Redis做延时队列,应用Redis的Zset做排行榜等等...
PS:这里列举场景肯定要挑本人了解的或用到过的,基本上每个场景用法都会牵扯进去很多的面试点,肯定要相熟~这里就不一一开展了,只举一个点吧,前面出一篇文章具体解说每个场景怎么实现和存在的面试点吧
老面:你能具体说说应用Redis保留用户登录token的流程吗?
用户应用账号密码登录,后端系统进行校验,如果失败,则返回失败起因;
如果胜利,会把用户id、名称等罕用的不变的数据封装成一个对象,而后用jwt生成一个token;
而后把token存入Redis,并设置过期工夫;
最初把token返回给前端,后续申请接口前端携带token进行验证。
老面:为什么要应用jwt呢?间接应用Redis保留用户信息不能够吗?
解读:去公司的时候我的项目就这样用的,谁晓得为啥用jwt呀。。。粗率了不该提jwt的
笑小枫:间接应用Redis保留也能够,应用Token做key,用户信息做value。然而应用jwt是应用用户id做key,token做value,这样请回申请时,能够先进行jwt解析,拿到id再去获取token;
如果产生大量歹意攻打时,能够通过jwt解析数据拦挡掉局部谬误token,缩小Redis服务器的压力;
当然jwt能够寄存用户信息,能够间接解析应用,在多办法间调用的时候,能够缩小数据的传递,使代码更加简洁。
老面:只应用jwt不能够满足token认证的需要吗?为什么还要应用Redis呢?
解读:都是jwt惹得祸,服了老面这个老6,咋就揪着这个点不放呀
笑小枫:
- jwt存在token续期的问题,每次续期都会生成一个新的token,而用Redis不须要,大部分零碎,每次申请接口token会主动续期,能够防止token的更换;
- 应用Redis能够查看用户的登录状态,只应用jwt则看不到;
- 如果账号要实现单设施登录,当另一个设施登录时,剔除前一个设施,应用Redis能够很不便的实现,只应用jwt无奈实现。
老面:你们我的项目中token过期工夫是怎么设置的?
解读:怎么还在token中,这不是一篇Redis的面试题嘛,跑题了跑题了~
笑小枫:这个不同的我的项目不肯定,像用户APP,咱们个别设置是7天,用户应用会主动续期;咱们外部用的软件,数据私密性强的,个别都是30分钟,30分钟未应用,则token过期。
token的过期工夫,咱们应用EXPIRE设置。应用TTL查看残余过期工夫或状态。
EXPIRE <key> <ttl> :示意将键 key 的生存工夫设置为 ttl 秒;PEXPIRE <key> <ttl> :示意将键 key 的生存工夫设置为 ttl 毫秒;EXPIREAT <key> <timestamp> :示意将键 key 的生存工夫设置为 timestamp 所指定的秒数工夫戳;PEXPIREAT <key> <timestamp> :示意将键 key 的生存工夫设置为 timestamp 所指定的毫秒数工夫戳;
TTL <key> :以秒的单位返回键 key 的残余生存工夫。PTTL <key> :以毫秒的单位返回键 key 的残余生存工夫。XX:具备时效性的数据;-1:永恒保留的数据;-2:曾经过期的数据或被删除的数据或未被定义的数据;
老面:Key过期后,Redis是怎么删除的呢?你能说说Redis的删除策略吗?
解读:终于回归主题了,这才是我善于的
笑小枫:有3种删除策略,定时删除、定期删除、惰性删除,Redis采纳的是定期删除 + 惰性删除。因为定时删除会占用大量的CPU资源,Redis没有采取这种策略。
- 定期删除由redis.c/activeExpireCycle函数实现,函数以肯定频率执行,每当Redis的服务器性执行redis.c/serverCron函数时,activeExpireCycle函数就会被调用,它在规定的工夫内,分屡次遍历服务器中的各个数据库,从数据库的expires字典中随机查看一部分键的过期工夫,并删除其中的过期键;
- 惰性删除由db.c/expireIfNeeded函数实现,所有键读写命令执行之前都会调用expireIfNeeded函数对其进行查看,如果过期,则删除该键,而后执行键不存在的操作;未过期则不作操作,继续执行原有的命令。
当然,除了定期删除和惰性删除外,如果Redis的内存不足时,Redis会依据内存淘汰机制,删除一些值。
老面:小伙子,你刚刚提到了内存淘汰机制,你能说说有哪几种吗?
解读:老面,这是我为你挖的坑,就晓得你会忍不住的问,又到了证实本人的时刻,千万不能翻车
笑小枫:Redis能够设置内存大小:maxmemory 100mb,超过了这个内存大小,就会触发内存淘汰机制;Redis默认maxmemory-policy noeviction,共有8种淘汰机制,别离是:
- noeviction: 不删除,间接返回报错信息。
- allkeys-lru:移除最久未应用(应用频率起码)应用的key。举荐应用这种。
- volatile-lru:在设置了过期工夫的key中,移除最久未应用的key。
- allkeys-random:随机移除某个key。
- volatile-random:在设置了过期工夫的key中,随机移除某个key。
- volatile-ttl: 在设置了过期工夫的key中,移除筹备过期的key。
- allkeys-lfu:移除最近起码应用的key。
- volatile-lfu:在设置了过期工夫的key中,移除最近起码应用的key。
应用策略规定:
- 如果数据出现幂律散布,也就是一部分数据拜访频率高,一部分数据拜访频率低,则应用allkeys-lru;
- 如果数据出现平等散布,也就是所有的数据拜访频率都雷同,则应用allkeys-random。
老面:小伙子不错嘛,如果Redis服务器挂了,重启后数据能够复原吗?
解读:这必定能够复原呀,老面你是想问我Redis的长久化机制的吧
笑小枫:能够复原,Redis提供两种长久化机制RDB和AOF机制。能够将内存上的数据长久化到硬盘。
老面:你能够说说RDB和AOF的优缺点吗?
笑小枫:
RDB(RedisDataBase)长久化形式:是指用数据集快照的形式半长久化模式,记录redis数据库的所有键值对,在某个工夫点将数据写入一个临时文件,长久化完结后,用这个临时文件替换上次长久化的文件,达到数据恢复。
长处:
- 只有一个文件dump.rdb,不便长久化。
- 容灾性好,一个文件能够保留到平安的磁盘。
- 性能最大化,fork子过程来实现写操作,让主过程持续解决命令,所以是IO最大化。
- 绝对于数据集大时,比AOF的启动效率更高。
毛病:
- 数据安全性低。RDB是距离一段时间进行长久化,如果长久化之间redis产生故障,会产生数据失落。所以这种形式更适宜数据要求不谨严的时候。
AOF(Append-onlyfile)长久化形式:是指所有的命令行记录以redis命令申请协定的格局齐全长久化存储)保留为aof文件。
长处:
- 数据更残缺,安全性更高,秒级数据失落(取决于 fsync 策略,如果是 everysec,最多失落 1 秒的数据);
- AOF 文件是一个只进行追加的命令文件,且写入操作是以 Redis 协定的格局保留的,内容是可读的,适宜误删紧急复原。
毛病:
- AOF文件比RDB文件大,且复原速度慢。
- 数据集大的时候,比rdb启动效率低。所以这种形式更适宜数据要求绝对谨严的时候。
Redis 4.0 版本提供了一套基于 AOF-RDB 的混合长久化机制,保留了两种长久化机制的长处。这样重写的 AOF 文件由两部份组成,一部分是 RDB 格局的头部数据,另一部分是 AOF 格局的尾部指令。
老面:能够说一下什么是缓存穿透、缓存击穿、缓存雪崩吗?
笑小枫:
缓存穿透说简略点就是大量申请的 key 基本不存在于缓存中,导致申请间接到了数据库上,基本没有通过缓存这一层。
解决方案:
- 最根本的就是做好参数校验,一些不非法的参数申请间接抛出异样信息返回给客户端。
比方查问的数据库 id 不能小于 0、传入的邮箱格局不对的时候间接返回谬误音讯给客户端等等。 - 缓存有效 key
如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期工夫,具体命令如下:SET key value EX 10086
。这种形式能够解决申请的 key 变动不频繁的状况,如果黑客歹意攻打,每次构建不同的申请 key,会导致 Redis 中缓存大量有效的 key 。很显著,这种计划并不能从根本上解决此问题。如果非要用这种形式来解决穿透问题的话,尽量将有效的 key 的过期工夫设置短一点比方 1 分钟。 - 布隆过滤器
把所有可能存在的申请的值都寄存在布隆过滤器中,当用户申请过去,先判断用户发来的申请的值是否存在于布隆过滤器中。不存在的话,间接返回申请参数错误信息给客户端,存在的话才会走上面的流程。
缓存雪崩是指缓存在同一时间大面积的生效,前面的申请都间接落到了数据库上,造成数据库短时间内接受大量申请。
解决方案:
针对 Redis 服务不可用的状况:
- 采纳 Redis 集群,防止单机呈现问题整个缓存服务都没方法应用。
- 限流,防止同时解决大量的申请。
针对热点缓存生效的状况:
- 设置不同的生效工夫比方随机设置缓存的生效工夫。
- 缓存永不生效。
缓存击穿问题也叫热点Key问题,就是缓存在某个工夫点过期的时候,恰好在这个工夫点对这个Key有大量的并发申请过去,这些申请发现缓存过期个别都会从后端DB加载数据并回设到缓存,这个时候大并发的申请可能会霎时把后端DB压垮。
缓存击穿和缓存雪崩的区别在于缓存击穿针对某一key缓存,缓存雪崩是很多key。
- 互斥锁:当同个业务不同线程拜访redis未命中时,先获取一把互斥锁,而后进行数据库操作,此时另外一个线程未命中时,拿不到锁,期待一段时间后从新查问缓存,此时之前的线程曾经从新把数据加载到redis之中了,线程二就间接缓存命中。这样就不会使得大量拜访进入数据库
长处:没有额定的内存耗费,保障一致性,实现简略
毛病:线程须要期待,性能受影响,可能有死锁危险
- 实时调整,监控哪些数据是热门数据,实时的调整key的过期时长
- 针对热点Key设置缓存永不生效
老面:Redis如何找出以某个前缀结尾的数据
笑小枫:如果数据量不大,能够应用keys指令能够扫出指定模式的key列表。
如果数据量很大,因为Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会进展,直到指令执行结束,服务能力复原。这个时候能够应用scan指令,scan指令能够无阻塞的提取出指定模式的key列表,然而会有肯定的反复概率,在客户端做一次去重就能够了,然而整体所破费的工夫会比间接用keys指令长。
老面:最初再说一下缓存数据一致性的问题?
笑小枫:
- 先更新数据库,在更新缓存(不举荐)
- 先删缓存,再更新数据库(不举荐)
- 延时双删
- 先更新数据库,再删除缓存,引入音讯队列
- 先更新数据库,再删除缓存,应用mysql binlog日志
先更新数据库,再更新缓存(不倡议,要理解起因!)
这套计划,大家是广泛拥护的。为什么呢?有如下两点起因。
起因一(线程平安角度)
同时有申请A和申请B进行更新操作,那么会呈现
- 线程A更新了数据库
- 线程B更新了数据库
- 线程B更新了缓存
- 线程A更新了缓存
这就呈现申请A更新缓存应该比申请B更新缓存早才对,然而因为网络等起因,B却比A更早更新了缓存。这就导致了脏数据,因而不思考。
起因二(业务场景角度)
有如下两点:
- 如果你是一个写数据库场景比拟多,而读数据场景比拟少的业务需要,采纳这种计划就会导致,数据压根还没读到,缓存就被频繁的更新,节约性能。
- 如果你写入数据库的值,并不是间接写入缓存的,而是要通过一系列简单的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是节约性能的。显然,删除缓存更为适宜。
接下来探讨的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。
先删缓存,再更新数据库
该计划会导致不统一的起因是。同时有一个申请A进行更新操作,另一个申请B进行查问操作。那么会呈现如下情景:
- 申请A进行写操作,删除缓存
- 申请B查问发现缓存不存在
- 申请B去数据库查问失去旧值
- 申请B将旧值写入缓存
- 申请A将新值写入数据库
上述情况就会导致不统一的情景呈现。而且,如果不采纳给缓存设置过期工夫策略,该数据永远都是脏数据。
延时双删策略
那么,如何解决呢?采纳延时双删策略
伪代码如下
public void write(String key,Object data) { redis.delKey(key); db.updateData(data); Thread.sleep(1000); redis.delKey(key); }
转化为中文形容就是
- 先淘汰缓存
- 再写数据库(这两步和原来一样)
- 休眠1秒,再次淘汰缓存
这么做,能够将1秒内所造成的缓存脏数据,再次删除。
那么,这个1秒怎么确定的,具体该休眠多久呢?
针对下面的情景,读者应该自行评估本人的我的项目的读数据业务逻辑的耗时。而后写数据的休眠工夫则在读数据业务逻辑的耗时根底上,加几百ms即可。这么做的目标,就是确保读申请完结,写申请能够删除读申请造成的缓存脏数据。
如何解决?
提供一个保障的重试机制即可,这里给出两套计划。
先更新数据库,再删除缓存,引入音讯队列
如下图所示
流程如下所示
- 更新数据库数据;
- 缓存因为种种问题删除失败
- 将须要删除的key发送至音讯队列
- 本人生产音讯,取得须要删除的key
- 持续重试删除操作,直到胜利
然而,该计划有一个毛病,对业务线代码造成大量的侵入。于是有了计划二,在计划二中,启动一个订阅程序去订阅数据库的binlog,取得须要操作的数据。在应用程序中,另起一段程序,取得这个订阅程序传来的信息,进行删除缓存操作。
先更新数据库,再删除缓存,应用mysql binlog日志
流程如下图所示:
- 更新数据库数据
- 数据库会将操作信息写入binlog日志当中
- 订阅程序提取出所须要的数据以及key
- 另起一段非业务代码,取得该信息
- 尝试删除缓存操作,发现删除失败
- 将这些信息发送至音讯队列
从新从音讯队列中取得该数据,重试操作。
备注阐明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,能够实现订阅binlog日志的性能。另外,重试机制,采纳的是音讯队列的形式。如果对一致性要求不是很高,间接在程序中另起一个线程,每隔一段时间去重试即可,这些大家能够灵便自由发挥,只是提供一个思路。