在40岁老架构师 尼恩的读者交流群(50+)中,最近有小伙伴拿到了一线互联网企业如阿里、滴滴、极兔、有赞、闲鱼、百度、网易的面试资格,遇到很多很重要的面试题:
在线统计 亿级数据,如何实现?
海量数据在线统计,如何实现?
小伙伴面试美团,又遇到了这个问题,小伙伴 没有回答好,导致面试挂了。
小伙伴面试完了之后,来求助尼恩:这个问题,如何才能回答得很漂亮,才能 让面试官刮目相看、口水直流。
尼恩由于在制作《DDD学习圣经》视频,这几天确实太忙,没时间写面试题答案。
所以,临时为小伙伴找到一个漂亮的生产级案例:《闲鱼团队亿级数据在线统计方案案例》。
借助这个案例,给大家做一下系统化、体系化的梳理,使得大家可以充分展示一下大家雄厚的 “技术肌肉”,让面试官爱到 “不能自已、口水直流”。
当然,这道面试题,以及参考答案,也会收入咱们的 《尼恩Java面试宝典》V148版本PDF集群,供后面的小伙伴参考,提升大家的 3高 架构、设计、开发水平。
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》的PDF,请到文末公号【技术自由圈】获取
作者:迟尘,闲鱼技术
关系型数据库在执行计数任务时,其执行效率会随着数据量级的增长而降低;
当尤其是在处理亿级数据时,计数的效率低下到让人难以接受。
在闲鱼团队的关系系统中,为了应对这一问题,我们采取了一种策略,以实现对亿级数据的高效计数,且确保计数操作的速度达到毫秒级。
闲鱼现有的业务场景中,用户收藏宝贝、关注他人的数据量,已经达到了亿级规模。
使用传统的关系型数据库如MySQL进行有条件计数查询时,效率低下,特别是在处理大规模数据时,无法满足线上业务的高效响应需求。
如上图,在亿级别数据量级的关系型数据库存储中按分表key执行count操作,即使是在响应时间这一单一指标上,也难以达到线上业务的高标准,更不用说频繁执行这些低性能查询对数据库性能的影响了。
业内针对海量数据的计数场景,通常采用的解决方案有计数器和定时离线计算两种。
两种方式各有优劣:
计数方案 | 优势 | 劣势 |
---|---|---|
计数器(redis等内存kv存储) | 实时的计数结果 | 一旦出错难以恢复 |
定时离线计算 | 数据出错可以重复执行计算任务进行恢复 | 数据时效性差 |
本文提出了一种基于离线批处理+在线增量统计的设计方案,将复杂耗时的数据库计数操作,替换为多次KV存储的读取和对接操作,在线上业务场景中实现了QPS峰值时响应保持在毫秒级别(10ms以内),成功率也始终接近100%的效果,从而有效地支持了业务的发展。
闲鱼团队为了克服关系型数据库在处理大规模数据计数时的性能瓶颈,采纳了一种创新的解决方案。
该方案通过结合离线批处理和在线实时统计的技术,显著提高了数据计数操作的效率,确保了在高流量环境下,计数操作能够迅速且准确完成,极大地提升了用户体验,并保证了业务流程的高效稳定运行。
本文提出的计数方案,简而言之,就是定时的将数据全量同步到离线库中进行批处理,实时在线上对增量数据保持统计,最后将这两部分的结果结合起来,以获得精准计数值。
闲鱼目前存储关系数据的方式如下(省略与本文无关字段):
source | 源(如:用户id) |
---|---|
target | 目标(如:收藏的宝贝id) |
status | 关系状态(0-正常,1-删除) |
gmt_modified | 关系最新修改时间 |
gmt_create | 数据创建时间 |
如果要统计某个用户的关系数量,例如用户A收藏的宝贝数量,只需要获取到source为A且status为0的数据的总条数即可。
利用数据的离线处理能力,我们首先考虑的是按时间点对数据进行分割,即:某个时间点(如每天凌晨0点)之前的数据同步到离线存储进行计算,之后再与今天的增量数据进行合并,从而得到最终结果,如下图:
下文中将指定的切割时间点称为**“预期快照读时间”**。
在执行上图中的离线数据同步任务时,闲鱼团队采用的阿里云ODPS离线同步任务,采用扫表的方式完成数据离线。
在单表的场景下,借助于mysql的快照读机制可以准确的获取到某个时间点的快照,从而实现数据的离线、在线精准切割。
在实际场景中,海量数据通常必须采用分库分表的方式进行存储。
而分库分表后,为了降低对线上业务的影响,离线数据同步任务往往是分批执行,这样就无法确保所有表的快照读操作时间完全一致,如下图:
多个分表的快照读时间不一致的情况下,快照读的实际执行时间必然会和预期快照读时间之间存在偏差,如下图:
假设在预期快照读时间和快照读实际执行时间的间隔内产生了一条数据变化(在上图中,此变化具体是在凌晨30分时,该关系的状态从0变为了1),那么实际执行的快照读将无法读取到预期快照读时间的快照,而是会读取到最新的数据状态。
换句话说,当预期快照时间≠数据实际读取时间时,在预期快照读时间到快照读实际执行时间之间产生的数据变化将会污染统计结果。
但是,由于数据量级过大,必须采用分库分表的存储、分批执行离线任务,这就使得按照固定时间点对数据进行分割的方法变得不可行。
为了规避这个问题,我们不得不放弃固定时间点的数据切割方式,转而采用一种新的策略,将同步任务开始时间作为数据快照读时间,并依据此时间对数据进行切割。
使用这样的同步方式,获取到的离线统计结果将如下图所示:
对不同source值计算后得到计数结果的同时,还可以得到该关系的最新更改时间,且不同关系的最新修改时间各不相同。本文将离线统计得到的包含该关系在某个时刻的最新计数值(所有关系中最新一条的修改时间+修改后的合计值),记做offlineTotal。
与一般离线任务产出的结果不同的是,offlineTotal中额外包含了该关系的最新一条修改时间,下文中的在线增量数据统计方案,将基于此时间来完成数据的对接与合并。
结合离线数据计数的结果,在线增量数据需要包含如下信息:
在闲鱼的实现中,我们采用KV存储的方式,来记录关系的变化情况。
具体记录的值为:每一个source每一天的总增量dailyIncrTotal,以及每一次发生关系更新的那一时刻的增量modifiedTimeIncr,如下表:
key | offlineTotal | dailylncrTotal | modifiedTimelncr |
---|---|---|---|
10002 | 2018-9-12 01:15:00|5 | 2018-9-12|2 | 2018-9-12 01:15:00|1 |
统计计数值时,首先使用离线计数值offlineTotal加上当天的总增量dailyIncrTotal得到一个合计值,如下图:
可以看到,由于离线任务统计的数据并不是严格按照时间点进行切割(通常离线任务会在每天凌晨0点至1点之间执行),离线计数值和当日增量之间可能存在数据重叠。
在这种情况下,再根据离线计数值中的最新修改时间,获取在该时刻的增量,从合计值中去掉这部分数据即可:
整理这段计算逻辑,可以得到如下公式:
currentTotal = offlineTotal + ∑dailyIncrTotal - modifiedTimelncr
注:ΣdailyIncrTotal表示从离线记录中得到的最新修改时间的日期一直到计数时的日期。
至此,一次完整的计数请求,被替换为2+N次KV存储的查询(N为离线计算结果距当前时间的日期差,通常不会超过一天)。
这种方案不仅具有与实时计数器相当的响应速度,而且在遇到异常情况时,可以利用离线批处理的能力,重复运行离线任务,以滚动的方式修正数据。
本文介绍了一种在亿数据量级场景下实现快速精准计数的方案,采用离线批处理来减少线上压力、提高计算效率,同时使用KV存储实时记录增量数据快照,实现了计数结果毫秒级响应,且可依赖离线数据订正。
从不同的角度思考问题,有时候在面对看似耗时且难以优化的场景时,转换思考方式可能会带来意想不到的解决方案。
海量数据在线统计的相关的面试题,是非常常见的面试题。
以上的内容,如果大家能对答如流,如数家珍,基本上 面试官会被你 震惊到、吸引到。最终,让面试官爱到 “不能自已、口水直流”。offer, 也就来了。
在面试之前,建议大家系统化的刷一波 5000页 《尼恩Java面试宝典PDF》,并且在刷题过程中,如果有啥问题,大家可以来 找 40岁老架构师尼恩交流。
另外,如果没有面试机会,可以找尼恩来帮扶、领路。尼恩指导了大量的就业困难的小伙伴上岸,前段时间帮助一个40岁+就业困难小伙伴,拿到了一个年薪100W的offer。
……完整版尼恩技术圣经PDF集群,请找尼恩领取
《尼恩 架构笔记》《尼恩高并发三部曲》《尼恩Java面试宝典》PDF,请到下面公号【技术自由圈】取↓↓↓