美文网首页
Redis---常见面试题整理

Redis---常见面试题整理

作者: wyn_做自己 | 来源:发表于2021-11-22 23:49 被阅读0次

    今日份鸡汤:成年人的世界哪有容易可言,但我们依旧要做打不死的小强!

    1、Redis用了哪些数据结构、场景?
    (1)String:单值缓存、对象缓存(对象设置成一个json串)、分布式锁、计数器(文章阅读数量)、分布式系统全局序列号(分库分表使用redis实现自增id:业务量大的时候,影响性能,因为他主要是用做缓存,可以批量获取,一次拿100个,incrby orderid 100)。
    (2)hash:对象缓存(一个表中有很多字段,但是我经常就只用某一个字段,比如余额,这种场景适合使用hash,不适合使用string)、电商购物车(用户id为key,商品id为filed,商品数量为value)。
    (3)list:常用数据结构(栈 = LPUSH + LPOP、队列 = LPUSH + RPOP、阻塞队列 = LPUSH + BRPOP,属于分布式数据结构,所以没用java的数据结构)、微博和微信公众号消息流(用数据库实现的话,性能低,因为是时间倒排)。
    (4)set:微信抽奖小程序、微信微博点赞收藏标签、集合操作实现微博微信关注模型(sinter set1 set2,找出共同关注的人)。
    (5)zset:排行榜(热搜新闻)。
    数据结构底层:跳表(skiplist:原始跳表是按照分值从小到大一次排列的一个链表。就是把我们的有序链表改造为支持“折半查找”算法的一个结构,每两个元素往上建立一个索引层,这个索引层也会建立一个指针关系,以空间换时间,耗费空间大约一倍,效率也提升一倍,元素越多用跳表越好,默认超过128个元素自动转为skiplist)、压缩列表(ziplist:原本是一个链表,他把指针去掉了,类似于数组,区别在于记录了相对的偏移位置,元素少的时候可以用压缩列表)。

    2、雪崩、穿透、击穿
    (1)雪崩
    原因:(1)redis服务挂掉。(2)redis中的key大面积失效,可能是key过期,也可能是服务宕机,导致请求直接打到数据库。(redis缓存key同一时间大面积失效,导致请求打到数据库,造成数据库挂掉)。
    解决:
    1》设置缓存失效时间,让他不要在同一时间失效,设置缓存时,随机初始化他的失效时间,让缓存不在同一时间全部失效。
    2》redis集群部署,热点key放到不同节点,让热点缓存平均分布在redis节点上。
    3》不设置缓存失效时间,让缓存永远不失效。
    4》跑定时任务,定时刷缓存,比如说设置三小时失效,失效前,把缓存刷进去。然后再设置三个小时,不断用定时任务去跑,这样就不会失效。

    (2)穿透
    原因:redis缓存和数据库中都没有这样的数据,一般出现这种情况都是恶意用户。
    解决:
    1》请求穿过redis直接到数据库,数据库无论查出什么结果,是空还是有值,都会缓存到redis里面去,这样下次同一参数发请求就不会穿透redis。但请求可以换不同的参数。
    2》把这个ip拉黑,但他可能换不同的ip。
    3》对参数合法性进行校验,参数不合法直接return掉。
    4》使用布隆过滤器。bloom算法类似一个hash set,用来判断某个元素(key)是否在某个集合中。就是他能保证,存在的数据一定会在集合中,但是不存在的数据有可能存在,存在误判率,但是很低

    (3)击穿
    原因:大量请求访问某个热点key,当这个热点的key突然失效,请求将打到数据库。
    解决:
    1》key不设置过期时间。
    2》使用分布式锁。当热点key失效的时候,请求打到数据库,将请求数据库这一步给他加上锁,这个时候就只有这一个线程能抢到这个锁,所以也就有一个线程能操作这个数据库,这个时候对数据库的压力就比较小,当他查询到这个数据后,再把这个数据重新写到redis里面去,其他没有抢到锁的线程,让他先睡几毫秒,然后再重新去redis里面去查询这个数据。

    3、缓存一致性怎么保证?
    (1)延迟双删:没有办法预估等待时间,可能还会出现数据不一致问题。
    延时双删的方案的思路是,为了避免更新数据库的时候,其他线程从缓存中读取不到数据,就在更新完数据库之后,再sleep一段时间,然后再次删除缓存。

    sleep的时间要对业务读写缓存的时间做出评估,sleep时间大于读写缓存的时间即可。

    流程如下:

    线程1删除缓存,然后去更新数据库

    线程2来读缓存,发现缓存已经被删除,所以直接从数据库中读取,这时候由于线程1还没有更新完成,所以读到的是旧值,然后把旧值写入缓存

    线程1,根据估算的时间,sleep,由于sleep的时间大于线程2读数据+写缓存的时间,所以缓存被再次删除

    如果还有其他线程来读取缓存的话,就会再次从数据库中读取到最新值

    (2)内存队列:性能低,相当于串行执行。
    这是网上很多文章里都有写过的方案。但是这个方案的缺陷会更明显一点。

    先更新数据库,成功后往消息队列发消息,消费到消息后再删除缓存,借助消息队列的重试机制来实现,达到最终一致性的效果。
    这个解决方案其实问题更多。

    引入消息中间件之后,问题更复杂了,怎么保证消息不丢失更麻烦

    就算更新数据库和删除缓存都没有发生问题,消息的延迟也会带来短暂的不一致性,不过这个延迟相对来说还是可以接受的

    (3)进阶版消息队列:
    为了解决缓存一致性的问题单独引入一个消息队列,太复杂了。

    其实,一般大公司本身都会有监听binlog消息的消息队列存在,主要是为了做一些核对的工作。

    这样,我们可以借助监听binlog的消息队列来做删除缓存的操作。这样做的好处是,不用你自己引入,侵入到你的业务代码中,中间件帮你做了解耦,同时,中间件的这个东西本身就保证了高可用。

    当然,这样消息延迟的问题依然存在,但是相比单纯引入消息队列的做法更好一点。

    而且,如果并发不是特别高的话,这种做法的实时性和一致性都还算可以接受的。

    (4)分布式锁,但是分布式锁效率比较低。

    (5)读写锁,适合于读多写少的场景。

    (6)设置缓存过期时间。
    每次放入缓存的时候,设置一个过期时间,比如5分钟,以后的操作只修改数据库,不操作缓存,等待缓存超时后从数据库重新读取。

    如果对于一致性要求不是很高的情况,可以采用这种方案。

    这个方案还会有另外一个问题,就是如果数据更新的特别频繁,不一致性的问题就很大了。

    在实际生产中,我们有一些活动的缓存数据是使用这种方式处理的。

    因为活动并不频繁发生改变,而且对于活动来说,短暂的不一致性并不会有什么大的问题。
    (7)TiDB。

    4、Redis分布式锁的实现及要注意的问题
    synchronized单机版是ok的,但是分布式项目中就不可以了,这时候就考虑到取消单机锁,使用redis的分布式锁setnx,使用分布式锁的情况,需要注意锁是否被释放,比如出现异常情况,所以要在代码层面finally中释放锁。除了这种情况,比如说宕机了,根本没有走到finally块里面,这样也没有办法保证释放锁,所以在缓存这个key的时候,需要设置过期时间。这个时候也要注意,必须要setnx和过期时间设置的代码写在同一行,来保证原子操作。除此以外,还要规定只能自己删除自己的锁,你不能把别人的锁删除了(比如说,我加锁超时时间为10s,然后我线程1过来执行了10s还没有执行完,然后锁释放了,这时候线程二过来,正好拿到了线程1释放的锁,然后线程1、线程二都执行5s,线程1现在执行结束了,然后他开始释放锁,这时候,他释放的就是线程2所加的锁。)另外,redis集群环境下,我们即便是上面的所有操作都保证没问题了,也不能保证加锁没问题,主节点加锁后挂了,从节点没有拿到锁数据,所以我们就使用redlock的redisson来实现,使用redisson在unlock的时候,加上判断,redisson.islocked() && redisson.isHeldByCurrentThread(),判断当前是否是加锁状态,以及是否是当前线程加的锁。

    5、Redis单线程为什么性能还很高
    (1)内存级别操作:数据读写在内存中,去除了磁盘IO的时间,异步持久化到磁盘。
    (2)基于非阻塞的io多路复用机制:加一个监视的效果,请求准备完毕就直接让redis对这个请求进行处理。epoll:监视请求的时候,给每个请求都设置一个标识符,标识请求是否准备完毕。
    (3)work线程是单线程,避免了线程上下文切换。
    (4)丰富的数据结构:底层数据结构。hash、压缩表、跳表。

    6、Redis淘汰策略
    (1)惰性过期:只有当访问一个key的时候,才会判断这个key是否过期,如果过期就删除,比较耗内存。
    (2)定时过期:redis中没有用到,但是也是一种常见的策略,给每一个key设置一个定时器,到时间就把key删除掉,对内存友好,但是对cpu不友好。
    (3)定期过期:每隔一段时间,会扫描一定数量的key,然后判断key是否过期,如果过期就删除。
    redis中同时使用了惰性过期和定期过期。

    7、Redis集群还是哨兵?集群key分布?
    使用的是哨兵模式,因为主从复制模式,如果master挂掉就无法提供服务,不是高可用,为了达到高可用使用了哨兵模式,主节点挂掉后,会内部投票机制从从节点中选择出来一个新的主节点,类似心跳机制,一般设置3-5秒去检测一次。耗费性能和资源
    redis集群模式是在3.0之后才有的:完成高可用高并发,三主三从,无中心结构,就是没有master

    Redis Cluster 集群引入了主从模式,一个主节点对应一个或者多个从节点,当主节点宕机的时候,就会启用从节点。当其它主节点 PING一个主节点时,如果半数以上的主节点与该主节点通信超时,那么认为主节点A下线,这时它的从节点会顶上去服务。但是如果该主节点和它的从节点都宕机了,那么此集群就无法再提供服务了。 在对应的所有键中通过 Redis 内部实现算法 CRC16 对键进行哈希算法运算得到一个整数值后在 mod上 16384后,将得到的值映射对应到编号为 0 ~ 16383的槽子中。 Cluster 还允许用户强制将某个 key 挂在特定槽位上,通过在 key 字符串里面写入 tag 标记,这时key 所挂在的槽位等于 tag 所在的槽位。

    8、Redis集群之间怎么通讯?
    在分布式存储中需要提供维护节点元数据信息的机制,所谓元数据是指:节点负责哪些数据,是否出现故障等状态信息。常见的元数据维护方式分为:集中式和P2P方式。Redis集群采用P2P的Gossip(流言)协议,Gossip协议工作原理就是节点彼此不断通信交换信息,一段时间后所有的节点都会知道集群完整的信息,这种方式类似流言传播
    集群中的每个节点都会单独开辟一个TCP通道,用于节点之间彼此通信,通信端口号在基础端口上加10000

    每个节点在固定周期内通过特定规则选择几个节点发送ping消息

    接收到ping消息的节点用pong消息作为响应

    集群中每个节点通过一定规则挑选要通信的节点,每个节点可能知道全部节点,也可能仅知道部分节点,只要这些节点彼此可以正常通信,最终它们会达到一致的状态。当节点出故障、新节点加入、主从角色变化、槽信息变更等事件发生时,通过不断的ping/pong消息通信,经过一段时间后所有的节点都会知道整个集群全部节点的最新状态,从而达到集群状态同步的目的。

    Gossip消息

    Gossip协议的主要职责就是信息交换。信息交换的载体就是节点彼此发送的Gossip消息,常用的Gossip消息可分为:ping消息、pong消息、meet消息、fail消息

    meet消息:用于通知新节点加入。消息发送者通知接收者加入到当前集群,meet消息通信正常完成后,接收节点会加入到集群中并进行周期性的ping、pong消息交换

    ping消息:集群内交换最频繁的消息,集群内每个节点每秒向多个其他节点发送ping消息,用于检测节点是否在线和交换彼此状态信息。ping消息发送封装了自身节点和部分其他节点的状态数据

    pong消息:当接收到ping、meet消息时,作为响应消息回复给发送方确认消息正常通信。pong消息内部封装了自身状态数据。节点也可以向集群内广播自身的pong消息来通知整个集群对自身状态进行更新

    fail消息:当节点判定集群内另一个节点下线时,会向集群内广播一个fail消息,其他节点接收到fail消息之后把对应节点更新为下线状态

    在所有的 redis 实例节点之间彼此使用 Gossip 协议进行通信,通过 PING-PONG 机制来达到互联状态,在传输过程中使用特殊的二进制协议相互交互集群信息。

    相关文章

      网友评论

          本文标题:Redis---常见面试题整理

          本文链接:https://www.haomeiwen.com/subject/keeitrtx.html