公司项目需求,数据库有上千万条数据需要导出excel,使用EasyExcel导出,数据量不大时,没什么问题,但数据量超过上百网时,mysql出现连接超时,,虚拟机内存也会出现问题,后来考虑使用多线程分批导出多个excel,再把多个excel压缩成zip包发送到浏览器,这里每批次可处理100000条数据,大概两到三分钟执行完,五万条每批次大概三到四分钟,具体根据java虚拟机情况测试
项目使用springboot框架,所以线程池也是用springboot配置
@Configuration
@EnableAsync
public class TaskPoolConfig {
@Bean("taskExecutor")
public Executor taskExecutro(){
int i = Runtime.getRuntime().availableProcessors();
System.out.println("系统最大线程数 : "+i);
ThreadPoolTaskExecutor taskExecutor = new ThreadPoolTaskExecutor();
taskExecutor.setCorePoolSize(i);
taskExecutor.setMaxPoolSize(i);
taskExecutor.setQueueCapacity(99999);
taskExecutor.setKeepAliveSeconds(60);
taskExecutor.setThreadNamePrefix("taskExecutor--");
taskExecutor.setWaitForTasksToCompleteOnShutdown(true);
taskExecutor.setAwaitTerminationSeconds(60);
return taskExecutor;
}
}
/**
* 分批次异步导出数据
* @param map 要导出的批次数据信息
* @param cdl countDownLatch这个类使一个线程等待其他线程各自执行完毕后再执行。
*/
@Async("taskExecutor")
public void executeAsyncTask(Map<String, Object> map, CountDownLatch cdl) {
long start = System.currentTimeMillis();
// 导出文件路径
List<CodeReceptionList> list = null;
try {
PageUtil.pageUtil(map);
//查询要导出的批次数据
list = codeReceptionListDao.queryAllByLimit(map);
} catch (Exception e) {
e.printStackTrace();
}
// 写法1
String filepath = map.get("path").toString() + map.get("page") + ".xlsx";
TestFileUtil.createFile(filepath);
// 这里 需要指定写用哪个class去读,然后写到第一个sheet,名字为模板 然后文件流会自动关闭
// 如果这里想使用03 则 传入excelType参数即可
EasyExcel.write(filepath, CodeReceptionList.class).sheet("模板").doWrite(list);
long end = System.currentTimeMillis();
System.out.println("线程:" + Thread.currentThread().getName() + " , 导出excel " + map.get("page") + ".xlsx 成功 , 导出数据 " + list.size() + " ,耗时 :" + (end - start));
list.clear();
//执行完毕线程数减一
cdl.countDown();
System.out.println("剩余任务数 ===========================> " + cdl.getCount());
}
这里使用mybatis操作数据库,先查询出数据库有多少数据,并处理每批次的分页信息存入队列,然后每从队列取出一个批次信息开启一个线程,调用异步导出方法,CountDownLatch记录任务执行完毕后对文件目录压缩成zip并发送到浏览器,这里从网上找了个zip压缩工具类
@Service
@Slf4j
public class TestService {
/**
*每批次处理的数据量
*/
private static final int LIMIT = 100000;
@Resource
private CodeReceptionListDao codeReceptionListDao;
public static Queue<Map<String, Object>> queue;//Queue是java自己的队列,具体可看API,是同步安全的
static {
queue = new ConcurrentLinkedQueue<Map<String, Object>>();
}
private String filePath = "/Users/fishfly/localFile/excel/";
@Resource
private AsyncTaskService asyncTaskService;
/**
* 初始化队列
*/
public void initQueue() {
// 设置数据
//long count = codeReceptionListDao.count(new HashMap<>());
long listCount = 5000000;
int listCount1 = (int) listCount;
//导出6万以上数据。。。
int count = listCount1 / LIMIT + (listCount1 % LIMIT > 0 ? 1 : 0);//循环次数
for (int i = 1; i <= count; i++) {
Map<String, Object> map = new HashMap<>();
map.put("page", i);
map.put("limit", LIMIT);
map.put("path", filePath);
//添加元素
queue.offer(map);
}
}
/**
* 多线程批量导出 excel
* @param response 用于浏览器下载
* @throws InterruptedException
*/
public void threadExcel(HttpServletResponse response) throws InterruptedException {
initQueue();
long start = System.currentTimeMillis();
//异步转同步,等待所有线程都执行完毕返会 主线程才会结束
try {
CountDownLatch cdl = new CountDownLatch(queue.size());
while (queue.size() > 0) {
asyncTaskService.executeAsyncTask(queue.poll(), cdl);
}
cdl.await();
//压缩文件
File zipFile = new File(filePath.substring(0, filePath.length() - 1) + ".zip");
FileOutputStream fos1 = new FileOutputStream(zipFile);
//压缩文件目录
ZipUtils.toZip(filePath, fos1, true);
//发送zip包
ZipUtils.sendZip(response, zipFile);
} catch (FileNotFoundException e) {
e.printStackTrace();
}
long end = System.currentTimeMillis();
System.out.println("任务执行完毕 共消耗 : " + (end - start) / 1000 / 60 + " 分钟");
}
}
@Component
@Slf4j
public class ZipUtils {
private static final int BUFFER_SIZE = 2 * 1024;
/**
* 压缩成ZIP 方法 * @param srcDir 压缩文件夹路径
*
* @param out 压缩文件输出流
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip(String srcDir, OutputStream out, boolean KeepDirStructure)
throws RuntimeException {
log.info("正在压缩文件。。。");
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(out);
File sourceFile = new File(srcDir);
compress(sourceFile, zos, sourceFile.getName(), KeepDirStructure);
long end = System.currentTimeMillis();
log.info("压缩完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("zip error from ZipUtils", e);
} finally {
if (zos != null) {
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 压缩成ZIP 方法 * @param srcFiles 需要压缩的文件列表
*
* @param out 压缩文件输出流
* @throws RuntimeException 压缩失败会抛出运行时异常
*/
public static void toZip(List<File> srcFiles, OutputStream out) throws RuntimeException {
long start = System.currentTimeMillis();
ZipOutputStream zos = null;
try {
zos = new ZipOutputStream(out);
for (File srcFile : srcFiles) {
byte[] buf = new byte[BUFFER_SIZE];
zos.putNextEntry(new ZipEntry(srcFile.getName()));
int len;
FileInputStream in = new FileInputStream(srcFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
zos.closeEntry();
in.close();
}
long end = System.currentTimeMillis();
System.out.println("压缩完成,耗时:" + (end - start) + " ms");
} catch (Exception e) {
throw new RuntimeException("zip error from ZipUtils", e);
} finally {
if (zos != null) {
try {
zos.close();
} catch (IOException e) {
e.printStackTrace();
}
}
}
}
/**
* 递归压缩方法
*
* @param sourceFile 源文件
* @param zos zip输出流
* @param name 压缩后的名称
* @param KeepDirStructure 是否保留原来的目录结构,true:保留目录结构;
* false:所有文件跑到压缩包根目录下(注意:不保留目录结构可能会出现同名文件,会压缩失败)
* @throws Exception
*/
private static void compress(File sourceFile, ZipOutputStream zos, String name,
boolean KeepDirStructure) throws Exception {
byte[] buf = new byte[BUFFER_SIZE];
if (sourceFile.isFile()) {
// 向zip输出流中添加一个zip实体,构造器中name为zip实体的文件的名字
zos.putNextEntry(new ZipEntry(name));
// copy文件到zip输出流中
int len;
FileInputStream in = new FileInputStream(sourceFile);
while ((len = in.read(buf)) != -1) {
zos.write(buf, 0, len);
}
// Complete the entry
zos.closeEntry();
in.close();
} else {
//是文件夹
File[] listFiles = sourceFile.listFiles();
if (listFiles == null || listFiles.length == 0) {
// 需要保留原来的文件结构时,需要对空文件夹进行处理
if (KeepDirStructure) {
// 空文件夹的处理
zos.putNextEntry(new ZipEntry(name + "/"));
// 没有文件,不需要文件的copy
zos.closeEntry();
}
} else {
for (File file : listFiles) {
// 判断是否需要保留原来的文件结构
if (KeepDirStructure) {
// 注意:file.getName()前面需要带上父文件夹的名字加一斜杠,
// 不然最后压缩包中就不能保留原来的文件结构,即:所有文件都跑到压缩包根目录下了
compress(file, zos, name + "/" + file.getName(), KeepDirStructure);
} else {
compress(file, zos, file.getName(), KeepDirStructure);
}
}
}
}
}
/**
* 向浏览器发送zip包
*
* @param response
*/
public static void sendZip(HttpServletResponse response, File zipFile) {
log.info("正在发送zip包");
OutputStream outputStream = null;
BufferedInputStream fis = null;
try {
// 以流的形式下载文件。
fis = new BufferedInputStream(new FileInputStream(zipFile.getPath()));
byte[] buffer = new byte[fis.available()];
fis.read(buffer);
// 清空response
response.reset();
outputStream = new BufferedOutputStream(response.getOutputStream());
response.setContentType("application/octet-stream");
response.setHeader("Content-Disposition", "attachment;filename=" + new String(zipFile.getName().getBytes("UTF-8"), "ISO-8859-1"));
outputStream.write(buffer);
outputStream.flush();
log.info("发送成功。");
} catch (Exception ex) {
ex.printStackTrace();
} finally {
try {
if (fis != null) { fis.close(); }
if (outputStream != null) { outputStream.close(); }
} catch (Exception e) {
e.printStackTrace();
}
}
}
}