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

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

一般我会这样做:

  • 通过POI读取需要导入的Excel;

  • 以文件名为表名、列头为列名、并将数据拼接成sql;

  • 通过JDBC或mybatis插入数据库;

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

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

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

  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("-", "");
  }

谁写的?拖出去,斩了!

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

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

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

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

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

readExcelCacheAsync控制类

  @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";
  }

分批读取超大Excel文件

  @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);
          }
      }
  }

异步批量入库

  @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);
  }

异步线程池工具类

@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重新配置:
    spring:
      task:
        execution:
          pool:
            max-size: 10
            core-size: 5
            keep-alive: 3s
            queue-capacity: 1000
            thread-name-prefix: my-executor
  • 也可以自定义线程池,下面通过简单的代码来实现以下@Async自定义线程池。

    @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;
        }
    }

异步失效的原因

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

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

  • 有一个问题,一直没时间摸索,线程池中的核心线程数CorePoolSize、最大线程数MaxPoolSize,设置成多少,最合适,效率最高。
  • 借着这个机会,测试一下。
我记得有这样一个说法,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,是不是效率最佳呢?

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

我记得大家都习惯性的将核心线程数CorePoolSize和最大线程数MaxPoolSize设置成一样的,都爱设置成200。
  • 是随便写的,还是经验而为之?
  • 测试发现,当你将核心线程数CorePoolSize和最大线程数MaxPoolSize都设置为200的时候,第一次它会同时开启150个线程,来进行工作。
  • 这个是为什么?
经过数十次的测试
  • 发现核心线程数好像差别不大
  • 每次读取和入库的数量是关键,不能太多,因为每次入库会变慢;
  • 也不能太少,如果太少,超过了150个线程,就会造成线程阻塞,也会变慢;

通过EasyExcel读取并插入数据库

  • EasyExcel的方式,我就不写双异步优化了,大家切记陷入低水平勤奋的怪圈。
ReadEasyExcelController
  @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";
  }
ReadEasyExeclAsyncListener
  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("-", "");
      }
  }
ReadEasyExeclServiceImpl
  @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);
      }
  }
UserInfo
  @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;
  }
相关推荐
这孩子叫逆4 分钟前
Spring Boot项目的创建与使用
java·spring boot·后端
星星法术嗲人8 分钟前
【Java】—— 集合框架:Collections工具类的使用
java·开发语言
爱写代码的小朋友14 分钟前
Python的几个高级特性
python
Eric.Lee202120 分钟前
数据集-目标检测系列- 螃蟹 检测数据集 crab >> DataBall
python·深度学习·算法·目标检测·计算机视觉·数据集·螃蟹检测
黑不溜秋的21 分钟前
C++ 语言特性29 - 协程介绍
开发语言·c++
一丝晨光26 分钟前
C++、Ruby和JavaScript
java·开发语言·javascript·c++·python·c·ruby
天上掉下来个程小白28 分钟前
Stream流的中间方法
java·开发语言·windows
xujinwei_gingko39 分钟前
JAVA基础面试题汇总(持续更新)
java·开发语言
liuyang-neu41 分钟前
力扣 简单 110.平衡二叉树
java·算法·leetcode·深度优先
sp_wxf1 小时前
Lambda表达式
开发语言·python