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怎么经常会丢掉一些数据?写进去了 ...
随机推荐
- 使用sonarqube对java项目进行分析
目前有两种办法,第一种是使用SonarQube-Scanner-Maven,第二种是结合gitlab-ci进行 前提条件:已安装并启动sonarqube,知道访问地址和登录的用户名及密码,具体参考文档 ...
- logstash安装插件修改使用的gem源
gem source -l # 查看当前使用的gem源 gem source --remove https://rubygems.org/ # 移除gem源 gem source -a https:/ ...
- 第二周python作业
print("今有不知其数,三三数之剩二,五五数之剩三,七七数之剩二,问几何?\n") number=int(input("请输入您认为符合条件的数: ")) ...
- k8s 中 Pod 的控制器
k8s 中 Pod 的控制器 前言 Replication Controller ReplicaSet Deployment 更新 Deployment 回滚 deployment StatefulS ...
- Linux+Wine运行QQTIM (2022年9月)
测试的版本Tim3.4.0 QQ9.6.7 如果你的系统没有Wine先装Wine,Wine在各大发行版的源都能找到.记住32位和64位的Wine都要装 去https://tubentubentu.pa ...
- 空 Maven项目转成 Web项目 & SpringMVC调用其他 Module中的方法可能会遇到的小问题
SpringMVC调用其他 模块内的方法的 坑 下次别在阴沟里翻船啦.. 一共花费 4个小时,解决项目中的这个问题 OMG 1. 首先是 Maven新建工程 一般使用 Maven都是先创建 空工程 当 ...
- 猫狗识别-CNN与VGG实现
本次项目首先使用CNN卷积神经网络模型进行训练,最终训练效果不太理想,出现了过拟合的情况.准确率达到0.72,loss达到0.54.使用预训练的VGG模型后,在测试集上准确率达到0.91,取得了不错的 ...
- tListener监听器
1.概念 监听器:专门用于对其他对象身上发生的事件或状态改变进行监听和相应处理的对象,当被监视的对象发生情况时,立即采取相应的行动. Servlet监听器:Servlet规范中定义的一种特殊类,它用于 ...
- 使用thymeleaf将查询的数据显示在前台。通过使用循环的形式
1.需要注意的点. 在 <tr th:each="book:${bookList}">中.book是自己命令的变量.${bookList}是将查询的数据放入这里,需要后 ...
- Java函数式编程:一、函数式接口,lambda表达式和方法引用
Java函数式编程 什么是函数式编程 通过整合现有代码来产生新的功能,而不是从零开始编写所有内容,由此我们会得到更加可靠的代码,并获得更高的效率 我们可以这样理解:面向对象编程抽象数据,函数式编程抽象 ...