查看原文
其他

当周杰伦把QQ音乐干翻的时候,作为程序猿我看到了什么?

why技术 why技术 2022-09-10

点击蓝字关注我吧



1

别人都会唱了,而我还没付钱!



        2019年9月16日晚23点整,周杰伦发布新歌《说好不哭》。

        我经过一系列猛如虎的操作:


        咦!这啥?

        发生错误了?what the fuck!

        虽然说好不哭,但是还没开始听之前我就哭了:
        再等二分钟,别人都会唱了,我还没付钱!不要这样吧!QQ音乐你要振作起来啊!

1

知识结构的差别

导致我们看的角度不同



        不知道大家有没有看过美剧《越狱》。
        引用《奇葩说》辩手,也是我的男神,陈铭说的一段话:

主角迈克进入监狱救他哥哥,他走进了监狱,看到了那所监狱。

那一瞬间,我才发现迈克是个建筑学家。

他看到的监狱和我看到的监狱根本不是同一个监狱,。

我看到的是囚牢、操场、移动的犯人和狱卒。 

而迈克一走进去,他看到的是通风管道、下水管道、紧急通道,他看到了墙后面所有的东西。

我这时意识到了一点,建筑学家跟我们因为知识背景和知识框架的不同,我们看到的是两个不一样的世界。 

这是知识结构的差别带来的我们眼睛看到的世界的截然不同。

        我举这个例子想要说明的是,当我站在程序员的角度看QQ音乐崩了这件事情的时候,我看到了什么,我想到了什么,这是一个由无数服务器、若干微服务、负载均衡、多级缓存、巨大流量、分库分表、读写分离、搜索引擎、性能优化、高速硬盘......组成的世界。

        周杰伦站在世界的这头,手机不停的响着:"微信到账三元"。
        程序员站在世界的这头,嘴里不停的喊着:"马上撑不住了,快降级、加机器、部服务"。

中间的架构图是我随便找的,和QQ音乐无关

1

正文开始



        好了,当顶级流量周杰伦把QQ音乐干翻的时候,我作为程序猿看到了什么?且听我细细道来。

        当我点击立即购买,没有弹出支付页面,而是弹出"发生错误了"的提示,如果那一晚你也在关注周董的新歌,我相信你和我做了同样的动作:马上关闭了页面,再次点击了立即购买,但是还是弹出错误提示。那个时候,我才反应过来,哦,原来是QQ音乐崩了。

        这个时候我的脑海里面立即浮现出了一个由请求,redis缓存,数据库组成画面。

        这图,是我这个灵魂画手亲手画的,当然还是和QQ音乐的架构没有半毛钱关系。甚至QQ音乐这次崩掉也许和缓存也没有半毛钱关系。但是我就是想到了这个画面。
        熟知redis的小伙伴一看到这里,下意识的就会说:"哟,这不是缓存击穿吗?"
        不知道缓存击穿的小伙伴,不要着急。看完这篇文章后,你不仅懂了缓存击穿,还会懂缓存穿透,缓存雪崩,以及对应的解决方案。

        熟知redis,并且对这几个概念烂熟于心的小伙伴这个时候可能想要走了,没关系,答应我,走之前,拉到最后点个"在看"。谢谢!

        再开始之前,我想多说一句话,垫个底:
        为什么我们要用缓存?
        其中大部分的原因是为了提高系统的响应速度,提升并发访问量。因为从内存中,比如redis读取数据和从磁盘中,比如mysql读取数据的响应速度是不在一个级别的。
        我给你打个形象但不是很恰到的比方吧:就类似于光速和音速的差距。

1

缓存击穿



缓存击穿的概念

        缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有。
这种情况一般来说就是缓存过期了。但是这时由于并发访问这个缓存的用户特别多,这是一个热点key,这么多用户的请求同时过来,在缓存里面都没有取到数据,所以又同时去访问数据库取数据,引起数据库流量激增,压力瞬间增大,直接崩溃给你看。

        所以一个数据有缓存,每次请求都从缓存中快速的返回了数据,但是某个时间点缓存失效了,某个请求在缓存中没有请求到数据,这时候我们就说这个请求就"击穿"了缓存。

缓存击穿

缓存击穿的解决方案

方案一 互斥锁
        互斥锁方案的思路就是如果从redis中没有获取到数据,就让一个线程去数据库查询数据,然后构建缓存,其他的线程就等着,过一段时间后再从redis中去获取。


        伪代码如下:
String get(String jay) {
   String music = redis.get(jay);
   if (music == null) {
   //nx的方式设置一个key=jay_lock,
   //value=aiwobieku_lock的数据,60秒后过期
    if (redis.set("jay_lock", "aiwobieku_lock","nx",60)) {
        //从数据库里获取数据
        music = db.query(jay);
        //构建数据,24*60*60s后过期
        redis.set(jay, music,24*60*60);
        //构建成功,可以删除互斥锁
        redis.delete("jay_lock");
    } else {
        //其他线程休息100ms后重试
        Thread.sleep(100);
        //再次获取数据,如果前面在100ms内设置成功,则有数据
        music = redis.get(jay);
    }
  }
}

        这个方案能解决问题,但是一个线程构建缓存的时候,另外的线程都在睡眠或者轮询。
        而且在这个四处宣讲高并发,低延时的时代,你居然让你的用户等待了宝贵的100ms。有可能别人比你快100ms,就抢走了大批用户。
你说,你是何居心?是不是敌人派来的卧底?

方案二 后台续命
        后台续命方案的思想就是,后台开一个定时任务,专门主动更新即将过期的数据。
        比如程序猿设置jay这个热点key的时候,同时设置了过期时间为60分钟,那后台程序在第55分钟的时候,会去数据库查询数据并重新放到缓存中,同时再次设置缓存为60分钟。

        呃,这个方案呢。怎么说呢,我感觉很奇怪。
        可能是没有想到合适的应用场景,而且觉得代码实现起来比较复杂。

        但是这种思想是没问题的,我之前就借助这样的思想开发过一个功能:
简单的描述一下就是:
        流水号系统,采用数据库自增主键来保证唯一性,但是属于非常关键的系统,为了降低数据库异常对服务带来的冲击,所以服务启动后会就会预先在缓存中缓存5000个流水号。然后后台job定时检查缓存中还剩下多少流水号,如果小于1000个,则再从数据库中生成流水号,补充到缓存中,让缓存中的流水号再次回到5000个。这样做的好处就是数据库异常后,我至少保证还有5000个缓存可以保证上游业务,我有一定的时间去恢复数据库。

后台续命的另一种展示


方案三 永不过期
        这个方案就有点简单粗暴了。
        见名知意,如果结合实际场景你用脚趾头都能想到这个key是一个热点key,会有大量的请求来访问这个数据。对于这样的数据你还设置过期时间干什么?直接放进去,永不过期。

永不过期

        这个热点流量就类似于
周董发新歌,鹿晗爱晓彤,唱跳rap和篮球的流量。
        比起产品经理给你提需求,让你开发的时候,给你预报的流量靠谱多了。
        我就遇见过产品经理来提需求的时候说:
这个需求特别急,最好明天就上线。上线流量马上来,你的系统要抗住。
        结果往往是:
熬夜加班通宵干,终于爆肝弄上线。
结果上线没动静,他说商户不接了。
        大道至简,我个人喜欢这个方案。

        但是具体情况具体分析,没有一套方案走天下的。
        比如,如果这个key是属于被各种"自来水"活生生的炒热的呢?就像哪吒一样,你预想不到这个key会闹出这么大的动静。这种情况你这么处理?
        所以,具体情况,具体分析。但是思路要清晰,最终方案都是常规方案的组合或者变种。

1

缓存穿透



        缓存穿透是指一个请求要访问的数据,缓存和数据库中都没有,而用户短时间、高密度的发起这样的请求,每次都打到数据库服务上,给数据库造成了压力。一般来说这样的请求属于恶意请求,

缓存穿透

        根据图片显示的,缓存中没有获取到数据,然后去请求数据库,没想到数据库里面也没有。
比如明明是周杰伦的演唱会,你冲过保安大哥,上台对周董说:"给我来个林俊杰的签名"。
最可恶的是你也知道,周杰伦那里没有林俊杰的签名。
恶意请求,占用资源。当有成千上万这样的恶意请求的时候,你不做处理,就会给周杰伦,哦不,数据库带来压力。

缓存穿透的解决方案

方案一 --- 缓存空对象
        缓存空对象就是在数据库即使查到的是空对象,我们也把这个空对象缓存起来。

缓存空对象

        下次同样请求就会命中这个空对象,缓存层就处理了这个请求,不会对数据库产生压力。

        这样实现起来简单,开发成本很低。但这样随之而来的两个面试题必须要注意一下:
第一个问题:如果在某个时间,缓存为空的记录,在数据库里面有值了,你怎么办?
        我知道三个解决方法:
解决方法一:设置缓存的时候,同时设置一个过期时间,这样过期之后,就会重新去数据库查询最新的数据并缓存起来。
解决方法二:如果对实时性要求非常高的话,那就写数据库的时候,同时写缓存。这样可以保障实时性。
解决方法三:如果对实时性要求不是那么高,那就写数据库的时候给消息队列发一条数据,让消息队列再通知处理缓存的逻辑去数据库取出最新的数据。
        另外说明一下:对于数据库和缓存一致性的问题,我不打算在这篇文章讨论。个人感觉这是一个引战的问题。后面会单独介绍我自己对于这个问题的看法。请大神不要在这"开杠"。
第二个问题:对于恶意攻击,请求的时候key往往各不相同,且只请求一次,那你要把这些key都缓存起来的话,因为每个key都只请求一次,那还是每次都会请求数据库,没有保护到数据库呀?
        这个时候,你就告诉他:"布隆过滤器,了解一下"。
    
方案二 --- 布隆过滤器
        什么是布隆过滤器?

本质上布隆过滤器是一种数据结构,比较巧妙的概率型数据结构(probabilistic data structure),特点是高效地插入和查询,可以用来告诉你 “某样东西一定不存在或者可能存在”。 


相比于传统的 List、Set、Map 等数据结构,它更高效、占用空间更少,但是缺点是其返回的结果是概率性的,而不是确切的。

        当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。
        所以布隆过滤器返回的结果是概率性的,所以它能缓解数据库的压力,并不能完全挡住,这点必须要明确。

        guava组件可以开箱即用的实现一个布隆过滤器,但是guava是基于内存的,所以不太适用于分布式环境下。
    
        要在分布式环境下使用布隆过滤器,那还得redis出马,redis可以用来实现布隆过滤器。
看到了吗,redis不仅仅是拿来做缓存的。这就是一个知识点呀。
        什么?你想看他是怎么实现的?对不起,我也只是知道并且会用它,内部原理我还说不太清楚,你可以自行查阅一下。所以:


1

缓存雪崩



        缓存雪崩是指缓存中大多数的数据在同一时间到达过期时间,而查询数据量巨大,这时候,又是缓存中没有,数据库中有的情况了。请求都打到数据库上,引起数据库流量激增,压力瞬间增大,直接崩溃给你看

        和前面讲的缓存击穿不同的是,缓存击穿指大量的请求并发查询同一条数据。
        缓存雪崩是不同数据都到了过期时间,导致这些数据在缓存中都查询不到,


        或是缓存服务直接挂掉了,所以缓存都没有了。

        总之,请求都打到了数据库上。对于数据库来说,流量雪崩了,很形象。

缓存雪崩的解决方案

方案一 --- 加互斥锁
        如果是大量缓存在同一时间过期的情况,那么我们可以加互斥锁。
        等等,互斥锁不是前面介绍过了吗?
        是的,缓存雪崩可以看成多个缓存击穿,所以也可以使用互斥锁的解决方案,这里就不再赘述。

方案二 --- "错峰"过期
        如果是大量缓存在同一时间过期的情况,我们还有一种解决方案就是在设置key过期时间的时候,在加上一个短的随机过期时间,这样就能避免大量缓存在同一时间过期,引起的缓存雪崩。
比如设置一类key的过期时间是10分钟,在10分钟的基础上再加上60秒的随机事件,就像这样:
redis.set(key,value,10*60+RandomUtils.nextInt(0, 60),TimeUnit.SECONDS)

方案三 --- 缓存集群
        如果对于缓存服务挂掉的情况,大多原因是单点应用。那么我们可以引入redis集群,使用主从加哨兵。用Redis Cluster部署集群很方便的,可以了解一下。
        当然这是属于一种事前方案,在使用单点的时候,你就得考虑服务宕机后引起的问题。所以,事前部署集群,提高服务的可用性。
   
方案四 --- 限流器+本地缓存
        那你要说如果Cluster集群也挂了怎么办呢?

        如果你能层层深入考虑到集群也挂了怎么办的话,那你可真是一个爱思考的好同学。其实就是对服务鲁棒性的考虑:

鲁棒性(robustness)就是系统的健壮性。它是指一个程序中对可能导致程序崩溃的各种情况都充分考虑到,并且作相应的处理,在程序遇到异常情况时还能正常工作,而不至于死机。

        这个时候,可以考虑一下引入限流器,比如 Hystrix,然后实现服务降级。
        假设你的限流器设置的一秒钟最多5千个请求,那么这个时候来了8千个请求,多出来的3000个就走降级流程,对用户进行友好提示。
        进来的这5000个请求,如果redis挂了,还是有可能会干翻数据库的,那么这个时候我们在加上如果redis挂了,就查询类似于echcache或者guava cache本地缓存的逻辑,则可以帮助数据库减轻压力,挺过难关。

方案五 --- 尽快恢复
        这个没啥说的了吧?
        大哥,你服务挂了诶?赶紧恢复服务啊。
        这个时候就涉及到redis的持久化和恢复数据的逻辑了,这里由于篇幅关系,就不展开讲述了,也是知识点啊,朋友们,全是知识点啊。

        但是你要说这是一个解决方案,我自己都觉得有点牵强,主要意思你懂的吧?尽快,争分夺秒的恢复数据。
        至于是勇敢承担还是积极甩锅的事,恢复后再慢慢考虑。

1

缓存之外



        据官方数据,周杰伦《说好不哭》发售不到半小时,销量200万张!

        由于我之前是做支付相关开发的,从程序猿的角度,不仅看到了白花花的银子,还去算了一下平均每秒的销售量。
2000000/30/60=1111
        约等于每秒1111张,一张就是一笔交易,按照规律,我保守猜测,在刚刚开始发售的时候,请求量至少是平均数的3倍吧。那么就是一秒约3500笔的交易。每一笔都是疯狂的写请求,再加上这期间大量的评论和转发。有可能这才是导致QQ音乐崩溃的诱因。

1

总结



       前面介绍了缓存击穿,缓存穿透,缓存雪崩的场景和其对应的各种解决方案。但是不同的解决方案有不同的适用场景和优缺点,你需要仔细权衡自己的需求之后妥善适用它们。
        

      先学习方案的思想,融会贯通之后,你就能触类旁通。
      写代码难吗?不难。相反应该是整个开发过程中最简单的一步。难的是你要理解需求,了解场景,拿出对应的解决方案。解决方案都有了,代码不就是呼之欲出吗?
      到了写代码的这一步了,难的不是实现需求,难的是你怎么优雅的实现需求。优雅,你懂的吧?程序猿的自我修养之一。

      最后,还是之前说的:
知识结构的差别,带来的我们眼睛看到的世界的截然不同。
      以上,是我个人看到周杰伦凭一首单曲,把QQ音乐干翻之后的一些思考和感悟。个人拙见,不足之处,欢迎大家指出问题。

1

表白这个男人



      最后,放一段我2014年,年终总结中的一段话吧:

     表白这个男人。
    《说好不哭》,你可以说不好听。那是你的话语权,我捍卫你说话的权利。但是我不接受这个观点,这是我的权利。
      或者说,这是我和我身边大多数人的青春。

      完结撒花,下周再见。
      谢谢大家的阅读,如果觉得还不错的话,欢迎关注,再看并转发哦。
      

往期精彩回顾

走心---普通二本,毕业三年,北漂之后,我是怎么成为程序猿的。

从源码角度解析---Dubbo2.7新特性之异步化改造!


事务没回滚?来,我们从现象到原理一起分析一波!


有的线程它死了,于是它变成了一道面试题!


     

图:why技术

文:why技术

排版:why技术

扫码查看更多内容

 


您可能也对以下帖子感兴趣

文章有问题?点此查看未经处理的缓存