SpringBoot实现异步导入导出任务(实现思路 + 超详细流程)

本文介绍了在用户量大、数据量大、并发高的场景下,如何通过异步导入导出模式优化用户体验和服务器压力。主要步骤包括:用户操作后保存至异步任务表,通过定时任务或调度器执行任务,使用CSV格式分批写入文件以防止内存溢出,并在任务完成后提供下载。此外,还分享了相关表结构、依赖管理和部分关键代码实现。
摘要由CSDN通过智能技术生成
前言:

在系统用户量较大,且用户较为依赖导入导出报表,而数据量又比较大的情况下,这时再采用即时下载Excel的方式用户体验就不太理想了。
所以退而求其次,需要找另外的实现模式在不影响用户使用的前提下,降低我们服务器压力,同时又能满足用户需求的解决方案。
本文介绍一下我们实际项目中使用的:异步导入导出模式。
主要分为几个步骤:
一、 用户操作导出时保存操作至异步任务表中,记录用户信息及请求时参数,同步响应给用户 “操作成功,请去任务中心查看”。
二、 用户操作导入时将导入文件暂时保存至服务器中,同时保存导入操作至异步任务表中,记录用户信息,同步响应给用户 “操作成功,请去任务中心查看”。
三、 需要一个"我的任务"菜单来展示用户导入/导出任务的状态及操作。
四、 需要定时任务或任务调度Quartz来实现我们异步任务表中的任务执行,任务执行可采用多线程执行来提高效率。
五、 例如导出操作:首先在服务器创建要导出的文件,由于导出数据可能过多,Excel写入效率低下,可采用.csv格式导出数据。
防止出现数据库一次查询数据过多导致内存溢出的情况,可以采用分批查询以流的方式写入csv文件,最终操作成功后更新异步任务表该任务的状态。
六、 在"我的任务"菜单中提供下载按钮,用户的导出任务若状态为:处理完成(成功)的状态时可进行下载操作,此时将服务器生成的scv文件下载到本地。
七、 由于该模式会生成很多临时文件,建议将临时文件保存至oss服务器,如果资源充足的情况可以将该模块作为异步服务,后续继续优化的空间还是非常大的。

主要流程:
  1. 需要一张异步任务表,这里贴出建表sql
CREATE TABLE `async_import_export_task` (
  `task_id` bigint(20) NOT NULL COMMENT '任务id',
  `task_name` varchar(255) DEFAULT NULL COMMENT '任务名称',
  `company_id` bigint(20) DEFAULT NULL COMMENT '公司id',
  `user_id` bigint(20) DEFAULT NULL COMMENT '用户id',
  `priority` tinyint(255) DEFAULT NULL COMMENT '优先级0-10',
  `req_param` varchar(8000) DEFAULT NULL COMMENT '请求参数',
  `task_status` tinyint(255) DEFAULT NULL COMMENT '任务状态',
  `created_time` datetime DEFAULT NULL COMMENT '创建时间',
  `deal_time` datetime DEFAULT NULL COMMENT '处理时间',
  `finish_time` datetime DEFAULT NULL COMMENT '完成时间',
  `result` mediumtext COMMENT '处理结果',
  `tag` varchar(255) DEFAULT NULL COMMENT '文件路径:\n导入任务时,路径为用户文件上传 oss 的路径\n导出任务时,路径为生成的文件在 oss 的路径',
  `task_code` varchar(255) DEFAULT NULL COMMENT '任务编码\r\n     * D-A-01 下载当天呼叫记录\r\n',
  `task_type` tinyint(4) DEFAULT NULL COMMENT '     * 任务类型\n     * 1:上传(导入)\n     * 2:下载(导出)',
  PRIMARY KEY (`task_id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
  1. 实现流程可能需要某些maven依赖,这里我将我自己的都贴出来了,需要的话可以从中引入
    <properties>
        <java.version>1.8</java.version>
        <lombok.version>1.16.18</lombok.version>
        <fastjson.version>1.2.39</fastjson.version>
        <commons.io.version>2.5</commons.io.version>
        <commons.lang3.version>3.5</commons.lang3.version>
        <mysql.connector.version>8.0.16</mysql.connector.version>
        <mybatis-plus-version>3.1.0</mybatis-plus-version>
        <druid-version>1.0.29</druid-version>
        <velocity-version>2.0</velocity-version>
        <swagger2-version>2.7.0</swagger2-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <!-- 引入aop相关 -->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
        <!--Lombok-->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <scope>provided</scope>
            <version>${lombok.version}</version>
        </dependency>
        <!--springboot配置元数据-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <!--Json-->
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>${fastjson.version}</version>
        </dependency>
        <!-- redis-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
        </dependency>
        <!--io常用工具类 -->
        <dependency>
            <groupId>commons-io</groupId>
            <artifactId>commons-io</artifactId>
            <version>${commons.io.version}</version>
        </dependency>
        <!--commons-lang3 工具类 -->
        <dependency>
            <groupId>org.apache.commons</groupId>
            <artifactId>commons-lang3</artifactId>
            <version>${commons.lang3.version}</version>
        </dependency>

        <!--使用Mysql数据库-->
        <dependency>
            <groupId>mysql</groupId>
            <artifactId>mysql-connector-java</artifactId>
            <version>${mysql.connector.version}</version>
        </dependency>

        <!--mybatis-plus -->
        <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-generator</artifactId>
            <version>${mybatis-plus-version}</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid</artifactId>
            <version>${druid-version}</version>
        </dependency>
        <dependency>
            <groupId>log4j</groupId>
            <artifactId>log4j</artifactId>
            <version>1.2.17</version>
        </dependency>
        <dependency>
            <groupId>org.apache.velocity</groupId>
            <artifactId>velocity-engine-core</artifactId>
            <version>${velocity-version}</version>
        </dependency>

        <!--swagger2 集成-->
        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger2</artifactId>
            <version>${swagger2-version}</version>
        </dependency>

        <dependency>
            <groupId>io.springfox</groupId>
            <artifactId>springfox-swagger-ui</artifactId>
            <version>${swagger2-version}</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>swagger-bootstrap-ui</artifactId>
            <version>1.9.6</version>
        </dependency>

        <dependency>
            <groupId>cn.hutool</groupId>
            <artifactId>hutool-all</artifactId>
            <version>5.3.10</version>
        </dependency>
    </dependencies>
  1. 我们按上方的流程来贴我的实现代码
  • 指定临时文件保存路径,在application.yml中配置
temp:
  file:
    path: D:\\
  • 会需要几个工具类及AsyncImportExportTask表的实体类,直接copy到项目中即可
package com.blog.entity;

import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;
import org.springframework.format.annotation.DateTimeFormat;

import java.util.Date;


/**
 * 异步导入导出任务表
 *
 * @author LiWT
 * @date 2021-06-27
 */
@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@TableName("async_import_export_task")
public class AsyncImportExportTask {

    /**
     * 任务id
     */
    @TableId(value = "task_id", type = IdType.ID_WORKER)
    private Long taskId;

    /**
     * 任务名称
     */
    private String taskName;

    /**
     * 公司 id
     */
    private Long companyId;

    /**
     * 用户 id
     */
    private Long userId;

    /**
     * 任务优先级
     * 只针对当前用户的任务起作用
     */
    private Integer priority;

    /**
     * 请求参数
     */
    private String reqParam;


    /**
     * D-A-01 下载当天呼叫记录
     * D-A-02 下载历史呼叫记录
     * U-C-01 导入客户数据
     * D-C-01 导出客户数据
     * U-U-01 导入用户数据
     * D-U-01 导出用户数据
     */
    private String taskCode;

    /**
     * 任务状态
     * 1:init
     * 2: running
     * 3: finish(success)
     * 4: finish(fail)
     */
    private Integer taskStatus;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date createdTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date dealTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private Date finishTime;

    private String tag;

    /**
     * 处理结果
     */
    private String result;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(exist = false)
    private Date beginTime;

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss", timezone = "GMT+8")
    @DateTimeFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    @TableField(exist = false)
    private Date endTime;

    /**
     * 任务类型
     * 1:上传(导入)
     * 2:下载(导出)
     */
    private Integer taskType;

}
  • 系统统一返回类型R
package com.blog.utils;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.*;
import lombok.experimental.Accessors;
import org.apache.commons.lang3.StringUtils;

import java.io.Serializable;

/**
 * @author LiWT
 * @date 2020-12-04
 */
@Builder
@ToString
@NoArgsConstructor
@Accessors(chain = true)
@ApiModel(value="R对象", description="返回信息的封装对象")
public class R<T> implements Serializable {
    private static final long serialVersionUID = 1L;

    /**
     * 成功标记
     */
    private static final Integer SUCCESS = 200;
    /**
     * 失败标记
     */
    private static final Integer FAIL = 500;

    @Getter
    @Setter
    @ApiModelProperty(value = "接口编码:200-成功;500-失败;401-权限不足")
    private int code;

    @Getter
    @Setter
    @ApiModelProperty(value = "接口提示信息")
    private String msg;


    @Getter
    @Setter
    @ApiModelProperty(value = "接口返回数据")
    private T data;

    @Getter
    @Setter
    @ApiModelProperty(value = "逻辑处理时间")
    private Long respTime;


    public static <T> R<T> ok() {
        return restResult(null, SUCCESS, null);
    }

    public static <T> R<T> ok(T data) {
        return restResult(data, SUCCESS, null);
    }

    public static <T> R<T> ok(T data, String msg) {
        return restResult(data, SUCCESS, msg);
    }

    public static <T> R<T> failed() {
        return restResult(null, FAIL, null);
    }

    public static <T> R<T> failed(String msg) {
        return restResult(null, FAIL, msg);
    }

    public static <T> R<T> failed(T data) {
        return restResult(data, FAIL, null);
    }

    public static <T> R<T> failed(T data, String msg) {
        return restResult(data, FAIL, msg);
    }
    public static <T> R<T> isOk(boolean isOk, String msg){
        if(isOk) {
            return restResult(null, SUCCESS, msg + "成功");
        } else {
            return restResult(null, FAIL, msg + "失败, 请重试");
        }
    }

    public static <T> R<T> result(int code, String msg, T data) {
        return restResult(data, code, msg);
    }

    private static <T> R<T> restResult(T data, int code, String msg) {
        R<T> apiResult = new R<>();
        apiResult.setCode(code);
        apiResult.setData(data);
        apiResult.setMsg(msg);
        if(StringUtils.isBlank(msg)) {
            if(SUCCESS != code) {
                apiResult.setMsg("操作失败");
            } else {
                apiResult.setMsg("操作成功");
            }
        }
        return apiResult;
    }

    public R(int code, String msg, T data, Long respTime) {
        this.code = code;
        this.msg = msg;
        this.data = data;
        this.respTime = respTime;
    }
}

  • 工具类:SpringContextHolder
package com.blog.utils;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.context.ApplicationEvent;
import org.springframework.context.annotation.Lazy;
import org.springframework.stereotype.Service;

/**
 * 以静态变量保存Spring ApplicationContext, 可在任何代码任何地方任何时候取出ApplicaitonContext.
 *
 * @author LiWT
 * @date 2021-06-27
 */
@Slf4j
@Service
@Lazy(false)
public class SpringContextHolder implements ApplicationContextAware, DisposableBean {

    private static ApplicationContext applicationContext = null;

    /**
     * 取得存储在静态变量中的ApplicationContext.
     */
    public static ApplicationContext getApplicationContext() {
        return applicationContext;
    }

    /**
     * 实现ApplicationContextAware接口, 注入Context到静态变量中.
     */
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        SpringContextHolder.applicationContext = applicationContext;
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    @SuppressWarnings("unchecked")
    public static <T> T getBean(String name) {
        return (T) applicationContext.getBean(name);
    }

    /**
     * 从静态变量applicationContext中取得Bean, 自动转型为所赋值对象的类型.
     */
    public static <T> T getBean(Class<T> requiredType) {
        return applicationContext.getBean(requiredType);
    }

    /**
     * 清除SpringContextHolder中的ApplicationContext为Null.
     */
    public static void clearHolder() {
        if (log.isDebugEnabled()) {
            log.debug("清除SpringContextHolder中的ApplicationContext:" + applicationContext);
        }
        applicationContext = null;
    }

    /**
     * 发布事件
     *
     * @param event
     */
    public static void publishEvent(ApplicationEvent event) {
        if (applicationContext == null) {
            return;
        }
        applicationContext.publishEvent(event);
    }

    /**
     * 实现DisposableBean接口, 在Context关闭时清理静态变量.
     */
    @Override
    public void destroy() throws Exception {
        SpringContextHolder.clearHolder();
    }

}

  • AsyncImportExportTask Mapper:
package com.blog.mapper;

import cn.hutool.core.date.DateTime;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.blog.entity.AsyncImportExportTask;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;
import org.springframework.stereotype.Repository;

import java.util.List;

/**
 * @author LiWT
 * @date 2021-06-27
 */
@Mapper
@Repository
public interface AsyncImportExportTaskMapper extends BaseMapper<AsyncImportExportTask> {
    /**
     * 根据时间和状态查询要处理的定时任务
     *
     * @param now 当前时间
     * @param i 状态
     * @return 任务列表
     */
    List<AsyncImportExportTask> selectByTimeAndStatus(@Param("beginTime") DateTime now, @Param("status") int i);

}

  • AsyncImportExportTask Mapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.blog.mapper.AsyncImportExportTaskMapper">

    <resultMap type="com.blog.entity.AsyncImportExportTask" id="AsyncImportExportTaskResult">
        <id property="taskId" column="task_id"/>
        <result property="taskName" column="task_name"/>
        <result property="taskCode" column="task_code"/>
        <result property="companyId" column="company_id"/>
        <result property="userId" column="user_id"/>
        <result property="priority" column="priority"/>
        <result property="reqParam" column="req_param"/>
        <result property="taskStatus" column="task_status"/>
        <result property="createdTime" column="created_time"/>
        <result property="dealTime" column="deal_time"/>
        <result property="finishTime" column="finish_time"/>
        <result property="result" column="result"/>
        <result property="tag" column="tag"/>
        <result property="taskType" column="task_type"/>
    </resultMap>


    <select id="selectByTimeAndStatus" resultMap="AsyncImportExportTaskResult">
        select * from async_import_export_task where created_time <![CDATA[ < ]]> #{beginTime} and task_status = #{status} for update;
    </select>

</mapper>
  • 步骤一:用户导出时保存用户导出任务至AsyncImportExportTask表中,直接响应用户
package com.blog.controller;

import com.alibaba.fastjson.JSONObject;
import com.blog.entity.AsyncImportExportTask;
import com.blog.service.IAsyncImportExportTaskService;
import com.blog.utils.R;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;

import javax.annotation.Resource;
import java.util.Date;
import java.util.UUID;

/**
 * 异步导入导出Controller
 *
 * @Author: LiWT
 * @Date: 2021/6/29 14:39
 */
@RestController
public class AsyncImportExportController {
    @Resource
    private IAsyncImportExportTaskService asyncImportExportTaskService;

    /**
     * 该导导出接口仅保存至AsyncImportExportTask中导出参数
     * 用户企业ID、用户ID应拿用户真实信息,方便查询我的任务时使用
     * @param jsonObject 导出参数
     * @return 导出成功,详情查看我的任务
     */
    @PostMapping("/export/user")
    public R<String> export(@RequestBody JSONObject jsonObject) {
        AsyncImportExportTask asyncImportExportTask = new AsyncImportExportTask();
        asyncImportExportTask.setCreatedTime(new Date());
        asyncImportExportTask.setTaskCode("D-A-01");
        asyncImportExportTask.setTaskName("导出当日呼叫记录");
        asyncImportExportTask.setReqParam(jsonObject.toJSONString());
        asyncImportExportTask.setTaskId(Long.parseLong(UUID.randomUUID().toString()));
        asyncImportExportTask.setCompanyId(-1L);//用户企业ID
        asyncImportExportTask.setTaskStatus(1);
        asyncImportExportTask.setTaskType(2);
        asyncImportExportTask.setUserId(1L);//用户ID
        asyncImportExportTaskService.save(asyncImportExportTask);
        return R.ok("操作成功,请去任务中心查看");
    }
}
  • 步骤二:用户操作导入时将导入文件暂时保存至服务器中,同时保存导入操作至异步任务表中,参考步骤一接口
  • 步骤三:需要一个"我的任务"菜单来展示用户导入/导出任务的状态及操作。/queryMyTask接口查询我的任务列表,/downloadFile接口下载对应任务的文件。
package com.blog.controller;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.blog.entity.AsyncImportExportTask;
import com.blog.service.IAsyncImportExportTaskService;
import com.blog.utils.R;
import io.swagger.annotations.Api;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;

/**
 * 我的任务Controller
 *
 * @author LiWT
 * @date 2021-06-27
 */
@RestController
@RequestMapping("/vos/custom/task")
@Api(value = "我的任务", tags = {"我的任务"})
public class MyTaskController {

    @Resource
    private IAsyncImportExportTaskService asyncImportExportTaskService;

    /**
     * 查询我的任务
     *
     * @param page                  分页参数
     * @param asyncImportExportTask 用户ID
     * @return 我的任务列表
     */
    @PostMapping("/queryMyTask")
    public R queryMyTask(IPage<AsyncImportExportTask> page, @RequestBody AsyncImportExportTask asyncImportExportTask) {
        asyncImportExportTask.setCompanyId(-1L);
        asyncImportExportTask.setUserId(1L);
        return R.ok(asyncImportExportTaskService.selectJobOrderByPage(page, asyncImportExportTask));
    }

    /**
     * 下载任务文件
     *
     * @param taskId 任务ID
     * @return 文件下载路径
     */
    @GetMapping("/downloadFile")
    public R downloadFile(Long taskId) {
        return R.ok("对应任务保存的文件路径");
    }
}

  • 异步任务Service层 IAsyncImportExportTaskService:
package com.blog.service;

import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.IService;
import com.blog.entity.AsyncImportExportTask;

import java.util.List;

/**
 * @author LiWT
 * @date 2021-06-27
 */
public interface IAsyncImportExportTaskService extends IService<AsyncImportExportTask> {

    /**
     * 查询我的任务列表
     *
     * @param page                  分页参数
     * @param asyncImportExportTask 公司ID、用户ID
     * @return 我的导入导出任务列表
     */
    IPage<AsyncImportExportTask> selectJobOrderByPage(IPage<AsyncImportExportTask> page, AsyncImportExportTask asyncImportExportTask);

    /**
     * 查询状态为 init 的任务
     * @return 需要处理的任务列表
     */
    List<AsyncImportExportTask> getTaskAndUpdateStatusSynchronized();
}

  • 异步任务ServiceImpl层 AsyncImportExportTaskServiceImpl:
package com.blog.service.impl;

import cn.hutool.core.date.DateUtil;
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.conditions.update.UpdateWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.blog.entity.AsyncImportExportTask;
import com.blog.mapper.AsyncImportExportTaskMapper;
import com.blog.service.IAsyncImportExportTaskService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.CollectionUtils;
import org.springframework.util.StringUtils;

import java.util.ArrayList;
import java.util.Date;
import java.util.List;


/**
 * @author LiWT
 * @date 2021-06-27
 */
@Service
public class AsyncImportExportTaskServiceImpl extends ServiceImpl<AsyncImportExportTaskMapper, AsyncImportExportTask> implements IAsyncImportExportTaskService {


    @Autowired
    private AsyncImportExportTaskMapper asyncImportExportTaskMapper;

    @Override
    public IPage<AsyncImportExportTask> selectJobOrderByPage(IPage<AsyncImportExportTask> page, AsyncImportExportTask asyncImportExportTask) {
        QueryWrapper<AsyncImportExportTask> queryWrapper = new QueryWrapper<>();
        queryWrapper.eq(asyncImportExportTask.getTaskStatus() != null, "task_status", asyncImportExportTask.getTaskStatus());
        queryWrapper.like(StringUtils.hasText(asyncImportExportTask.getTaskName()), "task_name", asyncImportExportTask.getTaskName());
        if (asyncImportExportTask.getBeginTime() != null) {
            queryWrapper.ge("created_time", asyncImportExportTask.getBeginTime());
        }
        if (asyncImportExportTask.getEndTime() != null) {
            queryWrapper.le("created_time", asyncImportExportTask.getEndTime());
        }
        queryWrapper.eq("user_id", asyncImportExportTask.getUserId());
        queryWrapper.eq("company_id", asyncImportExportTask.getCompanyId());
        queryWrapper.orderByDesc("created_time");
        return asyncImportExportTaskMapper.selectPage(page, queryWrapper);
    }

    @Override
    @Transactional(rollbackFor = Exception.class, propagation = Propagation.REQUIRES_NEW)
    public List<AsyncImportExportTask> getTaskAndUpdateStatusSynchronized() {
        // 查询状态为 init 的任务
        List<AsyncImportExportTask> list = asyncImportExportTaskMapper.selectByTimeAndStatus(DateUtil.date(), 1);
        if (CollectionUtils.isEmpty(list)) {
            return null;
        }
        List<Long> taskIds = new ArrayList<>();
        list.forEach(asyncImportExportTask -> {
            taskIds.add(asyncImportExportTask.getTaskId());
        });
        UpdateWrapper<AsyncImportExportTask> updateWrapper = new UpdateWrapper<>();
        updateWrapper.set("task_status", 2);
        updateWrapper.set("deal_time", new Date());
        updateWrapper.in("task_id", taskIds);
        asyncImportExportTaskMapper.update(null, updateWrapper);
        return list;
    }

}
  • 步骤四:定时任务来实现我们异步任务表中的任务执行,任务执行可采用多线程执行来提高效率。
package com.blog.task;

import com.blog.entity.AsyncImportExportTask;
import com.blog.service.AsyncTaskCommonService;
import com.blog.service.IAsyncImportExportTaskService;
import com.blog.utils.SpringContextHolder;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.util.CollectionUtils;

import java.util.List;
import java.util.concurrent.LinkedBlockingQueue;
import java.util.concurrent.ThreadFactory;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

/**
 * @author LiWT
 * @date 2021-06-27
 */
@Component
@Slf4j
public class AsyncImExportDataHandle {

    static ThreadPoolExecutor.CallerRunsPolicy policy = new ThreadPoolExecutor.CallerRunsPolicy();

    static ThreadPoolExecutor executor = new ThreadPoolExecutor(4, 4, 2, TimeUnit.SECONDS, new LinkedBlockingQueue<>(200), new ThreadFactory() {
        int num = 0;

        @Override
        public Thread newThread(Runnable r) {
            Thread thread = new Thread(r);
            thread.setName("async-task-thread-" + ++num);
            return thread;
        }
    }, policy);


    @Autowired
    private IAsyncImportExportTaskService asyncImportExportTaskService;

    public void dealAsyncTask() {
        List<AsyncImportExportTask> list = asyncImportExportTaskService.getTaskAndUpdateStatusSynchronized();
        if (CollectionUtils.isEmpty(list)) {
            return;
        }
        log.info("====> 需要处理任务条数:{}", list.size());
        for (AsyncImportExportTask asyncImportExportTask : list) {
            ((AsyncTaskCommonService) SpringContextHolder.getBean(asyncImportExportTask.getTaskCode())).invoke(asyncImportExportTask);
        }

        /*list.forEach(asyncImportExportTask -> CompletableFuture.runAsync(
                () -> ((AsyncTaskCommonService)SpringContextHolder.getBean(asyncImportExportTask.getTaskCode())).invoke(asyncImportExportTask), executor));*/
    }
}
  • 定时任务:
package com.blog.task.job;

import com.blog.task.AsyncImExportDataHandle;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.EnableScheduling;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
 * 每隔一分钟检查一下要处理的异步任务
 *
 * @author LiWT
 * @date 2021-06-27
 */
@Slf4j
@Component
@EnableScheduling
public class AsyncImExportDataTask {
    @Autowired
    private AsyncImExportDataHandle asyncImExportDataHandle;

    @Scheduled(cron = "0 0/1 * * * ?")
    private void asyncImExportData() {
        asyncImExportDataHandle.dealAsyncTask();
    }
}
  • 公共Service AsyncTaskCommonService,导出的实现类需实现该Service 重写 invoke()方法
package com.blog.service;

import com.blog.entity.AsyncImportExportTask;

/**
 * @author LiWT
 * @date 2021-06-27
 */
public interface AsyncTaskCommonService {
    /**
     * 执行任务
     *
     * @param asyncImportExportTask
     */
    void invoke(AsyncImportExportTask asyncImportExportTask);
}
  • 步骤五:首先在服务器创建要导出的文件,由于导出数据可能过多,Excel写入效率低下,可采用.csv格式导出数据。
    防止出现数据库一次查询数据过多导致内存溢出的情况,所以采用分批查询以流的方式写入csv文件,最终操作成功后更新异步任务表该任务的状态。
package com.blog.service.impl.async;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.blog.entity.AsyncImportExportTask;
import com.blog.entity.User;
import com.blog.mapper.UserMapper;
import com.blog.service.AsyncTaskCommonService;
import com.blog.service.IAsyncImportExportTaskService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.io.File;
import java.io.FileWriter;
import java.io.IOException;
import java.nio.ByteBuffer;
import java.nio.CharBuffer;
import java.nio.charset.Charset;
import java.nio.charset.StandardCharsets;
import java.util.*;

/**
 * @author LiWT
 * @date 2021-06-27
 */
@Component("D-A-01")
@Slf4j
public class ExportUserInfoData implements AsyncTaskCommonService {

    @Value("${temp.file.path}")
    private String baseTempFilePath;

    @Resource
    private UserMapper userMapper;

    @Autowired
    private IAsyncImportExportTaskService asyncImportExportTaskService;

    @Override
    public void invoke(AsyncImportExportTask asyncImportExportTask) {
        log.info(Thread.currentThread().getName() + "导出历史呼叫记录。。。");

        //导出时的参数,需要根据参数来导出数据
        final String reqParam = asyncImportExportTask.getReqParam();

        //创建csv文件到指定路径
        String filePath = baseTempFilePath + UUID.randomUUID() + ".csv";
        File file = new File(filePath);
        if (!file.exists()) {
            if (!file.getParentFile().exists()) {
                file.getParentFile().mkdirs();
            }
        }
        try (FileWriter fw = new FileWriter(file, true)) {
            if (!file.exists()) {
                file.createNewFile();
            }

            // 添加 BOM 头,防止 excel 打开 csv 中文乱码
            final byte[] bytes = {(byte) 0xEF, (byte) 0xBB, (byte) 0xBF};
            fw.write(getChars(bytes));
            fw.write("姓名,昵称,地址,生日,性别,手机");
            fw.write("\r\n");
            boolean continueFlag = true;
            int num = 0;
            //以分页的方式查询所需数据写入csv文件,此时的FileWriter仅执行flush 不要关闭
            while (continueFlag) {
                //自定义分页参数,每次查询1000条数据
                IPage<User> page = new Page<>();
                page.setCurrent(++num);
                page.setSize(1000);

                QueryWrapper<User> queryWrapper = new QueryWrapper<>();
                IPage<User> userPageInfo = userMapper.selectPage(page, queryWrapper);
                final List<User> rows = userPageInfo.getRecords();
                //若当次查询的条数小于1000,则设置continueFlag为false,跳出循环
                if (rows.size() < 1000) {
                    continueFlag = false;
                }
                //将每次查询的数据写入csv
                for (User row : rows) {
                    write(fw, row);
                }
                fw.flush();
            }
        } catch (Exception e) {
            log.error("invoke FileWriter error:", e);
        }
        String tag = filePath.substring(1);
        asyncImportExportTask.setResult("任务成功");
        asyncImportExportTask.setFinishTime(new Date());
        asyncImportExportTask.setTaskStatus(3);
        asyncImportExportTask.setTag(tag);
        asyncImportExportTaskService.saveOrUpdate(asyncImportExportTask);
        log.info(Thread.currentThread().getName() + "导出任务完成!!!!!!!!!!!!");
    }

    public static char[] getChars(byte[] bytes) {
        Charset cs = StandardCharsets.UTF_8;
        ByteBuffer bb = ByteBuffer.allocate(bytes.length);
        bb.put(bytes);
        bb.flip();
        CharBuffer cb = cs.decode(bb);
        return cb.array();
    }


    private static void write(FileWriter fw, User row) {
        try {
            fw.write(row.getName() + "\t,\""
                    + row.getNickName() + "\"\t,\""
                    + row.getAddress() + "\"\t,\""
                    + row.getBirthday() + "\"\t,\""
                    + row.getSex() + "\"\t,\""
                    + row.getMobile() + "\"\t");
            fw.write("\r\n");
        } catch (IOException e) {
            log.error("write error:", e);
        }
    }

}
  1. 实现流程到这里就结束了,本文提供于有一定用户量、数据量大、有一定并发量的场景,用以优化导入导出功能的解决方案。
    如果该方案可以落地的话,后续可优化的空间还是很大的,基本可以满足日常需求。
评论 2
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值