背景
内容点赞业务在得物社区中是一个十分高频的业务场景,性能自身复杂度不高,然而业务场景多、QPS高、而且因为社区的用户体量,整体点赞的数据量十分大。其中最外围、对响应性能要求最高的次要是“用户是否点赞内容”和“内容点赞数”场景。
在得物社区中但凡有内容生产的场景,都会有下面两个点赞场景的解决,所以整体点赞业务的QPS在社区都是十分高的。当咱们在刷各种Feed流时,每一次下滑,都须要对数十篇内容进行登录用户是否点赞状态的判断。作为根底业务,内容点赞业务的高性能响应,对上游内容场景的生产体验有极大的影响。
本文对得物社区的点赞业务如何做到高性能响应以及历史上在缓存应用上对于高性能、稳定性、低成本上的优化摸索过程进行讲述,心愿能给读者带来一些播种。
演进摸索
v1.0版本
性能需要
社区各种Feed流及内容详情页“登录用户是否已点赞内容” “内容被赞总数” “内容最新点赞用户列表”几个场景生产展现。
实现计划
点赞业务整体的高性能是基于Redis+MySQL架构。MySQL做数据存储和查问反对,Redis撑起业务的高性能响应。在1.0版本中,服务架构还是单体PHP服务,技术计划上将动静下所有的点赞用户查问进去放到PHP数组里,而后序列化为Json字符串以Key/Value的形式存储到Redis中,当用户浏览内容时,取出缓存数据,反序列化Json为PHP数组,而后通过in_array和count办法判断是否已点赞及内容点赞数。在缓存的保护上,则是每一次有新用户点赞或勾销赞则间接革除Redis。
缓存构造如下:cId => '[uid1,uid2,uid3...]'
流程图如下:
次要问题
这个版本的计划存在比拟多待优化点。
第一、缓存结构时要查问动静下所有点赞用户数据,数据量大,容易产生慢SQL,对DB和带宽都可能有比拟大的压力。
第二、缓存存储数据结构上为Key/Value构造,每次应用时需先从Redis查问,再反序列化成PHP数组,in_array()和count()办法都有比拟大的开销,尤其是查问热门动静时,对服务器的CPU和MEM资源都有肯定节约,对Redis也产生了比拟大的网络带宽开销。
第三、缓存保护上,每次新增点赞都间接革除缓存,热门动静大量点赞操作下会呈现缓存击穿,会造成大量DB回查操作。
v2.0版本
大家都晓得一些热点事件很容易在社区中发酵,得物社区天然也存在这种状况。在某一场热点事件中,得物社区霎时呈现多篇热点内容,大量用户进入得物社区浏览相干动静并点赞,从v1.0版本的点赞保护流程上能够看出执行缺点,即每次有新点赞都会革除缓存!当有大量用户浏览热点动静,同时又有大量用户在点赞而导致缓存革除的场景下,缓存被击穿的危险十分高,这样会导致大量查问申请打到DB层,研发侧在评估危险之后,连夜进行了缓存革新。
性能需要
1、解决热点内容缓存击穿的危险。
2、优化代码层面对缓存数据序列化和反序列化导致的服务器资源耗费。
实现计划
这次革新,除了优化解决缓存击穿的危险外,也针对之前缓存自身的一些不足之处,思考了一些更高效的实现。在缓存数据构造上摒弃了之前的Key/Value构造,采纳了汇合构造。汇合的个性保障汇合中的用户ID不会呈现反复,能够精确保护了动静下的点赞总数,通过查看用户是否在汇合中,能够高效判断用户是否点赞内容。这样解决每次查问时须要从Redis中获取全副数据和每次须要代码解析Json的过程,Redis汇合反对间接通过SISMEMBER和SCARD接口判断是否赞和计算点赞数,从而晋升了整个模块的响应速度和服务负载。在缓存保护上,每次有新增点赞时,被动向汇合中增加用户ID,并更新缓存过期工夫。每次查问时,也同样会查问缓存的残余过期工夫,如果低于三分之一,就会从新更新过期工夫,这样防止了热门动静有大量新增点赞动作时,呈现缓存击穿的状况。
缓存构造如下:cid => [uid1,uid2,uid3...]
流程图如下:
次要问题
在技术计划中,会将动静下全副的点赞记录全副查出,放入一个汇合中,当动静是一个热门动静时,点赞用户量会十分大,此时汇合变成了一个大Key,而大Key的清理对Redis的稳定性有比拟大的影响,随时可能会因为缓存过期,而引起Redis的抖动,进而引起服务的抖动。并且每次查问出全副的点赞用户,容易产生慢SQL,对网络带宽也比拟有压力。
v3.0版本
性能需要
1、解决V2.0版本中缓存大Key危险。
2、优化缓存重建时查问内容全副点赞用户产生的慢SQL场景。
实现计划
在3.0版本中,对大Key进行了打散解决,对同一个动静下的点赞用户,进行打散分片再保护到缓存,每次操作缓存时先依据用户ID计算分片值,这样每个分片都具备更小的体积和更快的保护和响应速度。而点赞总数的获取,此时社区服务曾经迁徙到Go服务架构,咱们也搭建了独自的计数服务,独自保护内容的被赞总数,节俭了scard接口的耗费。
缓存构造如下:
cid_slice1 => [uid1,uid11,uid111...] cid_slice2 => [uid2,uid22,uid222...] cid_slice3 => [uid3,uid33,uid333...] ...
流程图如下:
次要问题
如果仅仅从技术实现上看v3.0版本,仿佛曾经临时达到了一个程度,在肯定工夫内也能失常撑持社区点赞业务的高性能响应。然而如果从业务角度和全局观念下来思考,这个设计方案仍旧存在比拟多的优化点。例如:
缓存分片中仍旧保护了被浏览动静下全副的点赞用户数据,耗费了十分大的Redis资源,也减少了缓存保护难度。缓存数据的无效使用率很低,举荐流场景下,用户浏览过的动静,简直不会再次浏览到。
以后技术计划针对单篇内容进行设计,在各种Feed流场景中,查问工作在点赞服务里其实放大了十数倍。这种放大对服务器、Redis以及DB,都产生肯定的资源耗费节约。
一些点赞量特地多的历史动静,有人拜访时均会重建缓存,重建老本高,但使用率不高。
缓存汇合分片的设计保护了较多无用数数据,也产生了大量的Key,Key在Redis中同样是占用内存空间的。
... ...
总结一下,较高的服务器负载、Redis申请量、DB申请量。十分大的Redis资源应用(几十GB)。
所以咱们须要一个更优的计划,解决优化以下景象:
1、Feed流场景下批量查问内容工作放大导致的服务器负载,Redis申请,DB申请放大景象。
2、缓存更高效的存储和应用,升高缓存整体的使用量。
3、缓存更高的命中率。
4、辨别冷热数据。
理论Feed场景下的实现逻辑:
批量查问动静点赞数据
V4.0版本
性能需要
结合实际业务场景,大部分场景上游服务都是批量判断是否点赞,社区的动静自身也存在肯定的新鲜度(冷热)。对新缓存的要求是:
1、能解决Feed流场景下批量查问流量放大景象。
2、缓存数据辨别冷热,缩小有效存储(能在内容和点赞用户角度都辨别冷热数据)。
3、缓存构造要简略易保护,使业务实现要清晰明了。
实现计划
设计思路:
1、批量查问工作之所以放大是因为之前的缓存是以内容为维度进行设计,新计划要以用户为维度进行设计。
2、旧计划中拜访内容点赞数据会重建缓存,有些老旧内容重建缓存性价比低,而且内容下的点赞用户并不是始终沉闷和会从新拜访内容,新计划要等辨别冷热数据,冷数据间接拜访DB,不再进行缓存的重建/更新保护。
3、旧计划在保护缓存过期工夫和缩短过期工夫的设计中,每次操作缓存都会进行ttl接口操作,QPS间接x2。新计划要防止ttl操作,但同时又能够保护缓存过期工夫。
4、缓存操作和保护要简略,冀望一个Redis接口操作能达到目标。
所以新计划Redis数据结构的抉择中,能判断是否点赞、是否是冷热数据、是否须要缩短过期工夫,之前的汇合是不能满足了,咱们抉择Hash表构造。用户ID做Key,contentId做field,思考到社区内容ID是趋势递增的,肯定水平上coententID能代表数据的冷热,在缓存中只保护肯定工夫和肯定数量的contentID,并且减少minCotentnID用于辨别冷热数据,为了缩小ttl接口的调用,还减少ttl字段用户判断缓存有效期和缩短缓存过期工夫。一举三得!
缓存构造如下:
{ "userId":{ "ttl":1653532653, //缓存新建或更新时工夫戳 "cid1":1, //用户近一段时间点赞过的动静id "cid2":1, //用户近一段时间点赞过的动静id "cidn":1, //用户近一段时间点赞过的动静id "minCid":3540575, //缓存中最小的动静id,用以辨别冷热, }}
在理论业务场景流程如下:
通过流程图,咱们能够清晰看到, 上游Feed流,一次批量查问申请,没有了循环逻辑,最优状况下,只有一次Redis操作,业务实现也十分简单明了。
优化后果
优化前后Redis查问量QPS日常峰值降落了20倍。
优化前后接口均匀RT降落了10倍。
优化前后DB查问量QPS日常峰值降落了6倍。
优化前后缓存节俭了16G左右存储空间。
后续
优化不会完结,技术不会进行,技术计划会随着业务的演进而演进。
总结
本篇文章中对得物社区点赞业务缓存优化的摸索演进做了相干历史背景和技术计划的解析,以后其中还有更多的细节。而这么屡次版本的优化,都是依据理论的业务场景中呈现的危险点以及需要一直摸索进去的,每个版本的计划也都不是完满计划,v4.0也不是最终计划,还须要开发人员也须要进一步考虑,摸索更优的技术计划。
并且随着业务的一直倒退迭代,会涌现出更多的场景和艰难,咱们始终在优化摸索的路上。
*文/慎之
@得物技术公众号