【redis】redis面试讲解

>> 面试常问问题一node

redis 集群模式的工做原理能说一下么?在集群模式下, redis 的 key 是如何寻址的?分布式寻址都有哪些算法?了解一致性 hash 算法吗?mysql

一、面试官心理分析面试

在前几年,redis 若是要搞几个节点,每一个节点存储一部分的数据,得借助一些中间件来实现,好比说有codis,或者 twemproxy,都有。有一些 redis 中间件,你读写 redis 中间件,redis 中间件负责将你的数据分布式存储在多台机器上的 redis 实例中。redis

这两年,redis 不断在发展,redis 也不断有新的版本,如今的 redis 集群模式,能够作到在多台机器上,部署多个 redis 实例,每一个实例存储一部分的数据,同时每一个 redis 主实例能够挂 redis 从实例,自动确保说,若是 redis 主实例挂了,会自动切换到 redis 从实例上来。算法

如今 redis 的新版本,你们都是用 redis cluster 的,也就是 redis 原生支持的 redis 集群模式,那么面试官确定会就 redis cluster 对你来个几连炮。要是你没用过 redis cluster,正常,之前不少人用 codis 之类的客户端来支持集群,可是起码你得研究一下 redis cluster 吧。sql

若是你的数据量不多,主要是承载高并发高性能的场景,好比你的缓存通常就几个 G,单机就足够了,可使用 replication,一个 master 多个 slaves,要几个 slave 跟你要求的读吞吐量有关,而后本身搭建一个 sentinel 集群去保证 redis 主从架构的高可用性。数据库

redis cluster,主要是针对海量数据+高并发+高可用的场景。redis cluster支撑N个 redis master node,每一个master node均可以挂载多个slave node。这样整个redis就能够横向扩容了。若是你要支撑更大数据量的缓存,那就横向扩容更多的master节点,每一个master节点就能存放更多的数据了。api

二、面试题剖析缓存

redis cluster 介绍服务器

自动将数据进行分片,每一个 master 上放一部分数据

提供内置的高可用支持,部分 master 不可用时,仍是能够继续工做的

在 redis cluster 架构下,每一个 redis 要放开两个端口号,好比一个是 6379,另一个就是 加 1w 的端口号,好比 16379。

16379 端口号是用来进行节点间通讯的,也就是 cluster bus 的东西,cluster bus 的通讯,用来进行故障检测、配置更新、故障转移受权。cluster bus 用了另一种二进制的协议,gossip 协议,用于节点间进行高效的数据交换,占用更少的网络带宽和处理时间。

三、节点间的内部通讯机制

1)基本通讯原理

集群元数据的维护有两种方式:集中式、Gossip 协议。redis cluster 节点间采用 gossip 协议进行通讯。

集中式是将集群元数据(节点信息、故障等等)几种存储在某个节点上。集中式元数据集中存储的一个典型表明,就是大数据领域的 storm。它是分布式的大数据实时计算引擎,是集中式的元数据存储的结构,底层基于 zookeeper(分布式协调的中间件)对全部元数据进行存储维护。

redis 维护集群元数据采用另外一个方式, gossip 协议,全部节点都持有一份元数据,不一样的节点若是出现了元数据的变动,就不断将元数据发送给其它的节点,让其它节点也进行元数据的变动。

集中式的好处在于,元数据的读取和更新,时效性很是好,一旦元数据出现了变动,就当即更新到集中式的存储中,其它节点读取的时候就能够感知到;很差在于,全部的元数据的更新压力所有集中在一个地方,可能会致使元数据的存储有压力。

gossip 好处在于,元数据的更新比较分散,不是集中在一个地方,更新请求会陆陆续续打到全部节点上去更新,下降了压力;很差在于,元数据的更新有延时,可能致使集群中的一些操做会有一些滞后。

10000 端口:每一个节点都有一个专门用于节点间通讯的端口,就是本身提供服务的端口号+10000,好比7001,那么用于节点间通讯的就是 17001 端口。每一个节点每隔一段时间都会往另外几个节点发送 ping 消息,同时其它几个节点接收到 ping 以后返回 pong。交换的信息:信息包括故障信息,节点的增长和删除,hash slot 信息等等。

2)gossip 协议

gossip 协议包含多种消息,包含 ping、pong、meet、fail 等等。

meet:某个节点发送 meet 给新加入的节点,让新节点加入集群中,而后新节点就会开始与其它节点进行通讯。

redis-trib.rb add-node

其实内部就是发送了一个 gossip meet 消息给新加入的节点,通知那个节点去加入咱们的集群。

ping:每一个节点都会频繁给其它节点发送 ping,其中包含本身的状态还有本身维护的集群元数据,互相经过 ping 交换元数据。

pong:返回ping和meeet,包括本身的状态和其余信息,也用于信息广播和更新。fail:某个节点判断另外一个节点fail以后,就发送fail给其余节点,通知其余节点说,某个节点宕机啦。

3)ping 消息深刻

ping 时要携带一些元数据,若是很频繁,可能会加剧网络负担。

每一个节点每秒会执行 10 次 ping,每次会选择 5 个最久没有通讯的其它节点。固然若是发现某个节点通讯延时达到了 cluster_node_timeout / 2,那么当即发送 ping,避免数据交换延时过长,落后的时间太长了。

好比说,两个节点之间都 10 分钟没有交换数据了,那么整个集群处于严重的元数据不一致的状况,就会有问题。因此 cluster_node_timeout 能够调节,若是调得比较大,那么会下降 ping 的频率。

每次 ping,会带上本身节点的信息,还有就是带上 1/10 其它节点的信息,发送出去,进行交换。至少包含 3 个其它节点的信息,最多包含 总节点数减 2 个其它节点的信息。

4)分布式寻址算法

hash 算法(大量缓存重建)

一致性 hash 算法(自动缓存迁移)+ 虚拟节点(自动负载均衡)

redis cluster 的 hash slot 算法

5)hash 算法

来了一个 key,首先计算 hash 值,而后对节点数取模。

而后打在不一样的 master 节点上。一旦某一个master 节点宕机,全部请求过来,都会基于最新的剩余 master 节点数去取模,尝试去取数据。

这会致使大部分的请求过来,所有没法拿到有效的缓存,致使大量的流量涌入数据库。

6)一致性 hash 算法

一致性 hash 算法将整个 hash 值空间组织成一个虚拟的圆环,整个空间按顺时针方向组织,下一步将各个 master 节点(使用服务器的 ip 或主机名)进行 hash。这样就能肯定每一个节点在其哈希环上的位置。

来了一个 key,首先计算 hash 值,并肯定此数据在环上的位置,今后位置沿环顺时针“行走”,遇到的第一个 master 节点就是 key 所在位置。

在一致性哈希算法中,若是一个节点挂了,受影响的数据仅仅是此节点到环空间前一个节点(沿着逆时针方向行走遇到的第一个节点)之间的数据,其它不受影响。增长一个节点也同理。

燃鹅,一致性哈希算法在节点太少时,容易由于节点分布不均匀而形成缓存热点的问题。为了解决这种热点问题,一致性 hash 算法引入了虚拟节点机制,即对每个节点计算多个 hash,每一个计算结果位置都放置一个虚拟节点。这样就实现了数据的均匀分布,负载均衡。

7)redis cluster 的 hash slot 算法

redis cluster 有固定的 16384 个 hash slot,对每一个 key 计算 CRC16 值,而后对 16384 取模,能够获取 key 对应的 hash slot。

redis cluster 中每一个 master 都会持有部分 slot,好比有 3 个 master,那么可能每一个 master 持有5000 多个 hash slot。hash slot 让 node 的增长和移除很简单,增长一个 master,就将其余 master的 hash slot 移动部分过去,减小一个 master,就将它的 hash slot 移动到其余 master 上去。移动hash slot 的成本是很是低的。客户端的 api,能够对指定的数据,让他们走同一个 hash slot,经过 hash tag 来实现。

任何一台机器宕机,另外两个节点,不影响的。由于 key 找的是 hash slot,不是机器。

四、redis cluster 的高可用与主备切换原理

redis cluster 的高可用的原理,几乎跟哨兵是相似的。

1)判断节点宕机

若是一个节点认为另一个节点宕机,那么就是 pfail,主观宕机。若是多个节点都认为另一个节点宕机了,那么就是 fail,客观宕机,跟哨兵的原理几乎同样,sdown,odown。

在 cluster-node-timeout 内,某个节点一直没有返回 pong,那么就被认为 pfail。若是一个节点认为某个节点 pfail 了,那么会在 gossip ping 消息中,ping 给其余节点,若是超过半数的节点都认为 pfail 了,那么就会变成 fail。

2)从节点过滤

对宕机的 master node,从其全部的 slave node 中,选择一个切换成 master node。检查每一个 slave node 与 master node 断开链接的时间,若是超过了 ,那么就没有资格切换成 master。

3)从节点选举

每一个从节点,都根据本身对 master 复制数据的 offset,来设置一个选举时间,offset 越大(复制数据越多)的从节点,选举时间越靠前,优先进行选举。

全部的 master node 开始 slave 选举投票,给要进行选举的 slave 进行投票,若是大部分 master node(N/2 + 1)都投票给了某个从节点,那么选举经过,那个从节点能够切换成 master。

从节点执行主备切换,从节点切换为主节点。

与哨兵比较:

整个流程跟哨兵相比,很是相似,因此说,redis cluster 功能强大,直接集成了 replication 和sentinel 的功能。

>> 面试常问问题二

了解什么是 redis 的雪崩、穿透和击穿?redis 崩溃以后会 怎么样?系统该如何应对这种状况?如何处理 redis 的穿透?

一、面试官心理分析

其实这是问到缓存必问的,由于缓存雪崩和穿透,是缓存最大的两个问题,要么不出现,一旦出现就是致命性的问题,因此面试官必定会问你。

二、面试题剖析

1)缓存雪崩

对于系统 A,假设天天高峰期每秒 5000 个请求,原本缓存在高峰期能够扛住每秒 4000 个请求,可是缓存机器意外发生了全盘宕机。缓存挂了,此时 1 秒 5000 个请求所有落数据库,数据库必然扛不住,它会报一下警,而后就挂了。

此时,若是没有采用什么特别的方案来处理这个故障,DBA 很着急,重启数据库,可是数据库立马又被新的流量给打死了。

这就是缓存雪崩。

大约在 3 年前,国内比较知名的一个互联网公司,曾由于缓存事故,致使雪崩,后台系统所有崩溃,事故从当天下午持续到晚上凌晨 3~4 点,公司损失了几千万。

缓存雪崩的事前事中过后的解决方案以下。- 事前:redis 高可用,主从+哨兵,redis cluster,避免全盘崩溃。- 事中:本地 ehcache 缓存 + hystrix 限流&降级,避免 MySQL 被打死。- 过后:redis持久化,一旦重启,自动从磁盘上加载数据,快速恢复缓存数据。

用户发送一个请求,系统 A 收到请求后,先查本地 ehcache 缓存,若是没查到再查 redis。若是 ehcache和 redis 都没有,再查数据库,将数据库中的结果,写入 ehcache 和 redis 中。

限流组件,能够设置每秒的请求,有多少能经过组件,剩余的未经过的请求,怎么办?走降级!能够返回一些默认的值,或者友情提示,或者空白的值。

好处:- 数据库绝对不会死,限流组件确保了每秒只有多少个请求能经过。- 只要数据库不死,就是说,对用户来讲,2/5 的请求都是能够被处理的。- 只要有 2/5 的请求能够被处理,就意味着你的系统没死,对用户来讲,可能就是点击几回刷不出来页面,可是多点几回,就能够刷出来一次。

2)缓存穿透

对于系统 A,假设一秒 5000 个请求,结果其中 4000 个请求是黑客发出的恶意攻击。黑客发出的那 4000 个攻击,缓存中查不到,每次你去数据库里查,也查不到。

举个栗子。

数据库 id 是从 1 开始的,结果黑客发过来的请求 id 所有都是负数。这样的话,缓存中不会有,请求每次都“视缓存于无物”,直接查询数据库。这种恶意攻击场景的缓存穿透就会直接把数据库给打死。

解决方式很简单,每次系统 A 从数据库中只要没查到,就写一个空值到缓存里去,好比 set -999 UNKNOWN。而后设置一个过时时间,这样的话,下次有相同的 key 来访问的时候,在缓存失效以前,均可以直接从缓存中取数据。

3)缓存击穿

缓存击穿,就是说某个key很是热点,访问很是频繁,处于集中式高并发访问的状况,当这个key在失效的瞬间,大量的请求就击穿了缓存,直接请求数据库,就像是在一道屏障上凿开了一个洞。

解决方式也很简单,能够将热点数据设置为永远不过时;或者基于 redis or zookeeper 实现互斥锁,等待第一个请求构建完缓存以后,再释放锁,进而其它请求才能经过该 key 访问数据。

>> 面试常问问题三:

如何保证缓存与数据库的双写一致性?

一、面试官心理分析

你只要用缓存,就可能会涉及到缓存与数据库双存储双写,你只要是双写,就必定会有数据一致性的问题,那么你如何解决一致性问题?

二、面试题剖析

通常来讲,若是容许缓存能够稍微的跟数据库偶尔有不一致的状况,也就是说若是你的系统不是严格要求“缓存+数据库” 必须保持一致性的话,最好不要作这个方案,即:读请求和写请求串行化,串到一个内存队列里去。

串行化能够保证必定不会出现不一致的状况,可是它也会致使系统的吞吐量大幅度下降,用比正常状况下多几倍的机器去支撑线上的一个请求。

1)Cache Aside Pattern

最经典的缓存+数据库读写的模式,就是 Cache Aside Pattern。- 读的时候,先读缓存,缓存没有的话,就读数据库,而后取出数据后放入缓存,同时返回响应。- 更新的时候,先更新数据库,而后再删除缓存。

为何是删除缓存,而不是更新缓存?

缘由很简单,不少时候,在复杂点的缓存场景,缓存不仅仅是数据库中直接取出来的值。

好比可能更新了某个表的一个字段,而后其对应的缓存,是须要查询另外两个表的数据并进行运算,才能计算出缓存最新的值的。

另外更新缓存的代价有时候是很高的。是否是说,每次修改数据库的时候,都必定要将其对应的缓存更新一份?也许有的场景是这样,可是对于比较复杂的缓存数据计算的场景,就不是这样了。

若是你频繁修改一个缓存涉及的多个表,缓存也频繁更新。可是问题在于,这个缓存到底会不会被频繁访问到?

举个栗子,一个缓存涉及的表的字段,在 1 分钟内就修改了 20 次,或者是 100 次,那么缓存更新 20 次、100 次;可是这个缓存在 1 分钟内只被读取了 1 次,有大量的冷数据。

实际上,若是你只是删除缓存的话,那么在 1 分钟内,这个缓存不过就从新计算一次而已,开销大幅度下降。用到缓存才去算缓存。

其实删除缓存,而不是更新缓存,就是一个 lazy 计算的思想,不要每次都从新作复杂的计算,无论它会不会用到,而是让它到须要被使用的时候再从新计算。像 mybatis,hibernate,都有懒加载思想。

查询一个部门,部门带了一个员工的 list,没有必要说每次查询部门,都里面的 1000 个员工的数据也同时查出来啊。80%的状况,查这个部门,就只是要访问这个部门的信息就能够了。先查部门,同时要访问里面的员工,那么这个时候只有在你要访问里面的员工的时候,才会去数据库里面查询1000个员工。

2)最初级的缓存不一致问题及解决方案

问题:先更新数据库,再删除缓存。若是删除缓存失败了,那么会致使数据库中是新数据,缓存中是旧数据,数据就出现了不一致。

解决思路:先删除缓存,再更新数据库。若是数据库更新失败了,那么数据库中是旧数据,缓存中是空的,那么数据不会不一致。由于读的时候缓存没有,因此去读了数据库中的旧数据,而后更新到缓存中。

3)比较复杂的数据不一致问题分析

数据发生了变动,先删除了缓存,而后要去修改数据库,此时还没修改。一个请求过来,去读缓存,发现缓存空了,去查询数据库,查到了修改前的旧数据,放到了缓存中。随后数据变动的程序完成了数据库的修改。

完了,数据库和缓存中的数据不同了……

为何上亿流量高并发场景下,缓存会出现这个问题?

只有在对一个数据在并发的进行读写的时候,才可能会出现这种问题。其实若是说你的并发量很低的话,特别是读并发很低,天天访问量就 1 万次,那么不多的状况下,会出现刚才描述的那种不一致的场景。

可是问题是,若是天天的是上亿的流量,每秒并发读是几万,每秒只要有数据更新的请求,就可能会出现上述的数据库+缓存不一致的状况。

解决方案以下:

更新数据的时候,根据数据的惟一标识,将操做路由以后,发送到一个 jvm 内部队列中。读取数据的时候,若是发现数据不在缓存中,那么将从新读取数据+更新缓存的操做,根据惟一标识路由以后,也发送同一个jvm 内部队列中。

一个队列对应一个工做线程,每一个工做线程串行拿到对应的操做,而后一条一条的执行。这样的话一个数据变动的操做,先删除缓存,而后再去更新数据库,可是还没完成更新。此时若是一个读请求过来,没有读到缓存,那么能够先将缓存更新的请求发送到队列中,此时会在队列中积压,而后同步等待缓存更新完成。

这里有一个优化点,一个队列中,其实多个更新缓存请求串在一块儿是没意义的,所以能够作过滤,若是发现队列中已经有一个更新缓存的请求了,那么就不用再放个更新请求操做进去了,直接等待前面的更新操做请求完成便可。

待那个队列对应的工做线程完成了上一个操做的数据库的修改以后,才会去执行下一个操做,也就是缓存更新的操做,此时会从数据库中读取最新的值,而后写入缓存中。若是请求还在等待时间范围内,不断轮询发现能够取到值了,那么就直接返回;若是请求等待的时间超过必定时长,那么这一次直接从数据库中读取当前的旧值。

4)高并发的场景下,该解决方案要注意的问题

读请求长时阻塞

因为读请求进行了很是轻度的异步化,因此必定要注意读超时的问题,每一个读请求必须在超时时间范围内返回。该解决方案,最大的风险点在于说,可能数据更新很频繁,致使队列中积压了大量更新操做在里面,而后读请求会发生大量的超时,最后致使大量的请求直接走数据库。务必经过一些模拟真实的测试,看看更新数据的频率是怎样的。

另一点,由于一个队列中,可能会积压针对多个数据项的更新操做,所以须要根据本身的业务状况进行测试,可能须要部署多个服务,每一个服务分摊一些数据的更新操做。

若是一个内存队列里竟然会挤压 100 个商品的库存修改操做,每隔库存修改操做要耗费 10ms 去完成,那么最后一个商品的读请求,可能等待 后,才能获得数据,这个时候就致使读请求的长时阻塞。

必定要作根据实际业务系统的运行状况,去进行一些压力测试,和模拟线上环境,去看看最繁忙的时候,内存队列可能会挤压多少更新操做,可能会致使最后一个更新操做对应的读请求,会 hang 多少时间,若是读请求在 200ms 返回,若是你计算事后,哪怕是最繁忙的时候,积压 10 个更新操做,最多等待 200ms,那还能够的。

若是一个内存队列中可能积压的更新操做特别多,那么你就要加机器,让每一个机器上部署的服务实例处理更少的数据,那么每一个内存队列中积压的更新操做就会越少。

其实根据以前的项目经验,通常来讲,数据的写频率是很低的,所以实际上正常来讲,在队列中积压的更新操做应该是不多的。像这种针对读高并发、读缓存架构的项目,通常来讲写请求是很是少的,每秒的 QPS 能到几百就不错了。

咱们来实际粗略测算一下。

若是一秒有 500 的写操做,若是分红 5 个时间片,每 200ms 就 100 个写操做,放到 20 个内存队列中,每一个内存队列,可能就积压 5 个写操做。每一个写操做性能测试后,通常是在 20ms 左右就完成,那么针对每一个内存队列的数据的读请求,也就最多 hang 一下子,200ms 之内确定能返回了。

通过刚才简单的测算,咱们知道,单机支撑的写 QPS 在几百是没问题的,若是写 QPS 扩大了 10 倍,那么就扩容机器,扩容 10 倍的机器,每一个机器 20 个队列。

读请求并发量太高

这里还必须作好压力测试,确保恰巧碰上上述状况的时候,还有一个风险,就是忽然间大量读请求会在几十 毫秒的延时 hang 在服务上,看服务能不能扛的住,须要多少机器才能扛住最大的极限状况的峰值。

可是由于并非全部的数据都在同一时间更新,缓存也不会同一时间失效,因此每次可能也就是少数数据的缓存失效了,而后那些数据对应的读请求过来,并发量应该也不会特别大。

多服务实例部署的请求路由

可能这个服务部署了多个实例,那么必须保证说,执行数据更新操做,以及执行缓存更新操做的请求,都经过 Nginx 服务器路由到相同的服务实例上。

好比说,对同一个商品的读写请求,所有路由到同一台机器上。能够本身去作服务间的按照某个请求参数的hash 路由,也能够用 Nginx 的 hash 路由功能等等。

热点商品的路由问题,致使请求的倾斜

万一某个商品的读写请求特别高,所有打到相同的机器的相同的队列里面去了,可能会形成某台机器的压力过大。

就是说,由于只有在商品数据更新的时候才会清空缓存,而后才会致使读写并发,因此其实要根据业务系统去看,若是更新频率不是过高的话,这个问题的影响并非特别大,可是的确可能某些机器的负载会高一些。

>> 面试常问问题四

redis 的并发竞争问题是什么?如何解决这个问题?了解redis 事务的 CAS 方案吗?

一、面试官心理分析

这个也是线上很是常见的一个问题,就是多客户端同时并发写一个 key,可能原本应该先到的数据后到了,致使数据版本错了;或者是多客户端同时获取一个 key,修改值以后再写回去,只要顺序错了,数据就错了。

并且 redis 本身就有自然解决这个问题的 CAS 类的乐观锁方案。

二、面试题剖析

某个时刻,多个系统实例都去更新某个 key。能够基于 zookeeper 实现分布式锁。每一个系统经过zookeeper 获取分布式锁,确保同一时间,只能有一个系统实例在操做某个 key,别人都不容许读和写。

你要写入缓存的数据,都是从 mysql 里查出来的,都得写入 mysql 中,写入 mysql 中的时候必须保存一个时间戳,从 mysql 查出来的时候,时间戳也查出来。

每次要写以前,先判断一下当前这个 value 的时间戳是否比缓存里的 value 的时间戳要新。若是是的话,那么能够写,不然,就不能用旧的数据覆盖新的数据。

>> 面试常问问题五

生产环境中的 redis 是怎么部署的?

一、面试官心理分析

看看你了解不了解大家公司的 redis 生产集群的部署架构,若是你不了解,那么确实你就很失职了,你的redis 是主从架构?集群架构?用了哪一种集群方案?有没有作高可用保证?有没有开启持久化机制确保能够进行数据恢复?线上 redis 给几个 G 的内存?设置了哪些参数?压测后大家 redis 集群承载多少QPS?

兄弟,这些你必须是门儿清的,不然你确实是没好好思考过。

二、面试题剖析

redis cluster,10 台机器,5 台机器部署了 redis 主实例,另外 5 台机器部署了 redis 的从实例, 每一个主实例挂了一个从实例,5 个节点对外提供读写服务,每一个节点的读写高峰 qps 可能能够达到每秒 5 万,5 台机器最可能是 25 万读写请求/s。

机器是什么配置?32G 内存+ 8 核 CPU + 1T 磁盘,可是分配给 redis 进程的是 10g 内存,通常线上生产环境,redis 的内存尽可能不要超过 10g,超过 10g 可能会有问题。5 台机器对外提供读写,一共有 50g 内存。

由于每一个主实例都挂了一个从实例,因此是高可用的,任何一个主实例宕机,都会自动故障迁移,redis 从实例会自动变成主实例继续提供读写服务。

你往内存里写的是什么数据?每条数据的大小是多少?商品数据,每条数据是 10kb。100 条数据是 1mb,10 万条数据是 1g。常驻内存的是 200 万条商品数据,占用内存是 20g,仅仅不到总内存的 50%。目前高峰期每秒就是 3500 左右的请求量。

其实大型的公司,会有基础架构的 team 负责缓存集群的运维。

相关文章
相关标签/搜索