最近遇到的问题,可以总结是一个递进深入的过程,觉得有价值记录整理下。简单介绍下需求背景:有个定时执行的任务,每天早上执行,从数据库获取所有的分组id,给每个id发送消息。在开始设计的时候,就预料到如果是单机执行,随着上线后分组数逐渐增多,任务执行的延迟会逐渐增大。但开始限于某些原因,还是采用了单机的方案。定时器在固定时间触发任务执行,随机一个机器执行对应任务。采用的是消息通知的方式,只需要发送一条消息就可以了。下面说下后来遇到的问题和解决方案。
功能开始上线后,分组开始为20000多个,然后以每天5000个左右的数目增长,单机多线程执行的情况下(线程10个),任务耗时逐渐增长。从开始8分钟,长到了14分钟。可以预见在往后会更加变长。当时考虑了两种方案,分别是。
1、全量消息
在任务触发端,即查询到所有分组id后,全部作为消息发送给任务执行端。同时执行端可以多机器在各自线程池中执行。注意这里分组都是去重的。这种方式优点是编码量非常小,只需要实现全量查询,发送和接收,处理三个功能就可以。但问题消息量级比较大,比如最后会是5-6万的消息量,而且还会继续增长。虽然不多,但限于这边用消息量作为成本计算,所以没有采用,有点迫于形势。&-&
2、id分组
所谓的id分组,就是在任务触发端查询到全量的分组id之后,先排序,然后按照固定的范围切割,得到一些id的分组,再把分组按照消息发送出去,这样就减少了消息量,也实现了多机器并行执行。比如id总量是50000,排序后,按照每100个一组,1-100,101-200...类似,消息量就从10^5 降2个量级,到500。在采用这个方案后,目前接近6万的量,每天不到2分钟就可以执行完成,效果还是比较明显。
下面是分组的实现代码。
/**
* 分组发送id范围,闭区间
*/
@NoArgsConstructor
@AllArgsConstructor
@Data
private class IdRange {
/**
* 表示数据范围
*/
private long start;
private long end;
}
private List<IdRange> buildRangeList(List<Long> ids) {
int rangeSize = getRangeSizeConfig();
if (rangeSize <= 0) {
rangeSize = DEFAULT_RANGE_SIZE;
}
ids = ids.stream().distinct().sorted().collect(Collectors.toList());
List<IdRange> idRanges = Lists.newArrayList();
int size = ids.size();
int start = 0;
int end = 0;
while (end < size - 1) {
end += rangeSize;
if (end >= size) {
end = size - 1;
}
IdRange range = new IdRange(ids.get(start), ids.get(end));
idRanges.add(range);
start = end + 1;
}
return idRanges;
}
以上就是两种简单方案的介绍。补充说点,这里任务要求每天仅执行一次,因此开始全量方案用的日期作为标记,表示是否执行过。 在改为切割的方案后,就改为日期+范围左右区间作为key去重了。
总结
首先一点是,简洁高效的技术方案本该是推荐的,但有时候就是有各种情形的限制,没法采用技术上较为合理的方案。随着以后功能越来越多,估计也会经常碰到,就得考虑如何取舍了。其次,很多时候方案就是逐渐推进改造的,不同的应用场景下,就得考虑更加适当的方案来解决问题。有的人可能说,可以刚开始就采用完善的方案,但这样往往提高了整体的复杂度,不利于扩展,改造。而且谁都难以预料后期需求会发生怎样的变动,因此归纳说应该“简而高效,合理完善”
网友评论