前提
因为Excel数据的导出之前使用HSSFWorkbook,HSSFWorkbook导出的数据有上限,最多导出65535条数据,现在因为项目数据量变大,需要导出的数据变多,现在每次点击导出都卡死掉,所以现在就对其进行优化。
优化方案
使用异步的方式+任务的形式进行数据的导出,当点击的时候导出按钮的时候,进行添加任务到数据库中,Excel数据的异步导出做成一个模块,方便复用,把异步任务做成模块的思路是,项目启动时开始线程进行任务的执行。
代码和解析
首先先创建一张任务数据库表
以下是我业务需求的数据库表,你们自行根据你们的需要进行创建
CREATE TABLE ops_task
(
id
bigint(20) NOT NULL AUTO_INCREMENT COMMENT ‘导出任务列表’,
source
tinyint(2) NOT NULL DEFAULT 0 COMMENT ‘来源 0pc端 10渠道商端’,
account_id
bigint(20) NOT NULL DEFAULT 0 COMMENT ‘账号id’,
account_type
tinyint(2) NOT NULL DEFAULT 0 COMMENT ‘账号类型 0 admin 10部门账号 20主渠道 21子渠道’,
title
varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ‘’ COMMENT ‘标题’,
params
varchar(1024) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ‘’ COMMENT ‘导出参数’,
type
smallint(4) NOT NULL DEFAULT 0 COMMENT ‘类型:10:投手账户数据列表 20 用户充值记录列表 21 vip充值记录列表’,
status
tinyint(2) NOT NULL DEFAULT 0 COMMENT ‘状态:10:导出中,20:导出失败,30:已完成’,
url
varchar(255) CHARACTER SET utf8mb4 COLLATE utf8mb4_general_ci NOT NULL DEFAULT ‘’ COMMENT ‘oss的下载链接’,
create_time
datetime NOT NULL DEFAULT ‘2000-01-01 00:00:00’,
count
int(11) NOT NULL DEFAULT 0 COMMENT ‘导出条数’,
is_del
tinyint(1) NOT NULL DEFAULT 0,
update_time
timestamp NOT NULL DEFAULT CURRENT_TIMESTAMP,
PRIMARY KEY (id
) USING BTREE
) ENGINE = InnoDB AUTO_INCREMENT = 316 CHARACTER SET = utf8mb4 COLLATE = utf8mb4_general_ci ROW_FORMAT = Dynamic;
点击导出按钮生成任务的Controller具体实现
@RequestMapping(value=“/export-vip-excel”,method=RequestMethod.POST)
public void exportVipExcel(HttpServletResponse response,HttpServletRequest request)throws IOException{
try {
//获取当前的渠道商信息
Business business = (Business) WebUtils.getSessionAttribute(request, “BUSINESS”);
//把请求参数转化成Map
Map<String,String> map = MapUtils.paramsMap(request);
businessService.addMap(map,business);
//组成标题
String title = map.get(“startTime”) + “到” + map.get(“endTime”) + “VIP用户充值记录”;
//判断是否为主渠道商
if (business.getParentId() == 0) {
//进行添加任务
opsTaskService.add(Long.valueOf(business.getId()), OpsTask.SOURCE_TYPE.BUSINESS,
OpsTask.ACCOUNT_TYPE.MAIN_BUSINESS,JSONObject.toJSONString(map),
title,OpsTask.TYPE.VIP_RECHARGE_COLLECTION_REPORT);
}else {
opsTaskService.add(Long.valueOf(business.getId()),
//进行添加任务
OpsTask.SOURCE_TYPE.BUSINESS,OpsTask.ACCOUNT_TYPE.SON_BUSINESS,
JSONObject.toJSONString(map),title,OpsTask.TYPE.VIP_RECHARGE_COLLECTION_REPORT);
}
//页面跳转刷新
response.sendRedirect(“go/list-vip-rechargeLog”);
} catch (Exception e) {
e.printStackTrace();
ControllerHelper.sendErrorJson(response, “获取VIP用户充值记录列表失败,请稍后重试”, e);
}
}
异步导出Excel模块实现
-
首先先写一个类实现CommandLineRunner,这个类的作用时项目启动的时候就可以进行运行,然后在里面开启线程进行处理任务,目前只使用单个线程,new Thread()方法,需求量高了之后可以进行优化使用线程池。
@Component
public class AutoStart implements CommandLineRunner {Logger logger= LoggerFactory.getLogger(AutoStart.class);
@Override
public void run(String… args) throws Exception {
logger.info(“===AutoStart开始=导出任务启动”);
startExport();
}/**
- @Description: 导出任务
- @Param: []
- @return: void
- @Author linchuanndong
- @Date: 2024/3/1 10:39
*/
private void startExport() {
new Thread(new ExportProcessor()).start();
}
}
-
写一个线程类实现Runnable接口,实现思路:主要通过Reids存储当前执行任务来判断当前是否有任务在进行导出。
public class ExportProcessor implements Runnable{private static final Logger logger = LoggerFactory.getLogger(ExportProcessor.class);
OpsTaskService opsTaskService;public ExportProcessor() {
opsTaskService = SpringContextUtils.getBean(OpsTaskService.class);
}@Override
public void run() {
OpsTask opsTask = null;
//要redis中获取值的key值
String key = Key.REDIS_KEY.EXPORT_KEY;
while (true) {
try {
//如果当前key不存在就进行初始化0和进行+1,存在就直接+1
Long index = Redis.incr(key);
//如果index获取后为>1所名有任务在进行
if (index > 1L) {
//进行设置过期时间
expire(key, Key.EXPIRE_TIME.TEN_MIN);
logger.info(" -----处理导出任务正在执行------index:{}“, index);
//睡眠2分钟结束此次循环
Thread.sleep(1000602);
continue ;
}
//进行设置过期时间
expire(key, Key.EXPIRE_TIME.TEN_MIN);
//获取是否还要可以进行导出的 任务
opsTask = opsTaskService.get();
//为空情况
if (null == opsTask) {
//删除redis
Redis.del(key);
logger.info(“无导出任务…”);
//睡眠3分钟结束此次循环
Thread.sleep(1000603);
continue;
}
//不为空进行数据的导出操作,传入opsTask对象,根据里面的类型进行判断要进行什么方式导出
new ExportTask(opsTask).export();
Redis.del(key);
} catch (Exception e) {
Redis.del(key);
logger.error(“导出任务异常,taskId:”+(null == opsTask ? 0 : opsTask.getId())+”;msg:"+e.getMessage(), e);
}
}
}public static void expire(String key, int time) {
Long ttl = Redis.ttl(key);
if (ttl == -1 || ttl > Long.valueOf(time)) {
Redis.expire(key, time);
}
}
} -
使用阿里的Excel数据的导出逻辑
private void writeVipUserRechargeCollectionExcel(Map<String, String> paramsMap, Long id, String fileName) {
logger.info("线程开始导出VIP用户充值报表, " + fileName);
try {
long startTime = System.currentTimeMillis();
String path = generateFileName(fileName);
Integer count = extendDayReportService.exportVipUserRecharge(paramsMap, path, fileName);
File file = new File(path);
OssUtils.uploadFile(file, file.getName(), false);
opsTaskService.modify(id, OpsTask.STATUS.OK, count, OssUtils.FILE_URL + fileName + Constant.FILE_PATH.EXCEL_SUFFIX_XLSX);
boolean flag = file.delete();
logger.info(“线程导出VIP用户充值报表结束-export time:[{}], flag:[{}]”, (System.currentTimeMillis() - startTime), flag);
} catch (Exception e) {
opsTaskService.modify(id, OpsTask.STATUS.FAIL, 0, “”);
logger.error(“线程导出VIP用户充值报表异常,msg:” + e.getMessage(), e);
}
}
public Integer exportVipUserRecharge(Map<String, String> paramsMap, String path, String fileName) {
//使用阿里的Excel
ExcelWriter writer = EasyExcelUtil.buildEasyExcel(path, VipUserRechargeReportVO.class);
//设置文件名
WriteSheet sheet = EasyExcel.writerSheet(fileName).build();
//根据条件查询要导出的数据
List list = rechargeLogService.query(Constant.EXTREMITY.BUSINESS,RechargeLog.RECHARGE_TYPE.VIP,paramsMap);
//数据没空进行处理
if (CollectionUtils.isEmpty(list)) {
writer.write(new ArrayList<>(), sheet);
writer.finish();
return 0;
}
//把需要的数据进行导出
List userRechargeReportVOList = new ArrayList<>();
list.forEach(item -> userRechargeReportVOList.add(new VipUserRechargeReportVO(item)));
writer.write(userRechargeReportVOList, sheet);
writer.finish();
//返回导出的数量进行返回
return list.size();
}
public static ExcelWriter buildEasyExcel(String pathName, Class cls) {
return EasyExcel
.write(pathName, cls)
.registerWriteHandler(setHorizontalCellStyleStrategy())
.registerWriteHandler(new CustomCellWeightWeightConfig())
.build();
}