老面:小伙子,咱来聊聊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。

应用策略规定:

  1. 如果数据出现幂律散布,也就是一部分数据拜访频率高,一部分数据拜访频率低,则应用allkeys-lru;
  2. 如果数据出现平等散布,也就是所有的数据拜访频率都雷同,则应用allkeys-random。

老面:小伙子不错嘛,如果Redis服务器挂了,重启后数据能够复原吗?

解读:这必定能够复原呀,老面你是想问我Redis的长久化机制的吧

笑小枫:能够复原,Redis提供两种长久化机制RDB和AOF机制。能够将内存上的数据长久化到硬盘。


老面:你能够说说RDB和AOF的优缺点吗?

笑小枫:

RDB(RedisDataBase)长久化形式:是指用数据集快照的形式半长久化模式,记录redis数据库的所有键值对,在某个工夫点将数据写入一个临时文件,长久化完结后,用这个临时文件替换上次长久化的文件,达到数据恢复。

长处

  1. 只有一个文件dump.rdb,不便长久化。
  2. 容灾性好,一个文件能够保留到平安的磁盘。
  3. 性能最大化,fork子过程来实现写操作,让主过程持续解决命令,所以是IO最大化。
  4. 绝对于数据集大时,比AOF的启动效率更高。

毛病

  1. 数据安全性低。RDB是距离一段时间进行长久化,如果长久化之间redis产生故障,会产生数据失落。所以这种形式更适宜数据要求不谨严的时候。

AOF(Append-onlyfile)长久化形式:是指所有的命令行记录以redis命令申请协定的格局齐全长久化存储)保留为aof文件。

长处

  1. 数据更残缺,安全性更高,秒级数据失落(取决于 fsync 策略,如果是 everysec,最多失落 1 秒的数据);
  2. AOF 文件是一个只进行追加的命令文件,且写入操作是以 Redis 协定的格局保留的,内容是可读的,适宜误删紧急复原。

毛病

  1. AOF文件比RDB文件大,且复原速度慢。
  2. 数据集大的时候,比rdb启动效率低。所以这种形式更适宜数据要求绝对谨严的时候。

Redis 4.0 版本提供了一套基于 AOF-RDB 的混合长久化机制,保留了两种长久化机制的长处。这样重写的 AOF 文件由两部份组成,一部分是 RDB 格局的头部数据,另一部分是 AOF 格局的尾部指令。


老面:能够说一下什么是缓存穿透、缓存击穿、缓存雪崩吗?

笑小枫:

缓存穿透说简略点就是大量申请的 key 基本不存在于缓存中,导致申请间接到了数据库上,基本没有通过缓存这一层。

解决方案:

  1. 最根本的就是做好参数校验,一些不非法的参数申请间接抛出异样信息返回给客户端。
    比方查问的数据库 id 不能小于 0、传入的邮箱格局不对的时候间接返回谬误音讯给客户端等等。
  2. 缓存有效 key
    如果缓存和数据库都查不到某个 key 的数据就写一个到 Redis 中去并设置过期工夫,具体命令如下: SET key value EX 10086 。这种形式能够解决申请的 key 变动不频繁的状况,如果黑客歹意攻打,每次构建不同的申请 key,会导致 Redis 中缓存大量有效的 key 。很显著,这种计划并不能从根本上解决此问题。如果非要用这种形式来解决穿透问题的话,尽量将有效的 key 的过期工夫设置短一点比方 1 分钟。
  3. 布隆过滤器
    把所有可能存在的申请的值都寄存在布隆过滤器中,当用户申请过去,先判断用户发来的申请的值是否存在于布隆过滤器中。不存在的话,间接返回申请参数错误信息给客户端,存在的话才会走上面的流程。

缓存雪崩是指缓存在同一时间大面积的生效,前面的申请都间接落到了数据库上,造成数据库短时间内接受大量申请。

解决方案:

针对 Redis 服务不可用的状况:

  1. 采纳 Redis 集群,防止单机呈现问题整个缓存服务都没方法应用。
  2. 限流,防止同时解决大量的申请。

针对热点缓存生效的状况:

  1. 设置不同的生效工夫比方随机设置缓存的生效工夫。
  2. 缓存永不生效。

缓存击穿问题也叫热点Key问题,就是缓存在某个工夫点过期的时候,恰好在这个工夫点对这个Key有大量的并发申请过去,这些申请发现缓存过期个别都会从后端DB加载数据并回设到缓存,这个时候大并发的申请可能会霎时把后端DB压垮。

缓存击穿和缓存雪崩的区别在于缓存击穿针对某一key缓存,缓存雪崩是很多key。

  1. 互斥锁:当同个业务不同线程拜访redis未命中时,先获取一把互斥锁,而后进行数据库操作,此时另外一个线程未命中时,拿不到锁,期待一段时间后从新查问缓存,此时之前的线程曾经从新把数据加载到redis之中了,线程二就间接缓存命中。这样就不会使得大量拜访进入数据库

长处:没有额定的内存耗费,保障一致性,实现简略

毛病:线程须要期待,性能受影响,可能有死锁危险

  1. 实时调整,监控哪些数据是热门数据,实时的调整key的过期时长
  2. 针对热点Key设置缓存永不生效

老面:Redis如何找出以某个前缀结尾的数据

笑小枫:如果数据量不大,能够应用keys指令能够扫出指定模式的key列表。
如果数据量很大,因为Redis的单线程的。keys指令会导致线程阻塞一段时间,线上服务会进展,直到指令执行结束,服务能力复原。这个时候能够应用scan指令,scan指令能够无阻塞的提取出指定模式的key列表,然而会有肯定的反复概率,在客户端做一次去重就能够了,然而整体所破费的工夫会比间接用keys指令长。


老面:最初再说一下缓存数据一致性的问题?

笑小枫:

  1. 先更新数据库,在更新缓存(不举荐)
  2. 先删缓存,再更新数据库(不举荐)
  3. 延时双删
  4. 先更新数据库,再删除缓存,引入音讯队列
  5. 先更新数据库,再删除缓存,应用mysql binlog日志

先更新数据库,再更新缓存(不倡议,要理解起因!)

这套计划,大家是广泛拥护的。为什么呢?有如下两点起因。

起因一(线程平安角度)

同时有申请A和申请B进行更新操作,那么会呈现

  1. 线程A更新了数据库
  2. 线程B更新了数据库
  3. 线程B更新了缓存
  4. 线程A更新了缓存

这就呈现申请A更新缓存应该比申请B更新缓存早才对,然而因为网络等起因,B却比A更早更新了缓存。这就导致了脏数据,因而不思考。

起因二(业务场景角度)

有如下两点:

  1. 如果你是一个写数据库场景比拟多,而读数据场景比拟少的业务需要,采纳这种计划就会导致,数据压根还没读到,缓存就被频繁的更新,节约性能。
  2. 如果你写入数据库的值,并不是间接写入缓存的,而是要通过一系列简单的计算再写入缓存。那么,每次写入数据库后,都再次计算写入缓存的值,无疑是节约性能的。显然,删除缓存更为适宜。

接下来探讨的就是争议最大的,先删缓存,再更新数据库。还是先更新数据库,再删缓存的问题。

先删缓存,再更新数据库

该计划会导致不统一的起因是。同时有一个申请A进行更新操作,另一个申请B进行查问操作。那么会呈现如下情景:

  1. 申请A进行写操作,删除缓存
  2. 申请B查问发现缓存不存在
  3. 申请B去数据库查问失去旧值
  4. 申请B将旧值写入缓存
  5. 申请A将新值写入数据库

上述情况就会导致不统一的情景呈现。而且,如果不采纳给缓存设置过期工夫策略,该数据永远都是脏数据。

延时双删策略

那么,如何解决呢?采纳延时双删策略
伪代码如下

    public void write(String key,Object data) {        redis.delKey(key);        db.updateData(data);        Thread.sleep(1000);        redis.delKey(key);    }

转化为中文形容就是

  1. 先淘汰缓存
  2. 再写数据库(这两步和原来一样)
  3. 休眠1秒,再次淘汰缓存

这么做,能够将1秒内所造成的缓存脏数据,再次删除。

那么,这个1秒怎么确定的,具体该休眠多久呢?
针对下面的情景,读者应该自行评估本人的我的项目的读数据业务逻辑的耗时。而后写数据的休眠工夫则在读数据业务逻辑的耗时根底上,加几百ms即可。这么做的目标,就是确保读申请完结,写申请能够删除读申请造成的缓存脏数据。

如何解决?
提供一个保障的重试机制即可,这里给出两套计划。

先更新数据库,再删除缓存,引入音讯队列

如下图所示

流程如下所示

  1. 更新数据库数据;
  2. 缓存因为种种问题删除失败
  3. 将须要删除的key发送至音讯队列
  4. 本人生产音讯,取得须要删除的key
  5. 持续重试删除操作,直到胜利

然而,该计划有一个毛病,对业务线代码造成大量的侵入。于是有了计划二,在计划二中,启动一个订阅程序去订阅数据库的binlog,取得须要操作的数据。在应用程序中,另起一段程序,取得这个订阅程序传来的信息,进行删除缓存操作。

先更新数据库,再删除缓存,应用mysql binlog日志

流程如下图所示:

  1. 更新数据库数据
  2. 数据库会将操作信息写入binlog日志当中
  3. 订阅程序提取出所须要的数据以及key
  4. 另起一段非业务代码,取得该信息
  5. 尝试删除缓存操作,发现删除失败
  6. 将这些信息发送至音讯队列
  7. 从新从音讯队列中取得该数据,重试操作。

    备注阐明:上述的订阅binlog程序在mysql中有现成的中间件叫canal,能够实现订阅binlog日志的性能。另外,重试机制,采纳的是音讯队列的形式。如果对一致性要求不是很高,间接在程序中另起一个线程,每隔一段时间去重试即可,这些大家能够灵便自由发挥,只是提供一个思路。