• 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html
  • 极客专栏正式上线!欢迎访问 https://www.jikewenku.com/topic.html

六千八百万用户级APP首页优化之路

技术杂谈 勤劳的小蚂蚁 3个月前 (01-12) 77次浏览 已收录 0个评论 扫描二维码

如下图所示是华为手机应用商店首页截图,我们看到有几个模块:精品应用,精品新游,还有截图中没有展示出来的大家都在用模块等。无论这些模块的APP是以多复杂的算法推荐的,其最终展示给用户看的就是30个APP(一个大概数量)。
华为应用商店首页

SQL优化

项目初期,可能并发不大,且开发时间短促,一切为了尽快上线。这时候缓存都来不及使用,那么只需要优化好SQL即可:
select * from tb_app where app_id in(?, ?, ?)
SQL如上,批量查询,并且给app_id加上索引即可。

Redis优化

第一版很明显无法支持高并发,别说高并发,几十万用户估计都扛不住,这时候就需要引入Redis缓存。由于应用商店这种类型品类,具备典型的长尾效应,即用户最多只会接触到10%的APP,其他90%几乎很少有人访问(不止应用商店,淘宝购物,或者其他很多场景都符合长尾效应),如果APP的缓存不失效,那绝对是一个巨大的浪费。所以为了避免这种的浪费,每个KEY都会带上过期属性。那么这时候的伪代码如下:
// mget参数是app id集合
List<App> appListFromRedis = redisClient.mget(List<Integer>);
// 由于每个key都会缓存过期,所以可能会有部分app的信息无法从缓存区获取到,那么这部分app还需要从数据库中获取
List<App> appListFromDB = appMapper.getByIds(List<Integer>);
从缓存中批量取出来的有效结果和从数据库批量取出来的结果结合就是完整的结果。
谨慎的同学可能发现,这样处理就会出现缓存穿透的情况,缓存穿透可以分为两大类:
  1. 无效的app id导致缓存穿透:这类一般是非法请求,可以通过Redis的bitmaps对无效app id进行拦截,还有缓存一个空对象并设置过期时间等方法优化。
  2. 有效的app id导致缓存穿透:这类请求是由于缓存过期导致的,也有一些办法优化:为了避免大量KEY集中失效,可以设置一个EXPRE_TIME+随机数的过期时间值,另外可以分析总榜TOP-N,蹿升榜TOP-N,活动页等方式,对这些预判为热门APP的过期时间做特殊处理,例如监听失效自动reload缓存,EXPIRE_TIME是普通KEY的n倍等。
和阿里的谢照东大神聊过阿里双11的缓存优化策略,他们会提前分析用户的购物车,浏览记录等信息,对那些预判可能访问量很大的商品,提前将其缓存并设置过期时间延长到双11以后。
需要说明的是,缓存穿透不可能完全杜绝,例如一个刚上架的APP,这时候其信息不需要被缓存,那么第一次访问这个APP,就会缓存穿透,从而需要从数据库中查询。但是这种小流量是在数据库能承受的范围内,并且完善的系统一般每一层都有限流机制,防止数据库被击跨。
本文不重点讲解缓存穿透的优化,所以点到为止。

缓存集群优化

然后由于业务的增长,数据库中的APP越来越多,即使长尾效应,单机redis容量还是扛不住(sentinel架构的存储能力还是单机存储能力,只是多了几个节点的备份而已),所以迟早有一天缓存会采用redis cluster。
细心的网友发现我在前面用到的redis的mget命令,这是redis单机或者sentinel架构才有的命令。在redis3.x即redis集群架构体系下,是没有mget这个命令的。那么怎么办呢?总不至于for循环调用redis的get命令吧?这肯定不行,性能会下降一个量级。那怎么办呢?
网上有对该场景的优化方案,例如cachecloud给出的优化方案如下:
image.png
图片来自于:https://github.com/sohutv/cachecloud/wiki/5.%E8%AE%BE%E8%AE%A1%E6%96%87%E6%A1%A3
然而这个方案也有缺陷,当redis集群的节点过多,可能导致每次mget请求的30个app id分布在完全不同的节点上,那么网络IO就会成为不可忽视的因素,这时候即使是优化后的mget其性能也会一般。而且品类越多,需要的redis节点越多,出现这种问题的概率就越大。如下图所示,当Redis集群中节点过多,网络IO也会不断增长,而只有一个Redis节点时,多个KEY的mget查询只需要一次网络IO即可:
Redis meget网络IO
难道没有其他办法?

更近一步

办法总会有的,以应用商店首页为例,已知首页投放的30个APP,那么我们可以这样缓存数据:
key: PageNo:1
value: [{“appId”:“12”“appName”:“支付宝”“packageName”:“com.android.alipay”“iconUrl”:https://pp.myapp.com/ma_icon/0/icon_5294_1539921151/96&#8221;},……,{“appId”:“26”“appName”:“微信”“packageName”:“com.tecent.wechat”“iconUrl”:https://pp.myapp.com/ma_icon/0/icon_10910_1539831498/96&#8221;}]
缓存的value就是首页30个APP,即List<APP>的json字符串。
但是这样也会带来麻烦,即缓存更新的麻烦。如果要做到尽可能的实时更新,这30个APP无论哪个APP的属性有变更,PageNo:1的缓存都需要更新。当然,如果产品能够接受一定的缓存更新延迟,我们可以等待缓存失效后再加载PageNo:1这个KEY的缓存信息。
总之都是trade-off,架构师不就是根据软件和硬件的瓶颈,找出一种折中的方案吗?试想一下,如果MySQL单机能力秒天秒地,还要什么分库分表,还要什么Redis,还要什么本地缓存。

本地缓存

Redis缓存性能再优秀,还是有一次网络IO开销。当用户越来越多,并发越来越大,我们还需要进一步优化。这个时候我们还可以结合本地缓存进行优化(Guava提供了操作本地缓存的API)。本地缓存的KEY也是PageNo:1,value也是APP集合的json字符串。并且设置本地缓存失效时间不要太大,一般几秒钟即可(如此以来,用户看到过期数据的时间就越在可接受范围内)。失效后,从Redis缓存中重新加载数据到本地缓存。
这样一路优化下来,首页的性能杠杠的,即使有几千万甚至上亿的用户,那也是妥妥的!

文末彩蛋

还有没有可能再进一步优化,从而能够从容应对更大的流量?答案是肯定的!笔者给你推荐一款牛逼哄哄的产品:Varish–高性能的开源HTTP加速器。以前的淘宝,现在的拼多多都使用过varish来应对几个亿的用户产生的流量。本文不过多介绍,毕竟是彩蛋,希望接下来有机会做更深入的介绍,当然你也可以自己去研究这个牛逼的技术啦。

丨极客文库, 版权所有丨如未注明 , 均为原创丨
本网站采用知识共享署名-非商业性使用-相同方式共享 3.0 中国大陆许可协议进行授权
转载请注明原文链接:六千八百万用户级APP首页优化之路
喜欢 (0)
[247507792@qq.com]
分享 (0)
勤劳的小蚂蚁
关于作者:
温馨提示:本文来源于网络,转载文章皆标明了出处,如果您发现侵权文章,请及时向站长反馈删除。

您必须 登录 才能发表评论!

  • 精品技术教程
  • 编程资源分享
  • 问答交流社区
  • 极客文库知识库

客服QQ


QQ:2248886839


工作时间:09:00-23:00