使用双异步后,从 191s 优化到 2s(2)

2024-03-21 11:52
文章标签 使用 优化 异步 2s 191s

本文主要是介绍使用双异步后,从 191s 优化到 2s(2),希望对大家解决编程问题提供一定的参考价值,需要的开发者们随着小编来一起学习吧!

在开发中,我们经常会遇到这样的需求,将Excel的数据导入数据库中。

一、一般我会这样做:

  1. 通过POI读取需要导入的Excel;
  2. 以文件名为表名、列头为列名、并将数据拼接成sql;
  3. 通过JDBC或mybatis插入数据库;

操作起来,如果文件比较多,数据量都很大的时候,会非常慢。

访问之后,感觉没什么反应,实际上已经在读取 + 入库了,只是比较慢而已。

读取一个10万行的Excel,居然用了191s,我还以为它卡死了呢!

 

java

复制代码

private void readXls(String filePath, String filename) throws Exception { @SuppressWarnings("resource") XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath)); // 读取第一个工作表 XSSFSheet sheet = xssfWorkbook.getSheetAt(0); // 总行数 int maxRow = sheet.getLastRowNum(); StringBuilder insertBuilder = new StringBuilder(); insertBuilder.append("insert into ").append(filename).append(" ( UUID,"); XSSFRow row = sheet.getRow(0); for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) { insertBuilder.append(row.getCell(i)).append(","); } insertBuilder.deleteCharAt(insertBuilder.length() - 1); insertBuilder.append(" ) values ( "); StringBuilder stringBuilder = new StringBuilder(); for (int i = 1; i <= maxRow; i++) { XSSFRow xssfRow = sheet.getRow(i); String id = ""; String name = ""; for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) { if (j == 0) { id = xssfRow.getCell(j) + ""; } else if (j == 1) { name = xssfRow.getCell(j) + ""; } } boolean flag = isExisted(id, name); if (!flag) { stringBuilder.append(insertBuilder); stringBuilder.append('\'').append(uuid()).append('\'').append(","); for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) { stringBuilder.append('\'').append(value).append('\'').append(","); } stringBuilder.deleteCharAt(stringBuilder.length() - 1); stringBuilder.append(" )").append("\n"); } } List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList()); int sum = JdbcUtil.executeDML(collect); } private static boolean isExisted(String id, String name) { String sql = "select count(1) as num from " + static_TABLE + " where ID = '" + id + "' and NAME = '" + name + "'"; String num = JdbcUtil.executeSelect(sql, "num"); return Integer.valueOf(num) > 0; } private static String uuid() { return UUID.randomUUID().toString().replace("-", ""); }

二、谁写的?拖出去,斩了!

优化1:先查询全部数据,缓存到map中,插入前再进行判断,速度快了很多。

优化2:如果单个Excel文件过大,可以采用 异步 + 多线程 读取若干行,分批入库。

优化3:如果文件数量过多,可以采一个Excel一个异步,形成完美的双异步读取插入。

使用双异步后,从 191s 优化到 2s,你敢信?

下面贴出异步读取Excel文件、并分批读取大Excel文件的关键代码。

1、readExcelCacheAsync控制类

 

java

复制代码

@RequestMapping(value = "/readExcelCacheAsync", method = RequestMethod.POST) @ResponseBody public String readExcelCacheAsync() { String path = "G:\\测试\\data\\"; try { // 在读取Excel之前,缓存所有数据 USER_INFO_SET = getUserInfo(); File file = new File(path); String[] xlsxArr = file.list(); for (int i = 0; i < xlsxArr.length; i++) { File fileTemp = new File(path + "\\" + xlsxArr[i]); String filename = fileTemp.getName().replace(".xlsx", ""); readExcelCacheAsyncService.readXls(path + filename + ".xlsx", filename); } } catch (Exception e) { logger.error("|#ReadDBCsv|#异常: ", e); return "error"; } return "success"; }

2、分批读取超大Excel文件

 

java

复制代码

@Async("async-executor") public void readXls(String filePath, String filename) throws Exception { @SuppressWarnings("resource") XSSFWorkbook xssfWorkbook = new XSSFWorkbook(new FileInputStream(filePath)); // 读取第一个工作表 XSSFSheet sheet = xssfWorkbook.getSheetAt(0); // 总行数 int maxRow = sheet.getLastRowNum(); logger.info(filename + ".xlsx,一共" + maxRow + "行数据!"); StringBuilder insertBuilder = new StringBuilder(); insertBuilder.append("insert into ").append(filename).append(" ( UUID,"); XSSFRow row = sheet.getRow(0); for (int i = 0; i < row.getPhysicalNumberOfCells(); i++) { insertBuilder.append(row.getCell(i)).append(","); } insertBuilder.deleteCharAt(insertBuilder.length() - 1); insertBuilder.append(" ) values ( "); int times = maxRow / STEP + 1; //logger.info("将" + maxRow + "行数据分" + times + "次插入数据库!"); for (int time = 0; time < times; time++) { int start = STEP * time + 1; int end = STEP * time + STEP; if (time == times - 1) { end = maxRow; } if(end + 1 - start > 0){ //logger.info("第" + (time + 1) + "次插入数据库!" + "准备插入" + (end + 1 - start) + "条数据!"); //readExcelDataAsyncService.readXlsCacheAsync(sheet, row, start, end, insertBuilder); readExcelDataAsyncService.readXlsCacheAsyncMybatis(sheet, row, start, end, insertBuilder); } } }

3、异步批量入库

 

java

复制代码

@Async("async-executor") public void readXlsCacheAsync(XSSFSheet sheet, XSSFRow row, int start, int end, StringBuilder insertBuilder) { StringBuilder stringBuilder = new StringBuilder(); for (int i = start; i <= end; i++) { XSSFRow xssfRow = sheet.getRow(i); String id = ""; String name = ""; for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) { if (j == 0) { id = xssfRow.getCell(j) + ""; } else if (j == 1) { name = xssfRow.getCell(j) + ""; } } // 先在读取Excel之前,缓存所有数据,再做判断 boolean flag = isExisted(id, name); if (!flag) { stringBuilder.append(insertBuilder); stringBuilder.append('\'').append(uuid()).append('\'').append(","); for (int j = 0; j < row.getPhysicalNumberOfCells(); j++) { stringBuilder.append('\'').append(value).append('\'').append(","); } stringBuilder.deleteCharAt(stringBuilder.length() - 1); stringBuilder.append(" )").append("\n"); } } List<String> collect = Arrays.stream(stringBuilder.toString().split("\n")).collect(Collectors.toList()); if (collect != null && collect.size() > 0) { int sum = JdbcUtil.executeDML(collect); } } private boolean isExisted(String id, String name) { return ReadExcelCacheAsyncController.USER_INFO_SET.contains(id + "," + name); }

4、异步线程池工具类

@Async的作用就是异步处理任务。
  1. 在方法上添加@Async,表示此方法是异步方法;
  2. 在类上添加@Async,表示类中的所有方法都是异步方法;
  3. 使用此注解的类,必须是Spring管理的类;
  4. 需要在启动类或配置类中加入@EnableAsync注解,@Async才会生效;

在使用@Async时,如果不指定线程池的名称,也就是不自定义线程池,@Async是有默认线程池的,使用的是Spring默认的线程池SimpleAsyncTaskExecutor。

默认线程池的默认配置如下:
  1. 默认核心线程数:8;
  2. 最大线程数:Integet.MAX_VALUE;
  3. 队列使用LinkedBlockingQueue;
  4. 容量是:Integet.MAX_VALUE;
  5. 空闲线程保留时间:60s;
  6. 线程池拒绝策略:AbortPolicy;

从最大线程数可以看出,在并发情况下,会无限制的创建线程,我勒个吗啊。

也可以通过yml重新配置:
 

xml

复制代码

spring: task: execution: pool: max-size: 10 core-size: 5 keep-alive: 3s queue-capacity: 1000 thread-name-prefix: my-executor

也可以自定义线程池,下面通过简单的代码来实现以下@Async自定义线程池。

 

java

复制代码

@EnableAsync// 支持异步操作 @Configuration public class AsyncTaskConfig { /** * com.google.guava中的线程池 * @return */ @Bean("my-executor") public Executor firstExecutor() { ThreadFactory threadFactory = new ThreadFactoryBuilder().setNameFormat("my-executor").build(); // 获取CPU的处理器数量 int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2; ThreadPoolExecutor threadPool = new ThreadPoolExecutor(curSystemThreads, 100, 200, TimeUnit.SECONDS, new LinkedBlockingQueue<>(), threadFactory); threadPool.allowsCoreThreadTimeOut(); return threadPool; } /** * Spring线程池 * @return */ @Bean("async-executor") public Executor asyncExecutor() { ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor(); // 核心线程数 taskExecutor.setCorePoolSize(24); // 线程池维护线程的最大数量,只有在缓冲队列满了之后才会申请超过核心线程数的线程 taskExecutor.setMaxPoolSize(200); // 缓存队列 taskExecutor.setQueueCapacity(50); // 空闲时间,当超过了核心线程数之外的线程在空闲时间到达之后会被销毁 taskExecutor.setKeepAliveSeconds(200); // 异步方法内部线程名称 taskExecutor.setThreadNamePrefix("async-executor-"); /** * 当线程池的任务缓存队列已满并且线程池中的线程数目达到maximumPoolSize,如果还有任务到来就会采取任务拒绝策略 * 通常有以下四种策略: * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) * ThreadPoolExecutor.CallerRunsPolicy:重试添加当前的任务,自动重复调用 execute() 方法,直到成功 */ taskExecutor.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy()); taskExecutor.initialize(); return taskExecutor; } }

5、异步失效的原因

  1. 注解@Async的方法不是public方法;
  2. 注解@Async的返回值只能为void或Future;
  3. 注解@Async方法使用static修饰也会失效;
  4. 没加@EnableAsync注解;
  5. 调用方和@Async不能在一个类中;
  6. 在Async方法上标注@Transactional是没用的,但在Async方法调用的方法上标注@Transcational是有效的;

三、线程池中的核心线程数设置问题

有一个问题,一直没时间摸索,线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。

借着这个机会,测试一下。

1、我记得有这样一个说法,CPU的处理器数量

将核心线程数CorePoolSize设置成CPU的处理器数量,是不是效率最高的?

 

java

复制代码

// 获取CPU的处理器数量 int curSystemThreads = Runtime.getRuntime().availableProcessors() * 2;

Runtime.getRuntime().availableProcessors()获取的是CPU核心线程数,也就是计算资源。

  • CPU密集型,线程池大小设置为N,也就是和cpu的线程数相同,可以尽可能地避免线程间上下文切换,但在实际开发中,一般会设置为N+1,为了防止意外情况出现线程阻塞,如果出现阻塞,多出来的线程会继续执行任务,保证CPU的利用效率。
  • IO密集型,线程池大小设置为2N,这个数是根据业务压测出来的,如果不涉及业务就使用推荐。

在实际中,需要对具体的线程池大小进行调整,可以通过压测及机器设备现状,进行调整大小。

如果线程池太大,则会造成CPU不断的切换,对整个系统性能也不会有太大的提升,反而会导致系统缓慢。

我的电脑的CPU的处理器数量是24。

那么一次读取多少行最合适呢?

测试的Excel中含有10万条数据,10万/24 = 4166,那么我设置成4200,是不是效率最佳呢?

测试的过程中发现,好像真的是这样的。

2、我记得大家都习惯性的将核心线程数CorePoolSize和最大线程数MaxPoolSize设置成一样的,都爱设置成200。

是随便写的,还是经验而为之?

测试发现,当你将核心线程数CorePoolSize和最大线程数MaxPoolSize都设置为200的时候,第一次它会同时开启150个线程,来进行工作。

这个是为什么?

3、经过数十次的测试

  1. 发现核心线程数好像差别不大
  2. 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;
  3. 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;

四、通过EasyExcel读取并插入数据库

EasyExcel的方式,我就不写双异步优化了,大家切记陷入低水平勤奋的怪圈。

1、ReadEasyExcelController

 

java

复制代码

@RequestMapping(value = "/readEasyExcel", method = RequestMethod.POST) @ResponseBody public String readEasyExcel() { try { String path = "G:\\测试\\data\\"; String[] xlsxArr = new File(path).list(); for (int i = 0; i < xlsxArr.length; i++) { String filePath = path + xlsxArr[i]; File fileTemp = new File(path + xlsxArr[i]); String fileName = fileTemp.getName().replace(".xlsx", ""); List<UserInfo> list = new ArrayList<>(); EasyExcel.read(filePath, UserInfo.class, new ReadEasyExeclAsyncListener(readEasyExeclService, fileName, batchCount, list)).sheet().doRead(); } }catch (Exception e){ logger.error("readEasyExcel 异常:",e); return "error"; } return "suceess"; }

2、ReadEasyExeclAsyncListener

 

java

复制代码

public ReadEasyExeclService readEasyExeclService; // 表名 public String TABLE_NAME; // 批量插入阈值 private int BATCH_COUNT; // 数据集合 private List<UserInfo> LIST; public ReadEasyExeclAsyncListener(ReadEasyExeclService readEasyExeclService, String tableName, int batchCount, List<UserInfo> list) { this.readEasyExeclService = readEasyExeclService; this.TABLE_NAME = tableName; this.BATCH_COUNT = batchCount; this.LIST = list; } @Override public void invoke(UserInfo data, AnalysisContext analysisContext) { data.setUuid(uuid()); data.setTableName(TABLE_NAME); LIST.add(data); if(LIST.size() >= BATCH_COUNT){ // 批量入库 readEasyExeclService.saveDataBatch(LIST); } } @Override public void doAfterAllAnalysed(AnalysisContext analysisContext) { if(LIST.size() > 0){ // 最后一批入库 readEasyExeclService.saveDataBatch(LIST); } } public static String uuid() { return UUID.randomUUID().toString().replace("-", ""); } }

3、ReadEasyExeclServiceImpl

 

java

复制代码

@Service public class ReadEasyExeclServiceImpl implements ReadEasyExeclService { @Resource private ReadEasyExeclMapper readEasyExeclMapper; @Override public void saveDataBatch(List<UserInfo> list) { // 通过mybatis入库 readEasyExeclMapper.saveDataBatch(list); // 通过JDBC入库 // insertByJdbc(list); list.clear(); } private void insertByJdbc(List<UserInfo> list){ List<String> sqlList = new ArrayList<>(); for (UserInfo u : list){ StringBuilder sqlBuilder = new StringBuilder(); sqlBuilder.append("insert into ").append(u.getTableName()).append(" ( UUID,ID,NAME,AGE,ADDRESS,PHONE,OP_TIME ) values ( "); sqlBuilder.append("'").append(ReadEasyExeclAsyncListener.uuid()).append("',") .append("'").append(u.getId()).append("',") .append("'").append(u.getName()).append("',") .append("'").append(u.getAge()).append("',") .append("'").append(u.getAddress()).append("',") .append("'").append(u.getPhone()).append("',") .append("sysdate )"); sqlList.add(sqlBuilder.toString()); } JdbcUtil.executeDML(sqlList); } }

4、UserInfo

 

java

复制代码

@Data public class UserInfo { private String tableName; private String uuid; @ExcelProperty(value = "ID") private String id; @ExcelProperty(value = "NAME") private String name; @ExcelProperty(value = "AGE") private String age; @ExcelProperty(value = "ADDRESS") private String address; @ExcelProperty(value = "PHONE") private String phone; }

这篇关于使用双异步后,从 191s 优化到 2s(2)的文章就介绍到这儿,希望我们推荐的文章对编程师们有所帮助!



http://www.chinasem.cn/article/832572

相关文章

使用Vue-ECharts实现数据可视化图表功能

《使用Vue-ECharts实现数据可视化图表功能》在前端开发中,经常会遇到需要展示数据可视化的需求,比如柱状图、折线图、饼图等,这类需求不仅要求我们准确地将数据呈现出来,还需要兼顾美观与交互体验,所... 目录前言为什么选择 vue-ECharts?1. 基于 ECharts,功能强大2. 更符合 Vue

如何合理使用Spring的事务方式

《如何合理使用Spring的事务方式》:本文主要介绍如何合理使用Spring的事务方式,具有很好的参考价值,希望对大家有所帮助,如有错误或未考虑完全的地方,望不吝赐教... 目录1、介绍1.1、底层构造1.1.事务管理器1.2.事务定义信息1.3.事务状态1.4.联系1.2、特点1.3、原理2. Sprin

Vue中插槽slot的使用示例详解

《Vue中插槽slot的使用示例详解》:本文主要介绍Vue中插槽slot的使用示例详解,本文通过实例代码给大家介绍的非常详细,对大家的学习或工作具有一定的参考借鉴价值,需要的朋友参考下吧... 目录一、插槽是什么二、插槽分类2.1 匿名插槽2.2 具名插槽2.3 作用域插槽三、插槽的基本使用3.1 匿名插槽

使用WPF实现窗口抖动动画效果

《使用WPF实现窗口抖动动画效果》在用户界面设计中,适当的动画反馈可以提升用户体验,尤其是在错误提示、操作失败等场景下,窗口抖动作为一种常见且直观的视觉反馈方式,常用于提醒用户注意当前状态,本文将详细... 目录前言实现思路概述核心代码实现1、 获取目标窗口2、初始化基础位置值3、创建抖动动画4、动画完成后

PyQt5 QDate类的具体使用

《PyQt5QDate类的具体使用》QDate是PyQt5中处理日期的核心类,本文主要介绍了PyQt5QDate类的具体使用,文中通过示例代码介绍的非常详细,对大家的学习或者工作具有一定的参考学习价... 目录核心功能常用方法及代码示例​1. 创建日期对象​2. 获取日期信息​3. 日期计算与比较​4. 日

Go语言使用slices包轻松实现排序功能

《Go语言使用slices包轻松实现排序功能》在Go语言开发中,对数据进行排序是常见的需求,Go1.18版本引入的slices包提供了简洁高效的排序解决方案,支持内置类型和用户自定义类型的排序操作,本... 目录一、内置类型排序:字符串与整数的应用1. 字符串切片排序2. 整数切片排序二、检查切片排序状态:

使用Java将实体类转换为JSON并输出到控制台的完整过程

《使用Java将实体类转换为JSON并输出到控制台的完整过程》在软件开发的过程中,Java是一种广泛使用的编程语言,而在众多应用中,数据的传输和存储经常需要使用JSON格式,用Java将实体类转换为J... 在软件开发的过程中,Java是一种广泛使用的编程语言,而在众多应用中,数据的传输和存储经常需要使用j

Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例

《Nginx使用Keepalived部署web集群(高可用高性能负载均衡)实战案例》本文介绍Nginx+Keepalived实现Web集群高可用负载均衡的部署与测试,涵盖架构设计、环境配置、健康检查、... 目录前言一、架构设计二、环境准备三、案例部署配置 前端 Keepalived配置 前端 Nginx

Python logging模块使用示例详解

《Pythonlogging模块使用示例详解》Python的logging模块是一个灵活且强大的日志记录工具,广泛应用于应用程序的调试、运行监控和问题排查,下面给大家介绍Pythonlogging模... 目录一、为什么使用 logging 模块?二、核心组件三、日志级别四、基本使用步骤五、快速配置(bas

SpringBoot中HTTP连接池的配置与优化

《SpringBoot中HTTP连接池的配置与优化》这篇文章主要为大家详细介绍了SpringBoot中HTTP连接池的配置与优化的相关知识,文中的示例代码讲解详细,感兴趣的小伙伴可以跟随小编一起学习一... 目录一、HTTP连接池的核心价值二、Spring Boot集成方案方案1:Apache HttpCl