场景分析
接口需求:将分页查询的治疗记录列表数据进行导出
业务逻辑:根据搜索条件和登录人员权限进行分页查询(治疗操作记录表中治疗小项目数据是单独的一张表需要联表查询)再将查询到的数据导出
优化
如果一次数据库查询太多数据,jvm就会内存溢出,所以我使用分页每次查询2万条数据,使用for循环,再把结果合并到一起。但是这种串行方式是阻塞的,查询20w左右数据大约需要15秒,然后就通过线程池用多线程查询,用时3秒多。
@Override
public List<CureListVo> exportCureList(CureListRo ro) {
List<List<Long>> subLists = new ArrayList<>();
// 数据大小
List<Long> ids = buSignatureMapper.selectSignatureCount(ro);
// 批次大小(每个线程要处理数据量)
int batchSize = 20000; // 每个子列表的大小为2万
// 分割List为多个子列表
for (int i = 0; i < ids.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, ids.size());
List<Long> subList = ids.subList(i, endIndex);
subLists.add(subList);
}
Queue<CureListVo> results = new ConcurrentLinkedQueue<>();
if (ObjectUtil.isNotEmpty(subLists)) {
// 使用定长线程池,大小根据实际情况调整,例如数据库连接数、CPU核心数等
int poolSize = Math.min(subLists.size(), Runtime.getRuntime().availableProcessors());
ExecutorService executor = Executors.newFixedThreadPool(poolSize);
CountDownLatch latch = new CountDownLatch(subLists.size());
for (List<Long> subList : subLists) {
CureListRo ro1 = new CureListRo();
BeanUtil.copyProperties(ro,ro1);
ro1.setStartIndex(subList.get(0));
ro1.setEndIndex(subList.get(subList.size()-1));
executor.submit(() -> {
try {
// 查询MySQL表操作
List<CureListVo> cureListVos1 = buSignatureMapper
.selectCureList(ro1);
// 直接添加到ConcurrentLinkedQueue中,它是线程安全的
System.out.println(ro1.getStartIndex()+"到"+ro1.getEndIndex()+"total"+cureListVos1.size());
results.addAll(cureListVos1);
} finally {
// 计数器减一,表示一个任务完成
latch.countDown();
}
});
}
// 阻塞等待所有查询任务完成
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Thread pool interrupted while awaiting termination: {}", e.getMessage());
} finally {
// 关闭线程池
executor.shutdown();
}
}
List<CureListVo> cureListVos = results.stream().sorted(Comparator.comparing(CureListVo::getCureEndTime).reversed()).collect(Collectors.toList());
if(StringUtils.isNotBlank(ro.getSytchName()))
{
cureListVos = cureListVos.stream().filter(cureListVo -> cureListVo.getItems().contains(ro.getSytchName()))
.collect(Collectors.toList());
}
return cureListVos;
}
改进
不推荐使用Executors来创建线程池内置参数不合理容易造成OOM
@Override
public List<CureListVo> exportCureList(CureListRo ro) {
List<List<Long>> subLists = new ArrayList<>();
// 数据大小
List<Long> ids = buSignatureMapper.selectSignatureCount(ro);
// 批次大小(每个线程要处理数据量)
int batchSize = 20000; // 每个子列表的大小为2万
// 分割List为多个子列表
for (int i = 0; i < ids.size(); i += batchSize) {
int endIndex = Math.min(i + batchSize, ids.size());
List<Long> subList = ids.subList(i, endIndex);
subLists.add(subList);
}
Queue<CureListVo> results = new ConcurrentLinkedQueue<>();
if (ObjectUtil.isNotEmpty(subLists)) {
// 使用自定义程池,大小根据实际情况调整,例如数据库连接数、CPU核心数等
int poolSize = Math.min(subLists.size(), Runtime.getRuntime().availableProcessors());
ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
poolSize, poolSize, 1, TimeUnit.HOURS,
new SynchronousQueue<Runnable>(),
new ThreadPoolExecutor.AbortPolicy()
);
CountDownLatch latch = new CountDownLatch(subLists.size());
for (List<Long> subList : subLists) {
CureListRo ro1 = new CureListRo();
BeanUtil.copyProperties(ro,ro1);
ro1.setStartIndex(subList.get(0));
ro1.setEndIndex(subList.get(subList.size()-1));
executor.submit(() -> {
try {
// 查询MySQL表操作
List<CureListVo> cureListVos1 = buSignatureMapper
.selectCureList(ro1);
// 直接添加到ConcurrentLinkedQueue中,它是线程安全的
System.out.println(ro1.getStartIndex()+"到"+ro1.getEndIndex()+"total"+cureListVos1.size());
results.addAll(cureListVos1);
} finally {
// 计数器减一,表示一个任务完成
latch.countDown();
}
});
}
// 阻塞等待所有查询任务完成
try {
latch.await();
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
log.error("Thread pool interrupted while awaiting termination: {}", e.getMessage());
} finally {
// 关闭线程池
executor.shutdown();
}
}
List<CureListVo> cureListVos = results.stream().sorted(Comparator.comparing(CureListVo::getCureEndTime).reversed()).collect(Collectors.toList());
if(StringUtils.isNotBlank(ro.getSytchName()))
{
cureListVos = cureListVos.stream().filter(cureListVo -> cureListVo.getItems().contains(ro.getSytchName()))
.collect(Collectors.toList());
}
return cureListVos;
}
知识回顾
新方案(未完成)
public class CompletableFutureTests {
@Autowired
private UserDao userDao;
@Test
public void testSomeTaskAndJoin() throws Exception {
// DynamicDataSourceContextHolder.dataSourceIds根据动态数据源数量
// 异步执行每个数据源查询方法
// 返回一个Future集合
List<CompletableFuture<List<User>>> futures = DynamicDataSourceContextHolder.dataSourceIds.stream()
.map(this::queryUsers).collect(Collectors.toList());
// 多个异步执行结果合并到该集合
List<User> futureUsers = new ArrayList<>();
// 通过allOf对多个异步执行结果进行处理
CompletableFuture allFuture = CompletableFuture.allOf(futures.toArray(new CompletableFuture[futures.size()]))
.whenComplete((v, t) -> {
// 所有CompletableFuture执行完成后进行遍历
futures.forEach(future -> {
synchronized (this) {
// 查询结果合并
futureUsers.addAll(future.getNow(null));
}
});
});
// 阻塞等待所有CompletableFuture执行完成
allFuture.get();
// 对合并后的结果集进行去重处理
List<User> result = futureUsers.stream().distinct().collect(Collectors.toList());
log.info(result.toString());
}
/**
* 用户异步查询方法
* @param datasourceKey 动态数据源Key
* @return
*/
public CompletableFuture<List<>> queryUsers(String datasourceKey) {
// 定义异步查询Future对象
CompletableFuture<List<Long>> queryFuture = CompletableFuture.supplyAsync(() -> {
return userDao.selectAll();
});
// 异步完成执行方法
queryFuture.whenCompleteAsync(new BiConsumer<List<User>, Throwable>() {
@Override
public void accept(List<User> users, Throwable throwable) {
// 这里主要记录异步执行结果
log.info("数据源[{}]查询完成,查询记录[{}]条", datasourceKey, users.size());
}
});
// 返回future对象
return queryFuture;
}
}