前往小程序,Get更优阅读体验!
立即前往
首页
学习
活动
专区
工具
TVP
发布
社区首页 >专栏 >当周杰伦把QQ音乐干翻的时候,作为程序猿我看到了什么?

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

原创
作者头像
why技术
修改2019-09-20 14:34:57
7090
修改2019-09-20 14:34:57
举报
文章被收录于专栏:why技术why技术

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

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

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

        咦!这啥?

        发生错误了?what the fuck!

        虽然说好不哭,但是还没开始听之前我就哭了:

        再等二分钟,别人都会唱了,我还没付钱!不要这样吧!QQ音乐你要振作起来啊!


知识结构的差别,导致我们看的角度不同

        不知道大家有没有看过美剧《越狱》。

        引用《奇葩说》辩手,也是我的男神,陈铭说的一段话:

主角迈克进入监狱救他哥哥,他走进了监狱,看到了那所监狱。 那一瞬间,我才发现迈克是个建筑学家。 他看到的监狱和我看到的监狱根本不是同一个监狱,。 我看到的是囚牢、操场、移动的犯人和狱卒。  而迈克一走进去,他看到的是通风管道、下水管道、紧急通道,他看到了墙后面所有的东西。 我这时意识到了一点,建筑学家跟我们因为知识背景和知识框架的不同,我们看到的是两个不一样的世界。  这是知识结构的差别带来的我们眼睛看到的世界的截然不同。

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

        周杰伦站在世界的这头,手机不停的响着:"微信到账三元"。

        程序员站在世界的这头,嘴里不停的喊着:"马上撑不住了,快降级、加机器、部服务"。

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

正文开始

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

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

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

        这图,是我这个灵魂画手亲手画的,当然还是和QQ音乐的架构没有半毛钱关系。甚至QQ音乐这次崩掉也许和缓存也没有半毛钱关系。但是我就是想到了这个画面。

        熟知redis的小伙伴一看到这里,下意识的就会说:"哟,这不是缓存击穿吗?"

        不知道缓存击穿的小伙伴,不要着急。看完这篇文章后,你不仅懂了缓存击穿,还会懂缓存穿透,缓存雪崩,以及对应的解决方案。

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

        再开始之前,我想多说一句话,垫个底:

        为什么我们要用缓存?

        其中大部分的原因是为了提高系统的响应速度,提升并发访问量。因为从内存中,比如redis读取数据和从磁盘中,比如mysql读取数据的响应速度是不在一个级别的。

        我给你打个形象但不是很恰到的比方吧:就类似于光速和音速的差距。


缓存击穿

缓存击穿的概念

        缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有。

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

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

缓存击穿的解决方案

方案一 互斥锁

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

        伪代码如下:

代码语言:txt
复制
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不仅仅是拿来做缓存的。这就是一个知识点呀。

        什么?你想看他是怎么实现的?对不起,我也只是知道并且会用它,内部原理我还说不太清楚,你可以自行查阅一下。所以:


缓存雪崩

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

        和前面讲的缓存击穿不同的是,缓存击穿指大量的请求并发查询同一条数据。

        缓存雪崩是不同数据都到了过期时间,导致这些数据在缓存中都查询不到,

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

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

缓存雪崩的解决方案

方案一 --- 加互斥锁

        如果是大量缓存在同一时间过期的情况,那么我们可以加互斥锁。

        等等,互斥锁不是前面介绍过了吗?

        是的,缓存雪崩可以看成多个缓存击穿,所以也可以使用互斥锁的解决方案,这里就不再赘述。

方案二 --- "错峰"过期

        如果是大量缓存在同一时间过期的情况,我们还有一种解决方案就是在设置key过期时间的时候,在加上一个短的随机过期时间,这样就能避免大量缓存在同一时间过期,引起的缓存雪崩。

比如设置一类key的过期时间是10分钟,在10分钟的基础上再加上60秒的随机事件,就像这样:

代码语言:txt
复制
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的持久化和恢复数据的逻辑了,这里由于篇幅关系,就不展开讲述了,也是知识点啊,朋友们,全是知识点啊。

        但是你要说这是一个解决方案,我自己都觉得有点牵强,主要意思你懂的吧?尽快,争分夺秒的恢复数据。

        至于是勇敢承担还是积极甩锅的事,恢复后再慢慢考虑。


缓存之外

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

        由于我之前是做支付相关开发的,从程序猿的角度,不仅看到了白花花的银子,还去算了一下平均每秒的销售量。

代码语言:txt
复制
2000000/30/60=1111

        约等于每秒1111张,一张就是一笔交易,按照规律,我保守猜测,在刚刚开始发售的时候,请求量至少是平均数的3倍吧。那么就是一秒约3500笔的交易。每一笔都是疯狂的写请求,再加上这期间大量的评论和转发。有可能这才是导致QQ音乐崩溃的诱因。


总结

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

      先学习方案的思想,融会贯通之后,你就能触类旁通。

      写代码难吗?不难。相反应该是整个开发过程中最简单的一步。难的是你要理解需求,了解场景,拿出对应的解决方案。解决方案都有了,代码不就是呼之欲出吗?

      到了写代码的这一步了,难的不是实现需求,难的是你怎么优雅的实现需求。优雅,你懂的吧?程序猿的自我修养之一。

      最后,还是之前说的:

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

      以上,是我个人看到周杰伦凭一首单曲,把QQ音乐干翻之后的一些思考和感悟。个人拙见,不足之处,欢迎大家指出问题。


表白这个男人

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

     表白这个男人。

    《说好不哭》,你可以说不好听。那是你的话语权,我捍卫你说话的权利。但是我不接受这个观点,这是我的权利。

      或者说,这是我和我身边大多数人的青春。

      完结撒花,下周再见。

      谢谢大家的阅读,如果觉得还不错的话,欢迎关注,再看并转发哦。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

原创声明:本文系作者授权腾讯云开发者社区发表,未经许可,不得转载。

如有侵权,请联系 cloudcommunity@tencent.com 删除。

评论
登录后参与评论
0 条评论
热度
最新
推荐阅读
目录
  • 别人都会唱了,而我还没付钱!
  • 知识结构的差别,导致我们看的角度不同
  • 正文开始
  • 缓存击穿
  • 缓存穿透的解决方案
  • 缓存雪崩
  • 缓存雪崩的解决方案
  • 缓存之外
  • 总结
  • 表白这个男人
相关产品与服务
云数据库 Redis
腾讯云数据库 Redis(TencentDB for Redis)是腾讯云打造的兼容 Redis 协议的缓存和存储服务。丰富的数据结构能帮助您完成不同类型的业务场景开发。支持主从热备,提供自动容灾切换、数据备份、故障迁移、实例监控、在线扩容、数据回档等全套的数据库服务。
领券
问题归档专栏文章快讯文章归档关键词归档开发者手册归档开发者手册 Section 归档