1. 背景

偶尔会在公司的项目里看到这样的代码

List<Info> infoList = new ArrayList<Info>();
if (infoidList.size() > 100) {
int size = infoidList.size();
int batchSize = PER_IMC_INFO_MAX;
int queryTimes = (size - 1) / batchSize + 1;
for (int i = 0; i < queryTimes; i++) {
int start = batchSize * i;
int end = batchSize * (i + 1) > size ? size : batchSize * (i + 1);
Long[] ids = new Long[end - start];
for (int j = 0; j < end - start; j++) {
ids[j] = infoidList.get(j + start);
}
List<Info> tmpList = null;
try {
tmpList = getInfos(Lists.newArrayList(ids));
} catch (Exception e) {
errorlog.error("error.", e);
}
if (null != tmpList) {
infoList.addAll(tmpList);
}
}
}

2. 问题

这段代码是分批从其他服务获取帖子信息,功能上是没有问题的,但有以下缺点:

  1. 看起来有点繁琐,在业务逻辑中掺杂了分批获取数据的逻辑,看起来不太条理
  2. 性能可能有问题,分批的数据是在循环中一次一次的拿,耗时会随着数据的增长线性增长
  3. 从系统架构上考虑,这块代码是没办法复用的,也就是说,很有可能到处都是这样的分批获取数据的代码

3. 解决

其实在项目里面也有另外的一些同学的代码比这个写的更简洁和优雅

List<List<Long>> partitionInfoIdList = Lists.partition(infoIds, MAX_BATCH);
List<Future<List<JobRelevanceDTO>>> futureList = new ArrayList<>();
for(List<Long> infoIdList : partitionInfoIdList){
futureList.add( FilterStrategyThreadPool.THREAD_POOL.submit(() -> {
BatchJobRelevanceQuery batchJobRelevanceQuery = new BatchJobRelevanceQuery();
batchJobRelevanceQuery.setInfoIds(infoIdList); Response<List<JobRelevanceDTO>> jobRelevanceResponse = jobRelevanceService.batchQueryJobRelevance(batchJobRelevanceQuery); if (jobRelevanceResponse == null || jobRelevanceResponse.getEntity() == null || jobRelevanceResponse.getEntity().isEmpty()) {
LOG.info("DupJobIdShowUtil saveJobIdsToRedis jobRelevanceService return null, infoId size={}", infoIdList.size());
return new ArrayList<>();
} return jobRelevanceResponse.getEntity();
}));
}
for (Future<List<JobRelevanceDTO>> future : futureList) {
try {
List<JobRelevanceDTO> jobRelevanceDTOList = future.get();
for (JobRelevanceDTO jobRelevance : jobRelevanceDTOList) {
infoJobMap.put(jobRelevance.getInfoId(), jobRelevance.getJobId());
}
} catch (InterruptedException e) {
LOG.error("DupJobIdShowUtil getInfoJobMapFromInfo Exception", e);
} catch ( ExecutionException e) {
LOG.error("DupJobIdShowUtil getInfoJobMapFromInfo Exception", e);
}
}

上面的代码

  1. 使用了guava的工具类Lists.partition,让分批次更简洁了;
  2. 使用了线程池,性能会更好,这也是java并行任务的最常见的用法

但因为线程池的引入,又变的复杂了起来,需要处理这些Futrue

而且也没有解决代码复用的问题,这些的相同逻辑的代码仍然会重复的出现在项目中

4. 工具类

4.1 分析

于是打算自己写一个批量数据获取工具类,我们需要首先想一下,这个工具类需要什么功能?可能有哪些属性

  1. http/rpc,支持传入HttpClient或者RPC Service
  2. totalSize,一共有多少数据要获取呢
  3. batchSize,每批次有多大
  4. oneFetchRetryCount,每批次请求时需要重试吗?重试几次?
  5. oneFetchRetryTimeout,每批次请求时需要设置超时时间吗?
  6. 应该需要合并每批次的返回结果
  7. 需不需要加缓存
  8. 当单批次任务失败时,整体任务算作成功还是失败

这些是使用者会遇到的问题,上面的代码可以自己来处理这些事件,如果你想让别的开发者使用你的工具类,你要尽可能的处理所有可能出现的情况

4.2 实现

下面是我实现的工具类BatchFetcher,它支持以下功能:

  1. 支持传一个Function对象,也就是java的lambda函数,每一个批次执行会调用一次
  2. 支持传入线程池,会使用次线程池来执行所有的批次任务
  3. 支持整体超时时间,也就是说一旦超过这个时间,将不再等待结果,将目前获取到的结果返回
  4. 传入一个名称,同时会在任务结束后打印名称,任务耗时相关信息
import com.google.common.collect.Lists;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory; import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.ExecutorCompletionService;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Future;
import java.util.concurrent.TimeUnit;
import java.util.function.Function; public class BatchFetcher<P, R> {
private static final Logger LOG = LoggerFactory.getLogger(BatchFetcher.class); private Function<List<P>, List<R>> serivce;
private ExecutorService executorService;
private String name;
private int timeout = -1; public List<R> oneFecth(List<P> partParams) {
return serivce.apply(partParams);
} public List<R> fetch(List<P> params, int batchSize) {
long startTime = System.currentTimeMillis(); ExecutorCompletionService<List<R>> completionService = new ExecutorCompletionService<>(executorService); List<List<P>> partition = Lists.partition(params, batchSize); List<R> rsList = new ArrayList<>();
for (List<P> pList : partition) {
completionService.submit(() -> this.oneFecth(pList));
} int getRsCount = 0;
while (getRsCount < partition.size()) {
try {
List<R> rs;
if (timeout != -1) {
long elapsed = System.currentTimeMillis() - startTime;
if (elapsed >= timeout) {
LOG.error("{} batchFetcher fetch timout", name);
break;
}
Future<List<R>> poll = completionService.poll(timeout - elapsed, TimeUnit.MILLISECONDS);
if (poll == null) {
LOG.error("{} batchFetcher one fetch timout", name);
continue;
} else {
rs = poll.get();
}
} else {
rs = completionService.take().get();
}
rsList.addAll(rs);
} catch (Exception e) {
LOG.error("{} batchFetcher one fetch error", name, e);
} finally {
getRsCount += 1;
}
}
LOG.info("[BatchFetcher]: {} , total elements size: {}, task num: {}, batch size: {}, rs size: {}, cost time: {}, fetch done",
name, params.size(), partition.size(), batchSize, rsList.size(), System.currentTimeMillis() - startTime);
return rsList;
} public static final class BatchFetcherBuilder<P, R> {
private Function<List<P>, List<R>> serivce;
private ExecutorService executorService;
private String name;
private int timeout = -1; public BatchFetcherBuilder() {
} public BatchFetcherBuilder<P, R> serivce(Function<List<P>, List<R>> serivce) {
this.serivce = serivce;
return this;
} public BatchFetcherBuilder<P, R> executorService(ExecutorService executorService) {
this.executorService = executorService;
return this;
} public BatchFetcherBuilder<P, R> name(String name) {
this.name = name;
return this;
} public BatchFetcherBuilder<P, R> timeout(int timeout) {
this.timeout = timeout;
return this;
} public BatchFetcher<P, R> build() {
BatchFetcher<P, R> batchFetcher = new BatchFetcher<>();
batchFetcher.executorService = this.executorService;
batchFetcher.serivce = this.serivce;
batchFetcher.name = this.name;
batchFetcher.timeout = this.timeout; return batchFetcher;
}
}
}

4.3 使用

  • 案例一
BatchFetcher.BatchFetcherBuilder<Long, Map<Long, Map<String, Tag>>> builder = new BatchFetcher.BatchFetcherBuilder<>();

BatchFetcher<Long, Map<Long, Map<String, Tag>>> cUserTagBatchFetcher = builder
.serivce(this::queryCUserTags)
.name("cUserTagBatchFetcher")
.executorService(ExecutorServiceHolder.batchExecutorService)
.build(); List<Map<Long, Map<String, Tag>>> userIdToTags = cUserTagBatchFetcher.fetch(cuserIds, 200); Map<Long, Map<String, Tag>> cUserTag = new HashMap<>(); for (Map<Long, Map<String, Tag>> userIdToTag : userIdToTags) {
cUserTag.putAll(userIdToTag);
}
private List<Map<Long, Map<String, Tag>>> queryCUserTags(List<Long> cuserIdList) {
...
}
  • 案例二
public Map<Long, LinkResult> getBatchLinkResult(List<Long> cUserIds, Long bUserId) {

        List<LinkType> linkTypes = Lists.newArrayList();

        BatchFetcher.BatchFetcherBuilder<Long, Map<Long, LinkResult>> builder = new BatchFetcher.BatchFetcherBuilder<>();

        BatchFetcher<Long, Map<Long, LinkResult>> linkDataBatchFetcher = builder
.serivce(getLinkResult(linkTypes, bUserId))
.name("linkDataBatchFetcher")
.executorService(ExecutorServiceHolder.batchExecutorService)
.build(); List<Map<Long, LinkResult>> fetchRs = linkDataBatchFetcher.fetch(cUserIds, BATCH_NUM); Map<Long, LinkResult> rs = new HashMap<>(); for (Map<Long, LinkResult> partFetch : fetchRs) {
if (partFetch != null) {
rs.putAll(partFetch);
}
} return rs;
}
private Function<List<Long>, List<Map<Long, LinkResult>>> getLinkResult(List<LinkType> linkTypes, Long bUserId) {
return (partUserIds) -> {
Map<Long, LinkResult> idToLinkResult = null;
try {
idToLinkResult = linkService.getLink(bUserId, partUserIds, linkTypes);
} catch (Exception e) {
logger.error("LinkData getLinkResult error cUserId: {} bUserId: {}", partUserIds, bUserId);
}
return Lists.newArrayList(idToLinkResult);
};
}

4.4 问题

这两个使用的例子,只需要提供一个单次获取数据的Function、参数、最大批次就可以拿到数据,相比最初的两种做法是比较简单的,但也有一些别的问题

  1. Function的入参和返回结果都是List,有可能和Http或者RPC Service的不一致,需要转为List后在进行处理
  2. 忽略了单次请求失败

4.5 后续扩展

这个工具类目前解决了代码复用的问题,而且使用起来只需提供最小化的参数,封装了重复性的繁琐工作,相比之前更为简单。但是仍然有优化的空间,例如:

  1. 报警,当单次任务失败或者整体任务超时发送报警
  2. 更优雅的返回结果,支持返回自定义的结果
  3. 支持传递参数,用来确认是不是单次失败就算作整体任务失败

Java手写一个批量获取数据工具类的更多相关文章

  1. java从Swagger Api接口获取数据工具类

  2. 教你如何使用Java手写一个基于链表的队列

    在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...

  3. java 写一个JSON解析的工具类

    上面是一个标准的json的响应内容截图,第一个红圈”per_page”是一个json对象,我们可以根据”per_page”来找到对应值是3,而第二个红圈“data”是一个JSON数组,而不是对象,不能 ...

  4. 教你如何使用Java手写一个基于数组实现的队列

    一.概述 队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表.在具体应用中通常用链表或者数组来实现.队列只允许在后端(称为rear)进行插入操作,在 ...

  5. java连接外部接口获取数据工具类

    package com.yqzj.util; import org.apache.log4j.LogManager;import org.apache.log4j.Logger; import jav ...

  6. 手写一个LRU工具类

    LRU概述 LRU算法,即最近最少使用算法.其使用场景非常广泛,像我们日常用的手机的后台应用展示,软件的复制粘贴板等. 本文将基于算法思想手写一个具有LRU算法功能的Java工具类. 结构设计 在插入 ...

  7. sql 根据指定条件获取一个字段批量获取数据插入另外一张表字段中+MD5加密

    /****** Object: StoredProcedure [dbo].[getSplitValue] Script Date: 03/13/2014 13:58:12 ******/ SET A ...

  8. 浅析MyBatis(二):手写一个自己的MyBatis简单框架

    在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ...

  9. 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理

    摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...

  10. 搞定redis面试--Redis的过期策略?手写一个LRU?

    1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...

随机推荐

  1. 通过helm搭建Harbor

    文章转载自:http://www.mydlq.club/article/66/ 系统环境: kubernetes 版本:1.20.1 Traefik Ingress 版本:2.4.3 Harbor C ...

  2. 使用prometheus + granafa 监控mysql主从

    若主从同步数据库未同步默认的mysql表,则也需要在从库上创建mysql用户mysqld_exporter用来收集监控数据 mysqld_exporter安装部署 这里采取的是mysqld_expor ...

  3. filebeat测试output连通性

    在默认的情况下,直接运行filebeat的话,它选择的默认的配置文件是当前目录下的filebeat.yml文件. filebeat.yml文件内容 filebeat.inputs: - type: l ...

  4. 企业运维 | MySQL关系型数据库在Docker与Kubernetes容器环境中快速搭建部署主从实践

    [点击 关注「 WeiyiGeek」公众号 ] 设为「️ 星标」每天带你玩转网络安全运维.应用开发.物联网IOT学习! 希望各位看友[关注.点赞.评论.收藏.投币],助力每一个梦想. 本章目录 目录 ...

  5. C#并发编程-4 同步

    如果程序用到了并发技术,那就要特别留意这种情况:一段代码需要修改数据,同时其他代码需要访问同一个数据. 这种情况就需要考虑同步地访问数据. 如果下面三个条件都满足,就必须用同步来保护共享的数据. 多段 ...

  6. python合并多个excel

    前言 1.工作中,经常需要合并多个Excel文件.如果文件数量比较多,则工作量大,易出错,此时,可以使用Python来快速的完成合并. 2.使用方法:将需要合并的多个Excel文件放到同一个文件夹下, ...

  7. 驱动开发:内核中实现Dump进程转储

    多数ARK反内核工具中都存在驱动级别的内存转存功能,该功能可以将应用层中运行进程的内存镜像转存到特定目录下,内存转存功能在应对加壳程序的分析尤为重要,当进程在内存中解码后,我们可以很容易的将内存镜像导 ...

  8. rowkey设计原则和方法

    rowkey设计首先应当遵循三大原则: 1.rowkey长度原则 rowkey是一个二进制码流,可以为任意字符串,最大长度为64kb,实际应用中一般为10-100bytes,它以byte[]形式保存, ...

  9. 怎么在线预览.doc,.docx,.ofd,.pdf,.wps,.cad文件以及Office文档的在线解析方式。

    前言 Office文件在线预览是目前移动化办公的一种新趋势.Office在线预览指的是Office系列的文件在线查看而不依附域客户端的存在.在浏览器或者浏览器控件中可以预览查看Word.PDF.Exc ...

  10. linux安装达梦数据库8

    PS.本次测试只是为了项目需要,但是在部署和启动程序的时候发生了一系列的报错,由此记录下来为日后作参考 安装达梦数据库 1. 达梦数据库(DM8)简介 达梦数据库管理系统是武汉达梦公司推出的具有完全自 ...