侧边栏壁纸
博主头像
why

一个主要敲代码,经常怼文章,偶尔拍视频的成都人。

  • 累计撰写 181 篇文章
  • 累计创建 11 个标签
  • 累计收到 88 条评论

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

why
why
2019-09-20 / 1 评论 / 3 点赞 / 346 阅读 / 6,095 字
温馨提示:
关注公众号why技术,第一时间接收最新文章。

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

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

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

咦!这啥?

发生错误了?what the fuck!

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

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

看问题的角度

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

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

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

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

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

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

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

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

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

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

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

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

正文开始

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

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

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

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

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

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

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

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

为什么我们要用缓存?

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

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

缓存击穿

缓存击穿的概念

缓存击穿是指一个请求要访问的数据,缓存中没有,但数据库中有。
这种情况一般来说就是缓存过期了。但是这时由于并发访问这个缓存的用户特别多,这是一个热点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会闹出这么大的动静。

这种情况你这么处理?

所以,具体情况,具体分析。但是思路要清晰,最终方案都是常规方案的组合或者变种。

缓存穿透

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

根据图片显示的,缓存中没有获取到数据,然后去请求数据库,没想到数据库里面也没有

比如明明是周杰伦的演唱会,你冲过保安大哥,上台对周董说:"给我来个林俊杰的签名"。

最可恶的是你也知道,周杰伦那里没有林俊杰的签名。

恶意请求,占用资源。当有成千上万这样的恶意请求的时候,你不做处理,就会给周杰伦,哦不,数据库带来压力。

方案一:缓存空对象

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

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

这样实现起来简单,开发成本很低。但这样随之而来的两个面试题必须要注意一下。

第一个问题:

如果在某个时间,缓存为空的记录,在数据库里面有值了,你怎么办?

我知道三个解决方法:

  • 解决方法一:设置缓存的时候,同时设置一个过期时间,这样过期之后,就会重新去数据库查询最新的数据并缓存起来。
  • 解决方法二:如果对实时性要求非常高的话,那就写数据库的时候,同时写缓存。这样可以保障实时性。
  • 解决方法三:如果对实时性要求不是那么高,那就写数据库的时候给消息队列发一条数据,让消息队列再通知处理缓存的逻辑去数据库取出最新的数据。

另外说明一下:对于数据库和缓存一致性的问题,我不打算在这篇文章讨论。个人感觉这是一个引战的问题。后面会单独介绍我自己对于这个问题的看法。请大神不要在这"开杠"。

第二个问题:

对于恶意攻击,请求的时候key往往各不相同,且只请求一次,那你要把这些key都缓存起来的话,因为每个key都只请求一次,那还是每次都会请求数据库,没有保护到数据库呀?

这个时候,你就告诉他:"布隆过滤器,了解一下"。

方案二:布隆过滤器

什么是布隆过滤器?

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

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

当布隆过滤器说某个值存在时,这个值可能不存在;当它说不存在时,那就肯定不存在。

所以布隆过滤器返回的结果是概率性的,所以它能缓解数据库的压力,并不能完全挡住,这点必须要明确。

guava组件可以开箱即用的实现一个布隆过滤器,但是guava是基于内存的,所以不太适用于分布式环境下。

要在分布式环境下使用布隆过滤器,那还得redis出马,redis可以用来实现布隆过滤器。

看到了吗,redis不仅仅是拿来做缓存的。这就是一个知识点呀。

什么?你想看他是怎么实现的?

对不起,我也只是知道并且会用它,内部原理我还说不太清楚,你可以自行查阅一下。所以:

缓存雪崩

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

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

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

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

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

方案一:加互斥锁

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

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

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

方案二:"错峰"过期

如果是大量缓存在同一时间过期的情况,我们还有一种解决方案就是在设置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的持久化和恢复数据的逻辑了,这里由于篇幅关系,就不展开讲述了,也是知识点啊,朋友们,全是知识点啊。

但是你要说这是一个解决方案,我自己都觉得有点牵强,主要意思你懂的吧?

尽快,争分夺秒的恢复数据。

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

缓存之外

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

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

2000000/30/60=1111

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

总结

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

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

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

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

最后,还是之前说的:

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

以上,是我个人看到周杰伦凭一首单曲,把QQ音乐干翻之后的一些思考和感悟。

个人拙见,不足之处,欢迎大家指出问题。

表白这个男人

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

表白这个男人。

《说好不哭》,你可以说不好听。

那是你的话语权,我捍卫你说话的权利。但是我不接受这个观点,这是我的权利。

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

最后说一句

好了,看到了这里了,关注一下我的公众号[why技术]吧,文章写好后第一时间会先发布在公众号里面。

写文章很累的,需要一点正反馈。你的关注,就是强有力的正反馈!

抱拳了,铁子!

0

评论区