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