处理亿级数据的“定时任务”,如何缩短执行时间?

2,687 阅读4分钟

继续答水友提问。
问题抽象

(1)用户会员系统;

(2)用户会有分数流水,每个月要做一次分数统计,对不同分数等级的会员做不同业务处理;

数据假设

(1)假设用户在 100w 级别;

(2)假设用户日均 1 条流水,也就是说日增流水数据量在 100W 级别,月新增流水在 3kW 级别,3 个月流水数据量在亿级别;

常见解决方案

用一个定时任务,每个月的第一天计算一次。

//(1) 查询出所有用户

uids[] = select uid from t_user;

//(2) 遍历每个用户

foreach $uid in uids[]{

//(3) 查询用户 3 个月内分数流水

scores[]= select score from t_flow

where uid=$uid and time=[3 个月内];

//(4) 遍历分数流水

foreach $score in scores[]{

//(5) 计算总分数

sum+= $score;

}

//(6) 根据分数做业务处理

switch(sum)

升级降级,发优惠券,发奖励;

}

一个月执行一次的定时任务,会存在什么问题?

计算量很大,处理的数据量很大,耗时很久,按照水友的说法,需要 1-2 天。

画外音:外层循环 100W 级别用户;内层循环 9kW 级别流水;业务处理需要 10 几次数据库交互。

可不可以多线程并行处理?

可以,每个用户的流水处理不耦合。

改为多线程并行处理,例如按照用户拆分,会存在什么问题?

每个线程都要访问数据库做业务处理,数据库有可能扛不住。

这类问题的优化方向是:

(1)同一份数据,减少重复计算次数;

(2)分摊 CPU 计算时间,尽量分散处理,而不是集中处理;

(3)减少单次计算数据量;

如何减少同一份数据,重复计算次数?

如上图,假设每一个方格是 1 个月的分数流水数据(约 3kW)。

3 月底计算时,要查询并计算 1 月,2 月,3 月三个月的 9kW 数据;

4 月底计算时,要查询并计算 2 月,3 月,4 月三个月的 9kW 数据;

会发现,2 月和 3 月的数据(粉色部分),被重复查询和计算了多次。
_画外音:_该业务,每个月的数据会被计算 3 次。

新增月积分流水汇总表,每次只计算当月增量

flow_month_sum(month, uid, flow_sum)

(1)每到月底,只计算当月分数,数据量减少到 1/3,耗时也减少到 1/3;

(2)同时,把前 2 个月流水加和,就能得到最近 3 个月总分数(这个动作几乎不花时间);

画外音:该表的数量级和用户表数据量一致,100w 级别。

这样一来,每条分数流水只会被计算一次。

如何分摊 CPU 计算时间,减少单次计算数据量呢?

业务需求是一个月重新计算一次分数,但一个月集中计算,数据量太大,耗时太久,可以将计算分摊到每天。

如上图,月积分流水汇总表,升级为,日积分流水汇总表。

把每月 1 次集中计算,分摊为 30 次分散计算,每次计算数据量减少到 1/30,就只需要花几十分钟处理了。

甚至,每一个小时计算一次,每次计算数据量又能减少到 1/24,每次就只需要花几分钟处理了。

虽然时间缩短了,但毕竟是定时任务,能不能实时计算分数流水呢?

每天只新增 100w 分数流水,完全可以实时累加计算 “日积分流水汇总”。

使用 DTS(或者 canal) 增加一个分数流水表的监听,当用户的分数变化时,实时进行日分数流水累加,将 1 小时一次的定时任务计算,均匀分摊到 “每时每刻”,每天新增 100w 流水,数据库写压力每秒钟 10 多次,完全扛得住。

画外音:如果不能使用 DTS/canal,可以使用 MQ。

总结,对于这类一次性集中处理大量数据的定时任务,优化思路是:

(1)同一份数据,减少重复计算次数;

(2)分摊 CPU 计算时间,尽量分散处理(甚至可以实时),而不是集中处理;

(3)减少单次计算数据量;

希望大家有所启示,思路比结论重要。

课后作业

假设,某系统登录日志(日志比数据库更难,数据库可以建索引检索)如下:

2019-08-15 23:11:15 uid=123 action=login

2019-08-15 23:11:18 uid=234 action=logout

求,2019-8-15 这一天,系统同时在线用户数曲线,精确到秒。

说明:

(1)action 只能为 login/logout;

(2)在线用户的定义为,已经 login,还没有 logout,正在使用系统的用户;

(3)8-15 之前登录,8-15 还没有登出的用户,也算当天在线用户(潜台词是,只扫描当天的日志是不够的);