Java 多任务异步导出数据到excel

这是我在工作种遇到实际问题,百万数据量导出,由于长时间占据资源,导致系统无法响应其他请求。最后采用的任务队列的方式,利用线程池,将需要导出的任务加入线程池的等待队列种,并提供任务详情页面提供对应功能,采用将线程转为阻塞态的方式来让其他请求能抢到资源。
完整代码地址:

giteehttps://gitee.com/tmesh/export-task

githubhttps://github.com/ThousandMeshZ/export-task/tree/master

前提

遇到的一个百万数据量导出资源瓶颈问题。有 150万条数据需要导出,同时来 10 个导出任务,一个导出完成需要2分钟,在这两分钟内,资源会被这 10 个任务完全占据,导致其他功能没有资源去运行,导致整个系统无法响应任何请求。

方案

将导出作为任务,采用多线程的方式(本身应该使用消息队列来管理任务。先用线程池代替消息队列作为任务队列)导出到本地文件。 建立一个任务中心,可以去下载完成的任务文件。
最后选择了队列式分段异步查询

导出任务队列

采用多线程的方式(用线程池代替任务队列)导出到本地文件,然后在任务中心下载导出文件。这种方式解决了一个导出任务时间太长导致响应超时的问题。但是当任务量变大,查询压力随之变大,也会导致系统已经数据库阻塞,使系统无法响应请求。

分段异步查询

将查询语句进行分段,然后异步查询结果集,全部查询完成后,合并结果集,然后写入 excel。
会将数据库的瞬时压力剧增(假如分段 30 次,就会一次性将 30 次查询请求发送给数据库),在数据量大的情况下,有可能会直接将数据库的线程占满(比如数据库线程数为20),这时候其他的查询就会被阻塞,影响其他功能的使用。

分段异步写入(不可用)

将查询的结果集分段,然后异步写入。
由于 Sheet 是使用 TreeMap 作为数据结构的,异步写入会改变结构,导致 ConcurrentModificationException 错误。
由于 TreeMap 的存储结构是红黑树,异步写入行号整体是不连续的,会导致添加节点时间变慢。(还没有测试)
因此这种方法不可用

队列式分段异步查询

在分段异步查询的方式基础上,将各个分段放入队列中并发执行(用线程池代替任务队列)
这种方案下,可以控制用于查询的线程数量,以保证不会导致查询太多,导致数据库查询阻塞。

测试结果

线程池均是使用两个线程,每次进行四个导出任务,进行 5 轮

使用上面的导出任务队列结果


数据库查询平均消耗时间为 8707.0500 ms
excel 写入的平均消耗时间为 5947.3500 ms
总平均消耗时间为 14677.8500 ms
可以看出在数据库上消耗的时间 多于 excel 的写入时间

使用上面的队列式分段异步查询结果


数据库查询平均消耗时间为 5954.5000 ms
excel 写入的平均消耗时间为 5342.1500 ms
总平均消耗时间为 11353.3000 ms
可以看出在数据库上消耗的时间 少于 excel 的写入时间

总结

  • 在队列式分段异步查询下 数据库的查询时间 优于 导出任务队列。
  • 在队列式分段异步查询下效率 优于 导出任务队列,主要是在数据库查询上面。

实现

使用 jdk17 + vm 虚拟机中 centos7 系统下 docker 部署的 mysql8,springboot3,使用mybaits-plus 的 ORM框架

数据库准备

/*  
 Navicat Premium Dump SQL  
 Source Server         : docker-mysql8 Source Server Type    : MySQL Source Server Version : 80032 (8.0.32) Source Host           : 192.168.118.128:3306 Source Schema         : test_database  
 Target Server Type    : MySQL Target Server Version : 80032 (8.0.32) File Encoding         : 65001  
 Date: 23/02/2025 22:15:25*/  
  
CREATE DATABASE `test_database` DEFAULT CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci  DEFAULT ENCRYPTION='N'  
  
SET NAMES utf8mb4;  
SET FOREIGN_KEY_CHECKS = 0;  
  
-- ----------------------------  
-- Table structure for EXPORT_TASK  
-- ----------------------------  
DROP TABLE IF EXISTS `EXPORT_TASK`;  
CREATE TABLE `EXPORT_TASK`  (  
  `ID` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',  
  `MODULE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模块名',  
  `FILE_NAME` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名',  
  `FILE_PATH` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件路径',  
  `STATUS` int NULL DEFAULT NULL COMMENT '状态,0:未开始,1:开始,2:成功,3:失败',  
  `USER_NAME` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名',  
  `QUERY` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'sql语句',  
  `CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间',  
  `START_TIME` datetime NULL DEFAULT NULL COMMENT '开始时间',  
  `END_TIME` datetime NULL DEFAULT NULL COMMENT '结束时间',  
  `TIME_COST` int NULL DEFAULT NULL COMMENT '总时间消费',  
  `REASON` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原因',  
  `DB_START_DATE` datetime NULL DEFAULT NULL COMMENT 'db开始时间',  
  `DB_END_DATE` datetime NULL DEFAULT NULL COMMENT 'db结束时间',  
  `DB_TIME_COST` int NULL DEFAULT NULL COMMENT 'db时间消费',  
  `EXCEL_WRITE_START_DATE` datetime NULL DEFAULT NULL COMMENT 'excel写开始时间',  
  `EXCEL_WRITE_END_DATE` datetime NULL DEFAULT NULL COMMENT 'excel写结束时间',  
  `EXCEL_WRITE_TIME_COST` int NULL DEFAULT NULL COMMENT 'excel写时间消费',  
  PRIMARY KEY (`ID`) USING BTREE  
) ENGINE = InnoDB AUTO_INCREMENT = 57 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;  
  
-- ----------------------------  
-- Table structure for EXPORT_TASK_BAK  
-- ----------------------------  
DROP TABLE IF EXISTS `EXPORT_TASK_BAK`;  
CREATE TABLE `EXPORT_TASK_BAK`  (  
  `ID` bigint NOT NULL AUTO_INCREMENT COMMENT 'ID',  
  `MODULE` varchar(50) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '模块名',  
  `FILE_NAME` varchar(80) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件名',  
  `FILE_PATH` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '文件路径',  
  `STATUS` int NULL DEFAULT NULL COMMENT '状态,0:未开始,1:开始,2:成功,3:失败',  
  `USER_NAME` varchar(20) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名',  
  `QUERY` varchar(1000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT 'sql语句',  
  `CREATE_TIME` datetime NULL DEFAULT NULL COMMENT '创建时间',  
  `START_TIME` datetime NULL DEFAULT NULL COMMENT '开始时间',  
  `END_TIME` datetime NULL DEFAULT NULL COMMENT '结束时间',  
  `TIME_COST` int NULL DEFAULT NULL COMMENT '总时间消费',  
  `REASON` varchar(5000) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '原因',  
  `DB_START_DATE` datetime NULL DEFAULT NULL COMMENT 'db开始时间',  
  `DB_END_DATE` datetime NULL DEFAULT NULL COMMENT 'db结束时间',  
  `DB_TIME_COST` int NULL DEFAULT NULL COMMENT 'db时间消费',  
  `EXCEL_WRITE_START_DATE` datetime NULL DEFAULT NULL COMMENT 'excel写开始时间',  
  `EXCEL_WRITE_END_DATE` datetime NULL DEFAULT NULL COMMENT 'excel写结束时间',  
  `EXCEL_WRITE_TIME_COST` int NULL DEFAULT NULL COMMENT 'excel写时间消费',  
  PRIMARY KEY (`ID`) USING BTREE  
) ENGINE = InnoDB AUTO_INCREMENT = 41 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;  
  
-- ----------------------------  
-- Table structure for USER  
-- ----------------------------  
DROP TABLE IF EXISTS `USER`;  
CREATE TABLE `USER`  (  
  `ID` bigint NOT NULL AUTO_INCREMENT COMMENT 'id',  
  `NAME` varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_unicode_ci NULL DEFAULT NULL COMMENT '用户名',  
  PRIMARY KEY (`ID`) USING BTREE  
) ENGINE = InnoDB AUTO_INCREMENT = 1500001 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_unicode_ci ROW_FORMAT = Dynamic;  

创建测试数据

DROP PROCEDURE IF EXISTS `addTestData`;  
delimiter ;;  
CREATE PROCEDURE `addTestData`()  
begin  
declare number int;  
set number = 1;  
while number <= 1500000 -- 插入N条数据  
do  
insert into USER(ID,NAME)  
values(number, concat('用户_',number)); -- 为了区分姓名,我们加上后缀  
set number = number + 1;  
end  
while;  
end  
;;  
delimiter ;  
  
SET FOREIGN_KEY_CHECKS = 1;  

执行存储过程
在执行前,请注意,因为要插入 150万条数据,所以时间会比较长
在我的机器设备中全部插入需要大约43分钟

不同设备时间不是很客观。
我在阿里云的99元2g服务器同样执行了插入操作,全部插入需要大约120分钟

CALL addTestData

项目结构

配置文件

server:  
  port: 8080  
spring:  
  datasource:  
    # JDBC配置:  
    type: com.alibaba.druid.pool.DruidDataSource  
    driver-class-name: com.mysql.cj.jdbc.Driver  
    url: jdbc:mysql://127.0.0.1:3306/test_database?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai  
    username: root  
    password: TMesh729  
  
    # 连接池配置:  
    druid:  
      initial-size: 2 # 初始化时建立物理连接的个数。默认0  
      max-active: 10 # 最大连接池数量,默认8  
      min-idle: 1 # 最小连接池数量  
      max-wait: 2000 # 获取连接时最大等待时间,单位毫秒。  
      pool-prepared-statements: false # 是否缓存preparedStatement,也就是PSCache。PSCache对支持游标的数据库性能提升巨大,比如说oracle。在mysql下建议关闭。  
      max-pool-prepared-statement-per-connection-size: -1 # 要启用PSCache,必须配置大于0,当大于0时,poolPreparedStatements自动触发修改为true。在Druid中,不会存在Oracle下PSCache占用内存过多的问题,可以把这个数值配置大一些,比如说100  
      # ……druid节点下的其它参数见官方文档:https://github.com/alibaba/druid/wiki/DruidDataSource%E9%85%8D%E7%BD%AE%E5%B1%9E%E6%80%A7%E5%88%97%E8%A1%A8  
  
      # 启用Druid内置的Filter,会使用默认的配置。可自定义配置,见下方的各个filter节点。  
      filters: stat,wall  
  
      # StatViewServlet监控器。开启后,访问http://域名/druid/index.html  
      stat-view-servlet:  
        enabled: false # 开启 StatViewServlet,即开启监控功能  
        login-username: tmesh # 访问监控页面时登录的账号  
        login-password: 123456 # 密码  
        url-pattern: /druid/* # Servlet的映射地址,不填写默认为"/druid/*"。如填写其它地址,访问监控页面时,要使用相应的地址  
        reset-enable: false # 是否允许重置数据(在页面的重置按钮)。(停用后,依然会有重置按钮,但重置后不会真的重置数据)  
        allow: 192.168.1.2,192.168.1.1 # 监控页面访问白名单。默认为127.0.0.1。与黑名单一样,支持子网掩码,如128.242.127.1/24。多个ip用英文逗号分隔  
        deny: 18.2.1.3 # 监控页面访问黑名单  
  
  
      # 配置 WebStatFilter(StatFilter监控器中的Web模板)  
      web-stat-filter:  
        enabled: false # 开启 WebStatFilter,即开启监控功能中的 Web 监控功能  
        url-pattern: /* # 映射地址,即统计指定地址的web请求  
        exclusions: '*.js,*.gif,*.jpg,*.png,*.css,*.ico,/druid/*' # 不统计的web请求,如下是不统计静态资源及druid监控页面本身的请求  
        session-stat-enable: true # 是否启用session统计  
        session-stat-max-count: 1 # session统计的最大个数,默认是1000。当统计超过这个数,只统计最新的  
        principal-session-name: userName # 所存用户信息的serssion参数名。Druid会依照此参数名读取相应session对应的用户名记录下来(在监控页面可看到)。如果指定参数不是基础数据类型,将会自动调用相应参数对象的toString方法来取值  
        principal-cookie-name: userName # 与上类似,但这是通过Cookie名取到用户信息  
        profile-enable: true # 监控单个url调用的sql列表(试了没生效,以后需要用再研究)  
  
      filter:  
        wall:  
          enabled: true  # 开启SQL防火墙功能  
          config:  
            select-allow: true # 允许执行Select查询操作  
            delete-allow: false # 不允许执行delete操作  
            create-table-allow: false # 不允许创建表  
            # 更多用法,参考官方文档:https://github.com/alibaba/druid/wiki/%E9%85%8D%E7%BD%AE-wallfilter  
  
# MyBatis配置  
mybatis-plus:  
#  mapperLocations: classpath*:mapper/*.xml  
  map-underscore-to-camel-case: true  
  #实体扫描,多个package用逗号或者分号分隔  
  typeAliasesPackage: com.tmesh  
  typeEnumsPackage: com.tmesh  
  configuration:  
    defaultEnumTypeHandler: org.apache.ibatis.type.EnumOrdinalTypeHandler  
    mapUnderscoreToCamelCase: true  
    #日志打印  
#    log-impl: org.apache.ibatis.logging.stdout.StdOutImpl  
    cacheEnabled: true  
  global-config:  
    banner: false  
    db-config:  
      id-type: auto  
      logic-delete-value: 1  
      logic-not-delete-value: 0  
  
logging:  
  level:  
#    com.tmesh.exporttask: debug  
    com.tmesh.exporttask.mapper: debug  
#    org.springframework: debug  
#    root: debug  
    sql: debug  
#    tomcat: debug  
#    web: debug  
  
config:  
  # 是否异步数据库查询  
  useDbAsyn: false  
  # 是否异步设置数据,需要预生成所有单元格(不能使用SXSSFWorkbook),不建议使用,会有异步写入问题,需要给表加锁  
  useAsyn: false  
  # 是否设置节省内存,建议启用(使用SXSSFWorkbook,可以加快写入速度)  
  # 如果启用节省内存,则无法异步设置数据,也无法预生成excel对象  
  # 原因:使用的是SXSSFWorkbook类,该类会分段写入临时文件,此时如果使用异步写入会报这个错误 Attempting to write a row[0] in the range [0,0] that is already written to disk  # 如果报这个Java heap space错,就启用这个。如果还是报错,需要修改jvm参数  
  useEconimicMemory: true  
  # 是否预生成excel对象,sheet、row、cell  
  preGenerateExcel: false

pom文件

<parent>  
    <groupId>org.springframework.boot</groupId>  
    <artifactId>spring-boot-starter-parent</artifactId>  
    <version>3.1.2</version>  
    <relativePath/> <!-- lookup parent from repository -->  
</parent>

<properties>  
    <java.version>17</java.version>  
    <maven.compiler.source>17</maven.compiler.source>  
    <maven.compiler.target>17</maven.compiler.target>  
    <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>  
    <mybatis-plus.version>3.5.3</mybatis-plus.version>  
    <fastjson.version>2.0.4</fastjson.version>  
    <jackson.version>2.12.3</jackson.version>  
    <poi.version>4.1.2</poi.version>  
    <jakarta.servlet.version>6.0.0</jakarta.servlet.version>  
    <mysql.version>8.0.31</mysql.version>  
    <druid.version>1.2.23</druid.version>  
    <jakarta.version>2.3.3</jakarta.version>  
</properties>

<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-boot-starter</artifactId>  
    <version>${mybatis-plus.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-extension</artifactId>  
    <version>${mybatis-plus.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>com.baomidou</groupId>  
    <artifactId>mybatis-plus-generator</artifactId>  
    <version>${mybatis-plus.version}</version>  
</dependency>

<!-- 阿里JSON解析器 -->  
<dependency>  
    <groupId>com.alibaba.fastjson2</groupId>  
    <artifactId>fastjson2</artifactId>  
    <version>${fastjson.version}</version>  
</dependency>

<dependency>  
    <groupId>jakarta.xml.bind</groupId>  
    <artifactId>jakarta.xml.bind-api</artifactId>  
    <version>${jakarta.version}</version>  
</dependency>  
  
<!-- excel工具 -->  
<dependency>  
    <groupId>org.apache.poi</groupId>  
    <artifactId>poi-ooxml</artifactId>  
    <version>${poi.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-databind</artifactId>  
    <version>${jackson.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>com.fasterxml.jackson.core</groupId>  
    <artifactId>jackson-annotations</artifactId>  
    <version>${jackson.version}</version>  
</dependency>  
  
<dependency>  
    <groupId>jakarta.servlet</groupId>  
    <artifactId>jakarta.servlet-api</artifactId>  
    <version>${jakarta.servlet.version}</version>  
    <scope>provided</scope>  
</dependency>

<!-- Mysql驱动包 -->  
<dependency>  
	<groupId>mysql</groupId>  
	<artifactId>mysql-connector-java</artifactId>  
	<version>${mysql.version}</version>  
	<scope>runtime</scope>  
</dependency>  

<dependency>            
	<groupId>com.alibaba</groupId>  
	<artifactId>druid-spring-boot-3-starter</artifactId>  
	<version>${druid.version}</version>  
</dependency>

线程池工具类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 线程池工具  
 * @updateUser : TMesh  
 */@EnableAsync  
@Configuration  
public class ThreadPoolExecutorUtils {  
    private static Logger logger = LogManager.getLogger(ThreadPoolExecutorUtils.class);  
  
    private static LinkedBlockingQueue blockingQueue = new LinkedBlockingQueue();   // 导出线程队列  
  
    private static Map<String, JSONObject> threadRecordMap = new LinkedHashMap<>();   // 导出线线程记录Map  
  
    /**  
     * 采用注解的方式  
     **/  
    @Bean("taskExecutor")  
    public Executor taskExecutor() {  
        int i = Runtime.getRuntime().availableProcessors();  // CPU核数 = Runtime.getRuntime().availableProcessors()
        System.out.println("系统最大线程数  : " + i);  
        ThreadPoolExecutor executor = new ThreadPoolExecutor(235000, TimeUnit.SECONDS, blockingQueue, Executors.defaultThreadFactory(), new ThreadPoolExecutor.CallerRunsPolicy());  
        return executor;  
    }  
  
    /*  
     * @methodName: getDetailInfo     * @description: 获取任务详细信息  
     * @param: []     * @return: com.alibaba.fastjson.JSONObject     * @author: TMesh     * @Version: 1.0     */    public static JSONObject getDetailInfo() {  
        ThreadPoolExecutor instance = (ThreadPoolExecutor) BeanUtils.getBean("taskExecutor");  
        JSONObject jsonObject = new JSONObject();  
        jsonObject.put("任务总数", instance.getTaskCount());  
        jsonObject.put("已完成任务", instance.getCompletedTaskCount());  
        jsonObject.put("尚未完成任务", instance.getTaskCount() - instance.getCompletedTaskCount());  
        jsonObject.put("尚未完成任务信息", threadRecordMap);  
        return jsonObject;  
    }  
  
    /*  
     * @methodName: setThreadRecordMap     * @description: 添加/修改 任务信息  
     * @param: [key, value]     * @return: void     * @author: TMesh     * @Version: 1.0     */    public static void setThreadRecordMap(String key, JSONObject value) {  
        synchronized (ThreadPoolSingleton.class) {  
            threadRecordMap.put(key, value);  
        }  
    }  
    public static JSONObject getThreadRecordMap(String key) {  
        synchronized (ThreadPoolSingleton.class) {  
            return threadRecordMap.get(key);  
        }  
    }  
  
    /*  
     * @methodName: removeThreadRecordMap     * @description: 删除已完成任务信息  
     * @param: [key]     * @return: void     * @author: TMesh     * @Version: 1.0     */    public static void removeThreadRecordMap(String key) {  
        synchronized (ThreadPoolSingleton.class) {  
            threadRecordMap.remove(key);  
        }  
    }  
    /*  
     * @methodName: getInstanceBlockingQueue     * @description: 获取线程队列单例  
     * @param: []     * @return: java.util.concurrent.LinkedBlockingQueue     * @author: TMesh     * @Version: 1.0     */    private static LinkedBlockingQueue getInstanceBlockingQueue() {  
        if (blockingQueue == null) {  
            blockingQueue = new LinkedBlockingQueue<>();  
        }  
        return blockingQueue;  
    }  
}

ThreadPoolExecutor

先看 ThreadPoolExecutor(线程池)这个类的构造参数

构造参数参数介绍

  • corePoolSize: 核心线程数
    核心线程会一直存活,及时没有任务需要执行。
    当线程数小于核心线程数时,即使有线程空闲,线程池也会优先创建新线程处理。
    设置 allowCoreThreadTimeout=true(默认 false)时,核心线程会超时关闭。
    CPU密集型:corePoolSize = CPU核数 + 1
    IO密集型:corePoolSize = CPU核数 * 2
  • maximumPoolSize:最大线程数
    当线程数>=corePoolSize,且任务队列已满时。线程池会创建新线程来处理任务。
    当线程数=maxPoolSize,且任务队列已满时,线程池会拒绝处理任务而抛出异常。
  • keepAliveTime:线程空闲时间
    当线程空闲时间达到 keepAliveTime 时,线程会退出,直到线程数量=corePoolSize。
    如果 allowCoreThreadTimeout=true,则会直到线程数量=0。
  • workQueue:当线程数目超过核心线程数时用于保存任务的队列。
    主要有3种类型的 BlockingQueue 可供选择:无界队列有界队列同步移交。从参数中可以看到,此队列仅保存实现 Runnable 接口的任务。
    • 新任务进入时线程池的执行策略:
    • 当正在运行的线程小于c orePoolSize,线程池会创建新的线程。
    • 当大于 corePoolSize 而任务队列未满时,就会将整个任务塞入队列。
    • 当大于 corePoolSize 而且任务队列满时,并且小于 maximumPoolSize 时,就会创建新额线程执行任务。
    • 当大于 maximumPoolSize 时,会根据 handler 策略处理线程。
    1. 无界队列
      队列大小无限制,常用的为无界的 LinkedBlockingQueue,使用该队列作为阻塞队列时要尤其当心,当任务耗时较长时可能会导致大量新任务在队列中堆积最终导致 OOM。阅读代码发现,Executors.newFixedThreadPool 采用就是 LinkedBlockingQueue,而当 QPS 很高,发送数据很大,大量的任务被添加到这个无界 LinkedBlockingQueue 中,导致 cpu 和内存飙升服务器挂掉。
      当然这种队列,maximumPoolSize 的值也就无效了。当每个任务完全独立于其他任务,即任务执行互不影响时,适合于使用无界队列;例如,在 Web 页服务器中。这种排队可用于处理瞬态突发请求,当命令以超过队列所能处理的平均数连续到达时,此策略允许无界线程具有增长的可能性。
    2. 有界队列
      当使用有限的 maximumPoolSizes 时,有界队列有助于防止资源耗尽,但是可能较难调整和控制。常用的有两类,一类是遵循FIFO原则的队列如 ArrayBlockingQueue,另一类是优先级队列如 PriorityBlockingQueue。PriorityBlockingQueue 中的优先级由任务的 Comparator 决定。
      使用有界队列时队列大小需和线程池大小互相配合,线程池较小有界队列较大时可减少内存消耗,降低 cpu 使用率和上下文切换,但是可能会限制系统吞吐量。
    3. 同步移交队列
      如果不希望任务在队列中等待而是希望将任务直接移交给工作线程,可使用 SynchronousQueue 作为等待队列。SynchronousQueue 不是一个真正的队列,而是一种线程之间移交的机制。要将一个元素放入 SynchronousQueue 中,必须有另一个线程正在等待接收这个元素。只有在使用无界线程池或者有饱和策略时才建议使用该队列。
  • queueCapacity:任务队列容量(阻塞队列)
    当核心线程数达到最大时,新任务会放在队列中排队等待执行
  • allowCoreThreadTimeout:允许核心线程超时
  • rejectedExecutionHandler:任务拒绝处理器
    • 两种情况会拒绝处理任务:
      • 当线程数已经达到 maxPoolSize,且队列已满,会拒绝新任务。
      • 当线程池被调用 shutdown()后,会等待线程池里的任务执行完毕再 shutdown。如果在调用 shutdown()和线程池真正 shutdown 之间提交任务,会拒绝新任务。
        线程池会调用 rejectedExecutionHandler 来处理这个任务。如果没有设置默认是 AbortPolicy,会抛出异常。
    • 这几种策略模式都实现了 RejectedExecutionHandler 接口。
      • AbortPolicy 丢弃任务,抛运行时异常。
      • CallerRunsPolicy 执行任务。
      • DiscardPolicy 忽视,什么都不会发生。
      • DiscardOldestPolicy 从队列中踢出最先进入队列(最后一个执行)的任务。

异步设置数据线程池工具

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 异步设置数据线程池工具  
 * @updateUser : TMesh  
 */public class DataThreadPoolExecutorUtils {  
    private static Logger logger = LogManager.getLogger(DataThreadPoolExecutorUtils.class);  
  
    /**  
     * @author: TMesh  
     * @description: 线程池单例  
     * @Version: 1.0  
     **/    public static class SetDataThreadPoolSingleton {  
  
        private static ThreadPoolTaskExecutor instance;  // 导出线程池  
        private static final Map<String, CompletableFuture> threadRecordMap = new LinkedHashMap<>();   // 导出线线程记录Map  
  
        /*         * @methodName: getDetailInfo         * @description: 获取任务详细信息  
         * @param: []         * @return: com.alibaba.fastjson.JSONObject         * @author: TMesh         * @Version: 1.0         */        public static JSONObject getDetailInfo() {  
            JSONObject jsonObject = new JSONObject();  
            jsonObject.put("任务总数", getInstance().getThreadPoolExecutor().getTaskCount());  
            jsonObject.put("已完成任务", getInstance().getThreadPoolExecutor().getCompletedTaskCount());  
            jsonObject.put("尚未完成任务", getInstance().getThreadPoolExecutor().getTaskCount() - getInstance().getThreadPoolExecutor().getCompletedTaskCount());  
            jsonObject.put("尚未完成任务信息", threadRecordMap);  
            return jsonObject;  
        }  
  
  
        /*  
         * @methodName: setThreadRecordMap         * @description: 添加/修改 任务信息  
         * @param: [key, value]         * @return: void         * @author: TMesh         * @Version: 1.0         */        public static void setThreadRecordMap(String key, CompletableFuture threadTask) {  
            synchronized (SetDataThreadPoolSingleton.class) {  
                threadRecordMap.put(key, threadTask);  
            }  
        }  
  
        /*  
         * @methodName: removeThreadRecordMap         * @description: 删除已完成任务信息  
         * @param: [key]         * @return: void         * @author: TMesh         * @Version: 1.0         */        public static void removeThreadRecordMap(String key) {  
            synchronized (SetDataThreadPoolSingleton.class) {  
                threadRecordMap.remove(key);  
            }  
        }  
  
        /*  
         * @methodName: getInstance         * @description: 获取线程池单例  
         * @param: []         * @return: java.util.concurrent.ThreadPoolExecutor         * @author: TMesh         * @Version: 1.0         */        public static ThreadPoolTaskExecutor getInstance() {  
            if (instance == null) {  
                instance = new ThreadPoolTaskExecutor();  
                // 设置核心线程数  
                instance.setCorePoolSize(2);  
                // 设置最大线程数  
                instance.setMaxPoolSize(3);  
                // 设置队列容量  
                instance.setQueueCapacity(1000);  
                // 设置线程活跃时间(秒)  
                instance.setKeepAliveSeconds(5000);  
                // 设置默认线程名称  
                instance.setThreadNamePrefix("data-pool");  
                // 设置拒绝策略  
                instance.setRejectedExecutionHandler(new ThreadPoolExecutor.CallerRunsPolicy());  
                // 等待所有任务结束后再关闭线程池  
                instance.setWaitForTasksToCompleteOnShutdown(true);  
                // 初始化线程池  
                instance.initialize();  
            }  
            return instance;  
        }  
  
        public static void execute(Runnable runnable) {  
            getInstance().execute(runnable);  
        }  
                  
/*  
         * @methodName: getInstanceBlockingQueue         * @description: 获取线程队列单例  
         * @param: []         * @return: java.util.concurrent.BlockingQueue<Runnable>         * @author: TMesh         * @Version: 1.0         */        public static BlockingQueue<Runnable> getInstanceBlockingQueue() {  
            if (instance == null) {  
                return null;  
            }  
            return instance.getThreadPoolExecutor().getQueue();  
        }  
        }  
  
}

ThreadPoolTaskExecutor

ThreadPoolTaskExecutor 线程是 Spring 的线程池,其底层是依据 JDK 线程池 ThreadPoolExecutor 来实现的。

参数介绍

  • corePoolSize:线程池维护线程最小的数量,默认为 1
  • maxPoolSize:线程池维护线程最大数量,默认为 Integer.MAX_VALUE
  • keepAliveSeconds:(maxPoolSize-corePoolSize)部分线程空闲最大存活时间,默认存活时间是 60s
  • queueCapacity:阻塞任务队列的大小,默认为 Integer.MAX_VALUE,默认使用 LinkedBlockingQueue
  • allowCoreThreadTimeOut:设置为 true 的话,keepAliveSeconds 参数设置的有效时间对 corePoolSize 线程也有效,默认是 flase
  • threadFactory:用于设置创建线程的工厂,可以通过线程工厂给每个创建出来的线程设置更有意义的名字。使用开源框架 guava 提供的 ThreadFactoryBuilder 可以快速给线程池里的线程设置有意义的名字
  • rejectedExecutionHandler:拒绝策略,当队列 workQueue 和线程池 maxPoolSize 都满了,说明线程池处于饱和状态,那么必须采取一种策略处理提交的新任务。这个策略默认情况下是 AbortPolicy,表示无法处理新任务时抛出异常。
    拒绝策略
    拒绝策略有以下四种,默认情况下是 AbortPolicy,也可根据实际业务需求类实现RejectedExecutionHandler接口实现自己的处理策略
    1. AbortPolicy:丢弃任务,并且抛出 RejectedExecutionException 异常;
    2. DiscardPolicy:丢弃任务,不处理,不抛出异常;
    3. CallerRunsPolicy:直接在 execute 方法的调用线程中运行被拒绝的任务;
    4. DiscardOldestPolicy:丢弃队列中最前面的任务,然后重新尝试执行任务。

ThreadPoolTaskExecutor 和 ThreadPoolExecutor 都是线程池的实现,但它们有以下几点区别

  1. ThreadPoolTaskExecutorSpring 框架中编写的,它对 ThreadPoolExecutor 进行了封装,提供了更加丰富的功能,更易于在 Spring 中使用。而 ThreadPoolExecutor 是 JDK 中的实现。
  2. ThreadPoolTaskExecutor 实现了 InitializingBeanDisposableBean 接口,可以更好的与 Spring 容器进行整合。可以在 bean 初始化时进行自定义初始化,并在销毁时进行资源清理。
  3. ThreadPoolTaskExecutor 提供了代理模式,可以更方便的对 **Executor **进行扩展和管理。我们可以通过设置 TaskDecorator 来对任务进行装饰,比如统计任务耗时等。
  4. ThreadPoolTaskExecutor 对线程池中的异常进行了捕捉和处理,并通过FatalExceptionHandler 接口对 fatal 的异常进行处理。简化了资源管理和关闭逻辑。
  5. ThreadPoolTaskExecutor 可以设置 ThreadPoolExecutor 的 RejectedExecutionHandler 策略,提供了 CallerRunsPolicy 和 AbortPolicy 两个实现。
  6. ThreadPoolTaskExecutor 提供 setThreadFactory 方法方便设置线程工厂,可以给线程池中的线程设置有意义的名称。
  7. ThreadPoolTaskExecutor 在线程池关闭时,会先使用金线程池的 getActiveCount 方法判断线程数,如果大于 0 会先调用 shutdown() 方法关闭线程池,待任务执行完毕后再调用 lingering Close 方法终止线程池。
    除此之外,在具体的参数配置上也有一定的差异。所以总体来说, ThreadPoolTaskExecutorThreadPoolExecutor 进行了封装,并添加了更多的功能,使其可以更好的与 Spring 容器集成,管理资源和异常,方便进行扩展,是在 Spring 应用中使用线程池的首选。但底层它们的原理都是通过线程池来管理和调度线程。

ThreadPoolTaskExecutor 和 ThreadPoolExecutor 各有优缺点

ThreadPoolTaskExecutor 的优点

  1. 集成了 Spring 的初始化和销毁接口,更容易管理,
  2. 提供了异常处理和代理模式,更易于扩展和管理。
  3. 简化了线程池的关闭逻辑,更易于使用。
  4. 提供了丰富的 ReiectedExecutionHandler 策略实现
  5. 可以方便的为线程池设置线程工厂和线程名,

ThreadPoolTaskExecutor 的缺点

  1. 依赖 Spring 框架,不如 ThreadPoolExecutor 轻量
  2. 代理模式会略微降低性能。
  3. 配置不如 ThreadPoolExecutor 灵活,

ThreadPoolExecutor 的优点

  1. 是 JDK 的标准实现,更底层,更轻量级,没有第三方框架依赖,
  2. 配置更加灵活,可以设置更多细粒度的参数。
  3. 无代理模式,性能更高。

ThreadPoolExecutor 的缺点:

  1. 缺少与 Spring 的整合,资源管理稍微复杂
  2. 缺少异常处理机制,使用时需要额外实现,
  3. 缺少丰富的 ReiectedExecutionHandler 策略实现。
    综上,选择哪一个主要还是要根据实际应用场景来决定:
    如果项目使用 Spring 框架,并且需要简单快速地使用线程池,那么 ThreadPoolTaskExecutor 会是一个不错的选择

导出任务实体类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 导出任务实体类  
 * @updateUser : TMesh  
 */
@TableName("EXPORT_TASK")  
@Data  
@Accessors(chain = true)  
public class ExportTask implements Serializable {  
    private static final long serialVersionUID = 1L;  
    /**  
     * ID     **/    @TableId  
    private long id;  
  
    /**  
     * 模块名  
     **/  
    private String module;  
  
    /**  
     * 文件名  
     **/  
    private String fileName;  
  
    /**  
     * 文件路径  
     **/  
    private String filePath;  
  
    /**  
     * 状态,0:未开始,1:开始,2:成功,3:失败  
     **/  
    private int status;  
  
    /**  
     * 用户名  
     **/  
    private String userName;  
  
    /**  
     * sql语句  
     **/  
    private String query;  
  
    /**  
     * 创建时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date createTime;  
  
    /**  
     * 开始时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date startTime;  
  
    /**  
     * 结束时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date endTime;  
  
    /**  
     * 总时间消费  
     **/  
    private long timeCost;  
  
    /**  
     * 原因  
     **/  
    private String reason;  
  
    /**  
     * db开始时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date dbStartDate;  
  
    /**  
     * db结束时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date dbEndDate;  
  
    /**  
     * db时间消费  
     **/  
    private long dbTimeCost;  
  
    /**  
     * excel写开始时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date excelWriteStartDate;  
  
    /**  
     * excel写结束时间  
     **/  
    @JsonFormat(locale = "zh", timezone = "GMT+8", pattern = "yyyy-MM-dd HH:mm:ss SSS") // JSON格式 响应给浏览器  
    @JSONField(format="yyyy-MM-dd HH:mm:ss SSS") // fastjson 中用 @JSONField 格式化日期格式/指定日期属性的格式  
    private Date excelWriteEndDate;  
  
    /**  
     * excel写时间消费  
     **/  
    private long excelWriteTimeCost;  
}

USER 实体类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : USER实体类  
 * @updateUser : TMesh  
 */
@TableName("USER")  
@Data  
@Accessors(chain = true)  
public class UserEntity {  
  
    /**  
    * ID     
    **/    
    @TableId  
    private long id;  
  
    /**  
     * 用户名  
     **/  
    private String name;  
}

Bean 工具类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 一个类实现了ApplicationContextAware接口后,就可以获得ApplicationContext中的所有bean  
 *              用于解决某些类因为有被new出来的实例导致@Autowired失效的问题  
 * @updateUser : TMesh  
 */
@Component  
public class BeanUtils implements ApplicationContextAware {  
  
    protected static ApplicationContext applicationContext;  
  
    /**  
     * 实现ApplicationContextAware接口的回调方法,设置上下文环境  
     * @param arg spring上下文对象  
     * @throws BeansException 抛出spring异常  
     */  
    @Override  
    public void setApplicationContext(ApplicationContext arg) throws BeansException {  
        if (applicationContext == null) {  
            applicationContext = arg;  
        }  
    }  
  
    /**  
     * 获取spring上下文对象  
     * @return 上下文对象  
     */  
    public static ApplicationContext getContext() {  
        return applicationContext;  
    }  
  
    /**  
     * 根据beanName获取bean  
     * @param beanName bean的名称  
     * @return bean对象  
     */  
    public static Object getBean(String beanName) {  
        return applicationContext.getBean(beanName);  
    }  
  
    /**  
     * 根据beanName和类型获取bean  
     * @param beanName bean名称  
     * @param clazz    bean的Class类型  
     * @param <T>      bean的类型  
     * @return bean对象  
     */  
    public static <T> T getBean(String beanName, Class<T> clazz) {  
        return applicationContext.getBean(beanName, clazz);  
    }  
  
    /**  
     * 根据类型获取bean  
     * @param clazz bean的Class类型  
     * @param <T>   bean的类型  
     * @return bean对象  
     */  
    public static <T> T getBean(Class<T> clazz) {  
        return applicationContext.getBean(clazz);  
    }  
}

导出任务接口

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 导出任务继承Runnable,并设置常量(这些可以看情况是否卸载配置文件里面)  
 * @updateUser : TMesh  
 */
 public interface ExportThreadTask extends Runnable {  
    // SHEETLIMIT 要是 OFFSET 的倍数  
    // 偏移常量  
    // TODO 这里可以做成动态配置  
    public final static int OFFSET = 50000;  
    // sheet数量大小常量  
    public final static int SHEETLIMIT = 1000000;  
    // excel 本地存储路径  
//    public final static String DIR_PATH = "/data/excel/export";  
    public final static String DIR_PATH = "D:/data/excel/export";  
   
}

通用导出任务

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 通用导出线程任务  
 * @updateUser : TMesh  
 */
@Data  
@EqualsAndHashCode(callSuper = false)  
public class CommonExportThreadTask<T> implements ExportThreadTask {  
  
    private final Logger logger = LogManager.getLogger(this.getClass());  
  
    public static ExportTaskService exportTaskService = null;  
  
    protected int successCount; // 成功数量  
    protected long count; // 总数  
  
    protected Date startData; // 开始日期  
  
    protected String module; // 模块名  
    protected String filePath; // 文件路径  
    protected String fileName; // 文件名称  
    public String user; // 任务使用者  
    protected QueryWrapper<T> queryWapper; // 导出条件  
  
    protected BaseMapper<T> mapper; // 导出任务mapper  
    protected String query; // 导出sql  
    protected long identity; // 任务Id  
    protected boolean useDbAsyn; // 是否异步数据查询  
    protected boolean useAsyn; // 是否异步写入数据  
    protected boolean useEconimicMemory; // 是否使用节省内存  
    protected boolean preGenerateExcel; // 是否使用节省内存  
    protected String[] columnNames = {"ID", "NAME"}; // 表格标头  
    // map中的key  
    protected String[] keys = {"ID", "NAME"}; // 数据库对应查询字段名  
  
    protected int limit = 0; // sql limit 条件  
    protected int lastNumber = 0; // 当前数据位置  
  
  
    /**  
     * 初始化导出任务的必要信息  
     **/  
    protected CommonExportThreadTask(String module, String user, QueryWrapper<T> queryWapper, BaseMapper<T> mapper, boolean useDbAsyn, boolean useAsyn, boolean useEconimicMemory, boolean preGenerateExcel, String[] columnNames, String[] keys) {  
        // 获取导出任务服务  
        if (exportTaskService == null) {  
            exportTaskService = BeanUtils.getBean(ExportTaskService.class);  
        }  
        this.module = module;  
        this.startData = new Date();  
        // 设置文件信息  
        this.setFileInfo();  
        this.user = user;  
        this.queryWapper = queryWapper;  
        this.useEconimicMemory = useEconimicMemory;  
        // 如果启用节省内存使用的是SXSSFWorkbook类,该类会分段写入临时文件,此时如果使用异步写入会报这个错误 Attempting to write a row[0] in the range [0,0] that is already written to disk        // 总体生成将会成为累赘,需要频繁的读取临时文件  
        this.useDbAsyn = useDbAsyn;  
        if (useEconimicMemory) {  
            useAsyn = false;  
            preGenerateExcel = false;  
        }  
        this.preGenerateExcel = preGenerateExcel;  
        this.useAsyn = useAsyn;  
        this.count = 0;  
        if (columnNames != null && columnNames.length > 0) {  
            this.columnNames = columnNames;  
        }  
        if (keys != null && keys.length > 0) {  
            this.keys = keys;  
        }  
        if (mapper != null) {  
            this.mapper = mapper;  
        }  
        // 添加查询字段  
        queryWapper.select(keys);  
        // 获取查询sql  
        this.query = this.getSqlQuery();  
        // 添加任务信息  
        ThreadPoolExecutorUtils.ThreadPoolSingleton.setThreadRecordMap(this.fileName, this.toJsonObject());  
        // 保存导出任务,并返回任务id  
        long taskId = this.saveExportTask();  
        this.identity = taskId;  
    }  
    /**  
     * 获取查询sql  
     * 作为简单的展示,需要更好的可以重写  
     **/  
    private String getSqlQuery() {  
        String sql = "select " + this.queryWapper.getSqlSelect() + " from " + this.module;  
        if (this.queryWapper.getSqlSegment() != null && !this.queryWapper.getSqlSegment().isEmpty()) {  
            sql += " where " + this.queryWapper.getSqlSegment();  
        }  
        sql = sql.replace("ew.paramNameValuePairs.", "");  
        Map<String, Object> paramNameValuePairs = this.queryWapper.getParamNameValuePairs();  
        for(String k : paramNameValuePairs.keySet()) {  
            if (sql.contains(k)) {  
                Object v = paramNameValuePairs.get(k);  
                sql = sql.replace(k, String.valueOf(v));  
            }  
        }  
        return sql;  
    }  
    /**  
     * 设置文件的基本信息  
     **/  
    private void setFileInfo() {  
        String fileId = UUID.randomUUID().toString().replace("-", "");  
        this.fileName = this.module + "-" + fileId + ".xlsx";  
        this.filePath = ExportThreadTask.DIR_PATH + "/" + this.module + "/" + this.fileName;  
        File file = new File(this.filePath);  
        if(!file.exists()){  
            //先得到文件的上级目录,并创建上级目录,在创建文件  
            file.getParentFile().mkdirs();  
        }  
    }  
  
    /**  
     * 保存导出任务并返回任务id  
     **/    private long saveExportTask() {  
        ExportTask exportTask = new ExportTask().  
                setModule(this.module).  
                setFileName(this.fileName).  
                setFilePath(this.filePath).  
                setStatus(0).  
                setCreateTime(this.startData).  
                setUserName(user).  
                setQuery(this.query);  
        exportTaskService.save(exportTask);  
        return exportTask.getId();  
    }  
  
    @Override  
    public void run() {  
        this.logger.info("filePath={},任务={}开始运行", this.filePath, this.identity);  
        FileOutputStream out = null;  
        ByteArrayOutputStream outputStream = null;  
        Workbook wb = null;  
        Sheet sheet = null;  
        Map<String, List<Map<String, Object>>> dbMap = new ConcurrentHashMap<>();  
        List<CompletableFuture<Object>> taskDbList = new ArrayList<>();  
        List<CompletableFuture<Integer>> taskList = new ArrayList<>();  
        ExportTask exportTask = exportTaskService.getById(this.identity);  
        exportTask.setStatus(1);  
        exportTaskService.updateById(exportTask);  
  
        try {  
            out = new FileOutputStream(new File(this.filePath));  
            // 获取数据总数  
            outputStream = new ByteArrayOutputStream();  
  
            QueryWrapper<T> countQueryWapper = this.queryWapper.clone();  
            countQueryWapper.select("1");  
            this.count = this.mapper.selectCount(countQueryWapper);  
            // sheet数  
            int pageSheet = new BigDecimal(this.count).divide(new BigDecimal(ExportThreadTask.SHEETLIMIT), 0, RoundingMode.UP).setScale(0, RoundingMode.UP).intValue();  
            // 总限制次数  
            int limitSheet = new BigDecimal(this.count).divide(new BigDecimal(ExportThreadTask.OFFSET), 0,RoundingMode.UP).setScale(0, RoundingMode.UP).intValue();  
            // 每个sheet最大循环次数  
            int sheetNumberLimit = new BigDecimal(ExportThreadTask.SHEETLIMIT).divide(new BigDecimal(ExportThreadTask.OFFSET)).setScale(0, RoundingMode.UP).intValue();;  
  
            if (this.useEconimicMemory) {  
                wb = ExportPOIUtils.createSXlsxWorkBook();  
            } else {  
                wb = ExportPOIUtils.createXlsxWorkBook();  
            }  
            List<Sheet> sheetList = null;  
            if (this.preGenerateExcel) {  
                sheetList = ExportPOIUtils.createSheetList(wb, pageSheet, this.count, this.columnNames);  
            }  
  
            exportTask.setStartTime(new Date());  
            exportTask.setDbStartDate(new Date());  
            exportTask.setExcelWriteStartDate(new Date());  
            exportTaskService.updateById(exportTask);  
  
            long begTimeLDb = System.currentTimeMillis();  
            for (int j = 1; j < pageSheet + 1; j++) {  
                // 创建新的 sheet                if (this.preGenerateExcel) {  
                    sheet = sheetList.get(j - 1);  
                } else {  
                    sheet = ExportPOIUtils.createSheet(wb, this.columnNames);  
                }  
  
                // 修改最后一次循环次数,最后一次不一定能循环满  
                if (limitSheet < sheetNumberLimit) {  
                    sheetNumberLimit = limitSheet;  
                }  
                // 获取新的wapper用于添加分段信息  
                for (int k = 1; k < sheetNumberLimit + 1; k++) {  
                    QueryWrapper<T> newQueryWapper = this.queryWapper.clone();  
                    // 数据库操作,返回结果集  
                    List<Map<String, Object>> resultList = this.dbDataHandle(exportTask, sheet, newQueryWapper, taskDbList);  
                    // 数据写入excel实体  
                    this.dataWrite(exportTask, sheet, resultList, taskList);  
                }  
                limitSheet -= sheetNumberLimit;  
            }  
            exportTask.setDbEndDate(new Date());  
  
            long begTimeL = System.currentTimeMillis();  
            if (this.useDbAsyn) {  
                this.useDbAsynHandle(wb, pageSheet, taskList, taskDbList, dbMap, sheet, exportTask, begTimeLDb);  
            }  
  
            if (this.useAsyn) {  
                this.useAsynHandle(taskList, exportTask, begTimeL);  
            }  
            taskList.clear();  
            taskDbList.clear();  
            long writeTime = System.currentTimeMillis();  
            // 写入文件  
            wb.write(outputStream);  
  
            // 记录结束信息  
            exportTask.setExcelWriteEndDate(new Date());  
            exportTask.setExcelWriteTimeCost(exportTask.getExcelWriteTimeCost()  + exportTask.getExcelWriteEndDate().getTime() - writeTime);  
            exportTask.setEndTime(new Date());  
            exportTask.setTimeCost(exportTask.getEndTime().getTime() - exportTask.getStartTime().getTime());  
  
            ThreadPoolExecutorUtils.ThreadPoolSingleton.removeThreadRecordMap(this.fileName);  
  
            this.logger.info("{}-filePath={},数据全部导出至excel文件...耗时={} 毫秒,大小:{} 行:", this.module, this.filePath, exportTask.getTimeCost(), this.successCount);  
            exportTask.setStatus(2);  
            exportTask.setReason("成功");  
        } catch (Exception e) {  
            e.printStackTrace();  
            String failReason = e.toString();  
            exportTask.setStatus(3);  
            exportTask.setReason(failReason);  
            this.logger.error(e.getMessage(), e);  
        } finally {  
            // 进行任务更新,文件流,workbook的关闭  
            try {  
                exportTaskService.updateById(exportTask);  
                if (wb != null) {  
                    // 关闭Workbook  
                    if (wb instanceof SXSSFWorkbook) {  
                        // dispose of temporary files backing this workbook on disk -> 处理SXSSFWorkbook导出excel时,产生的临时文件  
                        ((SXSSFWorkbook) wb).dispose();  
                    }  
                    wb.close();  
                }  
                if (outputStream != null) {  
                    // 刷新缓存,并关闭流  
                    outputStream.flush();  
                    outputStream.close();  
                }  
                if (out != null) {  
                    // 刷新缓存,并关闭流  
                    out.flush();  
                    out.close();  
                }  
            } catch (IOException e) {  
                e.printStackTrace();  
                this.logger.error(e.getMessage(), e);  
            }  
        }  
    }  
    /**  
     * 数据库数据查询操作  
     **/  
    protected List<Map<String, Object>> dbDataHandle(ExportTask exportTask, Sheet sheet, QueryWrapper<T> newQueryWapper, List<CompletableFuture<Object>> taskList) {  
        // 添加限制条件  
        newQueryWapper.last("limit " + this.limit + "," + ExportThreadTask.OFFSET);  
        if (this.useDbAsyn) {  
            AsynDbThreadTask task = new AsynDbThreadTask(  
                    this.fileName, sheet, this.limit, this.module, this.mapper, newQueryWapper, exportTask, this.lastNumber, ExportThreadTask.OFFSET  
            );  
            CompletableFuture<Object> future = task.runFuture(DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.getInstance());  
            this.lastNumber += ExportThreadTask.OFFSET;  
            taskList.add(future);  
            return null;        } else {  
            long begTimeL = System.currentTimeMillis();  
            // 获取结果集  
            List<Map<String, Object>> resultList = this.mapper.selectMaps(newQueryWapper);  
            long endTime = System.currentTimeMillis();  
            exportTask.setDbTimeCost(exportTask.getDbTimeCost() + endTime - begTimeL);  
            this.logger.info("{}-线程={},filePath={},导出完成...耗时={} 毫秒,大小={}行", this.module, Thread.currentThread().getName(), this.filePath, endTime - begTimeL, resultList.size());  
            return resultList;  
        }  
  
    }  
  
  
    /**  
     * 数据写入excel实体  
     **/  
    protected void dataWrite(ExportTask exportTask, Sheet sheet, List<Map<String, Object>>resultList, List<CompletableFuture<Integer>> taskList) throws IOException, InterruptedException {  
        this.limit += ExportThreadTask.OFFSET;  
        // 判断是否开启异步数据库查询  
        if (this.useDbAsyn) {  
            return;  
        }  
        if (this.useAsyn) {  
            AsynSetRowDataThreadTask task = new AsynSetRowDataThreadTask(  
                    this.fileName, sheet, resultList, this.keys, this.lastNumber, this.lastNumber + 1, this.lastNumber + ExportThreadTask.OFFSET, ExportThreadTask.OFFSET  
            );  
            CompletableFuture<Integer> future = task.runFuture(DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.getInstance());  
            taskList.add(future);  
            this.lastNumber += resultList.size();  
        } else {  
            long begTimeL = System.currentTimeMillis();  
            ExportPOIUtils.setRowData(sheet, resultList, keys, this.lastNumber + 1, false);  
            long endTime = System.currentTimeMillis();  
            exportTask.setExcelWriteTimeCost(exportTask.getExcelWriteTimeCost() + endTime - begTimeL);  
            this.successCount += resultList.size();  
            this.lastNumber += resultList.size();  
            resultList.clear();  
        }  
        ThreadPoolExecutorUtils.ThreadPoolSingleton.setThreadRecordMap(this.fileName, this.toJsonObject());  
        // TODO 这里可以适当的让线程放弃 cpu,进入等待状态,以防止长时间占用资源  
//        Thread.sleep(1000);  
    }  
  
    /**  
     * 异步数据库查询数据处理方法  
     **/  
    protected void useDbAsynHandle(Workbook wb, int pageSheet,  
                                   List<CompletableFuture<Integer>> taskList,  
                                   List<CompletableFuture<Object>> taskDbList,  
                                   Map<String, List<Map<String, Object>>> dbMap,  
                                   Sheet sheet, ExportTask exportTask, long begTimeLDb) throws IOException {  
        CompletableFuture[] taskDbArray = new CompletableFuture[taskDbList.size()];  
        Sheet finalSheet = sheet;  
  
        CompletableFuture.allOf(taskDbList.toArray(taskDbArray)).join();  
        exportTask.setDbEndDate(new Date());  
        exportTask.setDbTimeCost(exportTask.getDbTimeCost() + System.currentTimeMillis() - begTimeLDb);  
  
        CompletableFuture.allOf(taskDbList.toArray(taskDbArray)).thenApply(v -> {  
            for(CompletableFuture future: taskDbList) {  
                try {  
                    long s = System.currentTimeMillis();;  
                    Map<String, List<Map<String, Object>>> map = (Map<String, List<Map<String, Object>>>) future.get();  
                    if (this.useAsyn) {  
                        String key = (String) map.keySet().toArray()[0];  
                        List<Map<String, Object>> mapList = map.get(key);  
                        int rowNum = Integer.parseInt(key);  
                        AsynSetRowDataThreadTask task = new AsynSetRowDataThreadTask(  
                                this.fileName, finalSheet, mapList, this.keys, rowNum, rowNum, rowNum + ExportThreadTask.OFFSET, ExportThreadTask.OFFSET  
                        );  
                        CompletableFuture<Integer> futureExcel = task.runFuture(DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.getInstance());  
                        this.lastNumber += ExportThreadTask.OFFSET;  
                        taskList.add(futureExcel);  
                    } else {  
                        String key = (String) map.keySet().toArray()[0];  
                        this.successCount += map.get(key).size();  
                        dbMap.putAll(map);  
                        long e = System.currentTimeMillis();;  
                        exportTask.setExcelWriteTimeCost(exportTask.getExcelWriteTimeCost() + e - s);  
                        exportTaskService.updateById(exportTask);  
                    }  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
            return null;  
        }).thenAccept(result -> {  
            System.out.println("最终结果是: " + result); // 打印最终结果  
        }).join();  
        if (!this.useAsyn) {  
            long s = System.currentTimeMillis();;  
            ExportPOIUtils.mergeDbData(wb, pageSheet, dbMap, this.keys, this.module, this.fileName, this.useAsyn);  
            exportTask.setExcelWriteTimeCost(exportTask.getExcelWriteTimeCost() + System.currentTimeMillis() - s);  
            dbMap.clear();  
        }  
    }  
  
    /**  
     * 异步设置数据处理方法  
     **/  
    protected void useAsynHandle(List<CompletableFuture<Integer>> taskList,  
                                 ExportTask exportTask, long begTimeL) {  
        CompletableFuture[] taskArray = new CompletableFuture[taskList.size()];  
        CompletableFuture.allOf(taskList.toArray(taskArray)).join();  
        exportTask.setExcelWriteTimeCost(exportTask.getExcelWriteTimeCost() + System.currentTimeMillis() - begTimeL);  
        exportTaskService.updateById(exportTask);  
        CompletableFuture.allOf(taskList.toArray(taskArray)).thenApply(v -> {  
            for(CompletableFuture future: taskList) {  
                try {  
                    Integer sum = (Integer) future.get();  
                    this.successCount += sum;  
                } catch (Exception e) {  
                    e.printStackTrace();  
                }  
            }  
            return null;  
        }).thenAccept(result -> {  
            System.out.println("最终结果是: " + result); // 打印最终结果  
        }).join();;  
    }  
  
    /**  
     * 返回任务信息,用于记录   
**/  
    protected JSONObject toJsonObject() {  
        JSONObject jsonObject = new JSONObject();  
        jsonObject.put("filePath", this.filePath);  
        jsonObject.put("successCount", this.successCount);  
        jsonObject.put("count", this.count);  
        jsonObject.put("identity", this.identity);  
        jsonObject.put("task", this);  
        return jsonObject;  
    }  
}

通用异步数据库查询任务

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 通用异步查询任务  
 * @updateUser : TMesh  
 */
@Component  
@Data  
@EqualsAndHashCode(callSuper = true)  
public class CommonDbDataThreadTask<T> extends CompletableFuture<Map<String, Object>> {  
  
    private final Logger logger = LogManager.getLogger(this.getClass());  
  
    public static ExportTaskService exportTaskService = null;  
  
    protected String fileName; // 文件名称  
    protected long count; // 数据总数  
    protected int startRow; // 开始行号  
    protected int endRow; // 结束行号  
    protected int offset; // 偏移量  
    protected Sheet sheet; // sheet  
    protected int limit; // sheet  
    protected String module; // sheet  
    protected BaseMapper<T> mapper; // sheet  
    protected QueryWrapper<T> queryWapper; // 当前数据位置  
    protected ExportTask exportTask; // sheet  
    protected int lastNumber; // 当前数据位置  
  
    protected CommonDbDataThreadTask() {  
  
    }  
    /**  
     * 必要数据初始化  
     **/  
    protected CommonDbDataThreadTask(String fileName, Sheet sheet, int limit, String module, BaseMapper<T> mapper,  
                                     QueryWrapper<T> queryWapper, ExportTask exportTask, int lastNumber, int offset) {  
        if (exportTaskService == null) {  
            exportTaskService = BeanUtils.getBean(ExportTaskService.class);  
        }  
        this.fileName = fileName;  
        this.sheet = sheet;  
        this.limit = limit;  
        this.module = module;  
        this.mapper = mapper;  
        this.queryWapper = queryWapper;  
        this.exportTask = exportTask;  
        this.lastNumber = lastNumber;  
        this.offset = offset;  
    }  
    
    /**  
     * 任务执行,没有指定线程池  
     **/  
    public CompletableFuture<Object> runFuture() {  
        DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.setThreadRecordMap(this.fileName, this);  
  
        return CompletableFuture.supplyAsync(() -> {  
            logger.info("fileName={},线程开始运行", this.fileName);  
            Map<String, List<Map<String, Object>>> map = new HashMap<>();  
            try {  
                // 添加限制条件  
                long begTimeL = System.currentTimeMillis();  
                // 获取结果集  
                List<Map<String, Object>> resultList = this.mapper.selectMaps(this.queryWapper);  
                long endTime = System.currentTimeMillis();  
                this.logger.info("{}-线程={},fileName={},导出完成...耗时={} 毫秒,大小={}行", this.module, Thread.currentThread().getName(), this.fileName, endTime - begTimeL, resultList.size());  
                map.put(String.valueOf(this.limit), resultList);  
                return map;  
            } catch (Exception e) {  
                e.printStackTrace();  
                this.logger.error(e.getMessage(), e);  
            }  
            return map;  
        });  
    }  
  
    /**  
     * 任务执行,指定线程池  
     **/  
    public CompletableFuture<Object> runFuture(ThreadPoolTaskExecutor executor) {  
        DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.setThreadRecordMap(this.fileName, this);  
  
        return CompletableFuture.supplyAsync(() -> {  
            logger.info("fileName={},线程开始运行", this.fileName);  
            Map<String, List<Map<String, Object>>> map = new HashMap<>();  
            try {  
                // 添加限制条件  
                long begTimeL = System.currentTimeMillis();  
                // 获取结果集  
                List<Map<String, Object>> resultList = this.mapper.selectMaps(this.queryWapper);  
                long endTime = System.currentTimeMillis();  
                this.logger.info("{}-线程={},fileName={},导出完成...耗时={} 毫秒,大小={}行", this.module, Thread.currentThread().getName(), this.fileName, endTime - begTimeL, resultList.size());  
                map.put(String.valueOf(this.limit), resultList);  
                return map;  
            } catch (Exception e) {  
                e.printStackTrace();  
                this.logger.error(e.getMessage(), e);  
            }  
            return map;  
        }, executor);  
    }  
}

通用异步设置数据任务

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 通用异步设置数据任务  
 * @updateUser : TMesh  
 */
@Component  
@Data  
@EqualsAndHashCode(callSuper = true)  
public class CommonSetRowDataThreadTask<T> extends CompletableFuture<T> {  
  
    private final Logger logger = LogManager.getLogger(this.getClass());  
    protected String fileName; // 文件名称  
    protected long count; // 数据总数  
    protected int startRow; // 开始行号  
    protected int endRow; // 结束行号  
    protected int offset; // 偏移量  
    protected Sheet sheet; // sheet  
    protected List<Map<String, Object>> list; // 数据列表  
    protected String[] keys; // 数据库对应字段  
    protected int lastNumber; // 当前数据位置  
    protected boolean useAsyn = true; // 标志  
  
    protected CommonSetRowDataThreadTask() {  
  
    }  
    /**  
     * 必要数据初始化  
     **/  
    protected CommonSetRowDataThreadTask(String fileName, Sheet sheet, List<Map<String, Object>> list,  
                                         String[] keys, int lastNumber, int startRow, int endRow, int offset) {  
        this.fileName = fileName;  
        this.sheet = sheet;  
        this.list = list;  
        this.keys = keys;  
        this.lastNumber = lastNumber;  
        this.startRow = startRow;  
        this.endRow = endRow;  
        this.offset = offset;  
    }  
    /**  
     * 任务执行,没有指定线程池  
     **/  
    public CompletableFuture<Integer> runFuture() {  
        DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.setThreadRecordMap(this.fileName, this);  
        return CompletableFuture.supplyAsync(() -> {  
            long begTime = System.currentTimeMillis();  
            this.logger.info("fileName={},线程开始运行", this.fileName);  
            try {  
                // excel设置数据  
                ExportPOIUtils.setRowData(this.sheet, this.list, this.keys, this.lastNumber + 1, this.useAsyn);  
                long endTime = System.currentTimeMillis();  
                JSONObject threadRecordMap = ThreadPoolExecutorUtils.ThreadPoolSingleton.getThreadRecordMap(this.fileName);  
                CommonExportThreadTask task = threadRecordMap.getObject("task", new TypeReference<CommonExportThreadTask>(){});  
                // 任务数量统计  
                task.setSuccessCount(task.getSuccessCount() + this.offset);  
                DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.removeThreadRecordMap(this.fileName);  
                logger.info("fileName={},数据全部导出至excel...耗时={} 毫秒,大小={} 行={}-{}", this.fileName, endTime - begTime, this.list.size(), this.lastNumber + 1, this.lastNumber + this.list.size());  
                // 清空已使用完的数据,节省内存  
                int size = this.list.size();  
                this.list.clear();  
                return size;  
            } catch (Exception e) {  
                e.printStackTrace();  
                this.logger.error(e.getMessage(), e);  
            } finally {  
  
            }            return 0;  
        });  
    }  
  
    /**  
     * 任务执行,指定线程池  
     **/  
    public CompletableFuture<Integer> runFuture(ThreadPoolTaskExecutor executor) {  
        DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.setThreadRecordMap(this.fileName, this);  
        return CompletableFuture.supplyAsync(() -> {  
            long begTime = System.currentTimeMillis();  
            this.logger.info("fileName={},线程开始运行", this.fileName);  
            try {  
                // excel设置数据  
                ExportPOIUtils.setRowData(this.sheet, this.list, this.keys, this.lastNumber + 1, this.useAsyn);  
                long endTime = System.currentTimeMillis();  
                JSONObject threadRecordMap = ThreadPoolExecutorUtils.ThreadPoolSingleton.getThreadRecordMap(this.fileName);  
                CommonExportThreadTask task = threadRecordMap.getObject("task", new TypeReference<CommonExportThreadTask>(){});  
                // 任务数量统计  
                task.setSuccessCount(task.getSuccessCount() + this.offset);  
                DataThreadPoolExecutorUtils.SetDataThreadPoolSingleton.removeThreadRecordMap(this.fileName);  
                logger.info("fileName={},数据全部导出至excel...耗时={} 毫秒,大小={} 行={}-{}", this.fileName, endTime - begTime, this.list.size(), this.lastNumber + 1, this.lastNumber + this.list.size());  
                // 清空已使用完的数据,节省内存  
                int size = this.list.size();  
                this.list.clear();  
                return size;  
            } catch (Exception e) {  
                e.printStackTrace();  
                this.logger.error(e.getMessage(), e);  
            } finally {  
  
            }            return 0;  
        }, executor);  
    }  
    }

用户控制器

用于用户添加导出任务

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 用户控制器  
 * @updateUser : TMesh  
 */
@RestController  
@RequestMapping("/user")  
public class UserController {  
    private final Logger logger = LogManager.getLogger(UserController.class);  
    @Autowired  
    private UserSerivce userSerivce;  
  
    @GetMapping("/exportList")  
    public JSONObject exportList(UserExportTaskDto userExportTaskDto) {  
        JSONObject jsonObject = new JSONObject();  
        // TODO 系统的获取用户信息  
        // String userName = user.getUserName;  
        this.userSerivce.exportListByAnnotation(userExportTaskDto);  
        return jsonObject;  
    }  
}

用户服务

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 用户服务  
 * @updateUser : TMesh  
 */
public interface UserSerivce extends IService<UserEntity> {  
  
    void exportListByAnnotation(UserExportTaskDto userExportTaskDto);  
}

用户服务实现类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 用户服务实现类  
 * @updateUser : TMesh  
 */
@Service("userSerivce")  
public class UserSerivceImpl extends ServiceImpl<UserMapper, UserEntity> implements UserSerivce {  
  
    private final Logger logger = LogManager.getLogger(UserController.class);  
  
    @Value("${config.useDbAsyn}")  
    private boolean useDbAsyn;  
    @Value("${config.useAsyn}")  
    private boolean useAsyn;  
    @Value("${config.useEconimicMemory}")  
    private boolean useEconimicMemory;  
    @Value("${config.preGenerateExcel}")  
    private boolean preGenerateExcel;  
  
    @Autowired  
    private UserMapper userMapper;  
  
    @Override  
    public void exportList(UserExportTaskDto userExportTaskDto) {  
        QueryWrapper<UserEntity> mapper = new QueryWrapper<>();  
        // TODO 组装查询条件  
        try {  
            // 将任务放入线程池  
            ThreadPoolExecutorUtils.ThreadPoolSingleton.execute(new UserExportThreadTask("user", mapper, this.userMapper, this.useDbAsyn, this.useAsyn, this.useEconimicMemory, this.preGenerateExcel));  
        } catch (Exception e) {  
            e.printStackTrace();  
            this.logger.error(e);  
        }  
    }  
  
    /**  
     * 采用注解的方式  
     **/  
    @Override  
    @Async("taskExecutor")  
    public void exportListByAnnotation(UserExportTaskDto userExportTaskDto) {  
        QueryWrapper<UserEntity> mapper = new QueryWrapper<>();  
        // TODO 组装查询条件  
        try {  
            // 开始任务,这里可以自己定义自己想要功能,这里直接复用代码了。  
            new UserExportThreadTask("user", mapper, this.userMapper, this.useDbAsyn, this.useAsyn, this.useEconimicMemory, this.preGenerateExcel)  
                    .run();  
        } catch (Exception e) {  
            e.printStackTrace();  
            this.logger.error(e);  
        }  
    }  
}  
  
/**  
 * 导出任务线程,继承自CommonExportThreadTask,这里为了方便写则作为内部类在服务实现类里面。可以单独作为一个任务类提出去  
 **/  
class UserExportThreadTask extends CommonExportThreadTask<UserEntity> {  
  
    private static final String MODULE = "USER";  
    private static final String[] COLUNM_NAMES = {"ID", "NAME"};  
    // map中的key  
    private static final String[] KEYS = {"ID", "NAME"};  
  
    private final Logger logger = LogManager.getLogger(this.getClass());  
  
    public UserExportThreadTask(String user, QueryWrapper<UserEntity> queryWapper, BaseMapper<UserEntity> mapper, boolean useDbAsyn, boolean useAsyn, boolean useEconimicMemory, boolean preGenerateExcel) {  
        super(MODULE, user, queryWapper, mapper, useDbAsyn, useAsyn, useEconimicMemory, preGenerateExcel, COLUNM_NAMES, KEYS);  
    }  
  
    /**  
     * 任务执行内容。可以重写 run 方法,实现需要的功能  
     **/  
    @Override  
    public void run() {  
        super.run();  
    }  
  
    /**  
     * 数据库数据查询操作,有其他需求可以重写  
     **/  
    @Override  
    protected List<Map<String, Object>> dbDataHandle(ExportTask exportTask, Sheet sheet, QueryWrapper<UserEntity> newQueryWapper, List<CompletableFuture<Object>> taskList) {  
        return super.dbDataHandle(exportTask, sheet, newQueryWapper, taskList);  
    }  
  
    /**  
     * 数据写入excel实体,有其他需求可以重写  
     **/  
    @Override  
    protected void dataWrite(ExportTask exportTask, Sheet sheet, List<Map<String, Object>>resultList, List<CompletableFuture<Integer>> taskList) throws IOException, InterruptedException {  
        super.dataWrite(exportTask, sheet, resultList, taskList);  
    }  
  
  
    /**  
     * 异步数据库查询数据处理方法,可以根据需求重写  
     **/  
    @Override  
    protected void useDbAsynHandle(Workbook wb, int pageSheet,  
                                   List<CompletableFuture<Integer>> taskList,  
                                    List<CompletableFuture<Object>> taskDbList,  
                                   Map<String, List<Map<String, Object>>> dbMap,  
                                   Sheet sheet, ExportTask exportTask, long begTimeLDb) throws IOException {  
        super.useDbAsynHandle(wb, pageSheet, taskList, taskDbList, dbMap, sheet, exportTask, begTimeLDb);  
    }  
  
    /**  
     * 异步设置数据处理方法,可以根据需求重写  
     **/  
    @Override  
    protected void useAsynHandle(List<CompletableFuture<Integer>> taskList,  
                                 ExportTask exportTask, long begTimeL) {  
        super.useAsynHandle(taskList, exportTask, begTimeL);  
    }  
}

用户Mapper

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : UserMapper  
 * @updateUser : TMesh  
 */
@Mapper  
public interface UserMapper extends BaseMapper<UserEntity> {  
}

导出任务监控控制器

/**  
 * @ClassName: ExportTaskController  
 * @description: 导出任务监控  
 * @author: TMesh  
 * @Version: 1.0  
 **/
@RestController  
@RequestMapping("/task")  
public class ExportTaskController {  
    private final Logger logger = LogManager.getLogger(ExportTaskController.class);  
    @Autowired  
    private ExportTaskService exportTaskService;  
  
    @GetMapping("/list")  
    public JSONObject list(TaskListDto taskListDto) {  
        // TODO 系统的获取用户信息  
        // String userName = user.getUserName;  
        JSONObject jsonObject = null;  
        try {  
            QueryWrapper<ExportTask> queryWrapper = new QueryWrapper<>();  
  
            // TODO 条件输入  
  
            List<ExportTask> list = this.exportTaskService.list(queryWrapper);  
            // TODO 数据组装  
  
            jsonObject = new JSONObject();  
            jsonObject.put("total", list.size());  
            jsonObject.put("rows", list);  
            // TODO 操作记录记录  
//            OperationLogRecorder.insert(user, "task任务查询成功", "task任务查询", "查询");  
        } catch (Exception e) {  
            e.printStackTrace();  
            // TODO 操作记录记录  
//            OperationLogRecorder.insert(user, "task任务查询失败:" + e.getMessage(), "task任务查询", "查询");  
            this.logger.error(e.getMessage(), e);  
        }  
        // TODO 这里可以自定义返回  
        return jsonObject;  
    }  
  
    @PostMapping("/delete")  
    public JSONObject delete(Map<String, Object> map) {  
        String id = (String) map.get("id");  
		this.exportTaskService.removeById(id);
        JSONObject jsonObject =  new JSONObject();  
        // TODO 操作记录记录  
//        OperationLogRecorder.insert(user, "删除任务task成功...userId={}", "删除", id);  
        // TODO 这里可以自定义返回  
        return jsonObject;  
    }  
  
    @PostMapping("/download")  
    public void download(String id, HttpServletResponse response) {  
        JSONObject jsonObject = null;  
        try {  
            ExportTask exportTask = this.exportTaskService.getById(id);  
            String filePath = exportTask.getFilePath();  
            // TODO 下载文件操作  
            ExportPOIUtils.downloadFromFile(response, filePath);  
            // TODO 操作记录记录  
//            OperationLogRecorder.insert(user, "下载任务task成功...userId={}", "下载", id);  
        } catch (Exception e) {  
            e.printStackTrace();  
            this.logger.error(e.getMessage(), e);  
            // TODO 操作记录记录  
//            OperationLogRecorder.insert(user, "下载任务task失败...userId={}", "下载", id);  
        }  
    }  
  
    public JSONObject getModule() {  
        JSONObject jsonObject = null;  
        // TODO 这里用于获取所有 Module 用于页面查询条件  
  
        // TODO 这里可以自定义返回  
        return jsonObject;  
    }  
  
    @GetMapping("/allExecuteDeatilInfo")  
    public JSONObject threadPoolExecutorDetailInfo() {  
        return ThreadPoolExecutorUtils.ThreadPoolSingleton.getDetailInfo();  
    }  
  
}

导出任务监控服务类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 导出任务服务类  
 * @updateUser : TMesh  
 */
 public interface ExportTaskService extends IService<ExportTask> {  
}

导出任务监控服务实现类

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 导出任务服务实现类  
 * @updateUser : TMesh  
 */@Service("exportTaskService")  
public class ExportTaskServiceImpl extends ServiceImpl<ExportTaskMapper, ExportTask> implements ExportTaskService {  
  
    @Autowired  
    private ExportTaskMapper exportTaskMapper;  
}

导出任务监控Mapper

/**  
 * @author : TMesh  
 * @version : 1.0.0  
 * @description : 导出任务Mapper  
 * @updateUser : TMesh  
 */
@Mapper  
public interface ExportTaskMapper extends BaseMapper<ExportTask> {  
    
}
posted @   Thousand_Mesh  阅读(9)  评论(0编辑  收藏  举报
相关博文:
阅读排行:
· 全程不用写代码,我用AI程序员写了一个飞机大战
· DeepSeek 开源周回顾「GitHub 热点速览」
· 记一次.NET内存居高不下排查解决与启示
· MongoDB 8.0这个新功能碉堡了,比商业数据库还牛
· .NET10 - 预览版1新功能体验(一)
点击右上角即可分享
微信分享提示