场景:项目中业务需要导出大批量数据,大到4,50w的数据到excel中,将需求写成功能
-
大批量数据一次性查询出较为耗时,需要对表结构设计要求高方可提升查询速度。
-
内存溢出,IO异常,一次性几十万的数据读取和写入很大,并发如果上来的场景下可能导致系统接口挂掉。
-
接口设计响应速度考虑
实现思路:
-
以异步接口的形式完成导出的需求功能,将导出业务分为两个过程:1.申请导出、2.下载导出文件。
-
文件分离设计,单个文件数据量最大为10w,总文件数为:n/n + n%n>0?1:0。
-
即使对文件做分离处理依旧无法解决IO溢出问题,对单个文件数据做边查边写处理,通过手动分页设置偏移量的形式,对单个文件的数据做分页处理。
老规矩直接上代码,注释写的比较清晰,评论区交流见解和问题
接口层代码:
* excel导出记录(异步)
* @param request
* @return
@PostMapping
(
"/export"
)
public
Rest
<
BaseResponse
>
export
(
@RequestBody
exportRequest request
)
{
Long
accountId
=
ContextHolder
.
currentAccountId
(
)
;
RBucket
<
Integer
>
bucket
=
redissonClient
.
getBucket
(
"EXPORT:LOCK:"
+
accountId
)
;
if
(
bucket
.
isExists
(
)
)
{
throw
new
RuntimeException
(
)
;
bucket
.
set
(
1
,
5
,
TimeUnit
.
MINUTES
)
;
long
count
=
0
;
try
{
count
=
PageHelper
.
count
(
(
)
->
exportRecordService
.
exportList
(
request
)
)
;
}
catch
(
Exception
e
)
{
bucket
.
delete
(
)
;
log
.
error
(
"统计数量sql出错"
,
e
)
;
throw
new
RuntimeException
(
)
;
if
(
count
<=
0
)
{
bucket
.
delete
(
)
;
throw
new
RuntimeException
(
)
;
int
shardNum
=
exportRecordService
.
getShardNum
(
(
int
)
count
,
100000
)
;
String
title
=
DATE_TIME_FORMATTER
.
format
(
request
.
getStartTime
(
)
)
+
"~"
+
DATE_TIME_FORMATTER
.
format
(
request
.
getEndTime
(
)
)
+
"-采样记录"
+
"-"
+
shardNum
+
"-"
;
List
<
ExportDTO
>
fileDTOList
=
new
ArrayList
<
>
(
)
;
for
(
int
i
=
1
;
i
<=
shardNum
;
i
++
)
{
String
fileName
=
title
+
i
;
ExportDTO
fileDTO
=
new
ExportDTO
(
)
.
setFileName
(
fileName
)
.
setFileNum
(
i
)
;
int
pageNum
=
50
;
if
(
i
==
shardNum
&&
count
%
100000
>
0
)
{
pageNum
=
samplingRecordService
.
getShardNum
(
Math
.
toIntExact
(
count
%
100000
)
,
2000
)
;
fileDTO
.
setPageNum
(
pageNum
)
;
fileDTOList
.
add
(
fileDTO
)
;
InheritableThreadLocal
<
Long
>
threadLocal
=
new
InheritableThreadLocal
<
>
(
)
;
threadLocal
.
set
(
accountId
)
;
exportRecordService
.
exportList
(
request
,
fileDTOList
,
threadLocal
)
;
return
Rest
.
success
(
)
;
service层代码:
@Async
@Override
@Transactional(rollbackFor = Exception.class)
public void exportList(ExportRequest request, List<ExportDTO> fileDTOList, InheritableThreadLocal<Long> threadLocal) {
long startTime = System.currentTimeMillis();
RBucket<Integer> bucket = redissonClient.getBucket("EXPORT:LOCK:" + threadLocal.get());
for (ExportDTO fileDTO : fileDTOList) {
String dirPath = new ApplicationHome(getClass()).getSource().getParentFile().toString();
File file = new File(dirPath + File.separator + fileDTO.getFileName() + ".xlsx");
AtomicInteger atomicInteger = new AtomicInteger();
int num = 0;
ExcelWriter excelWriter = null;
try {
excelWriter = EasyExcel.write(file, ExportResponse.ExportVo.class).build();
WriteSheet writeSheet = EasyExcel.writerSheet().build();
for (int i = 1; i <= fileDTO.getPageNum(); i++) {
PageHelper.startPage((fileDTO.getFileNum() - 1) * 50 + i, 2000, false);
List<ExportResponse.ExportVo> data = this.exportList(request);
if (CollUtil.isEmpty(data)) {
break;
for (ExportResponse.ExportVo vo : data) {
vo.setNo(atomicInteger.incrementAndGet());
excelWriter.write(data, writeSheet);
num += data.size();
PageHelper.clearPage();
excelWriter.finish();
log.info("{}写入完成路径:{}", fileDTO.getFileName(), file.getAbsolutePath());
String objectId = ossUtil.upload(file, "记录导出");
log.info(file.getAbsolutePath() + "上传完成,objectId:{}", objectId);
exportRecordService.save(objectId, num, threadLocal.get(), fileDTO.getFileName());
log.info("{}保存至记录表", fileDTO.getFileName());
} catch (Exception e) {
log.error("记录导出出错", e);
bucket.delete();
throw new RuntimeException();
} finally {
if (excelWriter != null) {
excelWriter.finish();
if (file.exists()) {
file.delete();
long endTime = System.currentTimeMillis();
bucket.delete();
log.info("耗时:{}", endTime - startTime);
@Override
public List<ExportResponse.ExportVo> exportList(ExportRequest request) {
return recordMapper.exportList(request);
@Override
public int getShardNum(Integer total, Integer shardSize) {
return (int) Math.ceil((double) total / (double) shardSize);
导入:使用Excel Streaming Reader进行海量数据读取
这个第三方工具会把一部分的行(可以设置)缓存到内存中,在迭代时不断加载行到内存中,而不是一次性的加载所有记录到内存,这样就可以不断的读取excel内容并且不影响内存的使用。
但是这个工具也有一定的限制:只能用于读取excel的内容,写入操作不可用;可以使用getSheetAt()方法获取到对应的Sheet,因为当前只是加载了...
解决该问题的一个思路是使用多线程并行处理,将数据分成若干块,每个线程负责导出一块数据。
以下是一个关键代码片段:
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
public class ExcelExport {
private final int threadCou...
SpringBoot3.x版本将swagger2.0升级到swagger3.0,使用knife4j-openapi3-jakarta-spring-boot-starter依赖
Tony666688888:
swagger文档无法测试下载文件的接口解决方式
Gavin_A_Chan:
swagger文档无法测试下载文件的接口解决方式
lngg057:
搞不懂『三级缓存』?与生命周期冲突?为什么代理对象二级缓存不可用?进来看看,助你掌握概念思路
奋斗中的小胖子:
swagger文档无法测试下载文件的接口解决方式
山椒鱼摆摆: