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. Django环境安装

    1.安装Django # 自动安装PyPi提供的最新版本 pip install django # 安装指定版本 pip install django==2.2 # 验证安装 >>> ...

  2. 【C++】从零开始的CS:GO逆向分析1——寻找偏移与基址的方法

    [C++]从零开始的CS:GO逆向分析1--寻找偏移与基址的方法   前言:此文章主要用于提供方法与思路,fps游戏基本都能如此找偏移,文章里找的偏移比较少,主要用来演示寻找思路,文章的后记中会附一个 ...

  3. 《吐血整理》高级系列教程-吃透Fiddler抓包教程(24)-Fiddler如何优雅地在正式和测试环境之间来回切换-中篇

    1.简介 在开发或者测试的过程中,由于项目环境比较多,往往需要来来回回地反复切换,那么如何优雅地切换呢?宏哥今天介绍几种方法供小伙伴或者童鞋们进行参考. 2.实际工作场景 2.1问题场景 (1)已发布 ...

  4. 线性回归大结局(岭(Ridge)、 Lasso回归原理、公式推导),你想要的这里都有

    本文已参与「新人创作礼」活动,一起开启掘金创作之路. 线性模型简介 所谓线性模型就是通过数据的线性组合来拟合一个数据,比如对于一个数据 \(X\) \[X = (x_1, x_2, x_3, ..., ...

  5. C#-01 关于C#中传入参数的一些用法

    实验环境 实验所处环境位于vs2019环境中 学习内容 一.最基础的参数传入:值参数 对于这种传入,和其他的c,c++编程语言参数传入一样,没有太大差别,在这里给如下例子: 虽然这里并没有进行传参但是 ...

  6. PHP全栈开发(八):CSS Ⅰ 选择器

    直到目前为止,我们把从HTML中的数据是如何通过PHP到服务器端,然后又通过PHP到数据库,然后从数据库中出来,通过PHP到HTML的整个过程通过一个案例过了一遍. 可以说,这些才刚刚开始.下面我们开 ...

  7. WiresShark

    WireShark 分析数据包技巧 确定WireShark的位置[是否在公网上] 选择捕获接口,一般都是internet网络接口 使用捕获过滤器 使用显示过滤器[捕获后的数据包还是很复杂,用显示过滤器 ...

  8. 齐博x1万能数据统计接口

    为何叫万能数据统计接口呢?因为可以调用全站任何数据表的数据总条数,并且可以设置查询条件http://qb.net/index.php/index/wxapp.count.html?table=memb ...

  9. Rocky之Mysql-MHA高可用

    9.半同步复制 安装插件三种方法: 第一种: mysql>INSTALL PLUGIN rpl_semi_sync_master SONAME 'semisync_master.so' 安装 在 ...

  10. salesforce零基础学习(一百二十)快去迁移你的代码中的 Alert / Confirm 以及 Prompt吧

    本篇参考: https://developer.salesforce.com/blogs/2022/01/preparing-your-components-for-the-removal-of-al ...