老面👴:小伙子,咱来聊聊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 秒;
PEXPIRE :表示将键 key 的生存时间设置为 ttl 毫秒;
EXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的秒数时间戳;
PEXPIREAT :表示将键 key 的生存时间设置为 timestamp 所指定的毫秒数时间戳;
TTL :以秒的单位返回键 key 的剩余生存时间。
PTTL :以毫秒的单位返回键 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日志的功能。另外,重试机制,采用的是消息队列的方式。如果对一致性要求不是很高,直接在程序中另起一个线程,每隔一段时间去重试即可,这些大家可以灵活自由发挥,只是提供一个思路。
服务器托管,北京服务器托管,服务器租用 http://www.fwqtg.net