乐趣区

关于后端:面试1v1实景模拟Redis面试官就爱这样问

老面👴:小伙子,咱来聊聊 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 日志的性能。另外,重试机制,采纳的是音讯队列的形式。如果对一致性要求不是很高,间接在程序中另起一个线程,每隔一段时间去重试即可,这些大家能够灵便自由发挥,只是提供一个思路。

退出移动版