大家好,我是伟伟。就像这样。前几天有位读者给我留言,说他在面试的时候遇到一道场景题:他说一时半会找不到回答问题的角度,感觉没回答到重点.仔细想了想,还真的觉得这道题有点奇怪,有一种让人说千言万语的魔力,突然又不知从何说起。你为什么这么说?我们先看题,仔细看题,我给你翻译一下。如果线上Redis挂了。然后所有请求都打到数据库,数据库也挂了。这是什么?Redis挂了,不就是缓存没了吗?缓存没了,不就是缓存雪崩吗?缓存雪崩,不会导致数据库挂掉吗?当你提到“缓存雪崩”这四个词,缓存穿透,缓存穿透,这几位兄弟,是不是立刻条件反射的在脑海中浮现出来,连同对应的几种解决方案。然后,就像背书一样,所有的缓存都没有了,数据库里没有缓存,缓存里也没有,数据库里也没有……嘴巴不停地张了几分钟。另外,很多同学会混淆缓存击穿和缓存穿透。你改变符号,缓存穿透,缓存穿透。磨损,刚刚通过缓存。彻底,直接到最后。仔细品尝应该不会记错。除了上面的千篇一律的“Redis缓存三重打击”之外,还隐藏着另一种千篇一律:Redis宕机了,为什么宕机了?为什么挂了?有单一的问题吗?这不就是个问题吗?你的Redis服务高可用吗?说到Redis的高可用,你的脑海里一定会立刻浮现出master-slave、sentry和cluster?想到这里,足足有几分钟的时间,嘴巴才没有停顿地张开。但是几千字过了几分钟,马上就被下面的问题搞晕了?这个时候应该怎么恢复?现在问你如何恢复是理所当然的事情。你得先谈如何恢复,再谈如何预防。上来就回答前面提到的“缓存三合一”和“高可用架构”,包括大部分同学能想到的多级缓存、限流措施、服务降级、熔断机制等。这些有点牵强。因为这些手段毕竟是事前的预防措施,据说这些背书的痕迹更加明显。必须回答这个问题。从恢复到事前预防是重点,那么如何才能更自然呢?首先,让我们谈谈如何在活动期间恢复。其实我觉得三言两语就结束了。服务挂了,大哥,怎么恢复,当然是重启服务。从运维人员的角度来说,当然优先重启Redis和数据库服务。但是在开始之前,你必须做一个小操作来移除流量。可以先在入口处拦截流量。比如简单粗暴的通过Nginx的配置把请求转移到一个设计良好的错误页面,就是这么一个意思。这样做的目的是防止出现流量过大的情况,新启动的服务直接启动,一个接一个,一个挂掉。如果上手之后还搞不定,请在心里默念一下分布式系统的三大武器:搞不定就加钱堆机器。如果觉得堆机没什么技术含量,可以从缓存预热的角度来回答。即当Redis服务重启时,系统先通过程序放入一些已知的热点key,然后系统对外提供服务,防止缓存崩溃。而且,以上这一系列操作与开发人员关系不大,主要是运维同学完成的。大部分开发同学在设计服务的时候都是为了让服务无状态化,从而达到快速水平扩展的目的。至于如何快速横向扩容,那是运维同学的事了。暂时不要抢别人的饭碗。回答完这个,就可以用“但是”提前过渡到预防,开始自己的表演了。故作沉思,我对面试官说“但是”:我觉得从技术方案的角度来说,我们应该提前做好预防。所有这些问题都是由Redis崩溃造成的,也就是缓存雪崩。在高并发的情况下,除了缓存雪崩,还要考虑缓存的击穿和穿透。为什么Redis会崩溃?使用它是错误的吗?不保证高可用性吗?是否需要在服务中考虑限流或熔断机制,最大程度的保护程序的运行?还是应该建立多级缓存机制?,防止Redis挂掉后大量流量直接打到MySQL服务,导致数据库崩溃?至此,“但是”完成,答题方向恢复到中途,转向事前预防。可以开始“背诵”了。这里简单说说缓存问题的三重打击和Redis的高可用。至于多级缓存,可以看我之前发的这篇文章:《这波舒服了,落地多级缓存!》。缓存击穿先说一下缓存击穿的概念。缓存击穿是指请求要访问的数据不在缓存中,而是在数据库中。这通常意味着缓存已过期。但是这个时候有很多用户并发访问缓存。这是一个热点键。那么多的用户请求同时过来,但是没有从缓存中取数据,于是同时去数据库取数据,导致数据库流量激增,压力增大。瞬间增大,直接崩塌给你看。所以一段数据是有缓存的,每次请求都很快从缓存中返回数据,但是在某个时间点,缓存失效了,某个请求没有请求到缓存中的数据,这个时候我们说请求“打坏”缓存。对于这种场景,一般有三种对应的解决方案。第一种是只释放一个请求到数据库,然后做构建缓存的操作。多个请求只会释放一个.怎么做呢?直接用Redis的setNX命令设置一个flag就可以了,设置成功就释放,设置失败就轮询等待。第二种方案是后台续命.这个方案的思路是在后台开启一个定时任务,主动更新即将过期的数据,比如在程序中设置whyhotspotkey时,设置过期时间为10分钟,然后后台程序会去数据库查询数据并在第8分钟将其放回缓存中,并再次将缓存设置为10分钟。怎么样,是不是有点像Redisson的分布式锁看门狗?我认为这些想法是一致的。只是当方案实现的时候,从代码编写的角度来说有点麻烦。我也用这个想法开发了一个序列号系统。大概是这样。序列号系统是一个非常关键的系统。为了减少数据库异常对服务的影响,服务启动后会在缓存中为每个业务系统预缓存5000个序号。然后后台Job定时检查缓存中还剩多少序号。如果小于1000,它会预先生成一个新的序列号加入到缓存中,这样缓存中的序列号就会重新回到5000。这样做的好处是,数据库异常后,我保证至少有5000个缓存来保证上游业务,我有一定的时间来恢复数据库。这也算是一种延续生命在后台的想法吧。第三种方法很简单:永不过期。为什么缓存坏了?是不是设置了超时时间,然后被回收了?那我就不用设置超时时间了。如果结合实际场景动动脚趾,可以认为这个键肯定会是热键。有大量访问此数据的请求。而这个key对应的value是不会改变的。为什么要为这样的数据设置过期时间?只要放进去,它就永远不会过期。其实上面背景中的延寿思想的最终体现就是永不过期。只是后台的lifeextensionidea会主动更新缓存,适合缓存会变化的场景。会出现缓存不一致的情况,这取决于你的业务场景能接受缓存不一致多久。总之,具体情况,具体分析。但思路应该清晰,最终的解决方案是常规方案的组合或变体。缓存穿透那么什么是缓存穿透呢?缓存穿透是指一个请求要访问的数据,不在缓存中,也不在数据库中,用户短时间高密度发起这样的请求,每次都命中数据库服务,它给数据库带来压力。一般来说,此类请求属于恶意请求。比如我是技术公众号,你明明知道我没有,却非要来我家买瓶啤酒,这就是恶意请求。如何解决?两种选择。第一个缓存空对象。即即使没有查询到数据库中的数据,我们也将这个请求缓存为key,value可以为NULL。同样的请求下次会命中这个NULL,缓存层会处理这个请求,不会给数据库造成压力。这实现起来很简单,开发成本也很低。但是一定要注意下面的面试题:对于恶意攻击,请求的时候key往往是不同的,并且只请求一次,那么如果要缓存这些key,因为每个key只请求一次,数据库就会还是每次都被请求,数据库没有保护?这个问题,布隆过滤器,明白了吗?之前写过这篇关于Bloomfilter的文章,大家可以看看:《布隆,牛逼!布谷鸟,牛逼!》Bloomfilter的特点是当某个值存在时,这个值可能不存在。当它说它不存在时,它肯定不存在。因此,基于这个特性,所有已有的数据都可以内置到Bloomfilter中。然后它可以帮助阻止大部分攻击。但是还有一系列容易被忽略的问题:面试官:Bloomfilter容量有限,不支持删除。随着里面内容的增多,误判率也会增加。请问,你是怎么解决这个问题的?回答问题也有两个方向。首先,如果不支持删除,就换一个支持删除的Bloom滤镜轮。比如我之前文章中提到的布谷鸟过滤器。或者只是提前重构布隆过滤器。例如,当容量达到50%时,申请一个新的更大的Bloomfilter来替换之前的filter。唯一需要注意的是,重构需要知道需要重构哪些数据,所以一定要有个地方记录下来。比如Redis、数据库,甚至内存缓存都可以。没登陆过没关系,可以放心回答。你要相信,80%的面试官都没有落地过,可能你们看的都是一样的资料。缓存雪崩缓存雪崩是指缓存中的大部分数据同时到达过期时间,查询数据量巨大。这个时候缓存是没有的,数据库里有。所有的请求都打到数据库上,导致数据库流量激增,压力瞬间增大,直接崩溃给你看。与上面提到的缓存击穿不同,缓存击穿是指大量请求并发查询同一条数据。缓存雪崩是指不同的数据都到了过期时间,导致无法在缓存中查询到这些数据。雪崩还是很形象的。防止雪崩的解决方案就是错峰呼气。在设置key过期时间的时候,加上一个很短的随机过期时间,避免大量缓存同时过期造成缓存雪崩。如果出现雪崩,我们可以有服务降级、熔断、限流的手段来拒绝一些请求,保证服务正常。但是,这些都对用户体验造成了一定的影响。假设我们的程序有这个逻辑,也是用来覆盖底线的。从用户的角度来看,他们不想陷入这样的逻辑。因此,防止雪崩仍然很重要。最后还有一种雪崩,就是整个Redis服务挂了。所以,接下来就要说说Redis服务的高可用架构了。Redis高可用架构说到Redis高可用架构,大家基本上可以想到三种模式:master-slave、sentinel、cluster。主从结构很简单,就不说了。主要缺点是出现故障时需要人工干预。如果需要人工干预,那不是真正的高可用。哨兵和集群是官网上写的两种方案:https://redis.io/topics/introduction如果划线如上,翻译过来就是:Redis通过Redis哨兵(Sentinel)和Cluster提供高可用性(highavailability)簇。其中,Sentinel是官方的高可用方案(officialhighavailabilitysolution):所以我们主要说说Sentinel模式。Sentinel用于管理多个Redis服务器。我从《Redis开发与运维》书上截了一张图给大家看看:它主要执行三类任务:监控(Monitoring):Sentinel会不断检查你的masterserver和slaveservers是否正常工作。通知:当被监控的Redis服务器出现问题时,Sentinel可以通过API向管理员或其他应用发送通知。自动故障转移(Automaticfailover):当一个主服务器无法正常工作时,Sentinel会启动一个自动故障转移操作,将故障主服务器的其中一台从服务器升级为新的主服务器,并让故障主服务器其他从属服务器复制新的主服务器;当客户端尝试连接到故障的主服务器时,集群也会将新的主服务器的地址返回给客户端,这样集群就可以使用新的主服务器来替换故障的服务器。Sentinel其实是一个分布式系统,我们可以运行多个Sentinel。然后这些Sentinels需要相互通信,交换信息,投票决定是否进行自动故障转移,并选择哪个从服务器作为新的主服务器。Sentinels之间采用的协议是gossip,这是一个去中心化的协议。它实现了最终一致性,这很有趣。另外,如果master节点挂掉了,Sentinel是用什么规则来选出新的master节点的,也就是election一般的过程是怎么样的,而且偶尔也会出现在面试过程中。以前也有人问过,还好我当时背的很熟练。我简单说一下规则。没有它,记忆就够了:挂在主节点下的从节点中,被标记为主观下线、断开连接,或者最后一次回复PING命令的时间大于五秒的从节点没有资格参与在选举中。在downmaster节点下的slave节点中,与downmaster节点的连接断开时间超过down-after配置指定时间十倍以上的slave节点没有资格参与选举。经过以上两轮淘汰后,在剩余的从服务器中,选择复制偏移量最大的从服务器作为新的主服务器。如果复制偏移量不可用,或者副本具有相同的复制偏移量,则具有最小运行ID的从服务器将成为新的主服务器。实际上,执行上述操作的是哨兵。而我们通常会有三个以上的Sentinel,那么这些操作会由哪个Sentinel来执行呢?其实这个Sentinel也需要从多个Sentinel中选举出来,选举出来的Sentinel就是leaderSentinel。在选举leadingsentinel时,采用Raft算法。至于哨兵模式的建设,一般来说就是运维的工作。不过网上有很多搭建教程,如果能跟着教程自己搭建一波就更好了。相信我,你在搭建过程中会遇到各种各样的问题,而这些问题就是你的收获。回到开篇,回到最开始的那个面试题:其实看到这个题我就想到了一直被炮轰的微博。巧的是,这周又吃了一波吴某凡的瓜。我在看女排的直播。当我看到报道的时候,我的表情大概是这样的:周六晚上我基本就是这个表情在瓜地里蹦蹦跳跳的,太好吃了。不过对于凡凡这波,不知道是因为凡凡的流量不好,还是微博的架构经受住了考验。微博还是比较流畅的,没有出现大规模的、非常明显的服务卡顿现象。在我的印象中,最近的一条微博是关于鹿晗和关晓彤的。不是因为我关注他们,而是因为我关注了那天要结婚的程序员。要说丁振凯这位同学,实在是太惨了。结婚的时候偶遇鹿晗,公布恋情。在海外度假时,偶遇双宋官宣。妻子临盆时,他偶遇华晨宇,承认自己与张碧晨私生一女。这次去看了,表现比较平静。应该是一手抱娃,一手扩容,顺便吃瓜。提到鹿晗,微博小助手说是因为一条微博的转发和评论太多了。这不全面,单纯的转发评论,压不住大微博。而那天鹿晗的微博,应该不是他所有微博中转发和评论最多的一条。就是因为并发转发和评论太高太高了,是我这辈子碰不到的即时流量。吃瓜群众也蜂拥而至,短时间内在线率飞速爆发,服务器被秒杀:关于这个问题,在知乎上看到一个评论,觉得很好。截图:https://www.zhihu.com/question/66346687看看这个场景是不是和面试官问的问题很像?强如微博,也加了1000台服务器应对流量高峰。那么,如果服务宕机了怎么办?重新开始。重启还是失败怎么办?加钱扩充机器。如果鹿晗关晓彤事件发生,著名狗仔卓伟就能提前爆料,提前采取措施。或许,微博能抵挡住那一波流量高峰。如果吴某签了这件事情,北京警方可以提前和微博沟通,在发布前通知相关微博人员,哪怕是提前10分钟?或许,更多的人可以吃得顺滑丝滑。
