Java手写一个批量获取数据工具类
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. 问题
这段代码是分批从其他服务获取帖子信息,功能上是没有问题的,但有以下缺点:
- 看起来有点繁琐,在业务逻辑中掺杂了分批获取数据的逻辑,看起来不太条理
- 性能可能有问题,分批的数据是在循环中一次一次的拿,耗时会随着数据的增长线性增长
- 从系统架构上考虑,这块代码是没办法复用的,也就是说,很有可能到处都是这样的分批获取数据的代码
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);
}
}
上面的代码
- 使用了guava的工具类Lists.partition,让分批次更简洁了;
- 使用了线程池,性能会更好,这也是java并行任务的最常见的用法
但因为线程池的引入,又变的复杂了起来,需要处理这些Futrue
而且也没有解决代码复用的问题,这些的相同逻辑的代码仍然会重复的出现在项目中
4. 工具类
4.1 分析
于是打算自己写一个批量数据获取工具类,我们需要首先想一下,这个工具类需要什么功能?可能有哪些属性
- http/rpc,支持传入HttpClient或者RPC Service
- totalSize,一共有多少数据要获取呢
- batchSize,每批次有多大
- oneFetchRetryCount,每批次请求时需要重试吗?重试几次?
- oneFetchRetryTimeout,每批次请求时需要设置超时时间吗?
- 应该需要合并每批次的返回结果
- 需不需要加缓存
- 当单批次任务失败时,整体任务算作成功还是失败
这些是使用者会遇到的问题,上面的代码可以自己来处理这些事件,如果你想让别的开发者使用你的工具类,你要尽可能的处理所有可能出现的情况
4.2 实现
下面是我实现的工具类BatchFetcher,它支持以下功能:
- 支持传一个Function对象,也就是java的lambda函数,每一个批次执行会调用一次
- 支持传入线程池,会使用次线程池来执行所有的批次任务
- 支持整体超时时间,也就是说一旦超过这个时间,将不再等待结果,将目前获取到的结果返回
- 传入一个名称,同时会在任务结束后打印名称,任务耗时相关信息
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、参数、最大批次就可以拿到数据,相比最初的两种做法是比较简单的,但也有一些别的问题
- Function的入参和返回结果都是List,有可能和Http或者RPC Service的不一致,需要转为List后在进行处理
- 忽略了单次请求失败
4.5 后续扩展
这个工具类目前解决了代码复用的问题,而且使用起来只需提供最小化的参数,封装了重复性的繁琐工作,相比之前更为简单。但是仍然有优化的空间,例如:
- 报警,当单次任务失败或者整体任务超时发送报警
- 更优雅的返回结果,支持返回自定义的结果
- 支持传递参数,用来确认是不是单次失败就算作整体任务失败
Java手写一个批量获取数据工具类的更多相关文章
- java从Swagger Api接口获取数据工具类
- 教你如何使用Java手写一个基于链表的队列
在上一篇博客[教你如何使用Java手写一个基于数组的队列]中已经介绍了队列,以及Java语言中对队列的实现,对队列不是很了解的可以我上一篇文章.那么,现在就直接进入主题吧. 这篇博客主要讲解的是如何使 ...
- java 写一个JSON解析的工具类
上面是一个标准的json的响应内容截图,第一个红圈”per_page”是一个json对象,我们可以根据”per_page”来找到对应值是3,而第二个红圈“data”是一个JSON数组,而不是对象,不能 ...
- 教你如何使用Java手写一个基于数组实现的队列
一.概述 队列,又称为伫列(queue),是先进先出(FIFO, First-In-First-Out)的线性表.在具体应用中通常用链表或者数组来实现.队列只允许在后端(称为rear)进行插入操作,在 ...
- java连接外部接口获取数据工具类
package com.yqzj.util; import org.apache.log4j.LogManager;import org.apache.log4j.Logger; import jav ...
- 手写一个LRU工具类
LRU概述 LRU算法,即最近最少使用算法.其使用场景非常广泛,像我们日常用的手机的后台应用展示,软件的复制粘贴板等. 本文将基于算法思想手写一个具有LRU算法功能的Java工具类. 结构设计 在插入 ...
- sql 根据指定条件获取一个字段批量获取数据插入另外一张表字段中+MD5加密
/****** Object: StoredProcedure [dbo].[getSplitValue] Script Date: 03/13/2014 13:58:12 ******/ SET A ...
- 浅析MyBatis(二):手写一个自己的MyBatis简单框架
在上一篇文章中,我们由一个快速案例剖析了 MyBatis 的整体架构与整体运行流程,在本篇文章中笔者会根据 MyBatis 的运行流程手写一个自定义 MyBatis 简单框架,在实践中加深对 MyBa ...
- 手写一个线程池,带你学习ThreadPoolExecutor线程池实现原理
摘要:从手写线程池开始,逐步的分析这些代码在Java的线程池中是如何实现的. 本文分享自华为云社区<手写线程池,对照学习ThreadPoolExecutor线程池实现原理!>,作者:小傅哥 ...
- 搞定redis面试--Redis的过期策略?手写一个LRU?
1 面试题 Redis的过期策略都有哪些?内存淘汰机制都有哪些?手写一下LRU代码实现? 2 考点分析 1)我往redis里写的数据怎么没了? 我们生产环境的redis怎么经常会丢掉一些数据?写进去了 ...
随机推荐
- tcp_tw_recycle参数引发的故障
文章转载自:https://blog.csdn.net/wireless_tech/article/details/6405755 故障描述: 2010年9月7日,新上线的手机游戏论坛有部分地区用户反 ...
- Alertmanager篇
报一直是整个监控系统中的重要组成部分,Prometheus监控系统中,采集与警报是分离的.警报规则在 Prometheus 定义,警报规则触发以后,才会将信息转发到给独立的组件 Alertmanage ...
- Kibana:如何周期性地为 Dashboard 生成 PDF Report
转载自:https://blog.csdn.net/UbuntuTouch/article/details/108449775 按照上面的方式填写.记得把之前的 URL 拷贝到 webhook 下的 ...
- vue 自定义千位符过滤器
在main.js页面全局引入 Vue.filter('formatNum', function(value) { if(!value) return '' let num = value.toStri ...
- 关于aws上ec2机型的种类总结汇总
在aws上ec2的机型是非常多的,但主要的种类为如下几种 General Purpose (通用型) ...
- vue2.x核心源码深入浅出,我还是去看源码了
平常的工作就是以vue2.x进行开发,因为我是个实用主义者,以前我就一直觉得,你既然选择了这个框架开发你首先就要先弄懂这玩意怎么用,也就是先熟悉vue语法和各种api,而不是去纠结实现它的原理是什么. ...
- AlexNet-文献阅读笔记
论文介绍 ImageNet Classification with Deep Convolutional Neural Networks- Alex Krizhevsky, Ilya Sutskeve ...
- 成功解决:snippet设置的开机自启没有效果
1.问题描述 勾选开机启动后.没有效果.每次开机都要我重新找到对应的安装目录.双击运行开启 2.解决方法 将snipaste的快捷方式放到开机启动目录下 C:\Users\Administrator\ ...
- 7.Vue常用属性
1. data:数据属性 在之前的学习中我们已经了解到了data,属性中存放的就是js变量 <script> new Vue({ el: '#app', // data data: { u ...
- ubuntu基本
ubuntu使用过程中遇到的指令 apt-get更新 当现出net-tools没有可安装候选 的提示时,可能是apt-get需要更新了.通过指令sudo apt install net-tools p ...